gproxy-transform 3.0.0-alpha.0

GPROXY v3 pairwise protocol transforms
Documentation
use bytes::Bytes;
use gproxy_protocol::{ContentGenerationKind, openai};

use crate::TransformError;

pub(crate) fn compact_request(
    target: ContentGenerationKind,
    body: Bytes,
    model: &str,
    stream: bool,
) -> Result<Bytes, TransformError> {
    let input: openai::CompactResponseRequestBody = serde_json::from_slice(&body)?;
    match target {
        ContentGenerationKind::OpenAiResponses => {
            encode(&compact_to_responses_typed(input, model))
        }
        ContentGenerationKind::OpenAiChat => encode(
            &crate::generate_content::openai_responses_to_openai_chat::request::transform_typed(
                compact_to_responses_typed(input, model),
                model,
                stream,
            )?,
        ),
        ContentGenerationKind::GeminiGenerateContent => encode(
            &crate::generate_content::openai_responses_to_gemini_generate_content::request::transform_typed(
                compact_to_responses_typed(input, model),
                model,
                stream,
            )?,
        ),
        ContentGenerationKind::ClaudeMessages => encode(
            &crate::compact::openai_to_claude_messages::request::transform_typed(input, model)?,
        ),
        ContentGenerationKind::OpenAiResponsesWebSocket => Err(TransformError::shape(
            "compact",
            "websocket is an envelope, not a compact semantic target",
        )),
        #[cfg(not(feature = "exhaustive"))]
        _ => return Err(crate::TransformError::unsupported("protocol enum", "unrecognized external variant")),
    }
}

pub(crate) fn compact_response(
    target: ContentGenerationKind,
    body: Bytes,
) -> Result<Bytes, TransformError> {
    let responses = match target {
        ContentGenerationKind::OpenAiResponses => serde_json::from_slice(&body)?,
        ContentGenerationKind::OpenAiChat => {
            crate::generate_content::openai_responses_to_openai_chat::response::transform_typed(
                serde_json::from_slice(&body)?,
            )?
        }
        ContentGenerationKind::GeminiGenerateContent => {
            crate::generate_content::openai_responses_to_gemini_generate_content::response::transform_typed(
                serde_json::from_slice(&body)?,
            )?
        }
        ContentGenerationKind::ClaudeMessages => {
            return encode(
                &crate::compact::openai_to_claude_messages::response::transform_typed(
                    serde_json::from_slice(&body)?,
                )?,
            );
        }
        ContentGenerationKind::OpenAiResponsesWebSocket => {
            return Err(TransformError::shape("compact", "websocket response envelope"));
        },
        #[cfg(not(feature = "exhaustive"))]
        _ => return Err(crate::TransformError::unsupported("protocol enum", "unrecognized external variant"))
    };
    encode(&crate::compact::openai_to_claude_messages::response::from_responses_typed(responses)?)
}

pub(crate) fn content_request(
    source: ContentGenerationKind,
    body: Bytes,
    model: &str,
) -> Result<Bytes, TransformError> {
    let input = to_responses_request(source, body, model)?;
    encode(&responses_to_compact_request_typed(input))
}

pub(crate) fn responses_to_compact_request_typed(
    input: openai::ResponseCreateRequest,
) -> openai::CompactResponseRequestBody {
    crate::wire!(openai::CompactResponseRequestBody {
        input: input.input,
        instructions: input.instructions,
        model: input.model,
        previous_response_id: input.previous_response_id,
        prompt_cache_key: input.prompt_cache_key,
        prompt_cache_options: input.prompt_cache_options,
        prompt_cache_retention: input.prompt_cache_retention,
        service_tier: input.service_tier,
        rest: Default::default(),
    })
}

pub(crate) fn content_response(
    source: ContentGenerationKind,
    body: Bytes,
) -> Result<Bytes, TransformError> {
    let compact: openai::CompactedResponseObject = serde_json::from_slice(&body)?;
    let responses = compact_object_to_responses_typed(compact)?;
    match source {
        ContentGenerationKind::OpenAiResponses => encode(&responses),
        ContentGenerationKind::OpenAiChat => encode(
            &crate::generate_content::openai_chat_to_openai_responses::response::transform_typed(
                responses,
            )?,
        ),
        ContentGenerationKind::GeminiGenerateContent => encode(
            &crate::generate_content::gemini_generate_content_to_openai_responses::response::transform_typed(
                responses,
            )?,
        ),
        ContentGenerationKind::ClaudeMessages => encode(
            &crate::generate_content::claude_messages_to_openai_responses::response::transform_typed(
                responses,
            )?,
        ),
        ContentGenerationKind::OpenAiResponsesWebSocket => {
            Err(TransformError::shape("compact", "websocket response envelope"))
        },
        #[cfg(not(feature = "exhaustive"))]
        _ => return Err(crate::TransformError::unsupported("protocol enum", "unrecognized external variant"))
    }
}

