call-agent 1.5.4

A multimodal chat API library with tool support, OpenAI API compatible
Documentation
use std::collections::VecDeque;

use serde::{ser::SerializeStruct, Deserialize, Serialize, Serializer};

use super::function::ToolDef;

use super::prompt::{Choice, Message};

/// API Response Headers struct
#[derive(Debug, Clone)]
pub struct APIResponseHeaders {
    /// Retry-After header value (in seconds)
    pub retry_after: Option<u64>,
    /// X-RateLimit-Reset header value (timestamp or seconds)
    pub reset: Option<u64>,
    /// X-RateLimit-Remaining header value (number of remaining requests)
    pub rate_limit: Option<u64>,
    /// X-RateLimit-Limit header value (maximum allowed requests)
    pub limit: Option<u64>,

    /// Additional custom headers as key-value pairs
    pub extra_other: Vec<(String, String)>,
}

/// API Request structure for sending prompt and function information
#[derive(Debug, Deserialize)]
pub struct APIRequest {
    /// Specifies the model name
    pub model: String,

    /// Array of prompt messages
    pub messages: VecDeque<Message>,

    /// Defines the tools available to the model
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub tools: Vec<ToolDef>,

    /// Instructions for function calls:
    /// - "auto": AI will make one or more function calls if needed
    /// - "none": No function calls will be made
    /// - "required": One or more function calls are mandatory
    /// - {"type": "function", "function": {"name": "<function_name>"}}: Calls the function <function_name>
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub tool_choice: serde_json::Value,

    /// Specifies whether to make parallel tool calls
    /// default: true
    #[serde(skip_serializing_if = "Option::is_none")]
    pub parallel_tool_calls: Option<bool>,

    /// Specifies the diversity of tokens generated by the model
    /// Range: 0.0..2.0
    /// default: 0.8 (soft specification)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub temperature: Option<f64>,

    /// Specifies the maximum number of tokens generated by the model
    #[serde(skip_serializing_if = "Option::is_none")]
    pub max_completion_tokens: Option<u64>,

    /// Specifies the width of the probability distribution for selecting the next token
    /// Lower values result in more predictable text
    #[serde(skip_serializing_if = "Option::is_none")]
    pub top_p: Option<f64>,

    /// Specifies the level of effort for model reasoning:
    /// - "low": Low effort
    /// - "medium": Medium effort
    /// - "high": High effort
    /// default: "medium"
    #[serde(skip_serializing_if = "Option::is_none")]
    pub reasoning_effort: Option<String>,

    /// Specifies whether to apply a repetition penalty to the model
    /// Range: 2.0..-2.0
    #[serde(skip_serializing_if = "Option::is_none")]
    pub presence_penalty: Option<f64>,

    /// Options for performing web search with available models
    #[serde(skip_serializing_if = "Option::is_none")]
    pub web_search_options: Option<WebSearchOptions>,
}

// Custom Serialize implementation for APIRequest
impl Serialize for APIRequest {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        // Serialize with capacity for potential optional fields
        let mut state = serializer.serialize_struct("APIRequest", 10)?;

        state.serialize_field("model", &self.model)?;
        state.serialize_field("messages", &self.messages)?;

        // Serialize "tools" only if not empty
        if !self.tools.is_empty() {
            state.serialize_field("tools", &self.tools)?;
        }

        // Serialize "tool_choice" only if it is not equal to the string "none"
        if self.tool_choice != serde_json::Value::String("none".to_string()) {
            state.serialize_field("tool_choice", &self.tool_choice)?;
        }

