gproxy-transform 3.0.0-alpha.0

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

use crate::TransformError;
use crate::common::{content, tools};
use crate::models::common::wire_string;

#[allow(deprecated)] // The public Claude wire still carries the deprecated output_format slot.
pub(crate) fn transform(
    body: bytes::Bytes,
    model: &str,
    stream: bool,
) -> Result<bytes::Bytes, TransformError> {
    let input: openai::ChatCompletionRequest = serde_json::from_slice(&body)?;
    let output = transform_typed(input, model, stream)?;
    Ok(bytes::Bytes::from(serde_json::to_vec(&output)?))
}

#[allow(deprecated)] // The public Claude wire still carries the deprecated output_format slot.
pub(crate) fn transform_typed(
    input: openai::ChatCompletionRequest,
    model: &str,
    stream: bool,
) -> Result<claude::CreateMessageRequestBody, TransformError> {
    let _ = wire_string(&input.model)?;
    let mid_conv_supported = supports_mid_conv_system(model);
    let last_non_system = input.messages.iter().rposition(|message| {
        !matches!(
            message,
            openai::ChatCompletionMessageParam::Developer(_)
                | openai::ChatCompletionMessageParam::System(_)
        )
    });
    let mut messages = Vec::new();
    let mut system = Vec::new();
    let mut seen_turn = false;
    for (index, message) in input.messages.into_iter().enumerate() {
        match message {
            openai::ChatCompletionMessageParam::Developer(message) => {
                push_system(
                    content::chat_text_blocks(message.content)?,
                    seen_turn,
                    mid_conv_supported,
                    last_non_system.is_some_and(|last| index > last),
                    &mut system,
                    &mut messages,
                );
            }
            openai::ChatCompletionMessageParam::System(message) => {
                push_system(
                    content::chat_text_blocks(message.content)?,
                    seen_turn,
                    mid_conv_supported,
                    last_non_system.is_some_and(|last| index > last),
                    &mut system,
                    &mut messages,
                );
            }
            openai::ChatCompletionMessageParam::User(message) => {
                seen_turn = true;
                push_message(
                    &mut messages,
                    claude::MessageRoleKnown::User,
                    content::chat_user_blocks(message.content)?,
                );
            }
            openai::ChatCompletionMessageParam::Assistant(message) => {
                seen_turn = true;
                let mut blocks = message
                    .content
                    .map(content::chat_assistant_blocks)
                    .transpose()?
                    .into_iter()
                    .flatten()
                    .collect::<Vec<_>>();
                if let Some(reasoning) = message.reasoning_content.filter(|value| !value.is_empty())
                {
                    blocks.insert(
                        0,
                        claude::ContentBlockParam::Thinking(crate::wire!(claude::ThinkingBlock {
                            signature: None,
                            thinking: reasoning,
                            type_: claude::ThinkingBlockType::Thinking,
                            rest: Default::default(),
                        })),
                    );
                }
                if message.function_call.is_some() {
                    return Err(TransformError::shape(
                        "OpenAI Chat function_call",
                        "tool call id is missing",
                    ));
                }
                if let Some(calls) = message.tool_calls {
                    for call in calls {
                        if let Some(block) = tool_call(call)? {
                            blocks.push(block);
                        }
                    }
                }
                push_message(&mut messages, claude::MessageRoleKnown::Assistant, blocks);
            }
            openai::ChatCompletionMessageParam::Tool(message) => {
                seen_turn = true;
                push_message(
                    &mut messages,
                    claude::MessageRoleKnown::User,
                    vec![claude::ContentBlockParam::ToolResult(crate::wire!(
                        claude::ToolResultBlock {
                            tool_use_id: message.tool_call_id,
                            type_: claude::ToolResultBlockType::ToolResult,
                            cache_control: None,
                            content: Some(tool_result_content(message.content)?),
                            is_error: None,
                            rest: Default::default(),
                        }
                    ))],
                );
            }
            openai::ChatCompletionMessageParam::Function(message) => {
                seen_turn = true;
                let content = message.content.ok_or_else(|| {
                    TransformError::unsupported("OpenAI Chat function message", "null content")
                })?;
                push_message(
                    &mut messages,
                    claude::MessageRoleKnown::User,
                    content::chat_text_blocks(openai::ChatTextContent::Text(format!(
                        "function:{}\n{}",
                        message.name, content
                    )))?,
                );
            }
            openai::ChatCompletionMessageParam::Unknown(_) => {}
            #[cfg(not(feature = "exhaustive"))]
            _ => {
                return Err(crate::TransformError::unsupported(
                    "protocol enum",
                    "unrecognized external variant",
                ));
            }
        }
    }
    let service_tier_value = input.service_tier.clone();
    let max_tokens = input
        .max_completion_tokens
        .or(input.max_tokens)
        .map(u64::from)
        .unwrap_or(crate::common::DEFAULT_CLAUDE_MAX_TOKENS);
    let output = crate::wire!(claude::CreateMessageRequestBody {
        model: model.to_owned().into(),
        messages,
        max_tokens,
        cache_control: None,
        container: None,
        context_management: None,
        diagnostics: None,
        fallback_credit_token: None,
        fallbacks: None,
        inference_geo: None,
        mcp_servers: None,
        metadata: input.user.map(|user_id| claude::Metadata {
            user_id: Some(user_id),
            rest: Default::default(),
        }),
        output_config: output_config(input.response_format, input.reasoning_effort)?,
        output_format: None,
        service_tier: service_tier(service_tier_value)?,
        speed: speed(input.service_tier)?,
        stop_sequences: input.stop.map(stop_sequences),
        stream: Some(stream),
        system: (!system.is_empty()).then_some(claude::StringOrArray::Array(system)),
        temperature: input.temperature,
        thinking: None,
        tool_choice: tools::chat_choice_to_claude(input.tool_choice, input.parallel_tool_calls)?,
        tools: tools::chat_to_claude(input.tools)?,
        top_k: None,
        top_p: input.top_p,
        user_profile_id: None,
        rest: Default::default(),
    });
    Ok(output)
}

