gproxy-protocol 4.1.12

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

pub(crate) fn to_responses(
    input: Vec<g::Content>,
    flow: &mut IdentityFlow,
    policy: &TargetIdPolicy,
    report: &mut Report,
) -> Result<Vec<r::InputItem>, TransformError> {
    let mut out = Vec::new();
    let mut names: std::collections::BTreeMap<String, Vec<String>> =
        std::collections::BTreeMap::new();
    let mut calls = std::collections::BTreeMap::new();
    let mut index = 0u64;
    for content in input {
        let role = match content.role.as_deref() {
            None | Some("user") => r::MessageRole::User,
            Some("model") => r::MessageRole::Assistant,
            Some("system") => r::MessageRole::System,
            Some(_) => continue,
        };
        for part in content.parts.unwrap_or_default() {
            crate::transform::generate::gemini_controls::omitted(&part, report);
            if role == r::MessageRole::Assistant
                && let Some(blob) = &part.inline_data
            {
                let id = super::identity::id(
                    flow,
                    policy,
                    IdentityRole::Message,
                    IdentityRole::OutputItem(OutputItemKind::ImageGenerationCall),
                    None,
                    index,
                )?;
                index += 1;
                out.push(r::InputItem::ImageGenerationCall(
                    super::images::to_responses(blob.clone(), id, blob.data.len() as u64)?,
                ));
                if part.thought_signature.is_some() {
                    report.omitted(
                        "thought_signature",
                        "a Gemini signature has no meaning to a Responses upstream",
                    );
                }
                continue;
            }
            if part.thought == Some(true) {
                let id = super::identity::id(
                    flow,
                    policy,
                    IdentityRole::Message,
                    IdentityRole::OutputItem(OutputItemKind::Reasoning),
                    None,
                    index,
                )?;
                index += 1;
                let mut item =
                    r::ReasoningItem::builder(r::ReasoningItemType::ReasoningItem, id, Vec::new())
                        .build();
                item.content = part.text.map(|text| {
                    vec![
                        r::ReasoningContent::builder(r::ReasoningTextType::ReasoningText, text)
                            .build(),
                    ]
                });
                out.push(r::InputItem::Reasoning(item));
                report.omitted(
                    "thought_signature",
                    "a Gemini signature has no meaning to a Responses upstream",
                );
                continue;
            }
            if part.thought_signature.is_some() {
                report.omitted(
                    "thought_signature",
                    "a Gemini signature has no meaning to a Responses upstream",
                );
            }
            if let Some(text) = part.text {
                out.push(r::InputItem::Easy(
                    r::EasyInputMessage::builder(r::MessageContent::Text(text), role).build(),
                ));
            }
            for media in [
                part.inline_data.map(super::media::inline),
                part.file_data.map(super::media::file),
            ]
            .into_iter()
            .flatten()
            {
                if role != r::MessageRole::User {
                    continue;
                }
                let Some(media) = crate::transform::optional(media)? else {
                    continue;
                };
                out.push(r::InputItem::Easy(
                    r::EasyInputMessage::builder(r::MessageContent::Parts(vec![media]), role)
                        .build(),
                ));
            }
            if let Some(call) = part.function_call {
                if role != r::MessageRole::Assistant {
                    return Err(TransformError::shape(
                        "function_call.role",
                        "expected model role",
                    ));
                }
                if call.id.as_ref().is_some_and(|id| calls.contains_key(id)) {
                    return Err(TransformError::shape(
                        "function_call.id",
                        "duplicate source call",
                    ));
                }
                let id = super::identity::id(
                    flow,
                    policy,
                    IdentityRole::ToolCall,
                    IdentityRole::ToolCall,
                    call.id.clone(),
                    index,
                )?;
                let item_id = super::identity::id(
                    flow,
                    policy,
                    IdentityRole::ToolCall,
                    IdentityRole::OutputItem(OutputItemKind::FunctionCall),
                    call.id.clone(),
                    index,
                )?;
                index += 1;
                if let Some(source) = call.id {
                    calls.insert(source, id.clone());
                }
                names.entry(call.name.clone()).or_default().push(id.clone());
                out.push(r::InputItem::FunctionCall(
                    r::FunctionCall::builder(
                        r::FunctionCallType::FunctionCall,
                        serde_json::to_string(&call.args.unwrap_or_default())?,
                        id,
                        call.name,
                    )
                    .id(item_id)
                    .build(),
                ));
            }
            if let Some(result) = part.function_response {
                if result.scheduling.is_some() || result.will_continue == Some(true) {
                    report.omitted(
                        "function_response.scheduling/will_continue",
                        "controls have no target representation",
                    );
                }
                let id = if let Some(id) = result.id.as_ref() {
                    calls.get(id).cloned().ok_or_else(|| {
                        TransformError::missing_metadata("function_response call id")
                    })?
                } else {
                    let ids = names.get_mut(&result.name).ok_or_else(|| {
                        TransformError::missing_metadata("function_response call name")
                    })?;
                    if ids.len() != 1 {
                        return Err(TransformError::missing_metadata(
                            "ambiguous same-name function response",
                        ));
                    }
                    ids.remove(0)
                };
                let output = super::media::output(result)?;
                out.push(r::InputItem::FunctionCallOutput(
                    r::FunctionCallOutput::builder(
                        r::FunctionCallOutputType::FunctionCallOutput,
                        output,
                    )
                    .call_id(Some(id))
                    .build(),
                ));
            }
        }
    }
    Ok(out)
}