pub(crate) fn compact_to_responses_typed(
    input: openai::CompactResponseRequestBody,
    model: &str,
) -> openai::ResponseCreateRequest {
    crate::wire!(openai::ResponseCreateRequest {
        input: input.input,
        instructions: input.instructions,
        model: input.model.or_else(|| Some(model.into())),
        previous_response_id: input.previous_response_id,
        prompt_cache_key: input.prompt_cache_key,
        prompt_cache_options: input.prompt_cache_options,
        prompt_cache_retention: input.prompt_cache_retention,
        service_tier: input.service_tier,
        stream: Some(false),
        max_output_tokens: Some(32_768),
        rest: Default::default(),
        ..Default::default()
    })
}

fn to_responses_request(
    source: ContentGenerationKind,
    body: Bytes,
    model: &str,
) -> Result<openai::ResponseCreateRequest, TransformError> {
    match source {
        ContentGenerationKind::OpenAiResponses => Ok(serde_json::from_slice(&body)?),
        ContentGenerationKind::OpenAiChat => {
            crate::generate_content::openai_chat_to_openai_responses::request::transform_typed(
                serde_json::from_slice(&body)?,
                model,
                false,
            )
        }
        ContentGenerationKind::GeminiGenerateContent => {
            crate::generate_content::gemini_generate_content_to_openai_responses::request::transform_typed(
                serde_json::from_slice(&body)?,
                model,
                false,
            )
        }
        ContentGenerationKind::ClaudeMessages => {
            crate::generate_content::claude_messages_to_openai_responses::request::transform_typed(
                serde_json::from_slice(&body)?,
                model,
                false,
            )
        }
        ContentGenerationKind::OpenAiResponsesWebSocket => Err(TransformError::shape(
            "compact",
            "websocket request envelope",
        )),
        #[cfg(not(feature = "exhaustive"))]
        _ => return Err(crate::TransformError::unsupported("protocol enum", "unrecognized external variant")),
    }
}

pub(crate) fn compact_object_to_responses_typed(
    input: openai::CompactedResponseObject,
) -> Result<openai::ResponseObject, TransformError> {
    let output = input
        .output
        .into_iter()
        .filter_map(|item| compact_item_to_response(item).transpose())
        .collect::<Result<Vec<_>, _>>()?;
    Ok(crate::wire!(openai::ResponseObject {
        id: input.id,
        created_at: input.created_at,
        background: None,
        completed_at: None,
        conversation: None,
        error: None,
        incomplete_details: None,
        instructions: None,
        max_output_tokens: None,
        max_tool_calls: None,
        metadata: None,
        model: None,
        moderation: None,
        multi_agent: None,
        object: openai::ResponseObjectType::Response,
        output,
        output_text: None,
        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: None,
        status: Some(openai::ResponseStatus::Completed),
        store: None,
        temperature: None,
        text: None,
        tool_choice: None,
        tools: None,
        top_logprobs: None,
        top_p: None,
        truncation: None,
        usage: Some(input.usage),
        user: None,
        rest: Default::default(),
    }))
}

fn compact_item_to_response(
    item: openai::CompactResponseItem,
) -> Result<Option<openai::ResponseItem>, TransformError> {
    Ok(match item {
        openai::CompactResponseItem::Typed(item) => Some(openai::ResponseItem::Typed(item)),
        openai::CompactResponseItem::Unknown(_) => None,
        openai::CompactResponseItem::Message(message)
            if message.role == openai::CompactMessageRole::Assistant =>
        {
            let content = message
                .content
                .into_iter()
                .filter_map(|part| match part {
                    openai::CompactMessageContentPart::Text(part) => {
                        Some(openai::ResponseMessageOutputContentPart::OutputText(
                            crate::wire!(openai::ResponseOutputText {
                                type_: openai::ResponseOutputTextType::OutputText,
                                annotations: Vec::new(),
                                logprobs: None,
                                text: part.text,
                                rest: Default::default(),
                            }),
                        ))
                    }
                    _ => None,
                })
                .collect();
            Some(openai::ResponseItem::Message(
                openai::ResponseMessageItem::Output(crate::wire!(
                    openai::ResponseOutputMessageItem {
                        type_: message.type_,
                        id: message.id.unwrap_or_else(|| "msg_compact".into()),
                        role: openai::ResponseOutputMessageRole::Assistant,
                        content,
                        status: message.status,
                        phase: message.phase,
                        rest: Default::default(),
                    }
                )),
            ))
        }
        openai::CompactResponseItem::Message(_) => None,
    })
}

fn encode(value: &impl serde::Serialize) -> Result<Bytes, TransformError> {
    Ok(Bytes::from(serde_json::to_vec(value)?))
}