gproxy-protocol 4.0.4

GPROXY v4 connection modelling, operation declarations, vendor wire types and pairwise conversion
Documentation
use crate::{
    transform::TransformError,
    wire::{gemini as g, openai::chat as c},
};

pub(super) fn to_chat(blob: g::Blob) -> Result<c::UserContentPart, TransformError> {
    Ok(if blob.mime_type.starts_with("image/") {
        c::UserContentPart::Image(
            c::ImagePart::builder(
                c::ImagePartType::ImageUrl,
                c::ImageUrl::builder(format!("data:{};base64,{}", blob.mime_type, blob.data))
                    .build(),
            )
            .build(),
        )
    } else if matches!(
        blob.mime_type.as_str(),
        "audio/wav" | "audio/x-wav" | "audio/mpeg" | "audio/mp3"
    ) {
        let format = if matches!(blob.mime_type.as_str(), "audio/wav" | "audio/x-wav") {
            c::AudioFormat::Wav
        } else {
            c::AudioFormat::Mp3
        };
        c::UserContentPart::InputAudio(
            c::InputAudioPart::builder(
                c::InputAudioPartType::InputAudio,
                c::InputAudio::builder(blob.data, format).build(),
            )
            .build(),
        )
    } else if blob.mime_type == "application/pdf" || blob.mime_type == "text/plain" {
        let mut file = c::FileRef::builder().build();
        file.file_data = Some(format!("data:{};base64,{}", blob.mime_type, blob.data));
        c::UserContentPart::File(c::FilePart::builder(c::FilePartType::File, file).build())
    } else {
        return Err(TransformError::unsupported(
            "inline_data.mime_type",
            "Chat has no equivalent media part for this MIME",
        ));
    })
}

pub(super) fn data_uri(value: &str) -> Result<g::Blob, TransformError> {
    let (mime, data) = value
        .strip_prefix("data:")
        .and_then(|v| v.split_once(";base64,"))
        .ok_or_else(|| {
            TransformError::missing_metadata("media URL requires resource bytes and exact MIME")
        })?;

    if mime.is_empty() {
        return Err(TransformError::shape("media.mime", "missing MIME"));
    }
    Ok(g::Blob::builder(mime.into(), data.into()).build())
}

pub(super) fn user(content: &c::UserContent) -> Result<Vec<g::Part>, TransformError> {
    match content {
        c::UserContent::Text(text) => Ok(vec![g::Part::builder().text(text.clone()).build()]),
        c::UserContent::Parts(parts) => parts
            .iter()
            .map(|part| {
                Ok(match part {
                    c::UserContentPart::Text(part) => {
                        g::Part::builder().text(part.text.clone()).build()
                    }
                    c::UserContentPart::Image(part) => g::Part::builder()
                        .inline_data(data_uri(&part.image_url.url)?)
                        .build(),
                    c::UserContentPart::InputAudio(part) => g::Part::builder()
                        .inline_data(
                            g::Blob::builder(
                                match part.input_audio.format {
                                    c::AudioFormat::Wav => "audio/wav",
                                    c::AudioFormat::Mp3 => "audio/mpeg",
                                }
                                .into(),
                                part.input_audio.data.clone(),
                            )
                            .build(),
                        )
                        .build(),
                    c::UserContentPart::File(part) => {
                        let data = part.file.file_data.as_deref().ok_or_else(|| {
                            TransformError::missing_metadata("file_id requires resource access")
                        })?;
                        g::Part::builder().inline_data(data_uri(data)?).build()
                    }
                })
            })
            .filter_map(|value| crate::transform::optional(value).transpose())
            .collect(),
    }
}