gproxy-protocol 4.0.2

GPROXY v4 connection modelling, operation declarations, vendor wire types and pairwise conversion
Documentation
use crate::{
    transform::{
        TransformError,
        identity::{IdentityFlow, IdentityRole, SourceIdentity, TargetIdPolicy},
    },
    wire::openai::chat::{content as c, response as r, stream as s},
};
use std::collections::BTreeMap;

#[derive(Default)]
struct FunctionAccum {
    name: String,
    args: String,
    id: Option<String>,
}

impl FunctionAccum {
    fn push(&mut self, function: s::DeltaFunctionCall) {
        if let Some(Some(v)) = function.name {
            self.name.push_str(&v);
        }
        if let Some(Some(v)) = function.arguments {
            self.args.push_str(&v);
        }
    }
    fn finish(self) -> Result<c::FunctionCall, TransformError> {
        if self.name.is_empty() {
            return Err(TransformError::missing_metadata("tool.function.name"));
        }
        Ok(c::FunctionCall::builder(self.args, self.name).build())
    }
}

#[derive(Default)]
pub(super) struct ChoiceAccum {
    role: Option<s::DeltaRole>,
    content: Option<String>,
    reasoning_content: Option<Option<String>>,
    reasoning: Option<Option<String>>,
    reasoning_details: Option<Option<Vec<crate::wire::openai::chat::ReasoningDetail>>>,
    refusal: Option<String>,
    legacy: Option<FunctionAccum>,
    tools: BTreeMap<i64, FunctionAccum>,
    finish: Option<r::FinishReason>,
    logs: Option<r::Logprobs>,
}

impl ChoiceAccum {
    pub(super) fn is_finished(&self) -> bool {
        self.finish.is_some()
    }
    pub(super) fn push(&mut self, choice: s::StreamChoice) -> Result<(), TransformError> {
        if self.finish.is_some() {
            return Err(TransformError::invalid_result(
                "choice",
                "delta or duplicate finish after choice terminal",
            ));
        }
        let d = choice.delta;
        if let Some(Some(role)) = d.role {
            if role != s::DeltaRole::Assistant {
                return Err(TransformError::invalid_result(
                    "delta.role",
                    "Chat response role must be assistant",
                ));
            }
            self.role = Some(role);
        }
        for (target, source) in [
            (&mut self.reasoning_content, d.reasoning_content),
            (&mut self.reasoning, d.reasoning),
        ] {
            match source {
                Some(Some(text)) => target
                    .get_or_insert(None)
                    .get_or_insert_with(String::new)
                    .push_str(&text),
                Some(None) if target.is_none() => *target = Some(None),
                _ => {}
            }
        }
        if let Some(details) = d.reasoning_details {
            match details {
                Some(details) => {
                    let target = self
                        .reasoning_details
                        .get_or_insert(None)
                        .get_or_insert_default();
                    crate::transform::generate::reasoning_details::merge(target, details)?;
                }
                None if self.reasoning_details.is_none() => self.reasoning_details = Some(None),
                None => {}
            }
        }
        if let Some(Some(text)) = d.content {
            self.content.get_or_insert_with(String::new).push_str(&text);
        }
        if let Some(Some(text)) = d.refusal {
            self.refusal.get_or_insert_with(String::new).push_str(&text);
        }
        if let Some(Some(call)) = d.function_call {
            self.legacy.get_or_insert_with(Default::default).push(call);
        }
        if let Some(Some(calls)) = d.tool_calls {
            for call in calls {
                if call.index < 0 {
                    return Err(TransformError::invalid_result(
                        "tool.index",
                        "negative tool index",
                    ));
                }
                let state = self.tools.entry(call.index).or_default();
                if let Some(Some(id)) = call.id {
                    if id.is_empty() || state.id.as_ref().is_some_and(|v| v != &id) {
                        return Err(TransformError::invalid_result(
                            "tool.id",
                            "empty or conflicting tool ID",
                        ));
                    }
                    state.id = Some(id);
                }
                if let Some(Some(function)) = call.function {
                    state.push(function);
                }
            }
        }
        if self.legacy.is_some() && !self.tools.is_empty() {
            return Err(TransformError::invalid_result(
                "tool_calls",
                "legacy and modern tool deltas conflict",
            ));
        }
        if let Some(Some(logs)) = choice.logprobs {
            super::logs::append(&mut self.logs, logs)?;
        }
        if let Some(Some(finish)) = choice.finish_reason {
            self.finish = Some(finish);
        }
        Ok(())
    }
    pub(super) fn finish(
        self,
        index: i64,
        ordinal: &mut u64,
        flow: &mut IdentityFlow,
        policy: &TargetIdPolicy,
    ) -> Result<r::Choice, TransformError> {
        let finish = self
            .finish
            .ok_or_else(|| TransformError::missing_metadata("finish_reason"))?;
        if (finish == r::FinishReason::ToolCalls && self.tools.is_empty())
            || (finish == r::FinishReason::FunctionCall && self.legacy.is_none())
            || (finish == r::FinishReason::Stop
                && (!self.tools.is_empty() || self.legacy.is_some()))
        {
            return Err(TransformError::invalid_result(
                "finish_reason",
                "tool payload conflicts with finish reason",
            ));
        }
        let mut tools = Vec::new();
        for (expected, (tool_index, state)) in self.tools.into_iter().enumerate() {
            if tool_index != expected as i64 {
                return Err(TransformError::invalid_result(
                    "tool.index",
                    "non-contiguous tool indexes",
                ));
            }
            let id = flow
                .resolve_or_allocate(
                    IdentityRole::ToolCall,
                    SourceIdentity::new(crate::Dialect::OpenAiChat, state.id.clone(), *ordinal),
                    policy,
                )
                .map_err(|e| TransformError::invalid_result("identity", e.to_string()))?
                .emitted_id;
            *ordinal = ordinal
                .checked_add(1)
                .ok_or_else(|| super::limit("tool_calls"))?;
            tools.push(c::MessageToolCall::Function(
                c::ChatToolCall::builder(id, state.finish()?, c::ChatToolCallType::Function)
                    .build(),
            ));
        }
        let mut message =
            r::ResponseMessage::builder(self.content, self.refusal, r::ResponseRole::Assistant)
                .build();
        message.reasoning_details = self.reasoning_details;
        message.reasoning_content = self.reasoning_content;
        message.reasoning = self.reasoning;
        message.function_call = self
            .legacy
            .map(FunctionAccum::finish)
            .map(crate::transform::optional)
            .transpose()?
            .flatten();
        if !tools.is_empty() {
            message.tool_calls = Some(tools);
        }
        Ok(r::Choice::builder(finish, index, self.logs, message).build())
    }
}