va-ai-api-bridge 0.1.7

Protocol translation primitives for AI API request and response shapes.
Documentation
use serde_json::{Map, Value};

use crate::Extensions;

pub(super) struct AnthropicImageSource {
    pub(super) media_type: Option<String>,
    pub(super) url: Option<String>,
    pub(super) data: Option<String>,
}

pub(super) struct AnthropicFileSource {
    pub(super) media_type: Option<String>,
    pub(super) url: Option<String>,
    pub(super) data: Option<String>,
    pub(super) file_id: Option<String>,
}

pub(super) fn anthropic_source_to_image(source: Option<&Value>) -> AnthropicImageSource {
    AnthropicImageSource {
        media_type: source
            .and_then(|source| source.get("media_type"))
            .and_then(Value::as_str)
            .map(ToString::to_string),
        url: source
            .and_then(|source| source.get("url").or_else(|| source.get("image_url")))
            .and_then(Value::as_str)
            .map(ToString::to_string),
        data: source
            .and_then(|source| source.get("data"))
            .and_then(Value::as_str)
            .map(ToString::to_string),
    }
}

pub(super) fn anthropic_source_to_file(source: Option<&Value>) -> AnthropicFileSource {
    AnthropicFileSource {
        media_type: source
            .and_then(|source| source.get("media_type"))
            .and_then(Value::as_str)
            .map(ToString::to_string),
        url: source
            .and_then(|source| source.get("url").or_else(|| source.get("file_url")))
            .and_then(Value::as_str)
            .map(ToString::to_string),
        data: source
            .and_then(|source| source.get("data"))
            .and_then(Value::as_str)
            .map(ToString::to_string),
        file_id: source
            .and_then(|source| source.get("file_id"))
            .and_then(Value::as_str)
            .map(ToString::to_string),
    }
}

pub(super) fn anthropic_image_source(
    media_type: Option<&str>,
    url: Option<&str>,
    data: Option<&str>,
    extensions: &Extensions,
) -> Value {
    let mut source = Map::new();
    if let Some(url) = url {
        source.insert("type".to_string(), Value::String("url".to_string()));
        source.insert("url".to_string(), Value::String(url.to_string()));
        return Value::Object(source);
    }

    source.insert("type".to_string(), Value::String("base64".to_string()));
    if let Some(media_type) = media_type {
        source.insert(
            "media_type".to_string(),
            Value::String(media_type.to_string()),
        );
    }
    if let Some(data) = data {
        source.insert("data".to_string(), Value::String(data.to_string()));
    }
    for (key, value) in extensions {
        source.insert(key.clone(), value.clone());
    }
    Value::Object(source)
}

pub(super) fn anthropic_file_source(
    media_type: Option<&str>,
    url: Option<&str>,
    data: Option<&str>,
    extensions: &Extensions,
) -> Value {
    let mut source = Map::new();
    if let Some(Value::String(file_id)) = extensions.get("file_id") {
        source.insert("type".to_string(), Value::String("file".to_string()));
        source.insert("file_id".to_string(), Value::String(file_id.clone()));
        return Value::Object(source);
    }
    if let Some(url) = url {
        source.insert("type".to_string(), Value::String("url".to_string()));
        source.insert("url".to_string(), Value::String(url.to_string()));
        return Value::Object(source);
    }

    let (data_url_media_type, data) = data.map(split_data_url).unwrap_or((None, ""));
    source.insert("type".to_string(), Value::String("base64".to_string()));
    if let Some(media_type) = media_type.or(data_url_media_type) {
        source.insert(
            "media_type".to_string(),
            Value::String(media_type.to_string()),
        );
    }
    source.insert("data".to_string(), Value::String(data.to_string()));
    Value::Object(source)
}

fn split_data_url(data: &str) -> (Option<&str>, &str) {
    let Some(rest) = data.strip_prefix("data:") else {
        return (None, data);
    };
    let Some((metadata, payload)) = rest.split_once(',') else {
        return (None, data);
    };
    let media_type = metadata
        .split(';')
        .next()
        .filter(|media_type| !media_type.is_empty());
    (media_type, payload)
}