gproxy-protocol 1.0.20

Wire-format types and cross-protocol transforms for Claude, OpenAI, and Gemini LLM APIs.
Documentation
use crate::openai::count_tokens::types as ot;
use crate::openai::create_chat_completions::types as ct;

fn chat_image_detail_to_response(
    detail: Option<ct::ChatCompletionImageDetail>,
) -> Option<ot::ResponseInputImageDetail> {
    detail.map(|value| match value {
        ct::ChatCompletionImageDetail::Auto => ot::ResponseInputImageDetail::Auto,
        ct::ChatCompletionImageDetail::Low => ot::ResponseInputImageDetail::Low,
        ct::ChatCompletionImageDetail::High => ot::ResponseInputImageDetail::High,
        ct::ChatCompletionImageDetail::Original => ot::ResponseInputImageDetail::Original,
    })
}

fn chat_content_part_to_response_input_content(
    part: ct::ChatCompletionContentPart,
) -> ot::ResponseInputContent {
    match part {
        ct::ChatCompletionContentPart::Text(part) => {
            ot::ResponseInputContent::Text(ot::ResponseInputText {
                text: part.text,
                type_: ot::ResponseInputTextType::InputText,
            })
        }
        ct::ChatCompletionContentPart::Image(part) => {
            let (file_id, image_url) = if part.image_url.url.starts_with("file:") {
                (
                    Some(part.image_url.url.trim_start_matches("file:").to_string()),
                    None,
                )
            } else {
                (None, Some(part.image_url.url))
            };
            ot::ResponseInputContent::Image(ot::ResponseInputImage {
                detail: chat_image_detail_to_response(part.image_url.detail),
                type_: ot::ResponseInputImageType::InputImage,
                file_id,
                image_url,
            })
        }
        ct::ChatCompletionContentPart::InputAudio(part) => {
            let filename = match part.input_audio.format {
                ct::ChatCompletionInputAudioFormat::Wav => Some("audio.wav".to_string()),
                ct::ChatCompletionInputAudioFormat::Mp3 => Some("audio.mp3".to_string()),
            };
            ot::ResponseInputContent::File(ot::ResponseInputFile {
                type_: ot::ResponseInputFileType::InputFile,
                detail: None,
                file_data: Some(part.input_audio.data),
                file_id: None,
                file_url: None,
                filename,
            })
        }
        ct::ChatCompletionContentPart::File(part) => {
            ot::ResponseInputContent::File(ot::ResponseInputFile {
                type_: ot::ResponseInputFileType::InputFile,
                detail: None,
                file_data: part.file.file_data,
                file_id: part.file.file_id,
                file_url: part.file.file_url,
                filename: part.file.filename,
            })
        }
    }
}

pub fn chat_text_content_to_response_input_message_content(
    content: ct::ChatCompletionTextContent,
) -> ot::ResponseInputMessageContent {
    match content {
        ct::ChatCompletionTextContent::Text(text) => ot::ResponseInputMessageContent::Text(text),
        ct::ChatCompletionTextContent::Parts(parts) => ot::ResponseInputMessageContent::List(
            parts
                .into_iter()
                .map(|part| {
                    ot::ResponseInputContent::Text(ot::ResponseInputText {
                        text: part.text,
                        type_: ot::ResponseInputTextType::InputText,
                    })
                })
                .collect::<Vec<_>>(),
        ),
    }
}

pub fn chat_text_content_to_plain_text(content: &ct::ChatCompletionTextContent) -> String {
    match content {
        ct::ChatCompletionTextContent::Text(text) => text.clone(),
        ct::ChatCompletionTextContent::Parts(parts) => parts
            .iter()
            .map(|part| part.text.clone())
            .filter(|text| !text.is_empty())
            .collect::<Vec<_>>()
            .join("\n"),
    }
}

pub fn chat_user_content_to_response_input_message_content(
    content: ct::ChatCompletionUserContent,
) -> ot::ResponseInputMessageContent {
    match content {
        ct::ChatCompletionUserContent::Text(text) => ot::ResponseInputMessageContent::Text(text),
        ct::ChatCompletionUserContent::Parts(parts) => ot::ResponseInputMessageContent::List(
            parts
                .into_iter()
                .map(chat_content_part_to_response_input_content)
                .collect::<Vec<_>>(),
        ),
    }
}

