gproxy-protocol 4.1.14

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

pub(super) fn to_chat(
    input: &g::GenerateContentRequestBody,
    out: &mut c::GenerateContentRequestBody,
    report: &mut Report,
) -> Result<(), TransformError> {
    if input.continuation_token.is_some() {
        return Err(super::super::gemini_controls::continuation_error());
    }
    for (present, field) in [
        (input.cached_content.is_some(), "cached_content"),
        (input.labels.is_some(), "labels"),
        (input.safety_settings.is_some(), "safety_settings"),
        (input.service_tier.is_some(), "service_tier"),
    ] {
        if present {
            report.omitted(field, "field has no target representation");
        }
    }
    out.store = input.store.map(Some);
    if let Some(config) = &input.generation_config {
        for (present, field) in [
            (config.top_k.is_some(), "top_k"),
            (config.speech_config.is_some(), "speech_config"),
            (
                config.enable_affective_dialog.is_some(),
                "enable_affective_dialog",
            ),
            (config.translation_config.is_some(), "translation_config"),
            (
                config.audio_transcription_config.is_some(),
                "audio_transcription_config",
            ),
            (config.image_config.is_some(), "image_config"),
            (config.media_resolution.is_some(), "media_resolution"),
            (config.response_format.is_some(), "response_format"),
            (
                config.enable_enhanced_civic_answers.is_some(),
                "enable_enhanced_civic_answers",
            ),
            (
                config
                    .response_modalities
                    .as_ref()
                    .is_some_and(|m| m.iter().any(|m| !matches!(m, g::Modality::Text))),
                "response_modalities",
            ),
        ] {
            if present {
                report.omitted(field, "field has no target representation");
            }
        }
        out.max_completion_tokens = config.max_output_tokens.map(Some);
        out.n = config.candidate_count.map(Some);
        out.temperature = config.temperature.map(Some);
        out.top_p = config.top_p.map(Some);
        out.seed = config.seed.map(Some);
        out.presence_penalty = config.presence_penalty.map(Some);
        out.frequency_penalty = config.frequency_penalty.map(Some);
        out.logprobs = config.response_logprobs.map(Some);
        out.top_logprobs = config.logprobs.map(Some);
        out.stop = config
            .stop_sequences
            .clone()
            .map(c::Stop::Sequences)
            .map(Some);
        if let Some(thinking) = &config.thinking_config {
            if thinking.include_thoughts == Some(true) {
                report.omitted(
                    "thinking.include_thoughts",
                    "field has no target representation",
                );
            }
            out.reasoning_effort = match thinking.thinking_level {
                Some(g::ThinkingLevel::Minimal) => Some(Some(c::ReasoningEffort::Minimal)),
                Some(g::ThinkingLevel::Low) => Some(Some(c::ReasoningEffort::Low)),
                Some(g::ThinkingLevel::Medium) => Some(Some(c::ReasoningEffort::Medium)),
                Some(g::ThinkingLevel::High) => Some(Some(c::ReasoningEffort::High)),
                _ if thinking.thinking_budget == Some(0) => Some(Some(c::ReasoningEffort::None)),
                _ => None,
            };
            if thinking.thinking_budget.is_some_and(|n| n != 0) {
                report.omitted("thinking_budget", "Chat has no token budget control");
            }
        }
        let typed = config
            .response_schema
            .as_ref()
            .map(super::super::gemini_schema::to_json)
            .map(crate::transform::optional)
            .transpose()?
            .flatten();
        if let Some(typed) = &typed {
            report.diagnostics.extend(typed.report.diagnostics.clone());
        }
        let raw = config
            .response_json_schema
            .as_ref()
            .or(config.response_json_schema_internal.as_ref());

        let raw = raw.and_then(|value| value.as_object().cloned());
        let schema = typed.map(|v| v.value).or(raw);
        out.response_format = if let Some(schema) = schema {
            let mut value = c::JsonSchemaFormat::builder("gemini_response".into()).build();
            value.schema = Some(schema);
            value.strict = Some(Some(true));
            Some(c::ResponseFormat::JsonSchema(
                c::JsonSchemaResponseFormat::builder(c::JsonSchemaResponseType::JsonSchema, value)
                    .build(),
            ))
        } else {
            match config.response_mime_type.as_deref() {
                None | Some("text/plain") => None,
                Some("application/json") => Some(c::ResponseFormat::JsonObject(
                    c::JsonObjectResponseFormat::builder(c::JsonObjectResponseType::JsonObject)
                        .build(),
                )),
                Some(_) => {
                    report.omitted("response_mime_type", "Chat has no matching MIME control");
                    None
                }
            }
        };
    }
    if let Some(config) = &input.tool_config {
        if config.retrieval_config.is_some()
            || config.include_server_side_tool_invocations == Some(true)
        {
            report.omitted("tool_config", "field has no target representation");
        }
        if let Some(function) = &config.function_calling_config {
            let mode = function
                .mode
                .clone()
                .unwrap_or(g::FunctionCallingMode::Auto);
            out.tool_choice = match mode {
                g::FunctionCallingMode::None => Some(c::ToolChoice::Mode(c::ToolChoiceMode::None)),
                g::FunctionCallingMode::Unspecified => None,
                mode => Some(if let Some(names) = &function.allowed_function_names {
                    c::ToolChoice::Allowed(c::AllowedToolChoice::builder(
                        c::AllowedToolChoiceType::AllowedTools,
                        c::AllowedTools::builder(
                            if mode == g::FunctionCallingMode::Any { c::AllowedToolsMode::Required }
                            else { c::AllowedToolsMode::Auto },
                            names.iter().map(|name| serde_json::json!({"type":"function","function":{"name":name}}).as_object().unwrap().clone()).collect(),
                        ).build(),
                    ).build())
                } else {
                    c::ToolChoice::Mode(if mode == g::FunctionCallingMode::Any {
                        c::ToolChoiceMode::Required
                    } else {
                        c::ToolChoiceMode::Auto
                    })
                }),
            };
        }
    }
    Ok(())
}

