gproxy-protocol 4.1.10

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

pub(crate) fn to_gemini(
    input: Option<Vec<ct::ToolUnion>>,
    choice: Option<c::ToolChoice>,
    report: &mut Report,
) -> Result<(Option<Vec<g::Tool>>, Option<g::ToolConfig>), TransformError> {
    let mut strict = false;
    let tools = input
        .map(|tools| {
            let mut functions = Vec::new();
            let mut search = false;
            for tool in tools {
                if matches!(tool,
                    ct::ToolUnion::WebSearch20250305(_)
                    | ct::ToolUnion::WebSearch20260209(_)
                    | ct::ToolUnion::WebSearch20260318(_)
                ) {
                    search = true;
                    report.changed("tools.web_search", "executed by Gemini Google Search; Claude search controls have no direct equivalent");
                    continue;
                }
                let ct::ToolUnion::Custom(tool) = tool else {
                    continue;
                };
                if tool.allowed_callers.as_ref().is_some_and(|v| {
                    v.iter()
                        .any(|caller| !matches!(caller, ct::AllowedCaller::Direct))
                }) || tool.defer_loading == Some(true)
                {
                    report.omitted(
                        "tools.allowed_callers/defer_loading",
                        "controls have no target representation",
                    );
                }
                strict |= tool.strict == Some(true);
                let schema = super::schema::to_json(&tool.input_schema)?;
                let mut function = g::FunctionDeclaration::builder(
                    tool.name,
                    tool.description.unwrap_or_default(),
                )
                .build();
                function.parameters_json_schema = Some(serde_json::Value::Object(schema));
                functions.push(function);
                if tool.cache_control.is_some()
                    || tool.eager_input_streaming.is_some()
                    || tool.input_examples.is_some()
                {
                    report.omitted(
                        "tools.advisory",
                        "Gemini has no tool cache/example/eager-streaming fields",
                    );
                }
            }
            let mut tools = Vec::new();
            if !functions.is_empty() {
                tools.push(g::Tool::builder().function_declarations(functions).build());
            }
            if search {
                tools.push(g::Tool::builder().google_search(g::GoogleSearch::builder().build()).build());
            }
            Ok::<_, TransformError>(tools)
        })
        .map(crate::transform::optional)
        .transpose()?
        .flatten();
    let calling = match choice {
        Some(c::ToolChoice::Auto(_v)) => Some(
            g::FunctionCallingConfig::builder()
                .mode(if strict {
                    g::FunctionCallingMode::Validated
                } else {
                    g::FunctionCallingMode::Auto
                })
                .build(),
        ),
        Some(c::ToolChoice::Any(_v)) => Some(
            g::FunctionCallingConfig::builder()
                .mode(g::FunctionCallingMode::Any)
                .build(),
        ),
        Some(c::ToolChoice::Tool(v)) => Some(
            g::FunctionCallingConfig::builder()
                .mode(g::FunctionCallingMode::Any)
                .allowed_function_names(vec![v.name])
                .build(),
        ),
        Some(c::ToolChoice::None(_)) => Some(
            g::FunctionCallingConfig::builder()
                .mode(g::FunctionCallingMode::None)
                .build(),
        ),
        None if strict => Some(
            g::FunctionCallingConfig::builder()
                .mode(g::FunctionCallingMode::Validated)
                .build(),
        ),
        None => None,
    };
    Ok((
        tools,
        calling.map(|config| {
            g::ToolConfig::builder()
                .function_calling_config(config)
                .build()
        }),
    ))
}

pub(crate) fn to_claude(
    input: Option<Vec<g::Tool>>,
    config: Option<g::ToolConfig>,
    report: &mut Report,
) -> Result<(Option<Vec<ct::ToolUnion>>, Option<c::ToolChoice>), TransformError> {
    let calling = if let Some(config) = config {
        if config.retrieval_config.is_some()
            || config.include_server_side_tool_invocations == Some(true)
        {
            report.omitted(
                "tool_config.retrieval",
                "control has no target representation",
            );
        }
        config.function_calling_config
    } else {
        None
    };
    let mode = calling.as_ref().and_then(|v| v.mode.clone());
    let strict = matches!(
        mode,
        Some(g::FunctionCallingMode::Any | g::FunctionCallingMode::Validated)
    );
    let allowed = calling
        .as_ref()
        .and_then(|v| v.allowed_function_names.as_ref());

    let tools = input
        .map(|tools| {
            let mut functions = Vec::new();
            for tool in tools {
                if tool.google_search_retrieval.is_some() || tool.google_search.is_some() {
                    functions.push(ct::ToolUnion::WebSearch20250305(ct::WebSearchTool20250305::builder(
                        ct::WebSearchTool20250305Name::Name, ct::WebSearchTool20250305Type::Tag,
                    ).build()));
                    report.changed("tools.google_search", "executed by Claude web search; provider-specific search controls are omitted");
                }
                if tool.code_execution.is_some()
                    || tool.computer_use.is_some()
                    || tool.url_context.is_some()
                    || tool.file_search.is_some()
                    || tool.mcp_servers.is_some()
                    || tool.google_maps.is_some()
                {
                    report.omitted("tools.hosted", "hosted tools have no target representation");
                }
                for function in tool.function_declarations.unwrap_or_default() {
                    if matches!(function.behavior, Some(g::Behavior::NonBlocking))
                        || function.response.is_some()
                        || function.response_json_schema.is_some()
                    {
                        report.omitted(
                            "tools.function_declarations.behavior/response",
                            "controls have no target representation",
                        );
                    }

                    if allowed.is_some_and(|v| !v.contains(&function.name)) {
                        continue;
                    }
                    let schema = match (function.parameters, function.parameters_json_schema) {
                        (Some(typed), _raw) => {
                            let converted = crate::transform::generate::gemini_schema::to_json(
                                &typed,
                            )?;
                            report.diagnostics.extend(converted.report.diagnostics);

                            converted.value
                        }
                        (None, Some(serde_json::Value::Object(raw))) => raw,
                        (None, Some(_)) => {
                            continue;
                        }
                        (None, None) => serde_json::Map::new(),
                    };
                    let schema = super::schema::from_json(&schema)?;
                    functions.push(ct::ToolUnion::Custom(
                        ct::Tool::builder(schema, function.name)
                            .description(function.description)
                            .strict(strict)
                            .build(),
                    ));
                }
            }
            Ok::<_, TransformError>(functions)
        })
        .map(crate::transform::optional)
        .transpose()?
        .flatten();

    let choice = match mode {
        Some(g::FunctionCallingMode::Unspecified) | None => None,
        Some(g::FunctionCallingMode::Auto | g::FunctionCallingMode::Validated) => {
            Some(c::ToolChoice::Auto(c::ToolChoiceAuto::builder().build()))
        }
        Some(g::FunctionCallingMode::None) => {
            Some(c::ToolChoice::None(c::ToolChoiceNone::builder().build()))
        }
        Some(g::FunctionCallingMode::Any) => {
            Some(c::ToolChoice::Any(c::ToolChoiceAny::builder().build()))
        }
    };
    Ok((tools, choice))
}