starweaver-model 0.0.5

Provider-neutral model protocol and wire adapters for Starweaver
Documentation
//! `OpenAI` Chat Completions wire mapper.

use serde_json::{json, Value};

use crate::{
    adapter::ToolDefinition,
    message::{
        ModelMessage, ModelRequestPart, ModelResponse, ModelResponsePart, ProviderInfo,
        ToolCallPart,
    },
    providers::{
        apply_common_settings_with_max_tokens, collect_system_parts_and_non_system,
        finish_reason_openai, insert_nonempty_description, openai_chat_content,
        parse_tool_call_arguments, provider_tool_schema_without_meta, usage_from_openai,
    },
    transport::MaxTokensParameter,
    ModelError, ModelSettings,
};

/// `OpenAI` Chat Completions wire mapper.
pub struct OpenAiChatAdapter;

impl OpenAiChatAdapter {
    /// Build a provider wire request.
    ///
    /// # Errors
    ///
    /// Returns an error when canonical history cannot be mapped into chat messages.
    #[allow(clippy::too_many_lines)]
    pub fn build_request(
        model: &str,
        messages: &[ModelMessage],
        settings: Option<&ModelSettings>,
        tools: &[ToolDefinition],
    ) -> Result<Value, ModelError> {
        Self::build_request_with_options(
            model,
            messages,
            settings,
            tools,
            MaxTokensParameter::MaxTokens,
        )
    }

    /// Build a provider wire request with explicit max-token parameter mapping.
    ///
    /// # Errors
    ///
    /// Returns an error when canonical history cannot be mapped into chat messages.
    #[allow(clippy::too_many_lines)]
    pub fn build_request_with_options(
        model: &str,
        messages: &[ModelMessage],
        settings: Option<&ModelSettings>,
        tools: &[ToolDefinition],
        max_tokens_parameter: MaxTokensParameter,
    ) -> Result<Value, ModelError> {
        let (system, rest) = collect_system_parts_and_non_system(messages);
        let mut system_messages = system
            .into_iter()
            .map(|part| json!({"role": "system", "content": part.text}))
            .collect::<Vec<_>>();
        let mut wire_messages = Vec::new();
        for message in rest {
            match message {
                ModelMessage::Request(request) => {
                    for part in &request.parts {
                        match part {
                            ModelRequestPart::SystemPrompt { .. }
                            | ModelRequestPart::Instruction { .. } => {}
                            ModelRequestPart::UserPrompt { content, .. } => {
                                wire_messages.push(
                                    json!({"role": "user", "content": openai_chat_content(content)}),
                                );
                            }
                            ModelRequestPart::ToolReturn(tool_return) => {
                                wire_messages.push(json!({
                                    "role": "tool",
                                    "tool_call_id": tool_return.tool_call_id,
                                    "content": tool_return.content.to_string(),
                                }));
                            }
                            ModelRequestPart::RetryPrompt { text, .. } => {
                                wire_messages.push(json!({"role": "user", "content": text}));
                            }
                        }
                    }
                }
                ModelMessage::Response(response) => {
                    let mut content = String::new();
                    let mut tool_calls = Vec::new();
                    for part in &response.parts {
                        match part {
                            ModelResponsePart::Text { text }
                            | ModelResponsePart::ProviderText { text, .. } => {
                                content.push_str(text);
                            }
                            ModelResponsePart::Thinking { text, .. }
                            | ModelResponsePart::ProviderThinking { text, .. }
                                if !text.is_empty() =>
                            {
                                content.push_str("<think>\n");
                                content.push_str(text);
                                content.push_str("\n</think>");
                            }
                            ModelResponsePart::ToolCall(call)
                            | ModelResponsePart::ProviderToolCall { call, .. } => {
                                tool_calls.push(json!({
                                    "id": call.id,
                                    "type": "function",
                                    "function": {
                                        "name": call.name,
                                        "arguments": call.arguments.wire_json_string(),
                                    }
                                }));
                            }
                            _ => {}
                        }
                    }
                    let mut item = json!({"role": "assistant", "content": content});
                    if !tool_calls.is_empty() {
                        item["tool_calls"] = json!(tool_calls);
                    }
                    wire_messages.push(item);
                }
            }
        }

        if !system_messages.is_empty() {
            system_messages.extend(wire_messages);
            wire_messages = system_messages;
        }

        let mut request = serde_json::Map::new();
        request.insert("model".to_string(), json!(model));
        request.insert("messages".to_string(), json!(wire_messages));
        apply_common_settings_with_max_tokens(&mut request, settings, max_tokens_parameter);
        if let Some(openai_settings) =
            settings.and_then(|settings| settings.provider_settings.openai_chat.as_ref())
        {
            if let Some(user) = &openai_settings.user {
                request.insert("user".to_string(), json!(user));
            }
            if let Some(store) = openai_settings.store {
                request.insert("store".to_string(), json!(store));
            }
            if let Some(logprobs) = openai_settings.logprobs {
                request.insert("logprobs".to_string(), json!(logprobs));
            }
            if let Some(top_logprobs) = openai_settings.top_logprobs {
                request.insert("top_logprobs".to_string(), json!(top_logprobs));
            }
            if let Some(prediction) = &openai_settings.prediction {
                request.insert("prediction".to_string(), prediction.clone());
            }
            if let Some(prompt_cache_key) = &openai_settings.prompt_cache_key {
                request.insert("prompt_cache_key".to_string(), json!(prompt_cache_key));
            }
            if let Some(prompt_cache_retention) = &openai_settings.prompt_cache_retention {
                request.insert(
                    "prompt_cache_retention".to_string(),
                    json!(prompt_cache_retention),
                );
            }
        }
        if let Some(tool_choice) = settings.and_then(|settings| settings.tool_choice.as_ref()) {
            request.insert(
                "tool_choice".to_string(),
                crate::providers::openai_chat_tool_choice(tool_choice),
            );
        }
        if !tools.is_empty() {
            request.insert(
                "tools".to_string(),
                json!(tools
                    .iter()
                    .map(|tool| {
                        let mut function = serde_json::Map::new();
                        function.insert("name".to_string(), json!(tool.name));
                        insert_nonempty_description(&mut function, tool.description.as_ref());
                        function.insert(
                            "parameters".to_string(),
                            provider_tool_schema_without_meta(&tool.parameters),
                        );
                        if let Some(strict) = tool.strict {
                            function.insert("strict".to_string(), json!(strict));
                        }
                        json!({
                            "type": "function",
                            "function": function,
                        })
                    })
                    .collect::<Vec<_>>()),
            );
        }
        Ok(Value::Object(request))
    }

