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};

pub(in crate::transform::embeddings) const DEFAULT_OPENAI_EMBEDDING_MODEL: &str = "unknown";

pub(in crate::transform::embeddings) struct ConvertedGeminiRequest {
    pub text: String,
    pub model: Option<String>,
    pub dimensions: Option<u32>,
}

pub(in crate::transform::embeddings) fn openai_to_gemini_requests(
    input: openai::EmbeddingRequest,
) -> Vec<gemini::EmbedContentRequest> {
    let model = model_name(&input.model);
    let embed_content_config = input
        .dimensions
        .map(|dimensions| gemini::EmbedContentConfig {
            output_dimensionality: Some(u32_to_i32(dimensions)),
            ..Default::default()
        });

    input
        .input
        .into_strings()
        .into_iter()
        .map(|text| gemini::EmbedContentRequest {
            model: Some(model.clone()),
            content: Some(text_content(text)),
            task_type: None,
            title: None,
            output_dimensionality: None,
            embed_content_config: embed_content_config.clone(),
            extra: Default::default(),
        })
        .collect()
}

pub(in crate::transform::embeddings) fn openai_to_gemini_embedding(
    input: openai::Embedding,
) -> gemini::ContentEmbedding {
    gemini::ContentEmbedding {
        values: input
            .embedding
            .into_iter()
            .map(|value| value as f32)
            .collect(),
        shape: Vec::new(),
        extra: Default::default(),
    }
}

pub(in crate::transform::embeddings) fn openai_to_gemini_usage(
    input: openai::EmbeddingUsage,
) -> gemini::EmbeddingUsageMetadata {
    gemini::EmbeddingUsageMetadata {
        prompt_token_count: Some(u32_to_i32(input.prompt_tokens)),
        total_token_count: Some(u32_to_i32(input.total_tokens)),
        input_token_count: Some(u32_to_i32(input.prompt_tokens)),
        prompt_token_details: Vec::new(),
        prompt_tokens_details: Vec::new(),
        extra: Default::default(),
    }
}

pub(in crate::transform::embeddings) fn gemini_request_parts(
    input: gemini::EmbedContentRequest,
) -> ConvertedGeminiRequest {
    let dimensions = input
        .embed_content_config
        .as_ref()
        .and_then(|config| config.output_dimensionality)
        .or(input.output_dimensionality)
        .map(i32_to_u32);

    ConvertedGeminiRequest {
        text: content_text(input.content),
        model: input.model,
        dimensions,
    }
}

pub(in crate::transform::embeddings) fn strings_to_openai_input(
    values: Vec<String>,
) -> openai::EmbeddingInput {
    let mut values = values;
    if values.len() == 1 {
        openai::EmbeddingInput::Text(values.remove(0))
    } else {
        openai::EmbeddingInput::TextList(values)
    }
}

pub(in crate::transform::embeddings) fn merge_model(
    target: &mut Option<String>,
    next: Option<String>,
) {
    let Some(next) = next else {
        return;
    };
    if target.is_none() {
        *target = Some(next);
    }
}

pub(in crate::transform::embeddings) fn merge_dimensions(
    target: &mut Option<u32>,
    next: Option<u32>,
) {
    let Some(next) = next else {
        return;
    };
    if target.is_none() {
        *target = Some(next);
    }
}

pub(in crate::transform::embeddings) fn gemini_to_openai_embedding(
    input: gemini::ContentEmbedding,
    index: usize,
) -> openai::Embedding {
    openai::Embedding {
        embedding: input.values.into_iter().map(f64::from).collect(),
        index: u32::try_from(index).unwrap_or(u32::MAX),
        object: openai::EmbeddingObjectType::Embedding,
        extra: Default::default(),
    }
}

pub(in crate::transform::embeddings) fn gemini_to_openai_usage(
    input: Option<gemini::EmbeddingUsageMetadata>,
) -> openai::EmbeddingUsage {
    let Some(input) = input else {
        return openai::EmbeddingUsage {
            prompt_tokens: 0,
            total_tokens: 0,
            extra: Default::default(),
        };
    };

    let prompt_tokens = input.prompt_token_count.or(input.input_token_count);
    let total_tokens = input.total_token_count.or(prompt_tokens);

    openai::EmbeddingUsage {
        prompt_tokens: prompt_tokens.map(i32_to_u32).unwrap_or_default(),
        total_tokens: total_tokens.map(i32_to_u32).unwrap_or_default(),
        extra: Default::default(),
    }
}

fn i32_to_u32(value: i32) -> u32 {
    u32::try_from(value).unwrap_or_default()
}

fn u32_to_i32(value: u32) -> i32 {
    i32::try_from(value).unwrap_or(i32::MAX)
}

fn model_name(model: &openai::OpenAiModelId) -> String {
    let Ok(value) = serde_json::to_value(model) else {
        return String::new();
    };
    value.as_str().map(str::to_owned).unwrap_or_default()
}

fn text_content(text: String) -> gemini::Content {
    gemini::Content {
        parts: vec![gemini::Part {
            thought: None,
            thought_signature: None,
            part_metadata: None,
            media_resolution: None,
            data: Some(gemini::PartData::Text { text }),
            metadata: None,
            extra: Default::default(),
        }],
        role: None,
        extra: Default::default(),
    }
}

fn content_text(input: Option<gemini::Content>) -> String {
    let Some(content) = input else {
        return String::new();
    };
    let mut text = String::new();

    for part in content.parts {
        if let Some(gemini::PartData::Text { text: value }) = part.data {
            text.push_str(&value);
        }
    }

    text
}

trait EmbeddingInputExt {
    fn into_strings(self) -> Vec<String>;
}

impl EmbeddingInputExt for openai::EmbeddingInput {
    fn into_strings(self) -> Vec<String> {
        match self {
            Self::Text(text) => vec![text],
            Self::TextList(values) => values,
            Self::TokenList(_) => vec![String::new()],
            Self::TokenLists(values) => values.into_iter().map(|_| String::new()).collect(),
        }
    }
}