gproxy-transform 2.2.2

Pairwise request/response/stream transforms between the OpenAI, Anthropic Claude, and Google Gemini APIs
Documentation
use crate::protocol::{gemini, openai};

use super::content::{file_image_part, inline_image_part, push_text_content, text_part};
use super::mime::{infer_mime_type_from_uri, is_image_mime, openai_output_format_mime};
use super::scalar::usize_to_i32;
use super::usage::{gemini_usage_to_openai, openai_usage_to_gemini};

pub(in crate::transform::images) fn openai_images_response_to_gemini(
    input: openai::ImagesResponse,
) -> gemini::GenerateContentResponse {
    let output_format = input.output_format;
    let usage = input.usage;
    let candidates = input
        .data
        .unwrap_or_default()
        .into_iter()
        .enumerate()
        .filter_map(|(index, image)| {
            openai_image_to_candidate(image, index, output_format.as_ref())
        })
        .collect();

    gemini::GenerateContentResponse {
        candidates,
        prompt_feedback: None,
        usage_metadata: usage.map(openai_usage_to_gemini),
        model_version: None,
        response_id: None,
        model_status: None,
        extra: Default::default(),
    }
}

fn openai_image_to_candidate(
    image: openai::Image,
    index: usize,
    output_format: Option<&openai::ImageOutputFormat>,
) -> Option<gemini::Candidate> {
    let mut parts = Vec::new();
    let output_mime_type = output_format.map(openai_output_format_mime);

    if let Some(text) = image.revised_prompt.filter(|text| !text.is_empty()) {
        parts.push(text_part(text));
    }
    if let Some(data) = image.b64_json {
        let mime_type = output_mime_type.unwrap_or("image/png").to_owned();
        parts.push(inline_image_part(data, mime_type));
    }
    if let Some(url) = image.url {
        let mime_type = output_mime_type
            .map(str::to_owned)
            .or_else(|| infer_mime_type_from_uri(Some(&url)));
        parts.push(file_image_part(url, mime_type));
    }

    if parts.is_empty() {
        return None;
    }

    Some(gemini::Candidate {
        content: Some(gemini::Content {
            parts,
            role: Some(gemini::ContentRole::Known(gemini::ContentRoleKnown::Model)),
            extra: Default::default(),
        }),
        finish_reason: Some(gemini::FinishReason::Known(gemini::FinishReasonKnown::Stop)),
        safety_ratings: Vec::new(),
        citation_metadata: None,
        token_count: None,
        grounding_metadata: None,
        avg_logprobs: None,
        logprobs_result: None,
        url_context_metadata: None,
        index: Some(usize_to_i32(index)),
        finish_message: None,
        extra: Default::default(),
    })
}

pub(in crate::transform::images) fn gemini_response_to_openai_images(
    input: gemini::GenerateContentResponse,
) -> openai::ImagesResponse {
    let data = gemini_candidates_to_openai_images(input.candidates);
    let usage = input.usage_metadata.map(gemini_usage_to_openai);

    openai::ImagesResponse {
        created: 0,
        background: None,
        data: (!data.is_empty()).then_some(data),
        output_format: None,
        quality: None,
        size: None,
        usage,
        extra: Default::default(),
    }
}

pub(super) fn gemini_candidates_to_openai_images(
    candidates: Vec<gemini::Candidate>,
) -> Vec<openai::Image> {
    candidates
        .into_iter()
        .flat_map(gemini_candidate_to_openai_images)
        .collect()
}

pub(super) fn gemini_candidate_to_openai_images(
    candidate: gemini::Candidate,
) -> Vec<openai::Image> {
    let Some(content) = candidate.content else {
        return Vec::new();
    };
    let revised_prompt = content_revised_prompt(&content);

    content
        .parts
        .into_iter()
        .filter_map(|part| part_to_openai_image(part, revised_prompt.clone()))
        .collect()
}

fn content_revised_prompt(content: &gemini::Content) -> Option<String> {
    let mut values = Vec::new();
    push_text_content(&mut values, content);

    if values.is_empty() {
        None
    } else {
        Some(values.join("\n"))
    }
}

fn part_to_openai_image(
    part: gemini::Part,
    revised_prompt: Option<String>,
) -> Option<openai::Image> {
    match part.data? {
        gemini::PartData::InlineData { inline_data } if is_image_mime(&inline_data.mime_type) => {
            Some(openai::Image {
                b64_json: Some(inline_data.data),
                revised_prompt,
                url: None,
                extra: Default::default(),
            })
        }
        gemini::PartData::FileData { file_data }
            if file_data
                .mime_type
                .as_deref()
                .map(is_image_mime)
                .unwrap_or(true) =>
        {
            Some(openai::Image {
                b64_json: None,
                revised_prompt,
                url: Some(file_data.file_uri),
                extra: Default::default(),
            })
        }
        _ => None,
    }
}