fn push_system(
    blocks: Vec<claude::ContentBlockParam>,
    seen_turn: bool,
    mid_conv_supported: bool,
    trailing: bool,
    system: &mut Vec<claude::TextBlock>,
    messages: &mut Vec<claude::MessageParam>,
) {
    if seen_turn {
        let role = if mid_conv_supported {
            claude::MessageRoleKnown::System
        } else if trailing {
            claude::MessageRoleKnown::User
        } else {
            claude::MessageRoleKnown::Assistant
        };
        push_message(messages, role, blocks);
    } else {
        system.extend(blocks.into_iter().filter_map(|block| match block {
            claude::ContentBlockParam::Text(block) => Some(block),
            _ => None,
        }));
    }
}

fn supports_mid_conv_system(model: &str) -> bool {
    const PRE_OPUS_48: &[&str] = &[
        "claude-instant",
        "claude-1",
        "claude-2",
        "claude-3",
        "claude-sonnet-4",
        "claude-haiku-4",
        "claude-4-",
        "claude-opus-4-0",
        "claude-opus-4-1",
        "claude-opus-4-2",
        "claude-opus-4-3",
        "claude-opus-4-4",
        "claude-opus-4-5",
        "claude-opus-4-6",
        "claude-opus-4-7",
        "claude-opus-4@",
        "claude-sonnet-5",
    ];
    let model = model.to_ascii_lowercase();
    !PRE_OPUS_48.iter().any(|pattern| model.contains(pattern))
}

fn push_message(
    messages: &mut Vec<claude::MessageParam>,
    role: claude::MessageRoleKnown,
    blocks: Vec<claude::ContentBlockParam>,
) {
    if !blocks.is_empty() {
        messages.push(crate::wire!(claude::MessageParam {
            role: claude::MessageRole::Known(role),
            content: claude::StringOrArray::Array(blocks),
            clear_at: None,
            output_config: None,
            rest: Default::default(),
        }));
    }
}

fn function_call(
    id: String,
    call: openai::FunctionCall,
) -> Result<claude::ContentBlockParam, TransformError> {
    let input = serde_json::from_str(&call.arguments).unwrap_or_default();
    Ok(claude::ContentBlockParam::ToolUse(crate::wire!(
        claude::ToolUseBlock {
            id,
            input,
            name: call.name,
            type_: claude::ToolUseBlockType::ToolUse,
            cache_control: None,
            caller: None,
            rest: Default::default(),
        }
    )))
}

