gproxy-protocol 1.0.20

Wire-format types and cross-protocol transforms for Claude, OpenAI, and Gemini LLM APIs.
Documentation
use crate::openai::create_chat_completions::response::OpenAiChatCompletionsResponse;
use crate::openai::create_chat_completions::stream::{
    ChatCompletionChunk, ChatCompletionChunkChoice, ChatCompletionChunkDelta,
    ChatCompletionChunkDeltaToolCall, ChatCompletionChunkDeltaToolCallType,
    ChatCompletionChunkObject, ChatCompletionFunctionCallDelta,
};
use crate::openai::create_chat_completions::types as ct;
use crate::transform::utils::TransformError;

fn to_stream_tool_call(
    tool_call: ct::ChatCompletionMessageToolCall,
    index: u32,
) -> ChatCompletionChunkDeltaToolCall {
    match tool_call {
        ct::ChatCompletionMessageToolCall::Function(call) => ChatCompletionChunkDeltaToolCall {
            index,
            id: Some(call.id),
            function: Some(ChatCompletionFunctionCallDelta {
                arguments: Some(call.function.arguments),
                name: Some(call.function.name),
            }),
            type_: Some(ChatCompletionChunkDeltaToolCallType::Function),
        },
        ct::ChatCompletionMessageToolCall::Custom(call) => ChatCompletionChunkDeltaToolCall {
            index,
            id: Some(call.id),
            function: Some(ChatCompletionFunctionCallDelta {
                arguments: Some(call.custom.input),
                name: Some(call.custom.name),
            }),
            type_: Some(ChatCompletionChunkDeltaToolCallType::Function),
        },
    }
}

impl TryFrom<OpenAiChatCompletionsResponse> for Vec<ChatCompletionChunk> {
    type Error = TransformError;

    fn try_from(value: OpenAiChatCompletionsResponse) -> Result<Self, TransformError> {
        match value {
            OpenAiChatCompletionsResponse::Success { body, .. } => {
                let mut chunks = Vec::new();

                for choice in body.choices {
                    let tool_calls = choice.message.tool_calls.map(|calls| {
                        calls
                            .into_iter()
                            .enumerate()
                            .map(|(tool_index, tool_call)| {
                                to_stream_tool_call(
                                    tool_call,
                                    u32::try_from(tool_index).unwrap_or(u32::MAX),
                                )
                            })
                            .collect::<Vec<_>>()
                    });

                    chunks.push(ChatCompletionChunk {
                        id: body.id.clone(),
                        choices: vec![ChatCompletionChunkChoice {
                            delta: ChatCompletionChunkDelta {
                                content: choice.message.content,
                                reasoning_content: choice.message.reasoning_content,
                                reasoning_details: choice.message.reasoning_details,
                                function_call: choice
                                    .message
                                    .function_call
                                    .map(ChatCompletionFunctionCallDelta::from),
                                refusal: choice.message.refusal,
                                role: Some(ct::ChatCompletionDeltaRole::Assistant),
                                annotations: choice.message.annotations,
                                tool_calls,
                                obfuscation: None,
                            },
                            finish_reason: Some(choice.finish_reason),
                            index: choice.index,
                            logprobs: choice.logprobs,
                        }],
                        created: body.created,
                        model: body.model.clone(),
                        object: ChatCompletionChunkObject::ChatCompletionChunk,
                        service_tier: body.service_tier.clone(),
                        system_fingerprint: body.system_fingerprint.clone(),
                        usage: None,
                    });
                }

                if chunks.is_empty() {
                    chunks.push(ChatCompletionChunk {
                        id: body.id.clone(),
                        choices: vec![ChatCompletionChunkChoice {
                            delta: ChatCompletionChunkDelta {
                                role: Some(ct::ChatCompletionDeltaRole::Assistant),
                                ..ChatCompletionChunkDelta::default()
                            },
                            finish_reason: Some(ct::ChatCompletionFinishReason::Stop),
                            index: 0,
                            logprobs: None,
                        }],
                        created: body.created,
                        model: body.model.clone(),
                        object: ChatCompletionChunkObject::ChatCompletionChunk,
                        service_tier: body.service_tier.clone(),
                        system_fingerprint: body.system_fingerprint.clone(),
                        usage: body.usage.clone(),
                    });
                } else if let Some(usage) = body.usage.clone()
                    && let Some(chunk) = chunks.last_mut()
                {
                    chunk.usage = Some(usage);
                }

                Ok(chunks)
            }
            OpenAiChatCompletionsResponse::Error { .. } => Err(TransformError::not_implemented(
                "cannot convert OpenAI chat error response to SSE stream body",
            )),
        }
    }
}