magi-openai 0.1.1

OpenAI compatible API SDK for Magi AI agents
Documentation
use std::collections::HashMap;

use serde::{Deserialize, Serialize};
use serde_json::json;

use super::request::{ContentPart, Role};

/// Error response returned by the OpenAI API.
#[derive(Debug, Deserialize, Clone, Default, PartialEq, Serialize)]
pub struct ErrResponse {
    pub error: Err,
}

/// OpenAI error payload.
#[derive(Debug, Deserialize, Clone, Default, PartialEq, Serialize)]
pub struct Err {
    pub message: String,
    pub r#type: String,
    pub param: Option<String>,
    pub code: Option<String>,
}

/// Error information for a failed response.
#[derive(Debug, Deserialize, Clone, Default, PartialEq, Serialize)]
pub struct ResponseError {
    /// The error code.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub code: Option<String>,
    /// The error message.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub message: Option<String>,
}

/// Reasoning configuration returned in responses.
#[derive(Debug, Deserialize, Clone, Default, PartialEq, Serialize)]
pub struct ResponseReasoning {
    /// The reasoning effort used.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub effort: Option<String>,
    /// Summary of the reasoning.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub summary: Option<String>,
}

/// Text response format configuration.
#[derive(Debug, Deserialize, Clone, Default, PartialEq, Serialize)]
pub struct TextConfig {
    /// The format configuration.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub format: Option<TextFormat>,
}

/// Text format configuration.
#[derive(Debug, Deserialize, Clone, Default, PartialEq, Serialize)]
pub struct TextFormat {
    /// The type of text format (text, json_object, json_schema).
    pub r#type: String,
}

/// A response generated by the Responses API.
#[derive(Debug, Deserialize, Clone, Default, PartialEq, Serialize)]
pub struct Response {
    /// A unique identifier for the response.
    pub id: String,
    /// Object type - always `response`.
    pub object: String,
    /// The Unix timestamp (in seconds) when the response was created.
    #[serde(alias = "created")]
    pub created_at: u64,
    /// Identifier of the model used to generate the response.
    pub model: String,
    /// Status of the response lifecycle.
    pub status: ResponseStatus,
    /// Error information if the response failed.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub error: Option<ResponseError>,
    /// Information about why the response is incomplete when applicable.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub incomplete_details: Option<IncompleteDetails>,
    /// Sequence of generated output items.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub output: Vec<Output>,
    /// Aggregated text output for convenience (SDK-only property).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub output_text: Option<String>,
    /// Usage statistics for the response request.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub usage: Option<Usage>,
    /// Additional metadata returned by the API.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub metadata: Option<HashMap<String, serde_json::Value>>,
    /// Whether to allow the model to run tool calls in parallel.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub parallel_tool_calls: Option<bool>,
    /// The ID of the previous response in a multi-turn conversation.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub previous_response_id: Option<String>,
    /// Reasoning configuration for the response.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub reasoning: Option<ResponseReasoning>,
    /// Whether the response is stored.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub store: Option<bool>,
    /// Sampling temperature used.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub temperature: Option<f32>,
    /// Text response format configuration.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub text: Option<TextConfig>,
    /// How the model selected tools.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub tool_choice: Option<serde_json::Value>,
    /// Tools available to the model.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub tools: Vec<serde_json::Value>,
    /// Nucleus sampling parameter used.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub top_p: Option<f32>,
    /// Truncation strategy used.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub truncation: Option<String>,
    /// A unique identifier for the end user.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub user: Option<String>,
    /// Instructions given to the model.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub instructions: Option<String>,
    /// Maximum output tokens requested.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub max_output_tokens: Option<u32>,
    /// Backend configuration fingerprint.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub system_fingerprint: Option<String>,
}

impl Response {
    /// Creates a new [`ResponseBuilder`].
    pub fn builder() -> ResponseBuilder {
        ResponseBuilder::new()
    }
}

/// Helper builder used to construct synthetic response objects.
#[derive(Debug, Clone)]
pub struct ResponseBuilder {
    inner: Response,
}

impl Default for ResponseBuilder {
    fn default() -> Self {
        Self::new()
    }
}

impl ResponseBuilder {
    /// Creates an empty response builder with sensible defaults.
    pub fn new() -> Self {
        Self {
            inner: Response {
                object: "response".to_string(),
                status: ResponseStatus::InProgress,
                ..Default::default()
            },
        }
    }

    /// Finalises the builder and returns the constructed response.
    pub fn build(self) -> Response {
        self.inner
    }

    /// Sets the response identifier.
    pub fn id(mut self, id: impl Into<String>) -> Self {
        self.inner.id = id.into();
        self
    }

    /// Sets the model identifier.
    pub fn model(mut self, model: impl Into<String>) -> Self {
        self.inner.model = model.into();
        self
    }

    /// Sets the response status.
    pub fn status(mut self, status: ResponseStatus) -> Self {
        self.inner.status = status;
        self
    }

    /// Sets the creation timestamp.
    pub fn created(mut self, created_at: u64) -> Self {
        self.inner.created_at = created_at;
        self
    }

    /// Alias for `created` to match the API field name.
    pub fn created_at(self, created_at: u64) -> Self {
        self.created(created_at)
    }

