gproxy-protocol 4.0.4

GPROXY v4 connection modelling, operation declarations, vendor wire types and pairwise conversion
Documentation
use crate::{
    transform::TransformError,
    wire::{claude::content as c, openai::responses::input as r},
};
use base64::{Engine as _, engine::general_purpose::STANDARD};

pub(super) fn image(source: c::ImageSource) -> Result<r::InputContent, TransformError> {
    let url = match source {
        c::ImageSource::Url(source) => source.url,
        c::ImageSource::Base64(source) => format!(
            "data:{};base64,{}",
            match source.media_type {
                c::ImageMediaType::Jpeg => "image/jpeg",
                c::ImageMediaType::Png => "image/png",
                c::ImageMediaType::Gif => "image/gif",
                c::ImageMediaType::Webp => "image/webp",
            },
            source.data
        ),
        c::ImageSource::File(_) => {
            return Err(TransformError::missing_metadata(
                "Claude image file requires target resource mapping",
            ));
        }
    };
    let mut value = r::ResponseInputImage::builder(
        r::ResponseInputImageType::ResponseInputImage,
        r::ImageDetail::Auto,
    )
    .build();
    value.image_url = Some(Some(url));
    Ok(r::InputContent::Image(value))
}

pub(super) fn document(block: c::DocumentBlock) -> Result<r::InputContent, TransformError> {
    let mut value =
        r::ResponseInputFile::builder(r::ResponseInputFileType::ResponseInputFile).build();
    value.filename = block.title;
    match block.source {
        c::DocumentSource::Url(source) => value.file_url = Some(source.url),
        c::DocumentSource::Base64(source) => {
            value.file_data = Some(format!("data:application/pdf;base64,{}", source.data))
        }
        c::DocumentSource::Text(source) => {
            value.file_data = Some(format!(
                "data:text/plain;base64,{}",
                STANDARD.encode(source.data)
            ))
        }
        c::DocumentSource::Content(source) => {
            let text = match source.content {
                c::DocumentContent::Text(text) => text,
                c::DocumentContent::Blocks(blocks) => blocks
                    .into_iter()
                    .map(|block| match block {
                        c::DocumentContentBlock::Text(text) => Ok(text.text),
                        c::DocumentContentBlock::Image(_) => Err(TransformError::missing_metadata(
                            "mixed document resource conversion",
                        )),
                    })
                    .filter_map(|value| crate::transform::optional(value).transpose())
                    .collect::<Result<Vec<_>, _>>()?
                    .join(""),
            };
            value.file_data = Some(format!("data:text/plain;base64,{}", STANDARD.encode(text)));
        }
        c::DocumentSource::File(_) => {
            return Err(TransformError::missing_metadata(
                "Claude document file requires target resource mapping",
            ));
        }
    }
    Ok(r::InputContent::File(value))
}

pub(super) fn to_claude(value: r::InputContent) -> Result<c::ContentBlock, TransformError> {
    Ok(match value {
        r::InputContent::Text(value) => {
            c::ContentBlock::Text(c::TextBlock::builder(c::TextBlockType::Tag, value.text).build())
        }
        r::InputContent::Image(value) => {
            if value.file_id.flatten().is_some() {
                return Err(TransformError::missing_metadata(
                    "Responses image file requires Claude resource mapping",
                ));
            }
            let url = value
                .image_url
                .flatten()
                .ok_or_else(|| TransformError::missing_metadata("image_url"))?;
            let source = if let Some(data) = url.strip_prefix("data:") {
                let (mime, bytes) = data
                    .split_once(";base64,")
                    .ok_or_else(|| TransformError::shape("image_url", "invalid data URI"))?;
                STANDARD
                    .decode(bytes)
                    .map_err(|e| TransformError::shape("image_url", e.to_string()))?;
                c::ImageSource::Base64(
                    c::Base64Source::builder(
                        bytes.into(),
                        match mime {
                            "image/jpeg" => c::ImageMediaType::Jpeg,
                            "image/png" => c::ImageMediaType::Png,
                            "image/gif" => c::ImageMediaType::Gif,
                            "image/webp" => c::ImageMediaType::Webp,
                            _ => {
                                return Err(TransformError::unsupported(
                                    "image.mime",
                                    "unsupported Claude image MIME",
                                ));
                            }
                        },
                    )
                    .build(),
                )
            } else {
                c::ImageSource::Url(c::UrlSource::builder(url).build())
            };
            c::ContentBlock::Image(c::ImageBlock::builder(c::ImageBlockType::Tag, source).build())
        }
        r::InputContent::File(value) => {
            if value.file_id.flatten().is_some() {
                return Err(TransformError::missing_metadata(
                    "Responses file requires Claude resource mapping",
                ));
            }
            let source = if let Some(url) = value.file_url {
                c::DocumentSource::Url(c::UrlSource::builder(url).build())
            } else {
                let data = value
                    .file_data
                    .ok_or_else(|| TransformError::missing_metadata("file_data"))?;
                if let Some(bytes) = data.strip_prefix("data:application/pdf;base64,") {
                    STANDARD
                        .decode(bytes)
                        .map_err(|e| TransformError::shape("file_data", e.to_string()))?;
                    c::DocumentSource::Base64(
                        c::PdfBase64Source::builder(bytes.into(), c::PdfMediaType::Pdf).build(),
                    )
                } else if let Some(bytes) = data.strip_prefix("data:text/plain;base64,") {
                    let bytes = STANDARD
                        .decode(bytes)
                        .map_err(|e| TransformError::shape("file_data", e.to_string()))?;
                    c::DocumentSource::Text(
                        c::PlainTextSource::builder(
                            String::from_utf8(bytes)
                                .map_err(|e| TransformError::shape("file_data", e.to_string()))?,
                            c::TextMediaType::Plain,
                        )
                        .build(),
                    )
                } else {
                    return Err(TransformError::missing_metadata("file_data MIME/data URI"));
                }
            };
            let mut block = c::DocumentBlock::builder(c::DocumentBlockType::Tag, source).build();
            block.title = value.filename;
            c::ContentBlock::Document(block)
        }
    })
}