pub(super) fn to_gemini(
    input: &c::GenerateContentRequestBody,
    out: &mut g::GenerateContentRequestBody,
    report: &mut Report,
) -> Result<(), TransformError> {
    for (present, field) in [
        (
            input.audio.as_ref().and_then(Option::as_ref).is_some(),
            "audio",
        ),
        (
            input
                .modalities
                .as_ref()
                .and_then(Option::as_ref)
                .is_some_and(|m| m.iter().any(|m| !matches!(m, c::Modality::Text))),
            "modalities",
        ),
        (
            input
                .logit_bias
                .as_ref()
                .and_then(Option::as_ref)
                .is_some_and(|m| !m.is_empty()),
            "logit_bias",
        ),
        (
            input.prediction.as_ref().and_then(Option::as_ref).is_some(),
            "prediction",
        ),
        (
            input.moderation.as_ref().and_then(Option::as_ref).is_some(),
            "moderation",
        ),
        (input.service_tier.flatten().is_some(), "service_tier"),
        (input.verbosity.flatten().is_some(), "verbosity"),
        (input.web_search_options.is_some(), "web_search_options"),
        (
            input.parallel_tool_calls == Some(false),
            "parallel_tool_calls",
        ),
    ] {
        if present {
            report.omitted(field, "field has no target representation");
        }
    }

    let mut config = g::GenerationConfig::builder().build();
    config.max_output_tokens = input
        .max_completion_tokens
        .flatten()
        .or(input.max_tokens.flatten());
    config.candidate_count = input.n.flatten();
    config.temperature = input.temperature.flatten();
    config.top_p = input.top_p.flatten();
    config.seed = input.seed.flatten();
    config.presence_penalty = input.presence_penalty.flatten();
    config.frequency_penalty = input.frequency_penalty.flatten();
    config.response_logprobs = input.logprobs.flatten();
    config.logprobs = input.top_logprobs.flatten();
    config.stop_sequences = input
        .stop
        .as_ref()
        .and_then(Option::as_ref)
        .map(|v| match v {
            c::Stop::String(v) => vec![v.clone()],
            c::Stop::Sequences(v) => v.clone(),
        });
    if let Some(effort) = input.reasoning_effort.flatten() {
        let mut thinking = g::ThinkingConfig::builder().build();
        match effort {
            c::ReasoningEffort::None => thinking.thinking_budget = Some(0),
            c::ReasoningEffort::Minimal => {
                thinking.thinking_level = Some(g::ThinkingLevel::Minimal)
            }
            c::ReasoningEffort::Low => thinking.thinking_level = Some(g::ThinkingLevel::Low),
            c::ReasoningEffort::Medium => thinking.thinking_level = Some(g::ThinkingLevel::Medium),
            c::ReasoningEffort::High => thinking.thinking_level = Some(g::ThinkingLevel::High),
            c::ReasoningEffort::XHigh | c::ReasoningEffort::Max => {
                report.omitted("reasoning_effort", "Gemini has no matching effort");
            }
        }
        config.thinking_config = Some(thinking);
    }
    if let Some(format) = &input.response_format {
        match format {
            c::ResponseFormat::Text(_) => config.response_mime_type = Some("text/plain".into()),
            c::ResponseFormat::JsonObject(_) => {
                config.response_mime_type = Some("application/json".into())
            }
            c::ResponseFormat::JsonSchema(format) => {
                if let Some(schema) = format.json_schema.schema.as_ref() {
                    config.response_mime_type = Some("application/json".into());
                    config.response_json_schema = Some(serde_json::Value::Object(schema.clone()));
                    report.changed("response_format", "schema mapped to Gemini raw JSON schema");
                }
            }
        }
    }

    if let Some(choice) = &input.function_call {
        let mut function = g::FunctionCallingConfig::builder().build();
        match choice {
            c::FunctionCallChoice::Mode(c::FunctionCallMode::Auto) => {
                function.mode = Some(g::FunctionCallingMode::Auto)
            }
            c::FunctionCallChoice::Mode(c::FunctionCallMode::None) => {
                function.mode = Some(g::FunctionCallingMode::None)
            }
            c::FunctionCallChoice::Name(choice) => {
                function.mode = Some(g::FunctionCallingMode::Any);
                function.allowed_function_names = Some(vec![choice.name.clone()]);
            }
        }
        out.tool_config = Some(
            g::ToolConfig::builder()
                .function_calling_config(function)
                .build(),
        );
    }
    if let Some(choice) = &input.tool_choice {
        let mut function = g::FunctionCallingConfig::builder().build();
        match choice {
            c::ToolChoice::Mode(mode) => {
                function.mode = Some(match mode {
                    c::ToolChoiceMode::Auto => g::FunctionCallingMode::Auto,
                    c::ToolChoiceMode::Required => g::FunctionCallingMode::Any,
                    c::ToolChoiceMode::None => g::FunctionCallingMode::None,
                })
            }
            c::ToolChoice::Function(choice) => {
                function.mode = Some(g::FunctionCallingMode::Any);
                function.allowed_function_names = Some(vec![choice.function.name.clone()]);
            }
            c::ToolChoice::Allowed(choice) => {
                function.mode = Some(match choice.allowed_tools.mode {
                    c::AllowedToolsMode::Auto => g::FunctionCallingMode::Auto,
                    c::AllowedToolsMode::Required => g::FunctionCallingMode::Any,
                });
                function.allowed_function_names = Some(
                    choice
                        .allowed_tools
                        .tools
                        .iter()
                        .filter_map(|v| {
                            v.get("function")
                                .and_then(|v| v.get("name"))
                                .and_then(|v| v.as_str())
                                .map(str::to_owned)
                        })
                        .collect(),
                );
            }
            c::ToolChoice::Custom(_) => {
                report.omitted("tool_choice.custom", "Gemini has no custom tool selector");
            }
        }
        out.tool_config = Some(
            g::ToolConfig::builder()
                .function_calling_config(function)
                .build(),
        );
    }
    for (present, field) in [
        (input.metadata.is_some(), "metadata"),
        (input.user.is_some(), "user"),
        (input.prompt_cache_key.is_some(), "prompt_cache_key"),
        (input.prompt_cache_options.is_some(), "prompt_cache_options"),
        (
            input.prompt_cache_retention.is_some(),
            "prompt_cache_retention",
        ),
        (input.safety_identifier.is_some(), "safety_identifier"),
        (input.stream.is_some(), "stream"),
        (input.stream_options.is_some(), "stream_options"),
    ] {
        if present {
            report.omitted(
                field,
                "Gemini host must retain request metadata/delivery preference; no body equivalent",
            );
        }
    }
    out.store = input.store.flatten();
    out.generation_config = Some(config);
    Ok(())
}