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;

mod helpers;
use helpers::*;

pub(crate) fn transform(body: bytes::Bytes) -> Result<bytes::Bytes, TransformError> {
    claude_to_responses(body)
}

pub(crate) fn claude_to_responses(body: bytes::Bytes) -> Result<bytes::Bytes, TransformError> {
    let input: claude::CreateMessageResponseBody = serde_json::from_slice(&body)?;
    let response = transform_typed(input)?;
    Ok(bytes::Bytes::from(serde_json::to_vec(&response)?))
}

pub(crate) fn transform_typed(
    input: claude::CreateMessageResponseBody,
) -> Result<openai::ResponseObject, TransformError> {
    let id = input.id;
    let service_tier = claude_service_tier(&input.usage)?;
    let mut output = Vec::new();
    let mut text = Vec::new();
    let mut parts = Vec::new();
    let mut message_id = None;
    let mut message_index = 0;
    for block in input.content {
        match block {
            claude::ResponseContentBlock::Text(block) => {
                text.push(block.text.clone());
                parts.push(openai::ResponseMessageOutputContentPart::OutputText(
                    crate::wire!(openai::ResponseOutputText {
                        type_: openai::ResponseOutputTextType::OutputText,
                        annotations: Vec::new(),
                        logprobs: None,
                        text: block.text,
                        rest: Default::default(),
                    }),
                ));
            }
            claude::ResponseContentBlock::Thinking(block) => {
                output.push(reasoning(
                    None,
                    Some(block.thinking),
                    block.signature,
                    Default::default(),
                ));
            }
            claude::ResponseContentBlock::RedactedThinking(block) => {
                output.push(reasoning(None, None, Some(block.data), Default::default()));
            }
            claude::ResponseContentBlock::ToolUse(block) => {
                let (item, _) = items::claude_call(
                    block.id,
                    block.input,
                    block.name,
                    openai::ResponseItemLifecycleStatus::Completed,
                )?;
                output.push(openai::ResponseItem::Typed(Box::new(item)));
            }
            claude::ResponseContentBlock::Compaction(block) => {
                output.push(openai::ResponseItem::Typed(Box::new(
                    openai::TypedResponseItem::Compaction {
                        encrypted_content: block.encrypted_content,
                        id: None,
                        created_by: None,
                        rest: Default::default(),
                    },
                )));
            }
            claude::ResponseContentBlock::Raw(_) => {}
            _ => {}
        }
    }
    flush_message(
        &mut output,
        &mut parts,
        &mut message_id,
        &id,
        &mut message_index,
    );
    let stop_reason = crate::models::common::wire_string(&input.stop_reason)?;
    let incomplete_reason = match stop_reason.as_str() {
        "max_tokens" | "model_context_window_exceeded" => {
            Some(openai::IncompleteReason::MaxOutputTokens)
        }
        "refusal" => Some(openai::IncompleteReason::ContentFilter),
        "end_turn" | "stop_sequence" | "tool_use" | "pause_turn" | "compaction" => None,
        _ => None,
    };
    let response = crate::wire!(openai::ResponseObject {
        id,
        created_at: None,
        background: None,
        completed_at: None,
        conversation: None,
        error: None,
        incomplete_details: incomplete_reason
            .clone()
            .map(|reason| openai::IncompleteDetails {
                reason: Some(reason),
                rest: Default::default(),
            }),
        instructions: None,
        max_output_tokens: None,
        max_tool_calls: None,
        metadata: None,
        model: Some(crate::models::common::wire_string(&input.model)?.into()),
        moderation: None,
        multi_agent: None,
        object: openai::ResponseObjectType::Response,
        output,
        output_text: (!text.is_empty()).then(|| text.join("")),
        parallel_tool_calls: None,
        prompt: None,
        prompt_cache_key: None,
        prompt_cache_options: None,
        prompt_cache_retention: None,
        previous_response_id: None,
        reasoning: None,
        safety_identifier: None,
        service_tier,
        status: Some(if incomplete_reason.is_some() {
            openai::ResponseStatus::Incomplete
        } else {
            openai::ResponseStatus::Completed
        }),
        store: None,
        temperature: None,
        text: None,
        tool_choice: None,
        tools: None,
        top_logprobs: None,
        top_p: None,
        truncation: None,
        usage: usage::claude_to_responses(input.usage),
        user: None,
        rest: Default::default(),
    });
    Ok(response)
}

fn claude_service_tier(
    usage: &claude::Usage,
) -> Result<Option<openai::ServiceTier>, TransformError> {
    if matches!(
        usage.speed,
        Some(claude::Speed::Known(claude::SpeedKnown::Fast))
    ) {
        return Ok(Some(openai::ServiceTier::Priority));
    }
    let Some(tier) = usage.service_tier.as_ref() else {
        return Ok(None);
    };
    let tier = crate::models::common::wire_string(tier)?;
    Ok(Some(if tier == "priority" {
        openai::ServiceTier::Priority
    } else {
        openai::ServiceTier::Default
    }))
}