gproxy-transform 3.0.0-alpha.0

GPROXY v3 pairwise protocol transforms
Documentation
use gproxy_protocol::{claude, openai};

use crate::TransformError;
use crate::common::native::items;
use crate::common::usage;

pub(crate) fn transform(body: bytes::Bytes) -> Result<bytes::Bytes, TransformError> {
    let input: openai::ResponseObject = serde_json::from_slice(&body)?;
    let output = transform_typed(input)?;
    Ok(bytes::Bytes::from(serde_json::to_vec(&output)?))
}

pub(crate) fn transform_typed(
    input: openai::ResponseObject,
) -> Result<claude::CreateMessageResponseBody, TransformError> {
    let model = input
        .model
        .as_ref()
        .map(crate::models::common::wire_string)
        .transpose()?
        .unwrap_or_else(|| "unknown".into())
        .into();
    let usage = usage::responses_to_claude(input.usage).unwrap_or_else(empty_usage);
    let mut blocks = Vec::new();
    for item in input.output {
        blocks.extend(item_blocks(item)?);
    }
    let has_tool = blocks
        .iter()
        .any(|block| matches!(block, claude::ResponseContentBlock::ToolUse(_)));
    let stop_reason = match input.status {
        Some(openai::ResponseStatus::Incomplete) => {
            claude::StopReason::Known(claude::StopReasonKnown::MaxTokens)
        }
        Some(openai::ResponseStatus::Failed) => {
            claude::StopReason::Known(claude::StopReasonKnown::Refusal)
        }
        _ if has_tool => claude::StopReason::Known(claude::StopReasonKnown::ToolUse),
        _ => claude::StopReason::Known(claude::StopReasonKnown::EndTurn),
    };
    let output = crate::wire!(claude::CreateMessageResponseBody {
        id: input.id,
        type_: claude::MessageObjectType::Known(claude::MessageObjectTypeKnown::Message),
        role: claude::AssistantRole::Known(claude::AssistantRoleKnown::Assistant),
        content: blocks,
        model,
        stop_reason,
        stop_sequence: None,
        usage,
        container: None,
        context_management: None,
        diagnostics: None,
        input_transformations: None,
        stop_details: None,
        rest: Default::default(),
    });
    Ok(output)
}

fn item_blocks(
    item: openai::ResponseItem,
) -> Result<Vec<claude::ResponseContentBlock>, TransformError> {
    match item {
        openai::ResponseItem::Message(message) => match message {
            openai::ResponseMessageItem::Output(message) => {
                let blocks = message
                    .content
                    .into_iter()
                    .map(|part| match part {
                        openai::ResponseMessageOutputContentPart::OutputText(part) => {
                            claude::ResponseContentBlock::Text(crate::wire!(claude::ResponseTextBlock {
                                citations: None,
                                text: part.text,
                                type_: claude::TextBlockType::Text,
                                rest: Default::default(),
                            }))
                        }
                        openai::ResponseMessageOutputContentPart::Refusal(part) => {
                            claude::ResponseContentBlock::Text(crate::wire!(claude::ResponseTextBlock {
                                citations: None,
                                text: part.refusal,
                                type_: claude::TextBlockType::Text,
                                rest: Default::default(),
                            }))
                        }
                        openai::ResponseMessageOutputContentPart::Unknown(_) => {
                            claude::ResponseContentBlock::Text(crate::wire!(claude::ResponseTextBlock {
                                citations: None,
                                text: String::new(),
                                type_: claude::TextBlockType::Text,
                                rest: Default::default(),
                            }))
                        },
                        #[cfg(not(feature = "exhaustive"))]
                        _ => claude::ResponseContentBlock::Text(crate::wire!(
                            claude::ResponseTextBlock {
                                citations: None,
                                text: String::new(),
                                type_: claude::TextBlockType::Text,
                                rest: Default::default(),
                            }
                        )),
                    })
                    .filter(|block| !matches!(block, claude::ResponseContentBlock::Text(text) if text.text.is_empty()))
                    .collect::<Vec<_>>();
                Ok(blocks)
            }
            openai::ResponseMessageItem::Unknown(_)
            | openai::ResponseMessageItem::Input(_)
            | openai::ResponseMessageItem::EasyInput(_) => Ok(Vec::new()),
            #[cfg(not(feature = "exhaustive"))]
            _ => {
                return Err(crate::TransformError::unsupported(
                    "protocol enum",
                    "unrecognized external variant",
                ));
            }
        },
        openai::ResponseItem::Typed(item) => typed_blocks(*item),
        openai::ResponseItem::Unknown(_) => Ok(Vec::new()),
        #[cfg(not(feature = "exhaustive"))]
        _ => {
            return Err(crate::TransformError::unsupported(
                "protocol enum",
                "unrecognized external variant",
            ));
        }
    }
}