    /// Sets the backend system fingerprint.
    pub fn system_fingerprint(mut self, fingerprint: impl Into<String>) -> Self {
        self.inner.system_fingerprint = Some(fingerprint.into());
        self
    }

    /// Pushes a text output item attributed to the assistant.
    pub fn push_text_output(mut self, text: impl Into<String>) -> Self {
        let text = text.into();
        let id = format!("output-{}", self.inner.output.len());
        self.inner.output.push(Output {
            id,
            r#type: OutputType::Message,
            role: Some(Role::Assistant),
            content: vec![json!({
                "type": "output_text",
                "text": text,
            })],
            ..Default::default()
        });
        self
    }
}

/// Individual output items produced by the Responses API.
#[derive(Debug, Deserialize, Clone, Default, PartialEq, Serialize)]
pub struct Output {
    /// Unique identifier for the output item.
    pub id: String,
    /// Type of output generated.
    #[serde(rename = "type")]
    pub r#type: OutputType,
    /// Status of the output item.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub status: Option<OutputStatus>,
    /// Role responsible for the output item (for message type).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub role: Option<Role>,
    /// Structured content payload (for message type).
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub content: Vec<ContentPart>,
    /// Summary of reasoning (for reasoning type).
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub summary: Vec<serde_json::Value>,
    /// Name of the function to call (for function_call type).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub name: Option<String>,
    /// Arguments for the function call (for function_call type).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub arguments: Option<String>,
    /// Unique ID for the function call (for function_call type).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub call_id: Option<String>,
}

/// Output type descriptor.
#[derive(Debug, Default, Deserialize, Clone, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum OutputType {
    #[default]
    Message,
    Reasoning,
    #[serde(rename = "function_call")]
    FunctionCall,
    #[serde(rename = "file_search_call")]
    FileSearchCall,
    #[serde(rename = "web_search_call")]
    WebSearchCall,
    #[serde(rename = "computer_call")]
    ComputerCall,
}

/// Status of an output item.
#[derive(Debug, Default, Deserialize, Clone, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum OutputStatus {
    #[default]
    InProgress,
    Completed,
    Incomplete,
}

/// Usage statistics for the response request.
#[derive(Debug, Deserialize, Serialize, Default, Clone, PartialEq)]
pub struct Usage {
    /// Number of tokens present in the input.
    pub input_tokens: u32,
    /// Number of tokens generated in the output.
    pub output_tokens: u32,
    /// Total number of tokens used for the request.
    pub total_tokens: u32,
    /// Detailed input token usage statistics.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub input_tokens_details: Option<InputTokensDetails>,
    /// Detailed output token usage statistics.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub output_tokens_details: Option<OutputTokensDetails>,
}

/// Detailed breakdown of input token usage.
#[derive(Debug, Deserialize, Serialize, Default, Clone, PartialEq)]
pub struct InputTokensDetails {
    /// Audio input tokens present in the prompt.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub audio_tokens: Option<u32>,
    /// Cached tokens present in the prompt.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub cached_tokens: Option<u32>,
}

/// Detailed breakdown of output token usage.
#[derive(Debug, Deserialize, Serialize, Default, Clone, PartialEq)]
pub struct OutputTokensDetails {
    /// Audio tokens generated by the model.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub audio_tokens: Option<u32>,
    /// Tokens generated by the model for reasoning.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub reasoning_tokens: Option<u32>,
    /// Number of prediction tokens accepted when using predicted outputs.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub accepted_prediction_tokens: Option<u32>,
    /// Number of prediction tokens rejected when using predicted outputs.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub rejected_prediction_tokens: Option<u32>,
}

/// Indicates the lifecycle state of a response.
#[derive(Debug, Default, Deserialize, Clone, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ResponseStatus {
    #[default]
    InProgress,
    Completed,
    Incomplete,
    Failed,
}

/// Details regarding incomplete responses.
#[derive(Debug, Deserialize, Serialize, Default, Clone, PartialEq)]
pub struct IncompleteDetails {
    /// Reason the response was truncated.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub reason: Option<IncompleteReason>,
}

/// Reason why a response is incomplete.
#[derive(Debug, Deserialize, Clone, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum IncompleteReason {
    /// The response exceeded the maximum output tokens.
    MaxOutputTokens,
    /// The response was filtered by content filters.
    ContentFilter,
}

/// Extra status information used in streaming responses.
#[derive(Debug, Deserialize, Serialize, Default, Clone, PartialEq)]
pub struct StatusDetails {
    /// Machine readable reason.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub reason: Option<String>,
    /// Optional human readable message.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub message: Option<String>,
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn builder_push_text_output() {
        let response = Response::builder()
            .id("resp_123")
            .model("gpt-4.1-mini")
            .status(ResponseStatus::Completed)
            .push_text_output("Hello there")
            .build();

        assert_eq!(response.id, "resp_123");
        assert_eq!(response.model, "gpt-4.1-mini");
        assert_eq!(response.status, ResponseStatus::Completed);
        assert_eq!(response.output.len(), 1);
        let item = &response.output[0];
        assert_eq!(item.r#type, OutputType::Message);
        assert_eq!(item.role, Some(Role::Assistant));
        assert_eq!(item.content.len(), 1);
        assert_eq!(
            item.content[0].get("text").and_then(|value| value.as_str()),
            Some("Hello there")
        );
    }
}