gproxy-transform 2.6.3

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

use super::tools::typed_item_to_claude_message;

pub(super) fn openai_input_to_claude_messages(
    input: Option<openai::ResponseInput>,
    system_role: claude::MessageRole,
) -> Vec<claude::MessageParam> {
    match input {
        Some(openai::ResponseInput::Text(text)) => text_to_claude_message(
            claude::MessageRole::Known(claude::MessageRoleKnown::User),
            text,
        )
        .into_iter()
        .collect(),
        Some(openai::ResponseInput::Items(items)) => items
            .into_iter()
            .filter_map(|item| openai_item_to_claude_message(item, &system_role))
            .collect(),
        None => Vec::new(),
        _ => {
            unreachable!("new non-exhaustive protocol variant requires a lockstep transform update")
        }
    }
}

fn openai_item_to_claude_message(
    item: openai::ResponseItem,
    system_role: &claude::MessageRole,
) -> Option<claude::MessageParam> {
    match item {
        openai::ResponseItem::Message(message) => {
            openai_message_to_claude_message(message, system_role)
        }
        openai::ResponseItem::Typed(openai::TypedResponseItem::Compaction {
            encrypted_content,
            ..
        }) => Some(crate::protocol::wire!(claude::MessageParam {
            role: claude::MessageRole::Known(claude::MessageRoleKnown::Assistant),
            content: claude::MessageContent::Array(vec![claude::ContentBlockParam::Compaction(
                crate::protocol::wire!(claude::CompactionBlock {
                    content: None,
                    encrypted_content: Some(encrypted_content),
                    type_: claude::CompactionBlockType::Compaction,
                    cache_control: None,
                }),
            )]),
            extra: Default::default(),
        })),
        openai::ResponseItem::Typed(typed) => typed_item_to_claude_message(typed),
        _ => None,
    }
}

fn openai_message_to_claude_message(
    message: openai::ResponseMessageItem,
    system_role: &claude::MessageRole,
) -> Option<claude::MessageParam> {
    match message {
        openai::ResponseMessageItem::EasyInput(message) => {
            let role = easy_input_role_to_claude(message.role, system_role);
            let blocks = easy_input_content_to_blocks(message.content);
            blocks_to_claude_message(role, blocks)
        }
        openai::ResponseMessageItem::Input(message) => {
            let role = input_role_to_claude(message.role, system_role);
            let blocks = input_parts_to_blocks(message.content);
            blocks_to_claude_message(role, blocks)
        }
        openai::ResponseMessageItem::Output(message) => {
            let blocks = output_parts_to_blocks(message.content);
            blocks_to_claude_message(
                claude::MessageRole::Known(claude::MessageRoleKnown::Assistant),
                blocks,
            )
        }
        _ => {
            unreachable!("new non-exhaustive protocol variant requires a lockstep transform update")
        }
    }
}

fn text_to_claude_message(role: claude::MessageRole, text: String) -> Option<claude::MessageParam> {
    if text.is_empty() {
        return None;
    }

    Some(crate::protocol::wire!(claude::MessageParam {
        role,
        content: claude::MessageContent::String(text),
        extra: Default::default(),
    }))
}

pub(super) fn blocks_to_claude_message(
    role: claude::MessageRole,
    blocks: Vec<claude::ContentBlockParam>,
) -> Option<claude::MessageParam> {
    if blocks.is_empty() {
        return None;
    }

    Some(crate::protocol::wire!(claude::MessageParam {
        role,
        content: claude::MessageContent::Array(blocks),
        extra: Default::default(),
    }))
}

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

fn input_role_to_claude(
    role: openai::ResponseInputMessageRole,
    system_role: &claude::MessageRole,
) -> claude::MessageRole {
    match role {
        openai::ResponseInputMessageRole::System | openai::ResponseInputMessageRole::Developer => {
            system_role.clone()
        }
        openai::ResponseInputMessageRole::User => {
            claude::MessageRole::Known(claude::MessageRoleKnown::User)
        }
        _ => {
            unreachable!("new non-exhaustive protocol variant requires a lockstep transform update")
        }
    }
}