fn chat_custom_tool_format_to_response(
    format: Option<ct::ChatCompletionCustomToolFormat>,
) -> Option<ot::ResponseCustomToolInputFormat> {
    match format {
        Some(ct::ChatCompletionCustomToolFormat::Text(_)) => Some(
            ot::ResponseCustomToolInputFormat::Text(ot::ResponseCustomToolTextFormat {
                type_: ot::ResponseCustomToolTextFormatType::Text,
            }),
        ),
        Some(ct::ChatCompletionCustomToolFormat::Grammar(grammar)) => Some(
            ot::ResponseCustomToolInputFormat::Grammar(ot::ResponseCustomToolGrammarFormat {
                definition: grammar.grammar.definition,
                syntax: match grammar.grammar.syntax {
                    ct::ChatCompletionCustomToolGrammarSyntax::Lark => {
                        ot::ResponseCustomToolGrammarSyntax::Lark
                    }
                    ct::ChatCompletionCustomToolGrammarSyntax::Regex => {
                        ot::ResponseCustomToolGrammarSyntax::Regex
                    }
                },
                type_: ot::ResponseCustomToolGrammarFormatType::Grammar,
            }),
        ),
        None => None,
    }
}

pub fn chat_tools_to_response_tools(
    tools: Option<Vec<ct::ChatCompletionTool>>,
    functions: Option<Vec<ct::ChatCompletionLegacyFunction>>,
    web_search_options: Option<ct::ChatCompletionWebSearchOptions>,
) -> Option<Vec<ot::ResponseTool>> {
    let mut output = Vec::new();

    if let Some(tools) = tools {
        for tool in tools {
            match tool {
                ct::ChatCompletionTool::Function(tool) => {
                    output.push(ot::ResponseTool::Function(ot::ResponseFunctionTool {
                        name: tool.function.name,
                        parameters: tool.function.parameters.unwrap_or_default(),
                        strict: tool.function.strict,
                        type_: ot::ResponseFunctionToolType::Function,
                        defer_loading: None,
                        description: tool.function.description,
                    }));
                }
                ct::ChatCompletionTool::Custom(tool) => {
                    output.push(ot::ResponseTool::Custom(ot::ResponseCustomTool {
                        name: tool.custom.name,
                        type_: ot::ResponseCustomToolType::Custom,
                        defer_loading: None,
                        description: tool.custom.description,
                        format: chat_custom_tool_format_to_response(tool.custom.format),
                    }));
                }
            }
        }
    }

    if let Some(functions) = functions {
        for function in functions {
            output.push(ot::ResponseTool::Function(ot::ResponseFunctionTool {
                name: function.name,
                parameters: function.parameters.unwrap_or_default(),
                strict: None,
                type_: ot::ResponseFunctionToolType::Function,
                defer_loading: None,
                description: function.description,
            }));
        }
    }

    if let Some(web_search) = web_search_options {
        output.push(ot::ResponseTool::WebSearchPreview(
            ot::ResponseWebSearchPreviewTool {
                type_: ot::ResponseWebSearchPreviewToolType::WebSearchPreview,
                search_content_types: None,
                search_context_size: web_search.search_context_size.map(|size| match size {
                    ct::ChatCompletionWebSearchContextSize::Low => {
                        ot::ResponseWebSearchContextSize::Low
                    }
                    ct::ChatCompletionWebSearchContextSize::Medium => {
                        ot::ResponseWebSearchContextSize::Medium
                    }
                    ct::ChatCompletionWebSearchContextSize::High => {
                        ot::ResponseWebSearchContextSize::High
                    }
                }),
                user_location: web_search.user_location.map(|location| {
                    ot::ResponseWebSearchPreviewUserLocation {
                        type_: ot::ResponseApproximateLocationType::Approximate,
                        city: location.approximate.city,
                        country: location.approximate.country,
                        region: location.approximate.region,
                        timezone: location.approximate.timezone,
                    }
                }),
            },
        ));
    }

    if output.is_empty() {
        None
    } else {
        Some(output)
    }
}

pub fn chat_reasoning_to_response_reasoning(
    reasoning_effort: Option<ct::ChatCompletionReasoningEffort>,
) -> Option<ot::ResponseReasoning> {
    reasoning_effort.map(|effort| ot::ResponseReasoning {
        effort: Some(match effort {
            ct::ChatCompletionReasoningEffort::None => ot::ResponseReasoningEffort::None,
            ct::ChatCompletionReasoningEffort::Minimal => ot::ResponseReasoningEffort::Minimal,
            ct::ChatCompletionReasoningEffort::Low => ot::ResponseReasoningEffort::Low,
            ct::ChatCompletionReasoningEffort::Medium => ot::ResponseReasoningEffort::Medium,
            ct::ChatCompletionReasoningEffort::High => ot::ResponseReasoningEffort::High,
            ct::ChatCompletionReasoningEffort::XHigh => ot::ResponseReasoningEffort::XHigh,
        }),
        generate_summary: None,
        summary: None,
    })
}

pub fn pseudo_reasoning_signature(message_index: usize, reasoning_ordinal: usize) -> String {
    format!("rs_{message_index}_{reasoning_ordinal}")
}

