agentkit-provider-anthropic 0.9.1

Anthropic Messages API provider adapter for agentkit.
Documentation
use agentkit_core::{DataRef, FilePart, MediaPart, Modality};
use base64::Engine;
use serde_json::{Value, json};

use crate::request::BuildError;

pub(crate) fn media_to_content(media: &MediaPart) -> Result<Value, BuildError> {
    match media.modality {
        Modality::Image => Ok(json!({
            "type": "image",
            "source": image_source(&media.data, &media.mime_type)?,
        })),
        Modality::Audio | Modality::Video | Modality::Binary => {
            Err(BuildError::UnsupportedModality(media.modality))
        }
    }
}

pub(crate) fn file_to_content(file: &FilePart) -> Result<Value, BuildError> {
    match file.mime_type.as_deref() {
        Some(mime) if mime.starts_with("image/") => Ok(json!({
            "type": "image",
            "source": image_source(&file.data, mime)?,
        })),
        Some("application/pdf") | Some("text/plain") => {
            let mut block = json!({
                "type": "document",
                "source": document_source(&file.data, file.mime_type.as_deref())?,
            });
            if let Some(name) = &file.name
                && let Some(map) = block.as_object_mut()
            {
                map.insert("title".into(), Value::String(name.clone()));
            }
            Ok(block)
        }
        _ => Ok(json!({
            "type": "text",
            "text": format!(
                "Attached file{}{}",
                file.name
                    .as_ref()
                    .map(|name| format!(": {name}"))
                    .unwrap_or_default(),
                file.mime_type
                    .as_ref()
                    .map(|mime| format!(" ({mime})"))
                    .unwrap_or_default(),
            ),
        })),
    }
}

fn image_source(data: &DataRef, mime_type: &str) -> Result<Value, BuildError> {
    match data {
        DataRef::Uri(uri) => Ok(json!({ "type": "url", "url": uri })),
        DataRef::InlineBytes(bytes) => Ok(json!({
            "type": "base64",
            "media_type": mime_type,
            "data": base64::engine::general_purpose::STANDARD.encode(bytes),
        })),
        DataRef::InlineText(text) => {
            if let Some((header, encoded)) = text.split_once(";base64,") {
                let media_type = header.strip_prefix("data:").unwrap_or(mime_type);
                Ok(json!({
                    "type": "base64",
                    "media_type": media_type,
                    "data": encoded,
                }))
            } else if text.starts_with("http://") || text.starts_with("https://") {
                Ok(json!({ "type": "url", "url": text }))
            } else {
                Err(BuildError::UnsupportedDataRef(
                    "image inputs must be a URL, data URL, or inline bytes".into(),
                ))
            }
        }
        DataRef::Handle(handle) => Err(BuildError::UnsupportedDataRef(format!(
            "artifact handle {} cannot be sent directly to Anthropic",
            handle.0
        ))),
    }
}

fn document_source(data: &DataRef, mime_type: Option<&str>) -> Result<Value, BuildError> {
    let mime = mime_type.unwrap_or("application/pdf");
    match data {
        DataRef::Uri(uri) => Ok(json!({ "type": "url", "url": uri })),
        DataRef::InlineBytes(bytes) => Ok(json!({
            "type": "base64",
            "media_type": mime,
            "data": base64::engine::general_purpose::STANDARD.encode(bytes),
        })),
        DataRef::InlineText(text) => {
            if mime == "text/plain" {
                Ok(json!({
                    "type": "text",
                    "media_type": "text/plain",
                    "data": text,
                }))
            } else if let Some((_, encoded)) = text.split_once(";base64,") {
                Ok(json!({
                    "type": "base64",
                    "media_type": mime,
                    "data": encoded,
                }))
            } else if text.starts_with("http://") || text.starts_with("https://") {
                Ok(json!({ "type": "url", "url": text }))
            } else {
                Err(BuildError::UnsupportedDataRef(
                    "document inputs must be a URL, data URL, or inline bytes".into(),
                ))
            }
        }
        DataRef::Handle(handle) => Err(BuildError::UnsupportedDataRef(format!(
            "artifact handle {} cannot be sent directly to Anthropic",
            handle.0
        ))),
    }
}