gproxy-transform 2.3.1

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

use super::input::ClaudeRequestBlockItem;
use super::util::{
    document_source_to_input_part, image_source_to_input_part, join_text, json_object_to_string,
    server_tool_name_to_string,
};

pub(super) fn server_tool_use_item(
    id: String,
    input: claude::JsonObject,
    name: claude::ServerToolUseName,
) -> ClaudeRequestBlockItem {
    ClaudeRequestBlockItem::Item(openai::ResponseItem::Typed(crate::protocol::wire!(
        openai::TypedResponseItem::FunctionCall {
            arguments: json_object_to_string(&input),
            call_id: id.clone(),
            name: server_tool_name_to_string(&name),
            id: Some(id),
            caller: None,
            namespace: None,
            status: Some(openai::ResponseItemLifecycleStatus::Completed),
            extra: Default::default(),
        }
    )))
}

pub(super) fn function_call_output_item(
    call_id: String,
    output: openai::ResponseOutput,
) -> ClaudeRequestBlockItem {
    ClaudeRequestBlockItem::Item(openai::ResponseItem::Typed(
        openai::TypedResponseItem::FunctionCallOutput {
            call_id,
            output,
            id: None,
            caller: None,
            name: None,
            namespace: None,
            status: Some(openai::ResponseItemLifecycleStatus::Completed),
            created_by: None,
            extra: Default::default(),
        },
    ))
}

pub(super) fn compact_server_tool_use_item(
    id: String,
    input: claude::JsonObject,
    name: claude::ServerToolUseName,
) -> openai::CompactResponseItem {
    openai::CompactResponseItem::Typed(crate::protocol::wire!(
        openai::TypedResponseItem::FunctionCall {
            arguments: json_object_to_string(&input),
            call_id: id.clone(),
            name: server_tool_name_to_string(&name),
            id: Some(id),
            caller: None,
            namespace: None,
            status: Some(openai::ResponseItemLifecycleStatus::Completed),
            extra: Default::default(),
        }
    ))
}

pub(super) fn compact_function_call_output_item(
    call_id: String,
    output: openai::ResponseOutput,
) -> openai::CompactResponseItem {
    openai::CompactResponseItem::Typed(openai::TypedResponseItem::FunctionCallOutput {
        call_id,
        output,
        id: None,
        caller: None,
        name: None,
        namespace: None,
        status: Some(openai::ResponseItemLifecycleStatus::Completed),
        created_by: None,
        extra: Default::default(),
    })
}

pub(super) fn tool_result_content_to_openai(
    content: Option<claude::ToolResultContent>,
) -> openai::ResponseOutput {
    match content {
        Some(claude::ToolResultContent::Text(text)) => openai::ResponseOutput::Text(text),
        Some(claude::ToolResultContent::Blocks(blocks)) => {
            let parts = blocks
                .into_iter()
                .filter_map(tool_result_block_to_openai)
                .collect::<Vec<_>>();
            openai::ResponseOutput::Parts(parts)
        }
        Some(claude::ToolResultContent::Raw(value)) => {
            openai::ResponseOutput::Text(value.to_string())
        }
        None => openai::ResponseOutput::Text(String::new()),
        _ => {
            unreachable!("new non-exhaustive protocol variant requires a lockstep transform update")
        }
    }
}

pub(super) fn server_tool_result_output<T: serde::Serialize>(
    content: &T,
) -> openai::ResponseOutput {
    openai::ResponseOutput::Text(serde_json::to_string(content).unwrap_or_else(|_| String::new()))
}

pub(super) fn mcp_tool_result_content_to_text(
    content: Option<claude::McpToolResultContent>,
) -> String {
    match content {
        Some(claude::McpToolResultContent::String(text)) => text,
        Some(claude::McpToolResultContent::Array(blocks)) => {
            join_text(blocks.into_iter().map(|block| block.text))
        }
        None => String::new(),
        _ => {
            unreachable!("new non-exhaustive protocol variant requires a lockstep transform update")
        }
    }
}

pub(super) fn response_mcp_tool_result_content_to_text(
    content: claude::ResponseMcpToolResultContent,
) -> String {
    match content {
        claude::ResponseMcpToolResultContent::String(text) => text,
        claude::ResponseMcpToolResultContent::Array(blocks) => {
            join_text(blocks.into_iter().map(|block| block.text))
        }
        _ => {
            unreachable!("new non-exhaustive protocol variant requires a lockstep transform update")
        }
    }
}

fn tool_result_block_to_openai(
    block: claude::ToolResultContentBlock,
) -> Option<openai::ResponseToolOutputContentPart> {
    match block {
        claude::ToolResultContentBlock::Text(block) => {
            Some(openai::ResponseToolOutputContentPart::InputText {
                text: block.text,
                prompt_cache_breakpoint: None,
                extra: Default::default(),
            })
        }
        claude::ToolResultContentBlock::Image(block) => {
            input_part_to_tool_output_part(image_source_to_input_part(block.source)?)
        }
        claude::ToolResultContentBlock::Document(block) => input_part_to_tool_output_part(
            document_source_to_input_part(block.source, block.title)?,
        ),
        claude::ToolResultContentBlock::SearchResult(block) => {
            let text = join_text(
                block
                    .content
                    .into_iter()
                    .map(|content_block| content_block.text)
                    .chain([block.source, block.title]),
            );
            (!text.is_empty()).then_some(openai::ResponseToolOutputContentPart::InputText {
                text,
                prompt_cache_breakpoint: None,
                extra: Default::default(),
            })
        }
        claude::ToolResultContentBlock::ToolReference(_)
        | claude::ToolResultContentBlock::Raw(_) => None,
        _ => {
            unreachable!("new non-exhaustive protocol variant requires a lockstep transform update")
        }
    }
}

fn input_part_to_tool_output_part(
    part: openai::ResponseInputContentPart,
) -> Option<openai::ResponseToolOutputContentPart> {
    match part {
        openai::ResponseInputContentPart::InputText { text, .. } => {
            Some(openai::ResponseToolOutputContentPart::InputText {
                text,
                prompt_cache_breakpoint: None,
                extra: Default::default(),
            })
        }
        openai::ResponseInputContentPart::InputImage {
            detail,
            file_id,
            image_url,
            ..
        } => Some(openai::ResponseToolOutputContentPart::InputImage {
            detail,
            file_id,
            image_url,
            prompt_cache_breakpoint: None,
            extra: Default::default(),
        }),
        openai::ResponseInputContentPart::InputFile {
            detail,
            file_data,
            file_id,
            file_url,
            filename,
            ..
        } => Some(openai::ResponseToolOutputContentPart::InputFile {
            detail,
            file_data,
            file_id,
            file_url,
            filename,
            prompt_cache_breakpoint: None,
            extra: Default::default(),
        }),
        openai::ResponseInputContentPart::InputAudio { .. } => None,
        _ => {
            unreachable!("new non-exhaustive protocol variant requires a lockstep transform update")
        }
    }
}