gproxy-protocol 4.0.0

GPROXY v4 connection modelling, operation declarations, vendor wire types and pairwise conversion
Documentation
use crate::{
    transform::{Report, TransformError},
    wire::{claude::content as c, gemini as g},
};
use serde_json::Value;

pub(super) fn to_gemini(
    block: c::ToolResultBlock,
    id: String,
    name: String,
    media: &super::media::MediaFacts,
    report: &mut Report,
) -> Result<g::FunctionResponse, TransformError> {
    let mut parts = Vec::new();
    let payload = match block.content {
        None => None,
        Some(c::ToolResultContent::Text(text)) => Some(Value::String(text)),
        Some(c::ToolResultContent::Blocks(blocks)) => {
            let mut texts = Vec::new();
            for block in blocks {
                match block {
                    c::ToolResultContentBlock::BrowserState(_) => {
                        report.omitted(
                            "tool_result.browser_state",
                            "target has no browser state block",
                        );
                        continue;
                    }
                    c::ToolResultContentBlock::Text(v) => {
                        if v.citations.is_some() || v.cache_control.is_some() {
                            report.omitted(
                                "tool_result.text.metadata",
                                "Gemini has no citations/cache field",
                            );
                        }
                        texts.push(v.text);
                    }
                    c::ToolResultContentBlock::Image(v) => {
                        let p = super::media::image(v, media, report)?;
                        let blob = p.inline_data.ok_or_else(|| {
                            TransformError::missing_metadata("tool result image inline bytes")
                        })?;
                        parts.push(g::FunctionResponsePart::builder().inline_data(blob).build());
                    }
                    c::ToolResultContentBlock::Document(v) => {
                        for p in super::media::document(v, media, report)? {
                            if let Some(t) = p.text {
                                texts.push(t);
                            } else if let Some(b) = p.inline_data {
                                parts.push(
                                    g::FunctionResponsePart::builder().inline_data(b).build(),
                                );
                            } else {
                                return Err(TransformError::missing_metadata(
                                    "tool result document inline bytes",
                                ));
                            }
                        }
                    }
                    c::ToolResultContentBlock::SearchResult(_)
                    | c::ToolResultContentBlock::ToolReference(_) => {
                        continue;
                    }
                }
            }
            Some(Value::Array(texts.into_iter().map(Value::String).collect()))
        }
    };
    let mut response = crate::Rest::new();
    if let Some(payload) = payload {
        response.insert(
            if block.is_error == Some(true) {
                "error"
            } else {
                "output"
            }
            .into(),
            payload,
        );
    } else if block.is_error == Some(true) {
        response.insert("error".into(), Value::Null);
    }
    if block.cache_control.is_some() {
        report.omitted("tool_result.cache_control", "Gemini has no cache field");
    }
    let mut out = g::FunctionResponse::builder(name, response).id(id).build();
    if !parts.is_empty() {
        out.parts = Some(parts);
    }
    Ok(out)
}

pub(super) fn to_claude(
    mut source: g::FunctionResponse,
    id: String,
) -> Result<c::ToolResultBlock, TransformError> {
    let is_error = source.response.contains_key("error");
    let text = if source.response.len() == 1 && (is_error || source.response.contains_key("output"))
    {
        let v = source
            .response
            .remove(if is_error { "error" } else { "output" })
            .expect("key checked");
        match v {
            Value::String(t) => t,
            v => v.to_string(),
        }
    } else {
        Value::Object(source.response).to_string()
    };
    let mut content = vec![c::ToolResultContentBlock::Text(
        c::TextBlock::builder(c::TextBlockType::Tag, text).build(),
    )];
    for p in source.parts.unwrap_or_default() {
        let block = super::media::inline(p.inline_data.ok_or_else(|| {
            TransformError::shape("function_response.parts", "inline_data required")
        })?)?;
        content.push(match block {
            c::ContentBlock::Image(v) => c::ToolResultContentBlock::Image(v),
            c::ContentBlock::Document(v) => c::ToolResultContentBlock::Document(v),
            _ => unreachable!("inline maps only media"),
        });
    }
    Ok(c::ToolResultBlock::builder(c::ToolResultBlockType::Tag, id)
        .content(c::ToolResultContent::Blocks(content))
        .is_error(is_error)
        .build())
}