starweaver-model 0.7.0

Provider-neutral model protocol and wire adapters for Starweaver
Documentation
//! Provider content part mapping helpers.

use base64::{Engine as _, engine::general_purpose::STANDARD};
use serde_json::{Value, json};

use crate::{ModelError, message::ContentPart, parse_data_url};

#[cfg(test)]
pub fn text_from_content(content: &[ContentPart]) -> String {
    content
        .iter()
        .filter_map(|part| match part {
            ContentPart::Text { text } => Some(text.as_str()),
            ContentPart::CachePoint { .. }
            | ContentPart::ImageUrl { .. }
            | ContentPart::FileUrl { .. }
            | ContentPart::Binary { .. }
            | ContentPart::ResourceRef { .. }
            | ContentPart::DataUrl { .. } => None,
        })
        .collect::<Vec<_>>()
        .join("")
}

pub fn openai_chat_content_with_cache_points(
    content: &[ContentPart],
    enable_cache_points: bool,
) -> Result<Value, ModelError> {
    if content.len() == 1
        && let ContentPart::Text { text } = &content[0]
    {
        return Ok(json!(text));
    }
    let mut blocks: Vec<Value> = Vec::new();
    for part in content {
        if let ContentPart::CachePoint { ttl } = part {
            if !enable_cache_points {
                continue;
            }
            if ttl.is_some() {
                return Err(ModelError::MessageMapping(
                    "OpenAI GPT-5.6 cache points use request-wide 30m TTL; omit the per-point TTL"
                        .to_string(),
                ));
            }
            let previous = blocks.last_mut().ok_or_else(|| {
                ModelError::MessageMapping(
                    "cache point cannot be the first content in an OpenAI message".to_string(),
                )
            })?;
            previous["prompt_cache_breakpoint"] = json!({"mode": "explicit"});
            continue;
        }
        blocks.push(match part {
            ContentPart::CachePoint { .. } => unreachable!(),
            ContentPart::Text { text } => json!({"type": "text", "text": text}),
            ContentPart::ImageUrl { url } => {
                json!({"type": "image_url", "image_url": {"url": url}})
            }
            ContentPart::FileUrl { url, media_type } => json!({
                "type": "file",
                "file": {"file_data": url, "media_type": media_type}
            }),
            ContentPart::Binary { data, media_type } => {
                if media_type.starts_with("image/") {
                    json!({"type": "image_url", "image_url": {"url": data_url(media_type, data)}})
                } else {
                    json!({"type": "file", "file": {"file_data": data_url(media_type, data), "media_type": media_type}})
                }
            }
            ContentPart::ResourceRef { uri, media_type, .. } => {
                if media_type.starts_with("image/") {
                    json!({"type": "image_url", "image_url": {"url": uri}})
                } else {
                    json!({"type": "file", "file": {"file_data": uri, "media_type": media_type}})
                }
            }
            ContentPart::DataUrl { data_url, media_type } => {
                if media_type.starts_with("image/") {
                    json!({"type": "image_url", "image_url": {"url": data_url}})
                } else {
                    json!({"type": "file", "file": {"file_data": data_url, "media_type": media_type}})
                }
            }
        });
    }
    Ok(Value::Array(blocks))
}

pub fn openai_responses_content_with_cache_points(
    content: &[ContentPart],
    enable_cache_points: bool,
) -> Result<Vec<Value>, ModelError> {
    let mut blocks: Vec<Value> = Vec::new();
    for part in content {
        if let ContentPart::CachePoint { ttl } = part {
            if !enable_cache_points {
                continue;
            }
            if ttl.is_some() {
                return Err(ModelError::MessageMapping(
                    "OpenAI GPT-5.6 cache points use request-wide 30m TTL; omit the per-point TTL"
                        .to_string(),
                ));
            }
            let previous = blocks.last_mut().ok_or_else(|| {
                ModelError::MessageMapping(
                    "cache point cannot be the first content in an OpenAI message".to_string(),
                )
            })?;
            previous["prompt_cache_breakpoint"] = json!({"mode": "explicit"});
            continue;
        }
        blocks.push(match part {
            ContentPart::CachePoint { .. } => unreachable!(),
            ContentPart::Text { text } => json!({"type": "input_text", "text": text}),
            ContentPart::ImageUrl { url } => json!({"type": "input_image", "image_url": url}),
            ContentPart::FileUrl { url, media_type } => json!({
                "type": "input_file",
                "file_url": url,
                "media_type": media_type
            }),
            ContentPart::Binary { data, media_type } => {
                if media_type.starts_with("image/") {
                    json!({"type": "input_image", "image_url": data_url(media_type, data)})
                } else {
                    json!({"type": "input_file", "file_url": data_url(media_type, data), "media_type": media_type})
                }
            }
            ContentPart::ResourceRef { uri, media_type, .. } => {
                if media_type.starts_with("image/") {
                    json!({"type": "input_image", "image_url": uri})
                } else {
                    json!({"type": "input_file", "file_url": uri, "media_type": media_type})
                }
            }
            ContentPart::DataUrl { data_url, media_type } => {
                if media_type.starts_with("image/") {
                    json!({"type": "input_image", "image_url": data_url})
                } else {
                    json!({"type": "input_file", "file_url": data_url, "media_type": media_type})
                }
            }
        });
    }
    Ok(blocks)
}

