gproxy-transform 2.8.3

Pairwise request/response/stream transforms between the OpenAI, Anthropic Claude, and Google Gemini APIs
Documentation
use std::collections::BTreeMap;

use crate::protocol::{claude, openai};

use super::tools::{
    ApproximateToolKind, apply_patch_result_item, approximate_tool_result_item,
    function_call_output_item, mcp_tool_result_content_to_text, server_tool_result_output,
    server_tool_use_item, shell_result_item, tool_result_content_to_openai,
    tool_search_result_item, tool_use_item,
};
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(crate) fn claude_messages_to_openai_items(
    messages: Vec<claude::MessageParam>,
) -> Vec<openai::ResponseItem> {
    let mut approximate_tools = BTreeMap::new();
    messages
        .into_iter()
        .flat_map(|message| claude_message_to_openai_items(message, &mut approximate_tools))
        .collect()
}

fn claude_message_to_openai_items(
    message: claude::MessageParam,
    approximate_tools: &mut BTreeMap<String, ApproximateToolKind>,
) -> Vec<openai::ResponseItem> {
    let role = claude_role_to_openai(message.role);
    let assistant = role == openai::ResponseEasyInputMessageRole::Assistant;
    let mut items = Vec::new();
    let mut input_parts = Vec::new();
    let mut output_parts = Vec::new();

    match message.content {
        claude::MessageContent::String(text) => {
            if !text.is_empty() {
                if assistant {
                    output_parts.push(response_output_text(text, None));
                } else {
                    input_parts.push(response_input_text(text, None));
                }
            }
        }
        claude::MessageContent::Array(blocks) => {
            for block in blocks {
                match claude_request_block_to_openai(block, assistant, approximate_tools) {
                    ClaudeRequestBlockItem::InputMessagePart(part) => input_parts.push(part),
                    ClaudeRequestBlockItem::OutputMessagePart(part) => output_parts.push(part),
                    ClaudeRequestBlockItem::Item(item) => items.push(item),
                    ClaudeRequestBlockItem::None => {}
                }
            }
        }
        _ => {
            unreachable!("new non-exhaustive protocol variant requires a lockstep transform update")
        }
    }

    let content = if assistant {
        (!output_parts.is_empty())
            .then_some(openai::ResponseEasyInputContent::OutputParts(output_parts))
    } else {
        (!input_parts.is_empty()).then_some(openai::ResponseEasyInputContent::Parts(input_parts))
    };
    if let Some(content) = content {
        items.push(openai::ResponseItem::Message(
            openai::ResponseMessageItem::EasyInput(crate::protocol::wire!(
                openai::ResponseEasyInputMessageItem {
                    type_: Some(openai::ResponseMessageItemType::Message),
                    role,
                    content,
                    phase: None,
                    extra: Default::default(),
                }
            )),
        ));
    }

    items
}

pub(super) enum ClaudeRequestBlockItem {
    InputMessagePart(openai::ResponseInputContentPart),
    OutputMessagePart(openai::ResponseMessageOutputContentPart),
    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,
    assistant: bool,
    approximate_tools: &mut BTreeMap<String, ApproximateToolKind>,
) -> ClaudeRequestBlockItem {
    match block {
        claude::ContentBlockParam::Text(block) => match assistant {
            true => ClaudeRequestBlockItem::OutputMessagePart(response_output_text(
                block.text,
                crate::transform::generate_content::common::cache::openai_breakpoint(
                    block.cache_control,
                ),
            )),
            false => ClaudeRequestBlockItem::InputMessagePart(response_input_text(
                block.text,
                crate::transform::generate_content::common::cache::openai_breakpoint(
                    block.cache_control,
                ),
            )),
        },
        claude::ContentBlockParam::Image(block) => image_source_to_input_part(block.source)
            .filter(|_| !assistant)
            .map(ClaudeRequestBlockItem::InputMessagePart)
            .unwrap_or(ClaudeRequestBlockItem::None),
        claude::ContentBlockParam::Document(block) => {
            document_source_to_input_part(block.source, block.title)
                .filter(|_| !assistant)
                .map(ClaudeRequestBlockItem::InputMessagePart)
                .unwrap_or(ClaudeRequestBlockItem::None)
        }
        claude::ContentBlockParam::ToolUse(block) => {
            let id = block.id;
            let (item, kind) = tool_use_item(id.clone(), block.input, block.name);
            if let Some(kind) = kind {
                approximate_tools.insert(id, kind);
            }
            item
        }
        claude::ContentBlockParam::ToolResult(block) => {
            if let Some(kind) = approximate_tools.get(&block.tool_use_id).copied() {
                approximate_tool_result_item(kind, block.tool_use_id, block.content, block.is_error)
            } else {
                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(
                    crate::transform::generate_content::common::id::response_reasoning_item_id(
                        &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: Some(block.signature),
                status: Some(openai::ResponseItemLifecycleStatus::Completed),
                extra: Default::default(),
            }),
        ),
        claude::ContentBlockParam::RedactedThinking(block) => ClaudeRequestBlockItem::Item(
            openai::ResponseItem::Typed(openai::TypedResponseItem::Reasoning {
                id: Some(
                    crate::transform::generate_content::common::id::response_reasoning_item_id(
                        &block.data,
                    ),
                ),
                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| {
                        if assistant {
                            ClaudeRequestBlockItem::OutputMessagePart(response_output_text(
                                text, None,
                            ))
                        } else {
                            ClaudeRequestBlockItem::InputMessagePart(response_input_text(
                                text, None,
                            ))
                        }
                    })
                    .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) => {
            let _ = block;
            ClaudeRequestBlockItem::None
        }
        claude::ContentBlockParam::WebFetchToolResult(block) => {
            let _ = block;
            ClaudeRequestBlockItem::None
        }
        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) => {
            shell_result_item(block.tool_use_id, &block.content)
        }
        claude::ContentBlockParam::TextEditorCodeExecutionToolResult(block) => {
            apply_patch_result_item(block.tool_use_id, &block.content)
        }
        claude::ContentBlockParam::ToolSearchToolResult(block) => {
            tool_search_result_item(block.tool_use_id, &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,
    }
}

fn response_input_text(
    text: String,
    prompt_cache_breakpoint: Option<openai::PromptCacheBreakpoint>,
) -> openai::ResponseInputContentPart {
    openai::ResponseInputContentPart::InputText {
        text,
        prompt_cache_breakpoint,
        extra: Default::default(),
    }
}

fn response_output_text(
    text: String,
    prompt_cache_breakpoint: Option<openai::PromptCacheBreakpoint>,
) -> openai::ResponseMessageOutputContentPart {
    let mut extra = openai::Extra::new();
    if let Some(breakpoint) = prompt_cache_breakpoint {
        extra.insert(
            "prompt_cache_breakpoint".to_owned(),
            serde_json::to_value(breakpoint).expect("prompt cache breakpoint serializes"),
        );
    }
    openai::ResponseMessageOutputContentPart::OutputText {
        annotations: Vec::new(),
        logprobs: None,
        text,
        extra,
    }
}