gproxy-protocol 4.1.11

GPROXY v4 connection modelling, operation declarations, vendor wire types and pairwise conversion
Documentation
use crate::{
    transform::{Report, TransformError},
    wire::openai::{
        chat as c,
        responses::{generate as r, input as i},
    },
};

fn number(
    value: Option<Option<f64>>,
) -> Result<Option<Option<serde_json::Number>>, TransformError> {
    Ok(match value {
        Some(Some(v)) => serde_json::Number::from_f64(v).map(Some),
        Some(None) => Some(None),
        None => None,
    })
}

fn float(
    value: &Option<Option<serde_json::Number>>,
) -> Result<Option<Option<f64>>, TransformError> {
    Ok(match value {
        Some(Some(v)) => v.as_f64().map(Some),
        Some(None) => Some(None),
        None => None,
    })
}

pub(super) fn to_responses(
    input: &c::GenerateContentRequestBody,
    out: &mut r::GenerateContentRequestBody,
    report: &mut Report,
) -> Result<(), TransformError> {
    for (present, field) in [
        (input.n.flatten().is_some_and(|n| n != 1), "n"),
        (
            input.stop.as_ref().and_then(Option::as_ref).is_some(),
            "stop",
        ),
        (
            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.frequency_penalty.flatten().is_some_and(|v| v != 0.0),
            "frequency_penalty",
        ),
        (
            input.presence_penalty.flatten().is_some_and(|v| v != 0.0),
            "presence_penalty",
        ),
        (
            input
                .logit_bias
                .as_ref()
                .and_then(Option::as_ref)
                .is_some_and(|v| !v.is_empty()),
            "logit_bias",
        ),
        (
            input.prediction.as_ref().and_then(Option::as_ref).is_some(),
            "prediction",
        ),
        (input.seed.flatten().is_some(), "seed"),
        (input.web_search_options.is_some(), "web_search_options"),
    ] {
        if present {
            report.omitted(field, "field has no target representation");
        }
    }
    out.max_output_tokens = match (input.max_completion_tokens, input.max_tokens) {
        (Some(Some(a)), _) => Some(Some(a)),
        (_, Some(Some(b))) => Some(Some(b)),
        (a, b) => a.or(b),
    };
    out.parallel_tool_calls = input.parallel_tool_calls.map(Some);
    out.temperature = number(input.temperature)?;
    out.top_p = number(input.top_p)?;
    out.top_logprobs = input.top_logprobs;
    if input.logprobs.flatten() == Some(true) {
        out.include = Some(Some(vec![r::ResponseIncludable::OutputLogprobs]));
    }
    out.service_tier = input.service_tier.map(|v| {
        v.map(|v| match v {
            c::ServiceTier::Auto => r::ServiceTier::Auto,
            c::ServiceTier::Default => r::ServiceTier::Default,
            c::ServiceTier::Flex => r::ServiceTier::Flex,
            c::ServiceTier::Scale => r::ServiceTier::Scale,
            c::ServiceTier::Priority => r::ServiceTier::Priority,
            c::ServiceTier::Fast => r::ServiceTier::Fast,
            c::ServiceTier::Ultrafast => r::ServiceTier::Ultrafast,
        })
    });
    out.reasoning = input.reasoning_effort.map(|effort| {
        let mut config = i::ReasoningConfig::builder().build();
        config.effort = Some(effort.map(|v| match v {
            c::ReasoningEffort::None => i::ReasoningEffort::None,
            c::ReasoningEffort::Minimal => i::ReasoningEffort::Minimal,
            c::ReasoningEffort::Low => i::ReasoningEffort::Low,
            c::ReasoningEffort::Medium => i::ReasoningEffort::Medium,
            c::ReasoningEffort::High => i::ReasoningEffort::High,
            c::ReasoningEffort::XHigh => i::ReasoningEffort::Xhigh,
            c::ReasoningEffort::Max => i::ReasoningEffort::Max,
        }));
        Some(config)
    });
    if input.response_format.is_some() || input.verbosity.is_some() {
        let mut text = i::TextConfig::builder().build();
        text.verbosity = input.verbosity.map(|v| {
            v.map(|v| match v {
                c::Verbosity::Low => i::TextVerbosity::Low,
                c::Verbosity::Medium => i::TextVerbosity::Medium,
                c::Verbosity::High => i::TextVerbosity::High,
            })
        });
        text.format = input
            .response_format
            .as_ref()
            .map(|format| {
                Ok::<_, TransformError>(match format {
                    c::ResponseFormat::Text(_) => {
                        i::TextFormat::Text(i::TextFormatText::builder().build())
                    }
                    c::ResponseFormat::JsonObject(_) => {
                        i::TextFormat::JsonObject(i::TextFormatJsonObject::builder().build())
                    }
                    c::ResponseFormat::JsonSchema(format) => {
                        let mut value = i::TextFormatJsonSchema::builder(
                            format.json_schema.name.clone(),
                            format.json_schema.schema.clone().ok_or_else(|| {
                                TransformError::shape("response_format.schema", "missing schema")
                            })?,
                        )
                        .build();
                        value.description = format.json_schema.description.clone();
                        value.strict = format.json_schema.strict;
                        i::TextFormat::JsonSchema(value)
                    }
                })
            })
            .map(crate::transform::optional)
            .transpose()?
            .flatten();
        out.text = Some(text);
    }
    if let Some(options) = input.stream_options.as_ref() {
        out.stream_options = Some(options.as_ref().map(|options| {
            let mut value = r::StreamOptions::builder().build();
            value.include_obfuscation = options.include_obfuscation;
            value
        }));
        if options.as_ref().is_some_and(|v| v.include_usage.is_some()) {
            report.omitted("stream_options.include_usage","Responses streams carry usage through terminal response; host must retain Chat delivery preference");
        }
    }
    super::shared_controls::to_responses(input, out)?;
    Ok(())
}