pub fn chat_response_text_config(
    response_format: Option<ct::ChatCompletionResponseFormat>,
    verbosity: Option<ct::ChatCompletionVerbosity>,
) -> Option<ot::ResponseTextConfig> {
    let format = response_format.map(|value| match value {
        ct::ChatCompletionResponseFormat::Text(_) => {
            ot::ResponseTextFormatConfig::Text(ot::ResponseFormatText {
                type_: ot::ResponseFormatTextType::Text,
            })
        }
        ct::ChatCompletionResponseFormat::JsonObject(_) => {
            ot::ResponseTextFormatConfig::JsonObject(ot::ResponseFormatJsonObject {
                type_: ot::ResponseFormatJsonObjectType::JsonObject,
            })
        }
        ct::ChatCompletionResponseFormat::JsonSchema(schema) => {
            ot::ResponseTextFormatConfig::JsonSchema(ot::ResponseFormatTextJsonSchemaConfig {
                name: schema.json_schema.name,
                schema: schema.json_schema.schema.unwrap_or_else(|| {
                    serde_json::from_str::<ot::JsonObject>(r#"{"type":"object"}"#)
                        .unwrap_or_default()
                }),
                type_: ot::ResponseFormatTextJsonSchemaConfigType::JsonSchema,
                description: schema.json_schema.description,
                strict: schema.json_schema.strict,
            })
        }
    });

    let verbosity = verbosity.map(|value| match value {
        ct::ChatCompletionVerbosity::Low => ot::ResponseTextVerbosity::Low,
        ct::ChatCompletionVerbosity::Medium => ot::ResponseTextVerbosity::Medium,
        ct::ChatCompletionVerbosity::High => ot::ResponseTextVerbosity::High,
    });

    if format.is_none() && verbosity.is_none() {
        None
    } else {
        Some(ot::ResponseTextConfig { format, verbosity })
    }
}

pub fn chat_tool_choice_to_response_tool_choice(
    tool_choice: Option<ct::ChatCompletionToolChoiceOption>,
    function_call: Option<ct::ChatCompletionFunctionCallOptionParam>,
) -> Option<ot::ResponseToolChoice> {
    if let Some(tool_choice) = tool_choice {
        return Some(match tool_choice {
            ct::ChatCompletionToolChoiceOption::Mode(mode) => {
                ot::ResponseToolChoice::Options(match mode {
                    ct::ChatCompletionToolChoiceMode::None => ot::ResponseToolChoiceOptions::None,
                    ct::ChatCompletionToolChoiceMode::Auto => ot::ResponseToolChoiceOptions::Auto,
                    ct::ChatCompletionToolChoiceMode::Required => {
                        ot::ResponseToolChoiceOptions::Required
                    }
                })
            }
            ct::ChatCompletionToolChoiceOption::Allowed(allowed) => {
                ot::ResponseToolChoice::Allowed(ot::ResponseToolChoiceAllowed {
                    mode: match allowed.allowed_tools.mode {
                        ct::ChatCompletionAllowedToolsMode::Auto => {
                            ot::ResponseToolChoiceAllowedMode::Auto
                        }
                        ct::ChatCompletionAllowedToolsMode::Required => {
                            ot::ResponseToolChoiceAllowedMode::Required
                        }
                    },
                    tools: allowed.allowed_tools.tools,
                    type_: ot::ResponseToolChoiceAllowedType::AllowedTools,
                })
            }
            ct::ChatCompletionToolChoiceOption::NamedFunction(tool) => {
                ot::ResponseToolChoice::Function(ot::ResponseToolChoiceFunction {
                    name: tool.function.name,
                    type_: ot::ResponseToolChoiceFunctionType::Function,
                })
            }
            ct::ChatCompletionToolChoiceOption::NamedCustom(tool) => {
                ot::ResponseToolChoice::Custom(ot::ResponseToolChoiceCustom {
                    name: tool.custom.name,
                    type_: ot::ResponseToolChoiceCustomType::Custom,
                })
            }
        });
    }

    function_call.map(|function_call| match function_call {
        ct::ChatCompletionFunctionCallOptionParam::Mode(mode) => {
            ot::ResponseToolChoice::Options(match mode {
                ct::ChatCompletionFunctionCallMode::None => ot::ResponseToolChoiceOptions::None,
                ct::ChatCompletionFunctionCallMode::Auto => ot::ResponseToolChoiceOptions::Auto,
            })
        }
        ct::ChatCompletionFunctionCallOptionParam::Named(named) => {
            ot::ResponseToolChoice::Function(ot::ResponseToolChoiceFunction {
                name: named.name,
                type_: ot::ResponseToolChoiceFunctionType::Function,
            })
        }
    })
}

pub fn chat_stop_to_vec(stop: Option<ct::ChatCompletionStop>) -> Option<Vec<String>> {
    match stop {
        Some(ct::ChatCompletionStop::Single(stop)) => Some(vec![stop]),
        Some(ct::ChatCompletionStop::Multiple(stops)) => Some(stops),
        None => None,
    }
}