gproxy-transform 2.4.3

Pairwise request/response/stream transforms between the OpenAI, Anthropic Claude, and Google Gemini APIs
Documentation
use crate::protocol::{claude, openai};

use super::DEFAULT_REASONING_ID;
use super::tools::{
    function_call_output_item, mcp_tool_result_content_to_text, server_tool_result_output,
    server_tool_use_item, tool_result_content_to_openai,
};
use super::util::{
    document_source_to_input_part, image_source_to_input_part, join_text, json_object_to_string,
};

pub(super) fn system_to_openai_item(text: String) -> openai::ResponseItem {
    openai::ResponseItem::Message(openai::ResponseMessageItem::EasyInput(
        crate::protocol::wire!(openai::ResponseEasyInputMessageItem {
            type_: Some(openai::ResponseMessageItemType::Message),
            role: openai::ResponseEasyInputMessageRole::System,
            content: openai::ResponseEasyInputContent::Text(text),
            phase: None,
            extra: Default::default(),
        }),
    ))
}

pub(super) fn claude_messages_to_openai_items(
    messages: Vec<claude::MessageParam>,
) -> Vec<openai::ResponseItem> {
    messages
        .into_iter()
        .flat_map(claude_message_to_openai_items)
        .collect()
}

fn claude_message_to_openai_items(message: claude::MessageParam) -> Vec<openai::ResponseItem> {
    let role = claude_role_to_openai(message.role);
    let mut items = Vec::new();
    let mut message_parts = Vec::new();

    match message.content {
        claude::MessageContent::String(text) => {
            if !text.is_empty() {
                message_parts.push(openai::ResponseInputContentPart::InputText {
                    text,
                    prompt_cache_breakpoint: None,
                    extra: Default::default(),
                });
            }
        }
        claude::MessageContent::Array(blocks) => {
            for block in blocks {
                match claude_request_block_to_openai(block) {
                    ClaudeRequestBlockItem::MessagePart(part) => message_parts.push(part),
                    ClaudeRequestBlockItem::Item(item) => items.push(item),
                    ClaudeRequestBlockItem::None => {}
                }
            }
        }
        _ => {
            unreachable!("new non-exhaustive protocol variant requires a lockstep transform update")
        }
    }

    if !message_parts.is_empty() {
        items.push(openai::ResponseItem::Message(
            openai::ResponseMessageItem::EasyInput(crate::protocol::wire!(
                openai::ResponseEasyInputMessageItem {
                    type_: Some(openai::ResponseMessageItemType::Message),
                    role,
                    content: openai::ResponseEasyInputContent::Parts(message_parts),
                    phase: None,
                    extra: Default::default(),
                }
            )),
        ));
    }

    items
}

pub(super) enum ClaudeRequestBlockItem {
    MessagePart(openai::ResponseInputContentPart),
    Item(openai::ResponseItem),
    None,
}

fn claude_role_to_openai(role: claude::MessageRole) -> openai::ResponseEasyInputMessageRole {
    match role {
        claude::MessageRole::Known(claude::MessageRoleKnown::Assistant) => {
            openai::ResponseEasyInputMessageRole::Assistant
        }
        claude::MessageRole::Known(claude::MessageRoleKnown::System) => {
            openai::ResponseEasyInputMessageRole::System
        }
        claude::MessageRole::Known(claude::MessageRoleKnown::User)
        | claude::MessageRole::Unknown(_) => openai::ResponseEasyInputMessageRole::User,
        _ => {
            unreachable!("new non-exhaustive protocol variant requires a lockstep transform update")
        }
    }
}