pub(super) fn to_chat(
    input: &r::GenerateContentRequestBody,
    out: &mut c::GenerateContentRequestBody,
    report: &mut Report,
) -> Result<(), TransformError> {
    for (present, field) in [
        (
            input
                .previous_response_id
                .as_ref()
                .and_then(Option::as_ref)
                .is_some(),
            "previous_response_id",
        ),
        (
            input
                .conversation
                .as_ref()
                .and_then(Option::as_ref)
                .is_some(),
            "conversation",
        ),
        (
            input.prompt.as_ref().and_then(Option::as_ref).is_some(),
            "prompt",
        ),
    ] {
        if present {
            report.omitted(
                format!("{field} resolved history"),
                "field has no target representation",
            );
        }
    }
    for (present, field) in [
        (input.background.flatten() == Some(true), "background"),
        (
            input
                .context_management
                .as_ref()
                .and_then(Option::as_ref)
                .is_some(),
            "context_management",
        ),
        (input.max_tool_calls.flatten().is_some(), "max_tool_calls"),
        (
            input.truncation.flatten() == Some(r::GenerateTruncation::Auto),
            "truncation",
        ),
    ] {
        if present {
            report.omitted(field, "field has no target representation");
        }
    }
    out.max_completion_tokens = input.max_output_tokens;
    out.parallel_tool_calls = input.parallel_tool_calls.flatten();
    out.temperature = float(&input.temperature)?;
    out.top_p = float(&input.top_p)?;
    out.top_logprobs = input.top_logprobs;
    out.service_tier = input.service_tier.map(|v| {
        v.map(|v| match v {
            r::ServiceTier::Auto => c::ServiceTier::Auto,
            r::ServiceTier::Default => c::ServiceTier::Default,
            r::ServiceTier::Flex => c::ServiceTier::Flex,
            r::ServiceTier::Scale => c::ServiceTier::Scale,
            r::ServiceTier::Priority => c::ServiceTier::Priority,
            r::ServiceTier::Fast => c::ServiceTier::Fast,
            r::ServiceTier::Ultrafast => c::ServiceTier::Ultrafast,
        })
    });
    if let Some(config) = input.reasoning.as_ref().and_then(Option::as_ref) {
        if config.mode.is_some()
            || config.summary.flatten().is_some()
            || config.generate_summary.flatten().is_some()
            || config.context.as_ref().and_then(Option::as_ref).is_some()
        {
            report.omitted("reasoning", "field has no target representation");
        }
        out.reasoning_effort = match config.effort.flatten() {
            Some(i::ReasoningEffort::Numeric(_)) => {
                report.omitted("reasoning.effort", "Chat has no numeric effort control");
                None
            }
            _ => config.effort.map(|v| {
                v.map(|v| match v {
                    i::ReasoningEffort::None => c::ReasoningEffort::None,
                    i::ReasoningEffort::Minimal => c::ReasoningEffort::Minimal,
                    i::ReasoningEffort::Low => c::ReasoningEffort::Low,
                    i::ReasoningEffort::Medium => c::ReasoningEffort::Medium,
                    i::ReasoningEffort::High => c::ReasoningEffort::High,
                    i::ReasoningEffort::Xhigh => c::ReasoningEffort::XHigh,
                    i::ReasoningEffort::Max => c::ReasoningEffort::Max,
                    i::ReasoningEffort::Numeric(_) => unreachable!(),
                })
            }),
        };
    }
    if let Some(text) = &input.text {
        out.verbosity = text.verbosity.map(|v| {
            v.map(|v| match v {
                i::TextVerbosity::Low => c::Verbosity::Low,
                i::TextVerbosity::Medium => c::Verbosity::Medium,
                i::TextVerbosity::High => c::Verbosity::High,
            })
        });
        out.response_format = text.format.as_ref().map(|format| match format {
            i::TextFormat::Text(_) => c::ResponseFormat::Text(
                c::TextResponseFormat::builder(c::TextResponseType::Text).build(),
            ),
            i::TextFormat::JsonObject(_) => c::ResponseFormat::JsonObject(
                c::JsonObjectResponseFormat::builder(c::JsonObjectResponseType::JsonObject).build(),
            ),
            i::TextFormat::JsonSchema(schema) => {
                let mut value = c::JsonSchemaFormat::builder(schema.name.clone()).build();
                value.schema = Some(schema.schema.clone());
                value.strict = schema.strict;
                value.description = schema.description.clone();
                c::ResponseFormat::JsonSchema(
                    c::JsonSchemaResponseFormat::builder(
                        c::JsonSchemaResponseType::JsonSchema,
                        value,
                    )
                    .build(),
                )
            }
        });
    }
    if let Some(options) = &input.stream_options {
        out.stream_options = Some(options.as_ref().map(|options| {
            let mut value = c::StreamOptions::builder().build();
            value.include_obfuscation = options.include_obfuscation;
            value
        }));
    }
    if let Some(Some(include)) = &input.include {
        for field in include {
            if matches!(field, r::ResponseIncludable::OutputLogprobs) {
                out.logprobs = Some(Some(true));
            } else {
                report.omitted(
                    "include",
                    "Responses supplementary item fields have no Chat equivalent",
                );
            }
        }
    }
    super::shared_controls::to_chat(input, out)?;
    Ok(())
}