fn easy_input_content_to_blocks(
    content: openai::ResponseEasyInputContent,
) -> Vec<claude::ContentBlockParam> {
    match content {
        openai::ResponseEasyInputContent::Text(text) => text_block(text).into_iter().collect(),
        openai::ResponseEasyInputContent::Parts(parts) => input_parts_to_blocks(parts),
        openai::ResponseEasyInputContent::OutputParts(parts) => output_parts_to_blocks(parts),
        _ => {
            unreachable!("new non-exhaustive protocol variant requires a lockstep transform update")
        }
    }
}

fn input_parts_to_blocks(
    parts: Vec<openai::ResponseInputContentPart>,
) -> Vec<claude::ContentBlockParam> {
    parts
        .into_iter()
        .filter_map(input_part_to_claude_block)
        .collect()
}

fn input_part_to_claude_block(
    part: openai::ResponseInputContentPart,
) -> Option<claude::ContentBlockParam> {
    match part {
        openai::ResponseInputContentPart::InputText { text, .. } => text_block(text),
        openai::ResponseInputContentPart::InputImage {
            file_id, image_url, ..
        } => image_block(file_id, image_url),
        openai::ResponseInputContentPart::InputFile {
            file_data,
            file_id,
            file_url,
            filename,
            ..
        } => document_block(file_id, file_url, file_data, filename),
        openai::ResponseInputContentPart::InputAudio { .. } => None,
        _ => {
            unreachable!("new non-exhaustive protocol variant requires a lockstep transform update")
        }
    }
}

fn output_parts_to_blocks(
    parts: Vec<openai::ResponseMessageOutputContentPart>,
) -> Vec<claude::ContentBlockParam> {
    parts
        .into_iter()
        .filter_map(|part| match part {
            openai::ResponseMessageOutputContentPart::OutputText { text, .. } => text_block(text),
            openai::ResponseMessageOutputContentPart::Refusal { refusal, .. } => {
                text_block(refusal)
            }
            _ => unreachable!(
                "new non-exhaustive protocol variant requires a lockstep transform update"
            ),
        })
        .collect()
}

pub(super) fn text_block(text: String) -> Option<claude::ContentBlockParam> {
    if text.is_empty() {
        return None;
    }

    Some(claude::ContentBlockParam::Text(crate::protocol::wire!(
        claude::TextBlock {
            text,
            type_: claude::TextBlockType::Text,
            cache_control: None,
            citations: None,
            extra: Default::default(),
        }
    )))
}

pub(super) fn image_block(
    file_id: Option<String>,
    image_url: Option<String>,
) -> Option<claude::ContentBlockParam> {
    let source = if let Some(file_id) = file_id {
        claude::ImageSource::File(crate::protocol::wire!(claude::FileImageSource {
            file_id,
            type_: claude::FileSourceType::File,
            extra: Default::default(),
        }))
    } else {
        claude::ImageSource::Url(crate::protocol::wire!(claude::UrlImageSource {
            type_: claude::UrlSourceType::Url,
            url: image_url?,
            extra: Default::default(),
        }))
    };

    Some(claude::ContentBlockParam::Image(crate::protocol::wire!(
        claude::ImageBlock {
            source,
            type_: claude::ImageBlockType::Image,
            cache_control: None,
        }
    )))
}

pub(super) fn document_block(
    file_id: Option<String>,
    file_url: Option<String>,
    file_data: Option<String>,
    filename: Option<String>,
) -> Option<claude::ContentBlockParam> {
    let source = if let Some(file_id) = file_id {
        claude::DocumentSource::File(crate::protocol::wire!(claude::FileDocumentSource {
            file_id,
            type_: claude::FileSourceType::File,
            extra: Default::default(),
        }))
    } else if let Some(file_url) = file_url {
        claude::DocumentSource::Url(crate::protocol::wire!(claude::UrlDocumentSource {
            type_: claude::UrlSourceType::Url,
            url: file_url,
            extra: Default::default(),
        }))
    } else {
        claude::DocumentSource::Text(crate::protocol::wire!(claude::PlainTextSource {
            data: file_data?,
            media_type: claude::PlainTextMediaType::TextPlain,
            type_: claude::TextSourceType::Text,
            extra: Default::default(),
        }))
    };

    Some(claude::ContentBlockParam::Document(crate::protocol::wire!(
        claude::DocumentBlock {
            source,
            type_: claude::DocumentBlockType::Document,
            cache_control: None,
            citations: None,
            context: None,
            title: filename,
        }
    )))
}