pub fn gemini_parts_from_content(content: &[ContentPart]) -> Vec<Value> {
    content
        .iter()
        .filter_map(|part| match part {
            ContentPart::CachePoint { .. } => None,
            ContentPart::Text { text } => Some(json!({"text": text})),
            ContentPart::ImageUrl { url } => Some(json!({
                "fileData": {"fileUri": url, "mimeType": "image/*"}
            })),
            ContentPart::FileUrl { url, media_type } => Some(json!({
                "fileData": {"fileUri": url, "mimeType": media_type}
            })),
            ContentPart::Binary { data, media_type } => Some(json!({
                "inlineData": {"data": base64_payload(data), "mimeType": media_type}
            })),
            ContentPart::ResourceRef {
                uri, media_type, ..
            } => Some(json!({
                "fileData": {"fileUri": uri, "mimeType": media_type}
            })),
            ContentPart::DataUrl {
                data_url,
                media_type,
            } => Some(gemini_data_url(data_url, media_type)),
        })
        .collect()
}

pub fn bedrock_content_from_content(content: &[ContentPart]) -> Vec<Value> {
    content
        .iter()
        .filter_map(|part| match part {
            ContentPart::CachePoint { .. } => None,
            ContentPart::Text { text } => Some(json!({"text": text})),
            ContentPart::ImageUrl { url } => {
                Some(json!({"image": {"source": {"bytes": url}}}))
            }
            ContentPart::FileUrl { url, media_type } => Some(json!({
                "document": {
                    "format": media_type,
                    "source": {"bytes": url},
                }
            })),
            ContentPart::Binary { data, media_type } => Some(if media_type.starts_with("image/") {
                json!({"image": {"format": bedrock_media_format(media_type), "source": {"bytes": base64_payload(data)}}})
            } else {
                json!({"document": {"format": bedrock_media_format(media_type), "source": {"bytes": base64_payload(data)}}})
            }),
            ContentPart::ResourceRef {
                uri, media_type, ..
            } => Some(if media_type.starts_with("image/") {
                json!({"image": {"format": bedrock_media_format(media_type), "source": {"bytes": uri}}})
            } else {
                json!({"document": {"format": bedrock_media_format(media_type), "source": {"bytes": uri}}})
            }),
            ContentPart::DataUrl {
                data_url,
                media_type,
            } => Some(bedrock_data_url(data_url, media_type)),
        })
        .collect()
}

fn data_url(media_type: &str, data: &[u8]) -> String {
    format!("data:{media_type};base64,{}", base64_payload(data))
}

fn gemini_data_url(data_url: &str, fallback_media_type: &str) -> Value {
    parse_data_url(data_url).map_or_else(
        |_| {
            json!({
                "fileData": {"fileUri": data_url, "mimeType": fallback_media_type}
            })
        },
        |parsed| {
            json!({
                "inlineData": {
                    "data": base64_payload(&parsed.data),
                    "mimeType": parsed.media_type,
                }
            })
        },
    )
}

fn bedrock_data_url(data_url: &str, fallback_media_type: &str) -> Value {
    parse_data_url(data_url).map_or_else(
        |_| {
            if fallback_media_type.starts_with("image/") {
                json!({"image": {"format": bedrock_media_format(fallback_media_type), "source": {"bytes": data_url}}})
            } else {
                json!({"document": {"format": bedrock_media_format(fallback_media_type), "source": {"bytes": data_url}}})
            }
        },
        |parsed| {
            if parsed.media_type.starts_with("image/") {
                json!({"image": {"format": bedrock_media_format(&parsed.media_type), "source": {"bytes": base64_payload(&parsed.data)}}})
            } else {
                json!({"document": {"format": bedrock_media_format(&parsed.media_type), "source": {"bytes": base64_payload(&parsed.data)}}})
            }
        },
    )
}

fn base64_payload(data: &[u8]) -> String {
    STANDARD.encode(data)
}

fn bedrock_media_format(media_type: &str) -> &str {
    media_type
        .strip_prefix("image/")
        .or_else(|| media_type.strip_prefix("application/"))
        .unwrap_or(media_type)
}