va-ai-api-bridge 0.1.7

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

use crate::schema::anthropic;
use crate::ContentBlock;

use super::media::{anthropic_file_source, anthropic_image_source};

pub(crate) fn block_to_anthropic_block(block: &ContentBlock) -> anthropic::AnthropicContentBlock {
    let mut content_block = anthropic::AnthropicContentBlock {
        kind: String::new(),
        text: None,
        source: None,
        id: None,
        name: None,
        input: None,
        tool_use_id: None,
        content: None,
        thinking: None,
        signature: None,
        extra: Default::default(),
    };

    match block {
        ContentBlock::Text { text } => {
            content_block.kind = "text".to_string();
            content_block.text = Some(text.clone());
        }
        ContentBlock::Image {
            media_type,
            url,
            data,
            extensions,
        } => {
            content_block.kind = "image".to_string();
            content_block.source = Some(anthropic_image_source(
                media_type.as_deref(),
                url.as_deref(),
                data.as_deref(),
                extensions,
            ));
        }
        ContentBlock::ToolCall {
            id,
            name,
            arguments,
            ..
        } => {
            content_block.kind = "tool_use".to_string();
            content_block.id = Some(id.clone());
            content_block.name = Some(name.clone());
            content_block.input = Some(arguments.clone());
        }
        ContentBlock::ToolResult {
            tool_call_id,
            content,
            is_error,
            ..
        } => {
            content_block.kind = "tool_result".to_string();
            content_block.tool_use_id = Some(tool_call_id.clone());
            content_block.content = Some(blocks_to_value(content));
            if *is_error {
                content_block
                    .extra
                    .insert("is_error".to_string(), Value::Bool(true));
            }
        }
        ContentBlock::Reasoning {
            text, encrypted, ..
        } => {
            content_block.kind = "thinking".to_string();
            content_block.thinking = text.clone();
            content_block.signature = encrypted.clone();
        }
        ContentBlock::File {
            media_type,
            filename,
            url,
            data,
            extensions,
        } => {
            content_block.kind = "document".to_string();
            content_block.source = Some(anthropic_file_source(
                media_type.as_deref(),
                url.as_deref(),
                data.as_deref(),
                extensions,
            ));
            if let Some(filename) = filename {
                content_block
                    .extra
                    .insert("title".to_string(), Value::String(filename.clone()));
            }
        }
        ContentBlock::Unknown { raw } => {
            if let Ok(raw_block) =
                serde_json::from_value::<anthropic::AnthropicContentBlock>(raw.clone())
            {
                return raw_block;
            }
            content_block.kind = "unknown".to_string();
            content_block.extra.insert(
                "raw".to_string(),
                serde_json::to_value(block).unwrap_or(Value::Null),
            );
        }
    }

    content_block
}

pub(crate) fn blocks_to_anthropic_content(blocks: &[ContentBlock]) -> anthropic::AnthropicContent {
    match blocks {
        [ContentBlock::Text { text }] => anthropic::AnthropicContent::Text(text.clone()),
        blocks => anthropic::AnthropicContent::Blocks(
            blocks.iter().map(block_to_anthropic_block).collect(),
        ),
    }
}

pub(crate) fn blocks_to_anthropic_system(
    blocks: &[ContentBlock],
) -> Option<anthropic::AnthropicSystem> {
    match blocks {
        [] => None,
        [ContentBlock::Text { text }] => Some(anthropic::AnthropicSystem::Text(text.clone())),
        blocks => Some(anthropic::AnthropicSystem::Blocks(
            blocks.iter().map(block_to_anthropic_block).collect(),
        )),
    }
}

fn blocks_to_value(blocks: &[ContentBlock]) -> Value {
    match blocks {
        [ContentBlock::Text { text }] => Value::String(text.clone()),
        blocks => Value::Array(
            blocks
                .iter()
                .map(|block| {
                    serde_json::to_value(block_to_anthropic_block(block)).unwrap_or(Value::Null)
                })
                .collect(),
        ),
    }
}