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;

pub(crate) fn input_to_claude(
    parts: Vec<openai::ResponseInputContentPart>,
) -> Result<Vec<claude::ContentBlockParam>, TransformError> {
    let mut chat = Vec::new();
    for part in parts {
        chat.push(match part {
            openai::ResponseInputContentPart::InputText(part) => {
                openai::ChatContentPart::Text(crate::wire!(openai::ChatTextPart {
                    type_: openai::ChatTextPartType::Text,
                    text: part.text,
                    prompt_cache_breakpoint: part.prompt_cache_breakpoint,
                    rest: Default::default(),
                }))
            }
            openai::ResponseInputContentPart::InputImage(part) => {
                if let Some(file_id) = part.file_id {
                    openai::ChatContentPart::File(openai::ChatFilePart {
                        type_: openai::ChatFilePartType::File,
                        file: crate::wire!(openai::ChatFileRef {
                            file_data: None,
                            file_id: Some(file_id),
                            filename: None,
                            rest: Default::default(),
                        }),
                        prompt_cache_breakpoint: part.prompt_cache_breakpoint,
                        rest: Default::default(),
                    })
                } else {
                    openai::ChatContentPart::ImageUrl(openai::ChatImageUrlPart {
                        type_: openai::ChatImageUrlPartType::ImageUrl,
                        image_url: crate::wire!(openai::ImageUrl {
                            url: part.image_url.ok_or_else(|| {
                                TransformError::shape("Responses image", "source is missing")
                            })?,
                            detail: None,
                            rest: Default::default(),
                        }),
                        prompt_cache_breakpoint: part.prompt_cache_breakpoint,
                        rest: Default::default(),
                    })
                }
            }
            openai::ResponseInputContentPart::InputFile(part) => {
                if part.file_url.is_some() {
                    return Err(TransformError::unsupported("Responses file", "file_url"));
                }
                openai::ChatContentPart::File(openai::ChatFilePart {
                    type_: openai::ChatFilePartType::File,
                    file: crate::wire!(openai::ChatFileRef {
                        file_data: part.file_data,
                        file_id: part.file_id,
                        filename: part.filename,
                        rest: Default::default(),
                    }),
                    prompt_cache_breakpoint: part.prompt_cache_breakpoint,
                    rest: Default::default(),
                })
            }
            openai::ResponseInputContentPart::InputAudio(_) => {
                return Err(TransformError::unsupported(
                    "Responses content",
                    "input_audio",
                ));
            }
            #[cfg(not(feature = "exhaustive"))]
            _ => {
                return Err(crate::TransformError::unsupported(
                    "protocol enum",
                    "unrecognized external variant",
                ));
            }
        });
    }
    content::chat_user_blocks(openai::ChatContent::Parts(chat))
}

pub(crate) fn claude_to_input(
    blocks: Vec<claude::ContentBlockParam>,
) -> Result<Vec<openai::ResponseInputContentPart>, TransformError> {
    content::claude_user_parts(blocks)?
        .into_iter()
        .map(|part| match part {
            openai::ChatContentPart::Text(part) => Ok(openai::ResponseInputContentPart::InputText(
                crate::wire!(openai::ResponseInputText {
                    text: part.text,
                    prompt_cache_breakpoint: part.prompt_cache_breakpoint,
                    rest: Default::default(),
                }),
            )),
            openai::ChatContentPart::ImageUrl(part) => {
                Ok(openai::ResponseInputContentPart::InputImage(crate::wire!(
                    openai::ResponseInputImage {
                        detail: None,
                        file_id: None,
                        image_url: Some(part.image_url.url),
                        prompt_cache_breakpoint: part.prompt_cache_breakpoint,
                        rest: Default::default(),
                    }
                )))
            }
            openai::ChatContentPart::File(part) => Ok(openai::ResponseInputContentPart::InputFile(
                crate::wire!(openai::ResponseInputFile {
                    detail: None,
                    file_data: part.file.file_data,
                    file_id: part.file.file_id,
                    file_url: None,
                    filename: part.file.filename,
                    prompt_cache_breakpoint: part.prompt_cache_breakpoint,
                    rest: Default::default(),
                }),
            )),
            openai::ChatContentPart::Unknown(raw) => Err(TransformError::unsupported(
                "Claude content",
                serde_json::to_string(&raw)?,
            )),
            openai::ChatContentPart::InputAudio(_) => {
                Err(TransformError::unsupported("Claude content", "audio"))
            }
            #[cfg(not(feature = "exhaustive"))]
            _ => {
                return Err(crate::TransformError::unsupported(
                    "protocol enum",
                    "unrecognized external variant",
                ));
            }
        })
        .collect()
}

pub(crate) fn output_to_claude(
    parts: Vec<openai::ResponseMessageOutputContentPart>,
) -> Result<Vec<claude::ContentBlockParam>, TransformError> {
    let chat = parts
        .into_iter()
        .filter_map(|part| match part {
            openai::ResponseMessageOutputContentPart::OutputText(part) => Some(
                openai::ChatAssistantContentPart::Text(crate::wire!(openai::ChatTextPart {
                    type_: openai::ChatTextPartType::Text,
                    text: part.text,
                    prompt_cache_breakpoint: None,
                    rest: Default::default(),
                })),
            ),
            openai::ResponseMessageOutputContentPart::Refusal(part) => Some(
                openai::ChatAssistantContentPart::Refusal(crate::wire!(openai::ChatRefusalPart {
                    type_: openai::ChatRefusalPartType::Refusal,
                    refusal: part.refusal,
                    prompt_cache_breakpoint: None,
                    rest: Default::default(),
                })),
            ),
            openai::ResponseMessageOutputContentPart::Unknown(_) => None,
            #[cfg(not(feature = "exhaustive"))]
            _ => None,
        })
        .collect();
    content::chat_assistant_blocks(openai::ChatAssistantContent::Parts(chat))
}