pub(super) fn result_to_responses(
    value: c::ToolResultContent,
) -> Result<r::FunctionOutput, TransformError> {
    Ok(match value {
        c::ToolResultContent::Text(text) => r::FunctionOutput::Text(text),
        c::ToolResultContent::Blocks(blocks) => r::FunctionOutput::Content(
            blocks
                .into_iter()
                .map(|block| {
                    let part = match block {
                        c::ToolResultContentBlock::Text(text) => {
                            return Ok(r::FunctionOutputContent::Text(
                                r::FunctionOutputText::builder(
                                    r::ResponseInputTextType::ResponseInputText,
                                    text.text,
                                )
                                .build(),
                            ));
                        }
                        c::ToolResultContentBlock::Image(block) => image(block.source)?,
                        c::ToolResultContentBlock::Document(doc) => document(doc)?,
                        c::ToolResultContentBlock::SearchResult(_)
                        | c::ToolResultContentBlock::BrowserState(_)
                        | c::ToolResultContentBlock::ToolReference(_) => {
                            return Err(TransformError::unsupported(
                                "tool_result",
                                "search/tool-reference result needs target metadata mapping",
                            ));
                        }
                    };
                    Ok(match part {
                        r::InputContent::Image(value) => {
                            let mut target = r::FunctionOutputImage::builder(
                                r::ResponseInputImageType::ResponseInputImage,
                            )
                            .build();
                            target.image_url = value.image_url;
                            target.detail = Some(Some(value.detail));
                            r::FunctionOutputContent::Image(target)
                        }
                        r::InputContent::File(value) => {
                            let mut target = r::FunctionOutputFile::builder(
                                r::ResponseInputFileType::ResponseInputFile,
                            )
                            .build();
                            target.file_data = value.file_data.map(Some);
                            target.file_url = value.file_url.map(Some);
                            target.filename = value.filename.map(Some);
                            r::FunctionOutputContent::File(target)
                        }
                        // image()/document() yield only Image or File parts; text returned earlier.
                        r::InputContent::Text(_) => unreachable!(),
                    })
                })
                .collect::<Result<Vec<_>, TransformError>>()?,
        ),
    })
}

pub(super) fn result_to_claude(
    value: r::FunctionOutput,
) -> Result<c::ToolResultContent, TransformError> {
    Ok(match value {
        r::FunctionOutput::Text(text) => c::ToolResultContent::Text(text),
        r::FunctionOutput::Content(parts) => c::ToolResultContent::Blocks(
            parts
                .into_iter()
                .map(|part| {
                    let input = match part {
                        r::FunctionOutputContent::Text(text) => {
                            return Ok(c::ToolResultContentBlock::Text(
                                c::TextBlock::builder(c::TextBlockType::Tag, text.text).build(),
                            ));
                        }
                        r::FunctionOutputContent::Image(value) => {
                            let mut input = r::ResponseInputImage::builder(
                                r::ResponseInputImageType::ResponseInputImage,
                                value.detail.flatten().unwrap_or(r::ImageDetail::Auto),
                            )
                            .build();
                            input.image_url = value.image_url;
                            input.file_id = value.file_id;
                            r::InputContent::Image(input)
                        }
                        r::FunctionOutputContent::File(value) => {
                            let mut input = r::ResponseInputFile::builder(
                                r::ResponseInputFileType::ResponseInputFile,
                            )
                            .build();
                            input.file_data = value.file_data.flatten();
                            input.file_url = value.file_url.flatten();
                            input.file_id = value.file_id;
                            input.filename = value.filename.flatten();
                            r::InputContent::File(input)
                        }
                    };
                    match to_claude(input)? {
                        c::ContentBlock::Image(image) => {
                            Ok(c::ToolResultContentBlock::Image(image))
                        }
                        c::ContentBlock::Document(doc) => {
                            Ok(c::ToolResultContentBlock::Document(doc))
                        }
                        c::ContentBlock::Text(_)
                        | c::ContentBlock::Thinking(_)
                        | c::ContentBlock::RedactedThinking(_)
                        | c::ContentBlock::ToolUse(_)
                        | c::ContentBlock::ToolResult(_)
                        | c::ContentBlock::ServerToolUse(_)
                        | c::ContentBlock::SearchResult(_)
                        | c::ContentBlock::WebSearchToolResult(_)
                        | c::ContentBlock::WebFetchToolResult(_)
                        | c::ContentBlock::AdvisorToolResult(_)
                        | c::ContentBlock::CodeExecutionToolResult(_)
                        | c::ContentBlock::BashCodeExecutionToolResult(_)
                        | c::ContentBlock::TextEditorCodeExecutionToolResult(_)
                        | c::ContentBlock::ToolSearchToolResult(_)
                        | c::ContentBlock::McpToolUse(_)
                        | c::ContentBlock::McpToolResult(_)
                        | c::ContentBlock::ContainerUpload(_)
                        | c::ContentBlock::McpToolListing(_)
                        | c::ContentBlock::Compaction(_)
                        | c::ContentBlock::MidConversationSystem(_)
                        | c::ContentBlock::ToolAddition(_)
                        | c::ContentBlock::ToolRemoval(_)
                        | c::ContentBlock::Fallback(_) => {
                            unreachable!("media helper returns only image/document")
                        }
                    }
                })
                .collect::<Result<Vec<_>, TransformError>>()?,
        ),
    })
}