gproxy-protocol 4.1.11

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

#[derive(Debug, Default, Clone, Copy)]
pub struct GeminiUsageFacts {
    pub cache_write_tokens: Option<i64>,
    pub cached_tokens: Option<i64>,
}

fn nonnegative(n: i64) -> Result<i64, TransformError> {
    Ok(n)
}

fn count(v: Option<i64>, field: &str) -> Result<i64, TransformError> {
    nonnegative(v.ok_or_else(|| TransformError::missing_metadata(field))?)
}

fn add(a: i64, b: i64) -> Result<i64, TransformError> {
    nonnegative(a)?
        .checked_add(nonnegative(b)?)
        .ok_or_else(|| TransformError::invalid_result("usage", "token overflow"))
}

pub(super) fn to_responses(
    input: g::UsageMetadata,
    facts: GeminiUsageFacts,
    report: &mut Report,
) -> Result<r::ResponseUsage, TransformError> {
    let prompt = add(
        count(input.prompt_token_count, "prompt_token_count")?,
        input.tool_use_prompt_token_count.unwrap_or(0),
    )?;
    let visible = count(input.candidates_token_count, "candidates_token_count")?;
    let total = count(input.total_token_count, "total_token_count")?;
    let output = total.checked_sub(prompt).ok_or_else(|| {
        TransformError::invalid_result("usage", "inconsistent input/output total")
    })?;
    let thinking = output - visible;

    let cached = count(
        input.cached_content_token_count.or(facts.cached_tokens),
        "cached_content_token_count actual fact",
    )?;
    let written = count(facts.cache_write_tokens, "cache_write_tokens actual fact")?;

    if input.prompt_tokens_details.is_some()
        || input.cache_tokens_details.is_some()
        || input.candidates_tokens_details.is_some()
        || input.tool_use_prompt_tokens_details.is_some()
    {
        report.omitted(
            "usage.modality_details",
            "Responses has no Gemini modality breakdown",
        );
    }
    Ok(r::ResponseUsage::builder(
        prompt,
        r::ResponseInputTokensDetails::builder(written, cached).build(),
        output,
        r::ResponseOutputTokensDetails::builder(thinking).build(),
        total,
    )
    .build())
}

pub(super) fn to_gemini(
    input: r::ResponseUsage,
    report: &mut Report,
) -> Result<g::UsageMetadata, TransformError> {
    let thinking = nonnegative(input.output_tokens_details.reasoning_tokens)?;
    let visible = input.output_tokens.checked_sub(thinking).ok_or_else(|| {
        TransformError::invalid_result("usage.reasoning", "reasoning exceeds output")
    })?;

    let mut out = g::UsageMetadata::builder().build();
    out.prompt_token_count = Some(input.input_tokens);
    out.candidates_token_count = Some(visible);
    out.thoughts_token_count = Some(thinking);
    out.total_token_count = Some(input.total_tokens);
    out.cached_content_token_count = Some(input.input_tokens_details.cached_tokens);
    report.omitted(
        "usage.cache_write_tokens",
        "Gemini has no distinct cache-write counter",
    );
    Ok(out)
}