fn claude_request_block_to_openai(block: claude::ContentBlockParam) -> ClaudeRequestBlockItem {
    match block {
        claude::ContentBlockParam::Text(block) => {
            ClaudeRequestBlockItem::MessagePart(openai::ResponseInputContentPart::InputText {
                text: block.text,
                prompt_cache_breakpoint: None,
                extra: Default::default(),
            })
        }
        claude::ContentBlockParam::Image(block) => image_source_to_input_part(block.source)
            .map(ClaudeRequestBlockItem::MessagePart)
            .unwrap_or(ClaudeRequestBlockItem::None),
        claude::ContentBlockParam::Document(block) => {
            document_source_to_input_part(block.source, block.title)
                .map(ClaudeRequestBlockItem::MessagePart)
                .unwrap_or(ClaudeRequestBlockItem::None)
        }
        claude::ContentBlockParam::ToolUse(block) => {
            ClaudeRequestBlockItem::Item(openai::ResponseItem::Typed(crate::protocol::wire!(
                openai::TypedResponseItem::FunctionCall {
                    arguments: json_object_to_string(&block.input),
                    call_id: block.id.clone(),
                    name: block.name,
                    id: Some(block.id),
                    caller: None,
                    namespace: None,
                    status: Some(openai::ResponseItemLifecycleStatus::Completed),
                    extra: Default::default(),
                }
            )))
        }
        claude::ContentBlockParam::ToolResult(block) => function_call_output_item(
            block.tool_use_id,
            tool_result_content_to_openai(block.content),
        ),
        claude::ContentBlockParam::Thinking(block) => ClaudeRequestBlockItem::Item(
            openai::ResponseItem::Typed(openai::TypedResponseItem::Reasoning {
                id: Some(block.signature),
                summary: Vec::new(),
                content: Some(vec![crate::protocol::wire!(
                    openai::ResponseReasoningTextPart {
                        text: block.thinking,
                        type_: openai::ResponseReasoningTextType::ReasoningText,
                        extra: Default::default(),
                    }
                )]),
                encrypted_content: None,
                status: Some(openai::ResponseItemLifecycleStatus::Completed),
                extra: Default::default(),
            }),
        ),
        claude::ContentBlockParam::RedactedThinking(block) => ClaudeRequestBlockItem::Item(
            openai::ResponseItem::Typed(openai::TypedResponseItem::Reasoning {
                id: Some(DEFAULT_REASONING_ID.to_owned()),
                summary: Vec::new(),
                content: None,
                encrypted_content: Some(block.data),
                status: Some(openai::ResponseItemLifecycleStatus::Completed),
                extra: Default::default(),
            }),
        ),
        claude::ContentBlockParam::Compaction(block) => {
            let Some(encrypted_content) = block.encrypted_content else {
                return block
                    .content
                    .map(|text| {
                        ClaudeRequestBlockItem::MessagePart(
                            openai::ResponseInputContentPart::InputText {
                                text,
                                prompt_cache_breakpoint: None,
                                extra: Default::default(),
                            },
                        )
                    })
                    .unwrap_or(ClaudeRequestBlockItem::None);
            };
            ClaudeRequestBlockItem::Item(openai::ResponseItem::Typed(
                openai::TypedResponseItem::Compaction {
                    encrypted_content,
                    id: None,
                    created_by: None,
                    extra: Default::default(),
                },
            ))
        }
        claude::ContentBlockParam::ServerToolUse(block) => {
            server_tool_use_item(block.id, block.input, block.name)
        }
        claude::ContentBlockParam::WebSearchToolResult(block) => {
            function_call_output_item(block.tool_use_id, server_tool_result_output(&block.content))
        }
        claude::ContentBlockParam::WebFetchToolResult(block) => {
            function_call_output_item(block.tool_use_id, server_tool_result_output(&block.content))
        }
        claude::ContentBlockParam::AdvisorToolResult(block) => {
            function_call_output_item(block.tool_use_id, server_tool_result_output(&block.content))
        }
        claude::ContentBlockParam::CodeExecutionToolResult(block) => {
            function_call_output_item(block.tool_use_id, server_tool_result_output(&block.content))
        }
        claude::ContentBlockParam::BashCodeExecutionToolResult(block) => {
            function_call_output_item(block.tool_use_id, server_tool_result_output(&block.content))
        }
        claude::ContentBlockParam::TextEditorCodeExecutionToolResult(block) => {
            function_call_output_item(block.tool_use_id, server_tool_result_output(&block.content))
        }
        claude::ContentBlockParam::ToolSearchToolResult(block) => {
            function_call_output_item(block.tool_use_id, server_tool_result_output(&block.content))
        }
        claude::ContentBlockParam::McpToolUse(block) => ClaudeRequestBlockItem::Item(
            openai::ResponseItem::Typed(openai::TypedResponseItem::McpCall {
                id: block.id,
                arguments: json_object_to_string(&block.input),
                name: block.name,
                server_label: block.server_name,
                approval_request_id: None,
                error: None,
                output: None,
                status: Some(openai::ResponseMcpCallStatus::Completed),
                extra: Default::default(),
            }),
        ),
        claude::ContentBlockParam::McpToolResult(block) => function_call_output_item(
            block.tool_use_id,
            openai::ResponseOutput::Text(mcp_tool_result_content_to_text(block.content)),
        ),
        claude::ContentBlockParam::MidConversationSystem(block) => {
            let text = join_text(block.content.into_iter().filter_map(|block| match block {
                claude::MidConversationSystemContentBlock::Text(block) => Some(block.text),
                _ => None,
            }));
            if text.is_empty() {
                ClaudeRequestBlockItem::None
            } else {
                ClaudeRequestBlockItem::Item(system_to_openai_item(text))
            }
        }
        _ => ClaudeRequestBlockItem::None,
    }
}