fn typed_blocks(
    item: openai::TypedResponseItem,
) -> Result<Vec<claude::ResponseContentBlock>, TransformError> {
    Ok(match item {
        openai::TypedResponseItem::FunctionCall {
            arguments,
            call_id,
            name,
            ..
        } => vec![claude::ResponseContentBlock::ToolUse(crate::wire!(
            claude::ResponseToolUseBlock {
                id: call_id,
                input: serde_json::from_str(&arguments).unwrap_or_default(),
                name,
                type_: claude::ToolUseBlockType::ToolUse,
                caller: None,
                rest: Default::default(),
            }
        ))],
        openai::TypedResponseItem::CustomToolCall {
            call_id,
            input,
            name,
            ..
        } => vec![claude::ResponseContentBlock::ToolUse(crate::wire!(
            claude::ResponseToolUseBlock {
                id: call_id,
                input: string_input(input),
                name,
                type_: claude::ToolUseBlockType::ToolUse,
                caller: None,
                rest: Default::default(),
            }
        ))],
        openai::TypedResponseItem::Reasoning {
            content,
            encrypted_content,
            ..
        } => {
            let thinking = content
                .into_iter()
                .flatten()
                .map(|part| part.text)
                .collect::<String>();
            match (thinking.is_empty(), encrypted_content) {
                (false, Some(signature)) => vec![claude::ResponseContentBlock::Thinking(
                    crate::wire!(claude::ThinkingBlock {
                        signature: Some(signature),
                        thinking,
                        type_: claude::ThinkingBlockType::Thinking,
                        rest: Default::default(),
                    }),
                )],
                (false, None) => vec![claude::ResponseContentBlock::Text(crate::wire!(
                    claude::ResponseTextBlock {
                        citations: None,
                        text: thinking,
                        type_: claude::TextBlockType::Text,
                        rest: Default::default(),
                    }
                ))],
                (true, Some(data)) => vec![claude::ResponseContentBlock::RedactedThinking(
                    crate::wire!(claude::RedactedThinkingBlock {
                        data,
                        type_: claude::RedactedThinkingBlockType::RedactedThinking,
                        rest: Default::default(),
                    }),
                )],
                (true, None) => Vec::new(),
            }
        }
        openai::TypedResponseItem::Compaction {
            encrypted_content, ..
        } => vec![claude::ResponseContentBlock::Compaction(crate::wire!(
            claude::ResponseCompactionBlock {
                content: None,
                encrypted_content,
                type_: claude::CompactionBlockType::Compaction,
                rest: Default::default(),
            }
        ))],
        other @ (openai::TypedResponseItem::FileSearchCall { .. }
        | openai::TypedResponseItem::ComputerCall { .. }
        | openai::TypedResponseItem::ComputerCallOutput { .. }
        | openai::TypedResponseItem::WebSearchCall { .. }
        | openai::TypedResponseItem::FunctionCallOutput { .. }
        | openai::TypedResponseItem::ToolSearchCall { .. }
        | openai::TypedResponseItem::ToolSearchOutput { .. }
        | openai::TypedResponseItem::AdditionalTools { .. }
        | openai::TypedResponseItem::ImageGenerationCall { .. }
        | openai::TypedResponseItem::CodeInterpreterCall { .. }
        | openai::TypedResponseItem::LocalShellCall { .. }
        | openai::TypedResponseItem::LocalShellCallOutput { .. }
        | openai::TypedResponseItem::ShellCall { .. }
        | openai::TypedResponseItem::ShellCallOutput { .. }
        | openai::TypedResponseItem::ApplyPatchCall { .. }
        | openai::TypedResponseItem::ApplyPatchCallOutput { .. }
        | openai::TypedResponseItem::McpListTools { .. }
        | openai::TypedResponseItem::McpApprovalRequest { .. }
        | openai::TypedResponseItem::McpApprovalResponse { .. }
        | openai::TypedResponseItem::McpCall { .. }
        | openai::TypedResponseItem::CustomToolCallOutput { .. }
        | openai::TypedResponseItem::Program { .. }
        | openai::TypedResponseItem::ProgramOutput { .. }
        | openai::TypedResponseItem::MultiAgentCall { .. }
        | openai::TypedResponseItem::MultiAgentCallOutput { .. }
        | openai::TypedResponseItem::AgentMessage { .. }
        | openai::TypedResponseItem::ConfigurationUpdate { .. }
        | openai::TypedResponseItem::CompactionTrigger { .. }
        | openai::TypedResponseItem::ItemReference { .. }) => {
            if let Some(call) = items::openai_call(other.clone())? {
                vec![items::response_block(call)]
            } else {
                Vec::new()
            }
        }
        #[cfg(not(feature = "exhaustive"))]
        _ => {
            return Err(crate::TransformError::unsupported(
                "protocol enum",
                "unrecognized external variant",
            ));
        }
    })
}

fn string_input(input: String) -> claude::JsonObject {
    serde_json::from_str(&input).unwrap_or_else(|_| {
        let mut object = claude::JsonObject::new();
        object.insert("input".into(), input.into());
        object
    })
}

fn empty_usage() -> claude::Usage {
    crate::wire!(claude::Usage {
        input_tokens: Some(0),
        output_tokens: Some(0),
        cache_creation_input_tokens: None,
        cache_read_input_tokens: None,
        cache_creation: None,
        output_tokens_details: None,
        server_tool_use: None,
        iterations: None,
        inference_geo: None,
        service_tier: None,
        speed: None,
        rest: Default::default(),
    })
}