    /// Parse a provider wire response.
    ///
    /// # Errors
    ///
    /// Returns an error when the response is missing the first choice or message.
    pub fn parse_response(value: &Value) -> Result<ModelResponse, ModelError> {
        let choice = value
            .get("choices")
            .and_then(Value::as_array)
            .and_then(|choices| choices.first())
            .ok_or_else(|| ModelError::ResponseParsing("missing choices[0]".to_string()))?;
        let message = choice
            .get("message")
            .ok_or_else(|| ModelError::ResponseParsing("missing message".to_string()))?;

        let mut parts = Vec::new();
        if let Some(content) = message.get("content").and_then(Value::as_str) {
            if !content.is_empty() {
                parts.push(ModelResponsePart::Text {
                    text: content.to_string(),
                });
            }
        }
        if let Some(refusal) = message.get("refusal").and_then(Value::as_str) {
            if !refusal.is_empty() {
                parts.push(ModelResponsePart::Text {
                    text: refusal.to_string(),
                });
            }
        }
        for call in message
            .get("tool_calls")
            .and_then(Value::as_array)
            .into_iter()
            .flatten()
        {
            let function = call.get("function").unwrap_or(&Value::Null);
            parts.push(ModelResponsePart::ToolCall(ToolCallPart {
                id: call
                    .get("id")
                    .and_then(Value::as_str)
                    .unwrap_or_default()
                    .to_string(),
                name: function
                    .get("name")
                    .and_then(Value::as_str)
                    .unwrap_or_default()
                    .to_string(),
                arguments: parse_tool_call_arguments(
                    function.get("arguments").unwrap_or(&Value::Null),
                ),
            }));
        }

        Ok(ModelResponse {
            parts,
            usage: usage_from_openai(value),
            model_name: value
                .get("model")
                .and_then(Value::as_str)
                .map(str::to_string),
            provider: Some(ProviderInfo {
                name: "openai".to_string(),
                response_id: value.get("id").and_then(Value::as_str).map(str::to_string),
                details: serde_json::Map::new(),
            }),
            finish_reason: choice
                .get("finish_reason")
                .and_then(Value::as_str)
                .map(finish_reason_openai),
            timestamp: None,
            run_id: None,
            conversation_id: None,
            metadata: serde_json::Map::new(),
        })
    }
}