gproxy-protocol 1.0.20

Wire-format types and cross-protocol transforms for Claude, OpenAI, and Gemini LLM APIs.
Documentation
use crate::claude::count_tokens::request::{
    ClaudeCountTokensRequest, PathParameters, QueryParameters, RequestBody, RequestHeaders,
};
use crate::claude::count_tokens::types as ct;
use crate::openai::count_tokens::request::OpenAiCountTokensRequest;
use crate::openai::count_tokens::types as ot;
use crate::transform::openai::count_tokens::claude::utils::{
    ClaudeToolUseIdMapper, mcp_allowed_tools_to_configs, openai_mcp_tool_to_server,
    openai_message_content_to_claude, openai_reasoning_to_claude, openai_role_to_claude,
    openai_tool_choice_to_claude, parallel_disable, push_message_block, tool_from_function,
};
use crate::transform::openai::count_tokens::utils::{
    openai_function_call_output_content_to_text, openai_input_to_items,
    openai_reasoning_summary_to_text,
};
use crate::transform::utils::TransformError;

impl TryFrom<OpenAiCountTokensRequest> for ClaudeCountTokensRequest {
    type Error = TransformError;

    fn try_from(value: OpenAiCountTokensRequest) -> Result<Self, TransformError> {
        let body = value.body;
        let mut messages = Vec::new();
        let mut tool_use_ids = ClaudeToolUseIdMapper::default();

        for item in openai_input_to_items(body.input) {
            match item {
                ot::ResponseInputItem::Message(message) => {
                    messages.push(ct::BetaMessageParam {
                        content: openai_message_content_to_claude(message.content),
                        role: openai_role_to_claude(message.role),
                    });
                }
                ot::ResponseInputItem::OutputMessage(message) => {
                    let text = message
                        .content
                        .into_iter()
                        .map(|part| match part {
                            ot::ResponseOutputContent::Text(text) => text.text,
                            ot::ResponseOutputContent::Refusal(refusal) => refusal.refusal,
                        })
                        .filter(|text| !text.is_empty())
                        .collect::<Vec<_>>()
                        .join("\n");
                    if !text.is_empty() {
                        messages.push(ct::BetaMessageParam {
                            content: ct::BetaMessageContent::Text(text),
                            role: ct::BetaMessageRole::Assistant,
                        });
                    }
                }
                ot::ResponseInputItem::FunctionToolCall(tool_call) => {
                    let tool_use_id = tool_use_ids.tool_use_id(tool_call.call_id);
                    let input = serde_json::from_str::<ct::JsonObject>(&tool_call.arguments)
                        .unwrap_or_default();
                    push_message_block(
                        &mut messages,
                        ct::BetaMessageRole::Assistant,
                        ct::BetaContentBlockParam::ToolUse(ct::BetaToolUseBlockParam {
                            id: tool_use_id,
                            input,
                            name: tool_call.name,
                            type_: ct::BetaToolUseBlockType::ToolUse,
                            cache_control: None,
                            caller: None,
                        }),
                    );
                }
                ot::ResponseInputItem::FunctionCallOutput(tool_result) => {
                    let tool_use_id = tool_use_ids.tool_use_id(tool_result.call_id);
                    let output_text =
                        openai_function_call_output_content_to_text(&tool_result.output);
                    push_message_block(
                        &mut messages,
                        ct::BetaMessageRole::User,
                        ct::BetaContentBlockParam::ToolResult(ct::BetaToolResultBlockParam {
                            tool_use_id,
                            type_: ct::BetaToolResultBlockType::ToolResult,
                            cache_control: None,
                            content: if output_text.is_empty() {
                                None
                            } else {
                                Some(ct::BetaToolResultBlockParamContent::Text(output_text))
                            },
                            is_error: None,
                        }),
                    );
                }
                ot::ResponseInputItem::ReasoningItem(reasoning) => {
                    let thinking = openai_reasoning_summary_to_text(&reasoning.summary);
                    if !thinking.is_empty()
                        && let Some(signature) = reasoning.id.filter(|id| !id.is_empty())
                    {
                        messages.push(ct::BetaMessageParam {
                            content: ct::BetaMessageContent::Blocks(vec![
                                ct::BetaContentBlockParam::Thinking(ct::BetaThinkingBlockParam {
                                    signature,
                                    thinking,
                                    type_: ct::BetaThinkingBlockType::Thinking,
                                }),
                            ]),
                            role: ct::BetaMessageRole::Assistant,
                        });
                    }
                }
                other => {
                    let text = format!("{other:?}");
                    if !text.is_empty() {
                        messages.push(ct::BetaMessageParam {
                            content: ct::BetaMessageContent::Text(text),
                            role: ct::BetaMessageRole::User,
                        });
                    }
                }
            }
        }

        let disable_parallel_tool_use = parallel_disable(body.parallel_tool_calls);
        let tool_choice = openai_tool_choice_to_claude(body.tool_choice, disable_parallel_tool_use);
        let model = ct::Model::Custom(body.model.clone().unwrap_or_default());
        let thinking = openai_reasoning_to_claude(body.reasoning, None, Some(&model));

        let output_effort = body
            .text
            .as_ref()
            .and_then(|text| text.verbosity.as_ref())
            .map(|verbosity| match verbosity {
                ot::ResponseTextVerbosity::Low => ct::BetaOutputEffort::Low,
                ot::ResponseTextVerbosity::Medium => ct::BetaOutputEffort::Medium,
                ot::ResponseTextVerbosity::High => ct::BetaOutputEffort::High,
            });

        let output_format = body
            .text
            .as_ref()
            .and_then(|text| text.format.as_ref())
            .and_then(|format| match format {
                ot::ResponseTextFormatConfig::JsonSchema(schema) => {
                    Some(ct::BetaJsonOutputFormat {
                        schema: schema.schema.clone(),
                        type_: ct::BetaJsonOutputFormatType::JsonSchema,
                    })
                }
                _ => None,
            });

        let output_config = if output_effort.is_some() || output_format.is_some() {
            Some(ct::BetaOutputConfig {
                effort: output_effort,
                format: output_format.clone(),
                task_budget: None,
            })
        } else {
            None
        };

        let context_management = match body.truncation {
            Some(ot::ResponseTruncation::Auto) => Some(ct::BetaContextManagementConfig {
                edits: Some(vec![ct::BetaContextManagementEdit::Compact(
                    ct::BetaCompact20260112Edit {
                        type_: ct::BetaCompactType::Compact20260112,
                        instructions: None,
                        pause_after_compaction: None,
                        trigger: None,
                    },
                )]),
            }),
            Some(ot::ResponseTruncation::Disabled) | None => None,
        };

        let mut converted_tools = Vec::new();
        let mut mcp_servers = Vec::new();
        if let Some(tools) = body.tools {
            for tool in tools {
                match tool {
                    ot::ResponseTool::Function(tool) => {
                        converted_tools.push(tool_from_function(tool))
                    }
                    ot::ResponseTool::Custom(tool) => {
                        converted_tools.push(ct::BetaToolUnion::Custom(ct::BetaTool {
                            input_schema: ct::BetaToolInputSchema {
                                type_: ct::BetaToolInputSchemaType::Object,
                                properties: None,
                                required: None,
                                extra_fields: Default::default(),
                            },
                            name: tool.name,
                            common: ct::BetaToolCommonFields::default(),
                            description: tool.description,
                            eager_input_streaming: None,
                            type_: Some(ct::BetaCustomToolType::Custom),
                        }));
                    }
                    ot::ResponseTool::CodeInterpreter(_)
                    | ot::ResponseTool::LocalShell(_)
                    | ot::ResponseTool::Shell(_)
                    | ot::ResponseTool::ApplyPatch(_) => {
                        converted_tools.push(ct::BetaToolUnion::CodeExecution20250825(
                            ct::BetaCodeExecutionTool20250825 {
                                name: ct::BetaCodeExecutionToolName::CodeExecution,
                                type_: ct::BetaCodeExecutionTool20250825Type::CodeExecution20250825,
                                common: ct::BetaToolCommonFields::default(),
                            },
                        ));
                    }
                    ot::ResponseTool::Computer(tool) => {
                        converted_tools.push(ct::BetaToolUnion::ComputerUse20251124(
                            ct::BetaToolComputerUse20251124 {
                                display_height_px: tool.display_height_or_default(),
                                display_width_px: tool.display_width_or_default(),
                                name: ct::BetaComputerToolName::Computer,
                                type_: ct::BetaToolComputerUse20251124Type::Computer20251124,
                                common: ct::BetaToolCommonFields::default(),
                                display_number: None,
                                enable_zoom: None,
                            },
                        ));
                    }
                    ot::ResponseTool::WebSearch(tool) => {
                        converted_tools.push(ct::BetaToolUnion::WebSearch20250305(
                            ct::BetaWebSearchTool20250305 {
                                name: ct::BetaWebSearchToolName::WebSearch,
                                type_: ct::BetaWebSearchTool20250305Type::WebSearch20250305,
                                common: ct::BetaToolCommonFields::default(),
                                allowed_domains: tool.filters.and_then(|f| f.allowed_domains),
                                blocked_domains: None,
                                max_uses: None,
                                user_location: tool.user_location.map(|location| {
                                    ct::BetaWebSearchUserLocation {
                                        type_: ct::BetaWebSearchUserLocationType::Approximate,
                                        city: location.city,
                                        country: location.country,
                                        region: location.region,
                                        timezone: location.timezone,
                                    }
                                }),
                            },
                        ));
                    }
                    ot::ResponseTool::WebSearchPreview(tool) => {
                        converted_tools.push(ct::BetaToolUnion::WebSearch20250305(
                            ct::BetaWebSearchTool20250305 {
                                name: ct::BetaWebSearchToolName::WebSearch,
                                type_: ct::BetaWebSearchTool20250305Type::WebSearch20250305,
                                common: ct::BetaToolCommonFields::default(),
                                allowed_domains: None,
                                blocked_domains: None,
                                max_uses: None,
                                user_location: tool.user_location.map(|location| {
                                    ct::BetaWebSearchUserLocation {
                                        type_: ct::BetaWebSearchUserLocationType::Approximate,
                                        city: location.city,
                                        country: location.country,
                                        region: location.region,
                                        timezone: location.timezone,
                                    }
                                }),
                            },
                        ));
                    }
                    ot::ResponseTool::FileSearch(_) => {
                        converted_tools.push(ct::BetaToolUnion::ToolSearchBm25_20251119(
                            ct::BetaToolSearchToolBm25_20251119 {
                                name: ct::BetaToolSearchToolBm25Name::ToolSearchToolBm25,
                                type_: ct::BetaToolSearchToolBm25Type::ToolSearchToolBm2520251119,
                                common: ct::BetaToolCommonFields::default(),
                            },
                        ));
                    }
                    ot::ResponseTool::Mcp(tool) => {
                        if let Some(server) = openai_mcp_tool_to_server(&tool) {
                            mcp_servers.push(server);
                        }
                        converted_tools.push(ct::BetaToolUnion::McpToolset(ct::BetaMcpToolset {
                            mcp_server_name: tool.server_label,
                            type_: ct::BetaMcpToolsetType::McpToolset,
                            cache_control: None,
                            configs: mcp_allowed_tools_to_configs(tool.allowed_tools.as_ref()),
                            default_config: None,
                        }));
                    }
                    ot::ResponseTool::Namespace(_) | ot::ResponseTool::ToolSearch(_) => {}
                    ot::ResponseTool::ImageGeneration(_) => {}
                }
            }
        }

        let system = body.instructions.and_then(|text| {
            if text.is_empty() {
                None
            } else {
                Some(ct::BetaSystemPrompt::Text(text))
            }
        });

        Ok(ClaudeCountTokensRequest {
            method: ct::HttpMethod::Post,
            path: PathParameters::default(),
            query: QueryParameters::default(),
            headers: RequestHeaders::default(),
            body: RequestBody {
                messages,
                model,
                context_management,
                mcp_servers: if mcp_servers.is_empty() {
                    None
                } else {
                    Some(mcp_servers)
                },
                cache_control: None,
                output_config,
                speed: None,
                system,
                thinking,
                tool_choice,
                tools: if converted_tools.is_empty() {
                    None
                } else {
                    Some(converted_tools)
                },
            },
        })
    }
}