// OpenAI Responses API types // https://platform.openai.com/docs/api-reference/responses use std::collections::HashMap; use serde::{Deserialize, Serialize}; use serde_json::Value; use validator::Validate; // Import shared types from common module use super::common::{ default_model, default_true, ChatLogProbs, GenerationRequest, PromptTokenUsageInfo, StringOrArray, ToolChoice, UsageInfo, }; // ============================================================================ // Response Tools (MCP and others) // ============================================================================ #[derive(Debug, Clone, Deserialize, Serialize)] pub struct ResponseTool { #[serde(rename = "type")] pub r#type: ResponseToolType, // MCP-specific fields (used when type == "mcp") #[serde(skip_serializing_if = "Option::is_none")] pub server_url: Option, #[serde(skip_serializing_if = "Option::is_none")] pub authorization: Option, #[serde(skip_serializing_if = "Option::is_none")] pub server_label: Option, #[serde(skip_serializing_if = "Option::is_none")] pub server_description: Option, #[serde(skip_serializing_if = "Option::is_none")] pub require_approval: Option, #[serde(skip_serializing_if = "Option::is_none")] pub allowed_tools: Option>, } impl Default for ResponseTool { fn default() -> Self { Self { r#type: ResponseToolType::WebSearchPreview, server_url: None, authorization: None, server_label: None, server_description: None, require_approval: None, allowed_tools: None, } } } #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(rename_all = "snake_case")] pub enum ResponseToolType { WebSearchPreview, CodeInterpreter, Mcp, } // ============================================================================ // Reasoning Parameters // ============================================================================ #[derive(Debug, Clone, Deserialize, Serialize)] pub struct ResponseReasoningParam { #[serde(default = "default_reasoning_effort")] #[serde(skip_serializing_if = "Option::is_none")] pub effort: Option, #[serde(skip_serializing_if = "Option::is_none")] pub summary: Option, } fn default_reasoning_effort() -> Option { Some(ReasoningEffort::Medium) } #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(rename_all = "snake_case")] pub enum ReasoningEffort { Low, Medium, High, } #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(rename_all = "snake_case")] pub enum ReasoningSummary { Auto, Concise, Detailed, } // ============================================================================ // Input/Output Items // ============================================================================ /// Content can be either a simple string or array of content parts (for SimpleInputMessage) #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(untagged)] pub enum StringOrContentParts { String(String), Array(Vec), } #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(tag = "type")] #[serde(rename_all = "snake_case")] pub enum ResponseInputOutputItem { #[serde(rename = "message")] Message { id: String, role: String, content: Vec, #[serde(skip_serializing_if = "Option::is_none")] status: Option, }, #[serde(rename = "reasoning")] Reasoning { id: String, #[serde(skip_serializing_if = "Vec::is_empty")] summary: Vec, content: Vec, #[serde(skip_serializing_if = "Option::is_none")] status: Option, }, #[serde(rename = "function_tool_call")] FunctionToolCall { id: String, name: String, arguments: String, #[serde(skip_serializing_if = "Option::is_none")] output: Option, #[serde(skip_serializing_if = "Option::is_none")] status: Option, }, #[serde(untagged)] SimpleInputMessage { content: StringOrContentParts, role: String, #[serde(skip_serializing_if = "Option::is_none")] #[serde(rename = "type")] r#type: Option, }, } #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(tag = "type")] #[serde(rename_all = "snake_case")] pub enum ResponseContentPart { #[serde(rename = "output_text")] OutputText { text: String, #[serde(skip_serializing_if = "Vec::is_empty")] annotations: Vec, #[serde(skip_serializing_if = "Option::is_none")] logprobs: Option, }, #[serde(rename = "input_text")] InputText { text: String }, #[serde(other)] Unknown, } #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(tag = "type")] #[serde(rename_all = "snake_case")] pub enum ResponseReasoningContent { #[serde(rename = "reasoning_text")] ReasoningText { text: String }, } /// MCP Tool information for the mcp_list_tools output item #[derive(Debug, Clone, Deserialize, Serialize)] pub struct McpToolInfo { pub name: String, #[serde(skip_serializing_if = "Option::is_none")] pub description: Option, pub input_schema: Value, #[serde(skip_serializing_if = "Option::is_none")] pub annotations: Option, } #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(tag = "type")] #[serde(rename_all = "snake_case")] pub enum ResponseOutputItem { #[serde(rename = "message")] Message { id: String, role: String, content: Vec, status: String, }, #[serde(rename = "reasoning")] Reasoning { id: String, #[serde(skip_serializing_if = "Vec::is_empty")] summary: Vec, content: Vec, #[serde(skip_serializing_if = "Option::is_none")] status: Option, }, #[serde(rename = "function_tool_call")] FunctionToolCall { id: String, name: String, arguments: String, #[serde(skip_serializing_if = "Option::is_none")] output: Option, status: String, }, #[serde(rename = "mcp_list_tools")] McpListTools { id: String, server_label: String, tools: Vec, }, #[serde(rename = "mcp_call")] McpCall { id: String, status: String, #[serde(skip_serializing_if = "Option::is_none")] approval_request_id: Option, arguments: String, #[serde(skip_serializing_if = "Option::is_none")] error: Option, name: String, output: String, server_label: String, }, } // ============================================================================ // Configuration Enums // ============================================================================ #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(rename_all = "snake_case")] pub enum ServiceTier { Auto, Default, Flex, Scale, Priority, } impl Default for ServiceTier { fn default() -> Self { Self::Auto } } #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(rename_all = "snake_case")] pub enum Truncation { Auto, Disabled, } impl Default for Truncation { fn default() -> Self { Self::Disabled } } #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(rename_all = "snake_case")] pub enum ResponseStatus { Queued, InProgress, Completed, Failed, Cancelled, } #[derive(Debug, Clone, Deserialize, Serialize)] pub struct ReasoningInfo { #[serde(skip_serializing_if = "Option::is_none")] pub effort: Option, #[serde(skip_serializing_if = "Option::is_none")] pub summary: Option, } #[derive(Debug, Clone, Deserialize, Serialize)] pub struct ResponseTextFormat { pub format: TextFormatType, } #[derive(Debug, Clone, Deserialize, Serialize)] pub struct TextFormatType { #[serde(rename = "type")] pub format_type: String, } #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(rename_all = "snake_case")] pub enum IncludeField { #[serde(rename = "code_interpreter_call.outputs")] CodeInterpreterCallOutputs, #[serde(rename = "computer_call_output.output.image_url")] ComputerCallOutputImageUrl, #[serde(rename = "file_search_call.results")] FileSearchCallResults, #[serde(rename = "message.input_image.image_url")] MessageInputImageUrl, #[serde(rename = "message.output_text.logprobs")] MessageOutputTextLogprobs, #[serde(rename = "reasoning.encrypted_content")] ReasoningEncryptedContent, } // ============================================================================ // Usage Types (Responses API format) // ============================================================================ /// OpenAI Responses API usage format (different from standard UsageInfo) #[derive(Debug, Clone, Deserialize, Serialize)] pub struct ResponseUsage { pub input_tokens: u32, pub output_tokens: u32, pub total_tokens: u32, #[serde(skip_serializing_if = "Option::is_none")] pub input_tokens_details: Option, #[serde(skip_serializing_if = "Option::is_none")] pub output_tokens_details: Option, } #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(untagged)] pub enum ResponsesUsage { Classic(UsageInfo), Modern(ResponseUsage), } #[derive(Debug, Clone, Deserialize, Serialize)] pub struct InputTokensDetails { pub cached_tokens: u32, } #[derive(Debug, Clone, Deserialize, Serialize)] pub struct OutputTokensDetails { pub reasoning_tokens: u32, } impl UsageInfo { /// Convert to OpenAI Responses API format pub fn to_response_usage(&self) -> ResponseUsage { ResponseUsage { input_tokens: self.prompt_tokens, output_tokens: self.completion_tokens, total_tokens: self.total_tokens, input_tokens_details: self.prompt_tokens_details.as_ref().map(|details| { InputTokensDetails { cached_tokens: details.cached_tokens, } }), output_tokens_details: self.reasoning_tokens.map(|tokens| OutputTokensDetails { reasoning_tokens: tokens, }), } } } impl From for ResponseUsage { fn from(usage: UsageInfo) -> Self { usage.to_response_usage() } } impl ResponseUsage { /// Convert back to standard UsageInfo format pub fn to_usage_info(&self) -> UsageInfo { UsageInfo { prompt_tokens: self.input_tokens, completion_tokens: self.output_tokens, total_tokens: self.total_tokens, reasoning_tokens: self .output_tokens_details .as_ref() .map(|details| details.reasoning_tokens), prompt_tokens_details: self.input_tokens_details.as_ref().map(|details| { PromptTokenUsageInfo { cached_tokens: details.cached_tokens, } }), } } } #[derive(Debug, Clone, Default, Deserialize, Serialize)] pub struct ResponsesGetParams { #[serde(default)] pub include: Vec, #[serde(default)] pub include_obfuscation: Option, #[serde(default)] pub starting_after: Option, #[serde(default)] pub stream: Option, } impl ResponsesUsage { pub fn to_response_usage(&self) -> ResponseUsage { match self { ResponsesUsage::Classic(usage) => usage.to_response_usage(), ResponsesUsage::Modern(usage) => usage.clone(), } } pub fn to_usage_info(&self) -> UsageInfo { match self { ResponsesUsage::Classic(usage) => usage.clone(), ResponsesUsage::Modern(usage) => usage.to_usage_info(), } } } // ============================================================================ // Helper Functions for Defaults // ============================================================================ fn default_top_k() -> i32 { -1 } fn default_repetition_penalty() -> f32 { 1.0 } fn default_temperature() -> Option { Some(1.0) } fn default_top_p() -> Option { Some(1.0) } // ============================================================================ // Request/Response Types // ============================================================================ #[derive(Debug, Clone, Deserialize, Serialize, Validate)] pub struct ResponsesRequest { /// Run the request in the background #[serde(skip_serializing_if = "Option::is_none")] pub background: Option, /// Fields to include in the response #[serde(skip_serializing_if = "Option::is_none")] pub include: Option>, /// Input content - can be string or structured items pub input: ResponseInput, /// System instructions for the model #[serde(skip_serializing_if = "Option::is_none")] pub instructions: Option, /// Maximum number of output tokens #[serde(skip_serializing_if = "Option::is_none")] pub max_output_tokens: Option, /// Maximum number of tool calls #[serde(skip_serializing_if = "Option::is_none")] pub max_tool_calls: Option, /// Additional metadata #[serde(skip_serializing_if = "Option::is_none")] pub metadata: Option>, /// Model to use #[serde(default = "default_model")] pub model: String, /// Optional conversation id to persist input/output as items #[serde(skip_serializing_if = "Option::is_none")] #[validate(custom(function = "validate_conversation_id"))] pub conversation: Option, /// Whether to enable parallel tool calls #[serde(skip_serializing_if = "Option::is_none")] pub parallel_tool_calls: Option, /// ID of previous response to continue from #[serde(skip_serializing_if = "Option::is_none")] pub previous_response_id: Option, /// Reasoning configuration #[serde(skip_serializing_if = "Option::is_none")] pub reasoning: Option, /// Service tier #[serde(skip_serializing_if = "Option::is_none")] pub service_tier: Option, /// Whether to store the response #[serde(skip_serializing_if = "Option::is_none")] pub store: Option, /// Whether to stream the response #[serde(skip_serializing_if = "Option::is_none")] pub stream: Option, /// Temperature for sampling #[serde( default = "default_temperature", skip_serializing_if = "Option::is_none" )] pub temperature: Option, /// Tool choice behavior #[serde(skip_serializing_if = "Option::is_none")] pub tool_choice: Option, /// Available tools #[serde(skip_serializing_if = "Option::is_none")] pub tools: Option>, /// Number of top logprobs to return #[serde(skip_serializing_if = "Option::is_none")] pub top_logprobs: Option, /// Top-p sampling parameter #[serde(default = "default_top_p", skip_serializing_if = "Option::is_none")] pub top_p: Option, /// Truncation behavior #[serde(skip_serializing_if = "Option::is_none")] pub truncation: Option, /// User identifier #[serde(skip_serializing_if = "Option::is_none")] pub user: Option, /// Request ID #[serde(skip_serializing_if = "Option::is_none")] pub request_id: Option, /// Request priority #[serde(default)] pub priority: i32, /// Frequency penalty #[serde(skip_serializing_if = "Option::is_none")] pub frequency_penalty: Option, /// Presence penalty #[serde(skip_serializing_if = "Option::is_none")] pub presence_penalty: Option, /// Stop sequences #[serde(skip_serializing_if = "Option::is_none")] pub stop: Option, /// Top-k sampling parameter (SGLang extension) #[serde(default = "default_top_k")] pub top_k: i32, /// Min-p sampling parameter (SGLang extension) #[serde(default)] pub min_p: f32, /// Repetition penalty (SGLang extension) #[serde(default = "default_repetition_penalty")] pub repetition_penalty: f32, } #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(untagged)] pub enum ResponseInput { Items(Vec), Text(String), } impl Default for ResponsesRequest { fn default() -> Self { Self { background: None, include: None, input: ResponseInput::Text(String::new()), instructions: None, max_output_tokens: None, max_tool_calls: None, metadata: None, model: default_model(), conversation: None, parallel_tool_calls: None, previous_response_id: None, reasoning: None, service_tier: None, store: None, stream: None, temperature: None, tool_choice: None, tools: None, top_logprobs: None, top_p: None, truncation: None, user: None, request_id: None, priority: 0, frequency_penalty: None, presence_penalty: None, stop: None, top_k: default_top_k(), min_p: 0.0, repetition_penalty: default_repetition_penalty(), } } } impl GenerationRequest for ResponsesRequest { fn is_stream(&self) -> bool { self.stream.unwrap_or(false) } fn get_model(&self) -> Option<&str> { Some(self.model.as_str()) } fn extract_text_for_routing(&self) -> String { match &self.input { ResponseInput::Text(text) => text.clone(), ResponseInput::Items(items) => items .iter() .filter_map(|item| match item { ResponseInputOutputItem::Message { content, .. } => { let texts: Vec = content .iter() .filter_map(|part| match part { ResponseContentPart::OutputText { text, .. } => Some(text.clone()), ResponseContentPart::InputText { text } => Some(text.clone()), ResponseContentPart::Unknown => None, }) .collect(); if texts.is_empty() { None } else { Some(texts.join(" ")) } } ResponseInputOutputItem::SimpleInputMessage { content, .. } => { match content { StringOrContentParts::String(s) => Some(s.clone()), StringOrContentParts::Array(parts) => { // SimpleInputMessage only supports InputText let texts: Vec = parts .iter() .filter_map(|part| match part { ResponseContentPart::InputText { text } => { Some(text.clone()) } _ => None, }) .collect(); if texts.is_empty() { None } else { Some(texts.join(" ")) } } } } ResponseInputOutputItem::Reasoning { content, .. } => { let texts: Vec = content .iter() .map(|part| match part { ResponseReasoningContent::ReasoningText { text } => text.clone(), }) .collect(); if texts.is_empty() { None } else { Some(texts.join(" ")) } } ResponseInputOutputItem::FunctionToolCall { arguments, .. } => { Some(arguments.clone()) } }) .collect::>() .join(" "), } } } /// Validate conversation ID format pub fn validate_conversation_id(conv_id: &str) -> Result<(), validator::ValidationError> { if !conv_id.starts_with("conv_") { let mut error = validator::ValidationError::new("invalid_conversation_id"); error.message = Some(std::borrow::Cow::Owned(format!( "Invalid 'conversation': '{}'. Expected an ID that begins with 'conv_'.", conv_id ))); return Err(error); } // Check if the conversation ID contains only valid characters let is_valid = conv_id .chars() .all(|c| c.is_alphanumeric() || c == '_' || c == '-'); if !is_valid { let mut error = validator::ValidationError::new("invalid_conversation_id"); error.message = Some(std::borrow::Cow::Owned(format!( "Invalid 'conversation': '{}'. Expected an ID that contains letters, numbers, underscores, or dashes, but this value contained additional characters.", conv_id ))); return Err(error); } Ok(()) } /// Normalize a SimpleInputMessage to a proper Message item /// /// This helper converts SimpleInputMessage (which can have flexible content) /// into a fully-structured Message item with a generated ID, role, and content array. /// /// SimpleInputMessage items are converted to Message items with IDs generated using /// the centralized ID generation pattern with "msg_" prefix for consistency. /// /// # Arguments /// * `item` - The input item to normalize /// /// # Returns /// A normalized ResponseInputOutputItem (either Message if converted, or original if not SimpleInputMessage) pub fn normalize_input_item(item: &ResponseInputOutputItem) -> ResponseInputOutputItem { match item { ResponseInputOutputItem::SimpleInputMessage { content, role, .. } => { let content_vec = match content { StringOrContentParts::String(s) => { vec![ResponseContentPart::InputText { text: s.clone() }] } StringOrContentParts::Array(parts) => parts.clone(), }; ResponseInputOutputItem::Message { id: generate_id("msg"), role: role.clone(), content: content_vec, status: Some("completed".to_string()), } } _ => item.clone(), } } pub fn generate_id(prefix: &str) -> String { use rand::RngCore; let mut rng = rand::rng(); // Generate exactly 50 hex characters (25 bytes) for the part after the underscore let mut bytes = [0u8; 25]; rng.fill_bytes(&mut bytes); let hex_string: String = bytes.iter().map(|b| format!("{:02x}", b)).collect(); format!("{}_{}", prefix, hex_string) } #[derive(Debug, Clone, Deserialize, Serialize)] pub struct ResponsesResponse { /// Response ID pub id: String, /// Object type #[serde(default = "default_object_type")] pub object: String, /// Creation timestamp pub created_at: i64, /// Response status pub status: ResponseStatus, /// Error information if status is failed #[serde(skip_serializing_if = "Option::is_none")] pub error: Option, /// Incomplete details if response was truncated #[serde(skip_serializing_if = "Option::is_none")] pub incomplete_details: Option, /// System instructions used #[serde(skip_serializing_if = "Option::is_none")] pub instructions: Option, /// Max output tokens setting #[serde(skip_serializing_if = "Option::is_none")] pub max_output_tokens: Option, /// Model name pub model: String, /// Output items #[serde(default)] pub output: Vec, /// Whether parallel tool calls are enabled #[serde(default = "default_true")] pub parallel_tool_calls: bool, /// Previous response ID if this is a continuation #[serde(skip_serializing_if = "Option::is_none")] pub previous_response_id: Option, /// Reasoning information #[serde(skip_serializing_if = "Option::is_none")] pub reasoning: Option, /// Whether the response is stored #[serde(default = "default_true")] pub store: bool, /// Temperature setting used #[serde(skip_serializing_if = "Option::is_none")] pub temperature: Option, /// Text format settings #[serde(skip_serializing_if = "Option::is_none")] pub text: Option, /// Tool choice setting #[serde(default = "default_tool_choice")] pub tool_choice: String, /// Available tools #[serde(default)] pub tools: Vec, /// Top-p setting used #[serde(skip_serializing_if = "Option::is_none")] pub top_p: Option, /// Truncation strategy used #[serde(skip_serializing_if = "Option::is_none")] pub truncation: Option, /// Usage information #[serde(skip_serializing_if = "Option::is_none")] pub usage: Option, /// User identifier #[serde(skip_serializing_if = "Option::is_none")] pub user: Option, /// Additional metadata #[serde(default)] pub metadata: HashMap, } fn default_object_type() -> String { "response".to_string() } fn default_tool_choice() -> String { "auto".to_string() } impl ResponsesResponse { /// Check if the response is complete pub fn is_complete(&self) -> bool { matches!(self.status, ResponseStatus::Completed) } /// Check if the response is in progress pub fn is_in_progress(&self) -> bool { matches!(self.status, ResponseStatus::InProgress) } /// Check if the response failed pub fn is_failed(&self) -> bool { matches!(self.status, ResponseStatus::Failed) } } impl ResponseOutputItem { /// Create a new message output item pub fn new_message( id: String, role: String, content: Vec, status: String, ) -> Self { Self::Message { id, role, content, status, } } /// Create a new reasoning output item pub fn new_reasoning( id: String, summary: Vec, content: Vec, status: Option, ) -> Self { Self::Reasoning { id, summary, content, status, } } /// Create a new function tool call output item pub fn new_function_tool_call( id: String, name: String, arguments: String, output: Option, status: String, ) -> Self { Self::FunctionToolCall { id, name, arguments, output, status, } } } impl ResponseContentPart { /// Create a new text content part pub fn new_text( text: String, annotations: Vec, logprobs: Option, ) -> Self { Self::OutputText { text, annotations, logprobs, } } } impl ResponseReasoningContent { /// Create a new reasoning text content pub fn new_reasoning_text(text: String) -> Self { Self::ReasoningText { text } } }