fn tool_call(
    call: openai::ChatToolCall,
) -> Result<Option<claude::ContentBlockParam>, TransformError> {
    match call {
        openai::ChatToolCall::Function(call) => function_call(call.id, call.function).map(Some),
        openai::ChatToolCall::Custom(call) => {
            let input = serde_json::from_str(&call.custom.input).unwrap_or_default();
            Ok(Some(claude::ContentBlockParam::ToolUse(crate::wire!(
                claude::ToolUseBlock {
                    id: call.id,
                    input,
                    name: call.custom.name,
                    type_: claude::ToolUseBlockType::ToolUse,
                    cache_control: None,
                    caller: None,
                    rest: Default::default(),
                }
            ))))
        }
        openai::ChatToolCall::Unknown(_) => Ok(None),
        #[cfg(not(feature = "exhaustive"))]
        _ => {
            return Err(crate::TransformError::unsupported(
                "protocol enum",
                "unrecognized external variant",
            ));
        }
    }
}

fn tool_result_content(
    content: openai::ChatTextContent,
) -> Result<claude::ToolResultContent, TransformError> {
    match content {
        openai::ChatTextContent::Text(text) => Ok(claude::ToolResultContent::Text(text)),
        openai::ChatTextContent::Parts(parts) => Ok(claude::ToolResultContent::Blocks(
            parts
                .into_iter()
                .filter_map(|part| match part {
                    openai::ChatTextContentPart::Text(part) => Some(Ok(
                        claude::ToolResultContentBlock::Text(crate::wire!(claude::TextBlock {
                            text: part.text,
                            type_: claude::TextBlockType::Text,
                            cache_control: None,
                            citations: None,
                            rest: Default::default(),
                        })),
                    )),
                    openai::ChatTextContentPart::Unknown(_) => None,
                    #[cfg(not(feature = "exhaustive"))]
                    _ => None,
                })
                .collect::<Result<_, TransformError>>()?,
        )),
        openai::ChatTextContent::Unknown(raw) => Err(TransformError::unsupported(
            "OpenAI Chat tool result",
            raw.to_string(),
        )),
        #[cfg(not(feature = "exhaustive"))]
        _ => {
            return Err(crate::TransformError::unsupported(
                "protocol enum",
                "unrecognized external variant",
            ));
        }
    }
}

fn output_config(
    format: Option<openai::ChatResponseFormat>,
    effort: Option<openai::ReasoningEffort>,
) -> Result<Option<claude::OutputConfig>, TransformError> {
    let format = match format {
        Some(openai::ChatResponseFormat::JsonSchema(format)) => {
            Some(crate::wire!(claude::JsonSchemaFormat {
                type_: claude::JsonSchemaFormatType::Known(
                    claude::JsonSchemaFormatTypeKnown::JsonSchema,
                ),
                schema: format.json_schema.schema.ok_or_else(|| {
                    TransformError::shape("OpenAI JSON schema response format", "schema is missing")
                })?,
                rest: Default::default(),
            }))
        }
        Some(openai::ChatResponseFormat::Text(_)) | None => None,
        Some(other) => {
            return Err(TransformError::unsupported(
                "OpenAI response format",
                serde_json::to_string(&other)?,
            ));
        }
    };
    let effort = effort
        .map(|effort| serde_json::from_value(serde_json::to_value(effort)?))
        .transpose()?;
    Ok(
        (format.is_some() || effort.is_some()).then_some(crate::wire!(claude::OutputConfig {
            effort,
            format,
            task_budget: None,
            rest: Default::default(),
        })),
    )
}

fn stop_sequences(stop: openai::StringOrList) -> Vec<String> {
    match stop {
        openai::StringOrList::String(stop) => vec![stop],
        openai::StringOrList::List(stops) => stops,
    }
}

fn service_tier(
    tier: Option<openai::ServiceTier>,
) -> Result<Option<claude::RequestServiceTier>, TransformError> {
    match tier {
        None => Ok(None),
        Some(openai::ServiceTier::Auto | openai::ServiceTier::Default) => Ok(Some(
            claude::RequestServiceTier::Known(claude::RequestServiceTierKnown::Auto),
        )),
        Some(openai::ServiceTier::Unknown(_)) => Ok(None),
        Some(_) => Ok(None),
    }
}

fn speed(tier: Option<openai::ServiceTier>) -> Result<Option<claude::Speed>, TransformError> {
    Ok(match tier {
        Some(
            openai::ServiceTier::Fast
            | openai::ServiceTier::Priority
            | openai::ServiceTier::Ultrafast,
        ) => Some(claude::Speed::Known(claude::SpeedKnown::Fast)),
        _ => None,
    })
}