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::types::BetaServerToolUseName;
use crate::claude::create_message::response::ClaudeCreateMessageResponse;
use crate::claude::create_message::types::{BetaContentBlock, BetaStopReason};
use crate::gemini::count_tokens::types::{GeminiContentRole, GeminiFunctionCall, GeminiPart};
use crate::gemini::generate_content::response::{GeminiGenerateContentResponse, ResponseBody};
use crate::gemini::generate_content::types::{
    GeminiCandidate, GeminiContent, GeminiFinishReason, GeminiUsageMetadata,
};
use crate::gemini::types::GeminiResponseHeaders;
use crate::transform::claude::utils::claude_model_to_string;
use crate::transform::gemini::generate_content::utils::gemini_error_response_from_claude;
use crate::transform::utils::TransformError;

impl TryFrom<ClaudeCreateMessageResponse> for GeminiGenerateContentResponse {
    type Error = TransformError;

    fn try_from(value: ClaudeCreateMessageResponse) -> Result<Self, TransformError> {
        Ok(match value {
            ClaudeCreateMessageResponse::Success {
                stats_code,
                headers,
                body,
            } => {
                let mut parts = Vec::new();
                for block in body.content {
                    match block {
                        BetaContentBlock::Text(block) if !block.text.is_empty() => {
                            parts.push(GeminiPart {
                                text: Some(block.text),
                                ..GeminiPart::default()
                            });
                        }
                        BetaContentBlock::Thinking(block) if !block.thinking.is_empty() => {
                            parts.push(GeminiPart {
                                thought: Some(true),
                                thought_signature: Some(block.signature),
                                text: Some(block.thinking),
                                ..GeminiPart::default()
                            });
                        }
                        BetaContentBlock::ToolUse(block) => {
                            parts.push(GeminiPart {
                                function_call: Some(GeminiFunctionCall {
                                    id: Some(block.id),
                                    name: block.name,
                                    args: Some(block.input),
                                }),
                                ..GeminiPart::default()
                            });
                        }
                        BetaContentBlock::ServerToolUse(block) => {
                            let name = match block.name {
                                BetaServerToolUseName::WebSearch => "web_search",
                                BetaServerToolUseName::WebFetch => "web_fetch",
                                BetaServerToolUseName::CodeExecution => "code_execution",
                                BetaServerToolUseName::BashCodeExecution => "bash_code_execution",
                                BetaServerToolUseName::TextEditorCodeExecution => {
                                    "text_editor_code_execution"
                                }
                                BetaServerToolUseName::ToolSearchToolRegex => "tool_search_regex",
                                BetaServerToolUseName::ToolSearchToolBm25 => "tool_search_bm25",
                            }
                            .to_string();
                            parts.push(GeminiPart {
                                function_call: Some(GeminiFunctionCall {
                                    id: Some(block.id),
                                    name,
                                    args: Some(block.input),
                                }),
                                ..GeminiPart::default()
                            });
                        }
                        BetaContentBlock::McpToolUse(block) => {
                            parts.push(GeminiPart {
                                function_call: Some(GeminiFunctionCall {
                                    id: Some(block.id),
                                    name: format!("mcp:{}:{}", block.server_name, block.name),
                                    args: Some(block.input),
                                }),
                                ..GeminiPart::default()
                            });
                        }
                        _ => {}
                    }
                }

                if parts.is_empty() {
                    parts.push(GeminiPart {
                        text: Some(String::new()),
                        ..GeminiPart::default()
                    });
                }

                let finish_reason = match body.stop_reason {
                    Some(BetaStopReason::MaxTokens)
                    | Some(BetaStopReason::ModelContextWindowExceeded) => {
                        Some(GeminiFinishReason::MaxTokens)
                    }
                    Some(BetaStopReason::ToolUse) => Some(GeminiFinishReason::UnexpectedToolCall),
                    Some(BetaStopReason::Refusal) => Some(GeminiFinishReason::Safety),
                    Some(BetaStopReason::Compaction) => Some(GeminiFinishReason::Other),
                    Some(BetaStopReason::PauseTurn) => Some(GeminiFinishReason::Other),
                    Some(BetaStopReason::EndTurn) | Some(BetaStopReason::StopSequence) | None => {
                        Some(GeminiFinishReason::Stop)
                    }
                };

                let usage_metadata = GeminiUsageMetadata {
                    prompt_token_count: Some(
                        body.usage
                            .input_tokens
                            .saturating_add(body.usage.cache_creation_input_tokens),
                    ),
                    cached_content_token_count: Some(body.usage.cache_read_input_tokens),
                    candidates_token_count: Some(body.usage.output_tokens),
                    total_token_count: Some(
                        body.usage
                            .input_tokens
                            .saturating_add(body.usage.cache_creation_input_tokens)
                            .saturating_add(body.usage.cache_read_input_tokens)
                            .saturating_add(body.usage.output_tokens),
                    ),
                    ..GeminiUsageMetadata::default()
                };

                GeminiGenerateContentResponse::Success {
                    stats_code,
                    headers: GeminiResponseHeaders {
                        extra: headers.extra,
                    },
                    body: ResponseBody {
                        candidates: Some(vec![GeminiCandidate {
                            content: Some(GeminiContent {
                                parts,
                                role: Some(GeminiContentRole::Model),
                            }),
                            finish_reason,
                            token_count: Some(body.usage.output_tokens),
                            index: Some(0),
                            ..GeminiCandidate::default()
                        }]),
                        prompt_feedback: None,
                        usage_metadata: Some(usage_metadata),
                        model_version: Some(claude_model_to_string(&body.model)),
                        response_id: Some(body.id),
                        model_status: None,
                    },
                }
            }
            ClaudeCreateMessageResponse::Error {
                stats_code,
                headers,
                body,
            } => GeminiGenerateContentResponse::Error {
                stats_code,
                headers: GeminiResponseHeaders {
                    extra: headers.extra,
                },
                body: gemini_error_response_from_claude(stats_code, body),
            },
        })
    }
}