        // Serialize optional fields if they are present
        if let Some(parallel_tool_calls) = &self.parallel_tool_calls {
            state.serialize_field("parallel_tool_calls", parallel_tool_calls)?;
        }
        if let Some(temperature) = &self.temperature {
            state.serialize_field("temperature", temperature)?;
        }
        if let Some(max_completion_tokens) = &self.max_completion_tokens {
            state.serialize_field("max_completion_tokens", max_completion_tokens)?;
        }
        if let Some(top_p) = &self.top_p {
            state.serialize_field("top_p", top_p)?;
        }
        if let Some(reasoning_effort) = &self.reasoning_effort {
            state.serialize_field("reasoning_effort", reasoning_effort)?;
        }
        if let Some(presence_penalty) = &self.presence_penalty {
            state.serialize_field("presence_penalty", presence_penalty)?;
        }

        state.end()
    }
}

/// API Response structure from the server
#[derive(Debug, Deserialize, Clone)]
pub struct APIResponse {
    /// Unique identifier for the API response
    pub id: String,
    /// IDK
    pub object: String,
    /// Model name used in the response
    pub model: Option<String>,
    /// Array of choices (results) returned by the API
    pub choices: Option<Vec<Choice>>,
    /// Error information if the request failed
    pub error: Option<APIError>,
    /// Information regarding token usage
    pub usage: Option<APIUsage>,
    /// Timestamp of when the response was created
    pub created: Option<u64>,
}

/// API Error information structure
#[derive(Debug, Deserialize, Clone)]
pub struct APIError {
    /// Error message text
    pub message: String,
    /// Error type (renamed from "type" to avoid keyword conflict)
    #[serde(rename = "type")]
    pub err_type: String,
    /// Error code number
    pub code: i32,
}

/// API Usage information detailing token counts
#[derive(Debug, Deserialize, Clone)]
pub struct APIUsage {
    /// Number of tokens used in the prompt
    pub prompt_tokens: Option<u64>,
    /// Number of tokens used in the response
    pub completion_tokens: Option<u64>,
    /// Total number of tokens used (prompt + response)
    pub total_tokens: Option<u64>,
}

#[derive(Debug, Clone, Deserialize)]
pub struct WebSearchOptions {
    /// Degree of context size used for web search
    /// - "low"
    /// - "medium"
    /// - "high"
    /// default: "medium"
    pub search_context_size: Option<String>,
    pub user_location: UserLocation,
}

impl Serialize for WebSearchOptions {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        let mut state = serializer.serialize_struct("WebSearchOptions", 2)?;
        // Use "medium" as default if search_context_size is None
        let size = self.search_context_size.as_deref().unwrap_or("medium");
        state.serialize_field("search_context_size", size)?;
        state.serialize_field("user_location", &self.user_location)?;
        state.end()
    }
}

/// User location information for req api web search options
#[derive(Debug, Clone, Deserialize)]
pub struct UserLocation {
    /// Free text input for the city of the user.
    /// e.g. "San Francisco"
    pub city: Option<String>,
    /// The two-letter ISO country code of the user.
    /// e.g. "US"
    pub country: Option<String>,
    /// Free text input for the region of the user.
    /// e.g. "California"
    pub region: Option<String>,
    /// The IANA timezone of the user.
    /// e.g. "America/Los_Angeles"
    pub timezone: Option<String>,
}

impl Serialize for UserLocation {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {

        let mut state = serializer.serialize_struct("UserLocation", 2)?;

        // Always set "type" to "approximate"
        state.serialize_field("type", "approximate")?;

        // Build the "approximate" object with only non-None fields
        let mut approximate = serde_json::Map::new();
        if let Some(ref city) = self.city {
            approximate.insert("city".to_string(), serde_json::Value::String(city.clone()));
        }
        if let Some(ref country) = self.country {
            approximate.insert("country".to_string(), serde_json::Value::String(country.clone()));
        }
        if let Some(ref region) = self.region {
            approximate.insert("region".to_string(), serde_json::Value::String(region.clone()));
        }
        if let Some(ref timezone) = self.timezone {
            approximate.insert("timezone".to_string(), serde_json::Value::String(timezone.clone()));
        }

        state.serialize_field("approximate", &approximate)?;
        state.end()
    }
}