Skip to main content

openai_protocol/
responses.rs

1// OpenAI Responses API types
2// https://platform.openai.com/docs/api-reference/responses
3
4use std::collections::{HashMap, HashSet};
5
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8use validator::{Validate, ValidationError};
9
10use super::{
11    common::{
12        default_model, default_true, validate_stop, ChatLogProbs, Function, GenerationRequest,
13        PromptTokenUsageInfo, StringOrArray, ToolChoice, ToolChoiceValue, ToolReference, UsageInfo,
14    },
15    sampling_params::{validate_top_k_value, validate_top_p_value},
16};
17use crate::{builders::ResponsesResponseBuilder, validated::Normalizable};
18
19// ============================================================================
20// Response Tools (MCP and others)
21// ============================================================================
22
23#[serde_with::skip_serializing_none]
24#[derive(Debug, Clone, Deserialize, Serialize)]
25pub struct ResponseTool {
26    #[serde(rename = "type")]
27    pub r#type: ResponseToolType,
28    // Function tool fields (used when type == "function")
29    // In Responses API, function fields are flattened at the top level
30    #[serde(flatten)]
31    pub function: Option<Function>,
32    // MCP-specific fields (used when type == "mcp")
33    pub server_url: Option<String>,
34    pub authorization: Option<String>,
35    /// Custom headers to send to MCP server (from request payload, not HTTP headers)
36    pub headers: Option<HashMap<String, String>>,
37    pub server_label: Option<String>,
38    pub server_description: Option<String>,
39    pub require_approval: Option<String>,
40    pub allowed_tools: Option<Vec<String>>,
41}
42
43impl Default for ResponseTool {
44    fn default() -> Self {
45        Self {
46            r#type: ResponseToolType::WebSearchPreview,
47            function: None,
48            server_url: None,
49            authorization: None,
50            headers: None,
51            server_label: None,
52            server_description: None,
53            require_approval: None,
54            allowed_tools: None,
55        }
56    }
57}
58
59#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
60#[serde(rename_all = "snake_case")]
61pub enum ResponseToolType {
62    Function,
63    WebSearchPreview,
64    CodeInterpreter,
65    Mcp,
66}
67
68// ============================================================================
69// Reasoning Parameters
70// ============================================================================
71
72#[serde_with::skip_serializing_none]
73#[derive(Debug, Clone, Deserialize, Serialize)]
74pub struct ResponseReasoningParam {
75    #[serde(default = "default_reasoning_effort")]
76    pub effort: Option<ReasoningEffort>,
77    pub summary: Option<ReasoningSummary>,
78}
79
80#[expect(
81    clippy::unnecessary_wraps,
82    reason = "serde default function must match field type Option<T>"
83)]
84fn default_reasoning_effort() -> Option<ReasoningEffort> {
85    Some(ReasoningEffort::Medium)
86}
87
88#[derive(Debug, Clone, Deserialize, Serialize)]
89#[serde(rename_all = "snake_case")]
90pub enum ReasoningEffort {
91    Minimal,
92    Low,
93    Medium,
94    High,
95}
96
97#[derive(Debug, Clone, Deserialize, Serialize)]
98#[serde(rename_all = "snake_case")]
99pub enum ReasoningSummary {
100    Auto,
101    Concise,
102    Detailed,
103}
104
105// ============================================================================
106// Input/Output Items
107// ============================================================================
108
109/// Content can be either a simple string or array of content parts (for SimpleInputMessage)
110#[derive(Debug, Clone, Deserialize, Serialize)]
111#[serde(untagged)]
112pub enum StringOrContentParts {
113    String(String),
114    Array(Vec<ResponseContentPart>),
115}
116
117#[derive(Debug, Clone, Deserialize, Serialize)]
118#[serde(tag = "type")]
119#[serde(rename_all = "snake_case")]
120pub enum ResponseInputOutputItem {
121    #[serde(rename = "message")]
122    Message {
123        id: String,
124        role: String,
125        content: Vec<ResponseContentPart>,
126        #[serde(skip_serializing_if = "Option::is_none")]
127        status: Option<String>,
128    },
129    #[serde(rename = "reasoning")]
130    Reasoning {
131        id: String,
132        summary: Vec<String>,
133        #[serde(skip_serializing_if = "Vec::is_empty")]
134        #[serde(default)]
135        content: Vec<ResponseReasoningContent>,
136        #[serde(skip_serializing_if = "Option::is_none")]
137        status: Option<String>,
138    },
139    #[serde(rename = "function_call")]
140    FunctionToolCall {
141        id: String,
142        call_id: String,
143        name: String,
144        arguments: String,
145        #[serde(skip_serializing_if = "Option::is_none")]
146        output: Option<String>,
147        #[serde(skip_serializing_if = "Option::is_none")]
148        status: Option<String>,
149    },
150    #[serde(rename = "function_call_output")]
151    FunctionCallOutput {
152        id: Option<String>,
153        call_id: String,
154        output: String,
155        #[serde(skip_serializing_if = "Option::is_none")]
156        status: Option<String>,
157    },
158    #[serde(untagged)]
159    SimpleInputMessage {
160        content: StringOrContentParts,
161        role: String,
162        #[serde(skip_serializing_if = "Option::is_none")]
163        #[serde(rename = "type")]
164        r#type: Option<String>,
165    },
166}
167
168#[derive(Debug, Clone, Deserialize, Serialize)]
169#[serde(tag = "type")]
170#[serde(rename_all = "snake_case")]
171pub enum ResponseContentPart {
172    #[serde(rename = "output_text")]
173    OutputText {
174        text: String,
175        #[serde(default)]
176        #[serde(skip_serializing_if = "Vec::is_empty")]
177        annotations: Vec<String>,
178        #[serde(skip_serializing_if = "Option::is_none")]
179        logprobs: Option<ChatLogProbs>,
180    },
181    #[serde(rename = "input_text")]
182    InputText { text: String },
183    #[serde(other)]
184    Unknown,
185}
186
187#[derive(Debug, Clone, Deserialize, Serialize)]
188#[serde(tag = "type")]
189#[serde(rename_all = "snake_case")]
190pub enum ResponseReasoningContent {
191    #[serde(rename = "reasoning_text")]
192    ReasoningText { text: String },
193}
194
195/// MCP Tool information for the mcp_list_tools output item
196#[serde_with::skip_serializing_none]
197#[derive(Debug, Clone, Deserialize, Serialize)]
198pub struct McpToolInfo {
199    pub name: String,
200    pub description: Option<String>,
201    pub input_schema: Value,
202    pub annotations: Option<Value>,
203}
204
205#[serde_with::skip_serializing_none]
206#[derive(Debug, Clone, Deserialize, Serialize)]
207#[serde(tag = "type")]
208#[serde(rename_all = "snake_case")]
209pub enum ResponseOutputItem {
210    #[serde(rename = "message")]
211    Message {
212        id: String,
213        role: String,
214        content: Vec<ResponseContentPart>,
215        status: String,
216    },
217    #[serde(rename = "reasoning")]
218    Reasoning {
219        id: String,
220        summary: Vec<String>,
221        content: Vec<ResponseReasoningContent>,
222        status: Option<String>,
223    },
224    #[serde(rename = "function_call")]
225    FunctionToolCall {
226        id: String,
227        call_id: String,
228        name: String,
229        arguments: String,
230        output: Option<String>,
231        status: String,
232    },
233    #[serde(rename = "mcp_list_tools")]
234    McpListTools {
235        id: String,
236        server_label: String,
237        tools: Vec<McpToolInfo>,
238    },
239    #[serde(rename = "mcp_call")]
240    McpCall {
241        id: String,
242        status: String,
243        approval_request_id: Option<String>,
244        arguments: String,
245        error: Option<String>,
246        name: String,
247        output: String,
248        server_label: String,
249    },
250    #[serde(rename = "web_search_call")]
251    WebSearchCall {
252        id: String,
253        status: WebSearchCallStatus,
254        action: WebSearchAction,
255    },
256    #[serde(rename = "code_interpreter_call")]
257    CodeInterpreterCall {
258        id: String,
259        status: CodeInterpreterCallStatus,
260        container_id: String,
261        code: Option<String>,
262        outputs: Option<Vec<CodeInterpreterOutput>>,
263    },
264    #[serde(rename = "file_search_call")]
265    FileSearchCall {
266        id: String,
267        status: FileSearchCallStatus,
268        queries: Vec<String>,
269        results: Option<Vec<FileSearchResult>>,
270    },
271}
272
273// ============================================================================
274// Built-in Tool Call Types
275// ============================================================================
276
277/// Status for web search tool calls.
278#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
279#[serde(rename_all = "snake_case")]
280pub enum WebSearchCallStatus {
281    InProgress,
282    Searching,
283    Completed,
284    Failed,
285}
286
287/// Action performed during a web search.
288#[derive(Debug, Clone, Deserialize, Serialize)]
289#[serde(tag = "type", rename_all = "snake_case")]
290pub enum WebSearchAction {
291    Search {
292        #[serde(skip_serializing_if = "Option::is_none")]
293        query: Option<String>,
294        #[serde(default, skip_serializing_if = "Vec::is_empty")]
295        queries: Vec<String>,
296        #[serde(default, skip_serializing_if = "Vec::is_empty")]
297        sources: Vec<WebSearchSource>,
298    },
299    OpenPage {
300        url: String,
301    },
302    Find {
303        url: String,
304        pattern: String,
305    },
306}
307
308/// A source returned from web search.
309#[derive(Debug, Clone, Deserialize, Serialize)]
310pub struct WebSearchSource {
311    #[serde(rename = "type")]
312    pub source_type: String,
313    pub url: String,
314}
315
316/// Status for code interpreter tool calls.
317#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
318#[serde(rename_all = "snake_case")]
319pub enum CodeInterpreterCallStatus {
320    InProgress,
321    Completed,
322    Incomplete,
323    Interpreting,
324    Failed,
325}
326
327/// Output from code interpreter execution.
328#[derive(Debug, Clone, Deserialize, Serialize)]
329#[serde(tag = "type", rename_all = "snake_case")]
330pub enum CodeInterpreterOutput {
331    Logs { logs: String },
332    Image { url: String },
333}
334
335/// Status for file search tool calls.
336#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
337#[serde(rename_all = "snake_case")]
338pub enum FileSearchCallStatus {
339    InProgress,
340    Searching,
341    Completed,
342    Incomplete,
343    Failed,
344}
345
346/// A result from file search.
347#[serde_with::skip_serializing_none]
348#[derive(Debug, Clone, Deserialize, Serialize)]
349pub struct FileSearchResult {
350    pub file_id: String,
351    pub filename: String,
352    pub text: Option<String>,
353    pub score: Option<f32>,
354    pub attributes: Option<Value>,
355}
356
357// ============================================================================
358// Configuration Enums
359// ============================================================================
360
361#[derive(Debug, Clone, Deserialize, Serialize, Default)]
362#[serde(rename_all = "snake_case")]
363pub enum ServiceTier {
364    #[default]
365    Auto,
366    Default,
367    Flex,
368    Scale,
369    Priority,
370}
371
372#[derive(Debug, Clone, Deserialize, Serialize, Default)]
373#[serde(rename_all = "snake_case")]
374pub enum Truncation {
375    Auto,
376    #[default]
377    Disabled,
378}
379
380#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
381#[serde(rename_all = "snake_case")]
382pub enum ResponseStatus {
383    Queued,
384    InProgress,
385    Completed,
386    Failed,
387    Cancelled,
388}
389
390#[serde_with::skip_serializing_none]
391#[derive(Debug, Clone, Deserialize, Serialize)]
392pub struct ReasoningInfo {
393    pub effort: Option<String>,
394    pub summary: Option<String>,
395}
396
397// ============================================================================
398// Text Format (structured outputs)
399// ============================================================================
400
401/// Text configuration for structured output requests
402#[derive(Debug, Clone, Deserialize, Serialize)]
403pub struct TextConfig {
404    #[serde(skip_serializing_if = "Option::is_none")]
405    pub format: Option<TextFormat>,
406}
407
408/// Text format: text (default), json_object (legacy), or json_schema (recommended)
409#[serde_with::skip_serializing_none]
410#[derive(Debug, Clone, Deserialize, Serialize)]
411#[serde(tag = "type")]
412pub enum TextFormat {
413    #[serde(rename = "text")]
414    Text,
415
416    #[serde(rename = "json_object")]
417    JsonObject,
418
419    #[serde(rename = "json_schema")]
420    JsonSchema {
421        name: String,
422        schema: Value,
423        description: Option<String>,
424        strict: Option<bool>,
425    },
426}
427
428#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
429#[serde(rename_all = "snake_case")]
430pub enum IncludeField {
431    #[serde(rename = "code_interpreter_call.outputs")]
432    CodeInterpreterCallOutputs,
433    #[serde(rename = "computer_call_output.output.image_url")]
434    ComputerCallOutputImageUrl,
435    #[serde(rename = "file_search_call.results")]
436    FileSearchCallResults,
437    #[serde(rename = "message.input_image.image_url")]
438    MessageInputImageUrl,
439    #[serde(rename = "message.output_text.logprobs")]
440    MessageOutputTextLogprobs,
441    #[serde(rename = "reasoning.encrypted_content")]
442    ReasoningEncryptedContent,
443}
444
445// ============================================================================
446// Usage Types (Responses API format)
447// ============================================================================
448
449/// OpenAI Responses API usage format (different from standard UsageInfo)
450#[serde_with::skip_serializing_none]
451#[derive(Debug, Clone, Deserialize, Serialize)]
452pub struct ResponseUsage {
453    pub input_tokens: u32,
454    pub output_tokens: u32,
455    pub total_tokens: u32,
456    pub input_tokens_details: Option<InputTokensDetails>,
457    pub output_tokens_details: Option<OutputTokensDetails>,
458}
459
460#[derive(Debug, Clone, Deserialize, Serialize)]
461#[serde(untagged)]
462pub enum ResponsesUsage {
463    Classic(UsageInfo),
464    Modern(ResponseUsage),
465}
466
467#[derive(Debug, Clone, Deserialize, Serialize)]
468pub struct InputTokensDetails {
469    pub cached_tokens: u32,
470}
471
472#[derive(Debug, Clone, Deserialize, Serialize)]
473pub struct OutputTokensDetails {
474    pub reasoning_tokens: u32,
475}
476
477impl UsageInfo {
478    /// Convert to OpenAI Responses API format
479    pub fn to_response_usage(&self) -> ResponseUsage {
480        ResponseUsage {
481            input_tokens: self.prompt_tokens,
482            output_tokens: self.completion_tokens,
483            total_tokens: self.total_tokens,
484            input_tokens_details: self.prompt_tokens_details.as_ref().map(|details| {
485                InputTokensDetails {
486                    cached_tokens: details.cached_tokens,
487                }
488            }),
489            output_tokens_details: self.reasoning_tokens.map(|tokens| OutputTokensDetails {
490                reasoning_tokens: tokens,
491            }),
492        }
493    }
494}
495
496impl From<UsageInfo> for ResponseUsage {
497    fn from(usage: UsageInfo) -> Self {
498        usage.to_response_usage()
499    }
500}
501
502impl ResponseUsage {
503    /// Convert back to standard UsageInfo format
504    pub fn to_usage_info(&self) -> UsageInfo {
505        UsageInfo {
506            prompt_tokens: self.input_tokens,
507            completion_tokens: self.output_tokens,
508            total_tokens: self.total_tokens,
509            reasoning_tokens: self
510                .output_tokens_details
511                .as_ref()
512                .map(|details| details.reasoning_tokens),
513            prompt_tokens_details: self.input_tokens_details.as_ref().map(|details| {
514                PromptTokenUsageInfo {
515                    cached_tokens: details.cached_tokens,
516                }
517            }),
518        }
519    }
520}
521
522#[derive(Debug, Clone, Default, Deserialize, Serialize)]
523pub struct ResponsesGetParams {
524    #[serde(default)]
525    pub include: Vec<String>,
526    #[serde(default)]
527    pub include_obfuscation: Option<bool>,
528    #[serde(default)]
529    pub starting_after: Option<i64>,
530    #[serde(default)]
531    pub stream: Option<bool>,
532}
533
534impl ResponsesUsage {
535    pub fn to_response_usage(&self) -> ResponseUsage {
536        match self {
537            ResponsesUsage::Classic(usage) => usage.to_response_usage(),
538            ResponsesUsage::Modern(usage) => usage.clone(),
539        }
540    }
541
542    pub fn to_usage_info(&self) -> UsageInfo {
543        match self {
544            ResponsesUsage::Classic(usage) => usage.clone(),
545            ResponsesUsage::Modern(usage) => usage.to_usage_info(),
546        }
547    }
548}
549
550// ============================================================================
551// Helper Functions for Defaults
552// ============================================================================
553
554fn default_top_k() -> i32 {
555    -1
556}
557
558fn default_repetition_penalty() -> f32 {
559    1.0
560}
561
562#[expect(
563    clippy::unnecessary_wraps,
564    reason = "serde default function must match field type Option<T>"
565)]
566fn default_temperature() -> Option<f32> {
567    Some(1.0)
568}
569
570#[expect(
571    clippy::unnecessary_wraps,
572    reason = "serde default function must match field type Option<T>"
573)]
574fn default_top_p() -> Option<f32> {
575    Some(1.0)
576}
577
578// ============================================================================
579// Request/Response Types
580// ============================================================================
581
582#[derive(Debug, Clone, Deserialize, Serialize, Validate)]
583#[validate(schema(function = "validate_responses_cross_parameters"))]
584pub struct ResponsesRequest {
585    /// Run the request in the background
586    #[serde(skip_serializing_if = "Option::is_none")]
587    pub background: Option<bool>,
588
589    /// Fields to include in the response
590    #[serde(skip_serializing_if = "Option::is_none")]
591    pub include: Option<Vec<IncludeField>>,
592
593    /// Input content - can be string or structured items
594    #[validate(custom(function = "validate_response_input"))]
595    pub input: ResponseInput,
596
597    /// System instructions for the model
598    #[serde(skip_serializing_if = "Option::is_none")]
599    pub instructions: Option<String>,
600
601    /// Maximum number of output tokens
602    #[serde(skip_serializing_if = "Option::is_none")]
603    #[validate(range(min = 1))]
604    pub max_output_tokens: Option<u32>,
605
606    /// Maximum number of tool calls
607    #[serde(skip_serializing_if = "Option::is_none")]
608    #[validate(range(min = 1))]
609    pub max_tool_calls: Option<u32>,
610
611    /// Additional metadata
612    #[serde(skip_serializing_if = "Option::is_none")]
613    pub metadata: Option<HashMap<String, Value>>,
614
615    /// Model to use
616    #[serde(default = "default_model")]
617    pub model: String,
618
619    /// Optional conversation id to persist input/output as items
620    #[serde(skip_serializing_if = "Option::is_none")]
621    #[validate(custom(function = "validate_conversation_id"))]
622    pub conversation: Option<String>,
623
624    /// Whether to enable parallel tool calls
625    #[serde(skip_serializing_if = "Option::is_none")]
626    pub parallel_tool_calls: Option<bool>,
627
628    /// ID of previous response to continue from
629    #[serde(skip_serializing_if = "Option::is_none")]
630    pub previous_response_id: Option<String>,
631
632    /// Reasoning configuration
633    #[serde(skip_serializing_if = "Option::is_none")]
634    pub reasoning: Option<ResponseReasoningParam>,
635
636    /// Service tier
637    #[serde(skip_serializing_if = "Option::is_none")]
638    pub service_tier: Option<ServiceTier>,
639
640    /// Whether to store the response
641    #[serde(skip_serializing_if = "Option::is_none")]
642    pub store: Option<bool>,
643
644    /// Whether to stream the response
645    #[serde(default)]
646    pub stream: Option<bool>,
647
648    /// Temperature for sampling
649    #[serde(
650        default = "default_temperature",
651        skip_serializing_if = "Option::is_none"
652    )]
653    #[validate(range(min = 0.0, max = 2.0))]
654    pub temperature: Option<f32>,
655
656    /// Tool choice behavior
657    #[serde(skip_serializing_if = "Option::is_none")]
658    pub tool_choice: Option<ToolChoice>,
659
660    /// Available tools
661    #[serde(skip_serializing_if = "Option::is_none")]
662    #[validate(custom(function = "validate_response_tools"))]
663    pub tools: Option<Vec<ResponseTool>>,
664
665    /// Number of top logprobs to return
666    #[serde(skip_serializing_if = "Option::is_none")]
667    #[validate(range(min = 0, max = 20))]
668    pub top_logprobs: Option<u32>,
669
670    /// Top-p sampling parameter
671    #[serde(default = "default_top_p", skip_serializing_if = "Option::is_none")]
672    #[validate(custom(function = "validate_top_p_value"))]
673    pub top_p: Option<f32>,
674
675    /// Truncation behavior
676    #[serde(skip_serializing_if = "Option::is_none")]
677    pub truncation: Option<Truncation>,
678
679    /// Text format for structured outputs (text, json_object, json_schema)
680    #[serde(skip_serializing_if = "Option::is_none")]
681    #[validate(custom(function = "validate_text_format"))]
682    pub text: Option<TextConfig>,
683
684    /// User identifier
685    #[serde(skip_serializing_if = "Option::is_none")]
686    pub user: Option<String>,
687
688    /// Request ID
689    #[serde(skip_serializing_if = "Option::is_none")]
690    pub request_id: Option<String>,
691
692    /// Request priority
693    #[serde(default)]
694    pub priority: i32,
695
696    /// Frequency penalty
697    #[serde(skip_serializing_if = "Option::is_none")]
698    #[validate(range(min = -2.0, max = 2.0))]
699    pub frequency_penalty: Option<f32>,
700
701    /// Presence penalty
702    #[serde(skip_serializing_if = "Option::is_none")]
703    #[validate(range(min = -2.0, max = 2.0))]
704    pub presence_penalty: Option<f32>,
705
706    /// Stop sequences
707    #[serde(skip_serializing_if = "Option::is_none")]
708    #[validate(custom(function = "validate_stop"))]
709    pub stop: Option<StringOrArray>,
710
711    /// Top-k sampling parameter (SGLang extension)
712    #[serde(default = "default_top_k")]
713    #[validate(custom(function = "validate_top_k_value"))]
714    pub top_k: i32,
715
716    /// Min-p sampling parameter (SGLang extension)
717    #[serde(default)]
718    #[validate(range(min = 0.0, max = 1.0))]
719    pub min_p: f32,
720
721    /// Repetition penalty (SGLang extension)
722    #[serde(default = "default_repetition_penalty")]
723    #[validate(range(min = 0.0, max = 2.0))]
724    pub repetition_penalty: f32,
725}
726
727#[derive(Debug, Clone, Deserialize, Serialize)]
728#[serde(untagged)]
729pub enum ResponseInput {
730    Items(Vec<ResponseInputOutputItem>),
731    Text(String),
732}
733
734impl Default for ResponsesRequest {
735    fn default() -> Self {
736        Self {
737            background: None,
738            include: None,
739            input: ResponseInput::Text(String::new()),
740            instructions: None,
741            max_output_tokens: None,
742            max_tool_calls: None,
743            metadata: None,
744            model: default_model(),
745            conversation: None,
746            parallel_tool_calls: None,
747            previous_response_id: None,
748            reasoning: None,
749            service_tier: None,
750            store: None,
751            stream: None,
752            temperature: None,
753            tool_choice: None,
754            tools: None,
755            top_logprobs: None,
756            top_p: None,
757            truncation: None,
758            text: None,
759            user: None,
760            request_id: None,
761            priority: 0,
762            frequency_penalty: None,
763            presence_penalty: None,
764            stop: None,
765            top_k: default_top_k(),
766            min_p: 0.0,
767            repetition_penalty: default_repetition_penalty(),
768        }
769    }
770}
771
772impl Normalizable for ResponsesRequest {
773    /// Normalize the request by applying defaults:
774    /// 1. Apply tool_choice defaults based on tools presence
775    /// 2. Apply parallel_tool_calls defaults
776    /// 3. Apply store field defaults
777    fn normalize(&mut self) {
778        // 1. Apply tool_choice defaults
779        if self.tool_choice.is_none() {
780            if let Some(tools) = &self.tools {
781                let choice_value = if tools.is_empty() {
782                    ToolChoiceValue::None
783                } else {
784                    ToolChoiceValue::Auto
785                };
786                self.tool_choice = Some(ToolChoice::Value(choice_value));
787            }
788            // If tools is None, leave tool_choice as None (don't set it)
789        }
790
791        // 2. Apply default for parallel_tool_calls if tools are present
792        if self.parallel_tool_calls.is_none() && self.tools.is_some() {
793            self.parallel_tool_calls = Some(true);
794        }
795
796        // 3. Ensure store defaults to true if not specified
797        if self.store.is_none() {
798            self.store = Some(true);
799        }
800    }
801}
802
803impl GenerationRequest for ResponsesRequest {
804    fn is_stream(&self) -> bool {
805        self.stream.unwrap_or(false)
806    }
807
808    fn get_model(&self) -> Option<&str> {
809        Some(self.model.as_str())
810    }
811
812    fn extract_text_for_routing(&self) -> String {
813        match &self.input {
814            ResponseInput::Text(text) => text.clone(),
815            ResponseInput::Items(items) => items
816                .iter()
817                .filter_map(|item| match item {
818                    ResponseInputOutputItem::Message { content, .. } => {
819                        let texts: Vec<String> = content
820                            .iter()
821                            .filter_map(|part| match part {
822                                ResponseContentPart::OutputText { text, .. } => Some(text.clone()),
823                                ResponseContentPart::InputText { text } => Some(text.clone()),
824                                ResponseContentPart::Unknown => None,
825                            })
826                            .collect();
827                        if texts.is_empty() {
828                            None
829                        } else {
830                            Some(texts.join(" "))
831                        }
832                    }
833                    ResponseInputOutputItem::SimpleInputMessage { content, .. } => {
834                        match content {
835                            StringOrContentParts::String(s) => Some(s.clone()),
836                            StringOrContentParts::Array(parts) => {
837                                // SimpleInputMessage only supports InputText
838                                let texts: Vec<String> = parts
839                                    .iter()
840                                    .filter_map(|part| match part {
841                                        ResponseContentPart::InputText { text } => {
842                                            Some(text.clone())
843                                        }
844                                        _ => None,
845                                    })
846                                    .collect();
847                                if texts.is_empty() {
848                                    None
849                                } else {
850                                    Some(texts.join(" "))
851                                }
852                            }
853                        }
854                    }
855                    ResponseInputOutputItem::Reasoning { content, .. } => {
856                        let texts: Vec<String> = content
857                            .iter()
858                            .map(|part| match part {
859                                ResponseReasoningContent::ReasoningText { text } => text.clone(),
860                            })
861                            .collect();
862                        if texts.is_empty() {
863                            None
864                        } else {
865                            Some(texts.join(" "))
866                        }
867                    }
868                    ResponseInputOutputItem::FunctionToolCall { arguments, .. } => {
869                        Some(arguments.clone())
870                    }
871                    ResponseInputOutputItem::FunctionCallOutput { output, .. } => {
872                        Some(output.clone())
873                    }
874                })
875                .collect::<Vec<String>>()
876                .join(" "),
877        }
878    }
879}
880
881/// Validate conversation ID format
882pub fn validate_conversation_id(conv_id: &str) -> Result<(), ValidationError> {
883    if !conv_id.starts_with("conv_") {
884        let mut error = ValidationError::new("invalid_conversation_id");
885        error.message = Some(std::borrow::Cow::Owned(format!(
886            "Invalid 'conversation': '{conv_id}'. Expected an ID that begins with 'conv_'."
887        )));
888        return Err(error);
889    }
890
891    // Check if the conversation ID contains only valid characters
892    let is_valid = conv_id
893        .chars()
894        .all(|c| c.is_alphanumeric() || c == '_' || c == '-');
895
896    if !is_valid {
897        let mut error = ValidationError::new("invalid_conversation_id");
898        error.message = Some(std::borrow::Cow::Owned(format!(
899            "Invalid 'conversation': '{conv_id}'. Expected an ID that contains letters, numbers, underscores, or dashes, but this value contained additional characters."
900        )));
901        return Err(error);
902    }
903    Ok(())
904}
905
906/// Validates tool_choice requires tools and references exist
907fn validate_tool_choice_with_tools(request: &ResponsesRequest) -> Result<(), ValidationError> {
908    let Some(tool_choice) = &request.tool_choice else {
909        return Ok(());
910    };
911
912    let has_tools = request.tools.as_ref().is_some_and(|t| !t.is_empty());
913    let is_some_choice = !matches!(tool_choice, ToolChoice::Value(ToolChoiceValue::None));
914
915    // Check if tool_choice requires tools but none are provided
916    if is_some_choice && !has_tools {
917        let mut e = ValidationError::new("tool_choice_requires_tools");
918        e.message = Some("Invalid value for 'tool_choice': 'tool_choice' is only allowed when 'tools' are specified.".into());
919        return Err(e);
920    }
921
922    // Validate tool references exist when tools are present
923    if !has_tools {
924        return Ok(());
925    }
926
927    // Extract function tool names from ResponseTools
928    // INVARIANT: has_tools is true here, so tools is Some and non-empty
929    let Some(tools) = request.tools.as_ref() else {
930        return Ok(());
931    };
932    let function_tool_names: Vec<&str> = tools
933        .iter()
934        .filter_map(|t| match t.r#type {
935            ResponseToolType::Function => t.function.as_ref().map(|f| f.name.as_str()),
936            _ => None,
937        })
938        .collect();
939
940    // Validate tool references exist
941    match tool_choice {
942        ToolChoice::Function { function, .. } => {
943            if !function_tool_names.contains(&function.name.as_str()) {
944                let mut e = ValidationError::new("tool_choice_function_not_found");
945                e.message = Some(
946                    format!(
947                        "Invalid value for 'tool_choice': function '{}' not found in 'tools'.",
948                        function.name
949                    )
950                    .into(),
951                );
952                return Err(e);
953            }
954        }
955        ToolChoice::AllowedTools {
956            mode,
957            tools: allowed_tools,
958            ..
959        } => {
960            // Validate mode is "auto" or "required"
961            if mode != "auto" && mode != "required" {
962                let mut e = ValidationError::new("tool_choice_invalid_mode");
963                e.message = Some(
964                    format!(
965                        "Invalid value for 'tool_choice.mode': must be 'auto' or 'required', got '{mode}'."
966                    )
967                    .into(),
968                );
969                return Err(e);
970            }
971
972            // Validate that all function tool references exist
973            for tool_ref in allowed_tools {
974                if let ToolReference::Function { name } = tool_ref {
975                    if !function_tool_names.contains(&name.as_str()) {
976                        let mut e = ValidationError::new("tool_choice_tool_not_found");
977                        e.message = Some(
978                            format!(
979                                "Invalid value for 'tool_choice.tools': tool '{name}' not found in 'tools'."
980                            )
981                            .into(),
982                        );
983                        return Err(e);
984                    }
985                }
986                // Note: MCP and hosted tools don't need existence validation here
987                // as they are resolved dynamically at runtime
988            }
989        }
990        ToolChoice::Value(_) => {}
991    }
992
993    Ok(())
994}
995
996/// Schema-level validation for cross-field dependencies
997fn validate_responses_cross_parameters(request: &ResponsesRequest) -> Result<(), ValidationError> {
998    // 1. Validate tool_choice requires tools (enhanced)
999    validate_tool_choice_with_tools(request)?;
1000
1001    // 2. Validate top_logprobs requires include field
1002    if request.top_logprobs.is_some() {
1003        let has_logprobs_include = request
1004            .include
1005            .as_ref()
1006            .is_some_and(|inc| inc.contains(&IncludeField::MessageOutputTextLogprobs));
1007
1008        if !has_logprobs_include {
1009            let mut e = ValidationError::new("top_logprobs_requires_include");
1010            e.message = Some(
1011                "top_logprobs requires include field with 'message.output_text.logprobs'".into(),
1012            );
1013            return Err(e);
1014        }
1015    }
1016
1017    // 3. Validate background/stream conflict
1018    if request.background == Some(true) && request.stream == Some(true) {
1019        let mut e = ValidationError::new("background_conflicts_with_stream");
1020        e.message = Some("Cannot use background mode with streaming".into());
1021        return Err(e);
1022    }
1023
1024    // 4. Validate conversation and previous_response_id are mutually exclusive
1025    if request.conversation.is_some() && request.previous_response_id.is_some() {
1026        let mut e = ValidationError::new("mutually_exclusive_parameters");
1027        e.message = Some("Mutually exclusive parameters. Ensure you are only providing one of: 'previous_response_id' or 'conversation'.".into());
1028        return Err(e);
1029    }
1030
1031    // 5. Validate input items structure
1032    if let ResponseInput::Items(items) = &request.input {
1033        // Check for at least one valid input message
1034        let has_valid_input = items.iter().any(|item| {
1035            matches!(
1036                item,
1037                ResponseInputOutputItem::Message { .. }
1038                    | ResponseInputOutputItem::SimpleInputMessage { .. }
1039            )
1040        });
1041
1042        if !has_valid_input {
1043            let mut e = ValidationError::new("input_missing_user_message");
1044            e.message = Some("Input items must contain at least one message".into());
1045            return Err(e);
1046        }
1047    }
1048
1049    // 6. Validate text format conflicts (for future structured output constraints)
1050    // Currently, Responses API doesn't have regex/ebnf like Chat API,
1051    // but this is here for completeness and future-proofing
1052
1053    Ok(())
1054}
1055
1056// ============================================================================
1057// Field-Level Validation Functions
1058// ============================================================================
1059
1060/// Validates response input is not empty and has valid content
1061fn validate_response_input(input: &ResponseInput) -> Result<(), ValidationError> {
1062    match input {
1063        ResponseInput::Text(text) => {
1064            if text.is_empty() {
1065                let mut e = ValidationError::new("input_text_empty");
1066                e.message = Some("Input text cannot be empty".into());
1067                return Err(e);
1068            }
1069        }
1070        ResponseInput::Items(items) => {
1071            if items.is_empty() {
1072                let mut e = ValidationError::new("input_items_empty");
1073                e.message = Some("Input items cannot be empty".into());
1074                return Err(e);
1075            }
1076            // Validate each item has valid content
1077            for item in items {
1078                validate_input_item(item)?;
1079            }
1080        }
1081    }
1082    Ok(())
1083}
1084
1085/// Validates individual input items have valid content
1086fn validate_input_item(item: &ResponseInputOutputItem) -> Result<(), ValidationError> {
1087    match item {
1088        ResponseInputOutputItem::Message { content, .. } => {
1089            if content.is_empty() {
1090                let mut e = ValidationError::new("message_content_empty");
1091                e.message = Some("Message content cannot be empty".into());
1092                return Err(e);
1093            }
1094        }
1095        ResponseInputOutputItem::SimpleInputMessage { content, .. } => match content {
1096            StringOrContentParts::String(s) if s.is_empty() => {
1097                let mut e = ValidationError::new("message_content_empty");
1098                e.message = Some("Message content cannot be empty".into());
1099                return Err(e);
1100            }
1101            StringOrContentParts::Array(parts) if parts.is_empty() => {
1102                let mut e = ValidationError::new("message_content_empty");
1103                e.message = Some("Message content parts cannot be empty".into());
1104                return Err(e);
1105            }
1106            _ => {}
1107        },
1108        ResponseInputOutputItem::Reasoning { .. } => {
1109            // Reasoning content can be empty - no validation needed
1110        }
1111        ResponseInputOutputItem::FunctionCallOutput { output, .. } => {
1112            if output.is_empty() {
1113                let mut e = ValidationError::new("function_output_empty");
1114                e.message = Some("Function call output cannot be empty".into());
1115                return Err(e);
1116            }
1117        }
1118        ResponseInputOutputItem::FunctionToolCall { .. } => {}
1119    }
1120    Ok(())
1121}
1122
1123/// Validates ResponseTool structure based on tool type
1124fn validate_response_tools(tools: &[ResponseTool]) -> Result<(), ValidationError> {
1125    // MCP server_label must be present and unique (case-insensitive).
1126    let mut seen_mcp_labels: HashSet<String> = HashSet::new();
1127
1128    for (idx, tool) in tools.iter().enumerate() {
1129        match tool.r#type {
1130            ResponseToolType::Function => {
1131                if tool.function.is_none() {
1132                    let mut e = ValidationError::new("function_tool_missing_function");
1133                    e.message = Some("Function tool must have a function definition".into());
1134                    return Err(e);
1135                }
1136            }
1137            ResponseToolType::Mcp => {
1138                let Some(raw_label) = tool.server_label.as_deref().filter(|s| !s.is_empty()) else {
1139                    let mut e = ValidationError::new("missing_required_parameter");
1140                    e.message = Some(
1141                        format!("Missing required parameter: 'tools[{idx}].server_label'.").into(),
1142                    );
1143                    return Err(e);
1144                };
1145
1146                // OpenAI spec-compatible validation: require a non-empty label that starts with a
1147                // letter and contains only letters, digits, '-' and '_'.
1148                let valid = raw_label.starts_with(|c: char| c.is_ascii_alphabetic())
1149                    && raw_label
1150                        .chars()
1151                        .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_');
1152                if !valid {
1153                    let mut e = ValidationError::new("invalid_server_label");
1154                    e.message = Some(
1155                        format!(
1156                            "Invalid input {raw_label}: 'server_label' must start with a letter and consist of only letters, digits, '-' and '_'"
1157                        )
1158                        .into(),
1159                    );
1160                    return Err(e);
1161                }
1162
1163                let normalized = raw_label.to_lowercase();
1164                if !seen_mcp_labels.insert(normalized) {
1165                    let mut e = ValidationError::new("mcp_tool_duplicate_server_label");
1166                    e.message = Some(
1167                        format!(
1168                            "Duplicate MCP server_label '{raw_label}' found in 'tools' parameter."
1169                        )
1170                        .into(),
1171                    );
1172                    return Err(e);
1173                }
1174            }
1175            _ => {}
1176        }
1177    }
1178    Ok(())
1179}
1180
1181/// Validates text format configuration (JSON schema name cannot be empty)
1182fn validate_text_format(text: &TextConfig) -> Result<(), ValidationError> {
1183    if let Some(TextFormat::JsonSchema { name, .. }) = &text.format {
1184        if name.is_empty() {
1185            let mut e = ValidationError::new("json_schema_name_empty");
1186            e.message = Some("JSON schema name cannot be empty".into());
1187            return Err(e);
1188        }
1189    }
1190    Ok(())
1191}
1192
1193/// Normalize a SimpleInputMessage to a proper Message item
1194///
1195/// This helper converts SimpleInputMessage (which can have flexible content)
1196/// into a fully-structured Message item with a generated ID, role, and content array.
1197///
1198/// SimpleInputMessage items are converted to Message items with IDs generated using
1199/// the centralized ID generation pattern with "msg_" prefix for consistency.
1200///
1201/// # Arguments
1202/// * `item` - The input item to normalize
1203///
1204/// # Returns
1205/// A normalized ResponseInputOutputItem (either Message if converted, or original if not SimpleInputMessage)
1206pub fn normalize_input_item(item: &ResponseInputOutputItem) -> ResponseInputOutputItem {
1207    match item {
1208        ResponseInputOutputItem::SimpleInputMessage { content, role, .. } => {
1209            let content_vec = match content {
1210                StringOrContentParts::String(s) => {
1211                    vec![ResponseContentPart::InputText { text: s.clone() }]
1212                }
1213                StringOrContentParts::Array(parts) => parts.clone(),
1214            };
1215
1216            ResponseInputOutputItem::Message {
1217                id: generate_id("msg"),
1218                role: role.clone(),
1219                content: content_vec,
1220                status: Some("completed".to_string()),
1221            }
1222        }
1223        _ => item.clone(),
1224    }
1225}
1226
1227pub fn generate_id(prefix: &str) -> String {
1228    use rand::RngCore;
1229    let mut rng = rand::rng();
1230    // Generate exactly 50 hex characters (25 bytes) for the part after the underscore
1231    let mut bytes = [0u8; 25];
1232    rng.fill_bytes(&mut bytes);
1233    let hex_string: String = bytes.iter().map(|b| format!("{b:02x}")).collect();
1234    format!("{prefix}_{hex_string}")
1235}
1236
1237#[serde_with::skip_serializing_none]
1238#[derive(Debug, Clone, Deserialize, Serialize)]
1239pub struct ResponsesResponse {
1240    /// Response ID
1241    pub id: String,
1242
1243    /// Object type
1244    #[serde(default = "default_object_type")]
1245    pub object: String,
1246
1247    /// Creation timestamp
1248    pub created_at: i64,
1249
1250    /// Response status
1251    pub status: ResponseStatus,
1252
1253    /// Error information if status is failed
1254    pub error: Option<Value>,
1255
1256    /// Incomplete details if response was truncated
1257    pub incomplete_details: Option<Value>,
1258
1259    /// System instructions used
1260    pub instructions: Option<String>,
1261
1262    /// Max output tokens setting
1263    pub max_output_tokens: Option<u32>,
1264
1265    /// Model name
1266    pub model: String,
1267
1268    /// Output items
1269    #[serde(default)]
1270    pub output: Vec<ResponseOutputItem>,
1271
1272    /// Whether parallel tool calls are enabled
1273    #[serde(default = "default_true")]
1274    pub parallel_tool_calls: bool,
1275
1276    /// Previous response ID if this is a continuation
1277    pub previous_response_id: Option<String>,
1278
1279    /// Reasoning information
1280    pub reasoning: Option<ReasoningInfo>,
1281
1282    /// Whether the response is stored
1283    #[serde(default = "default_true")]
1284    pub store: bool,
1285
1286    /// Temperature setting used
1287    pub temperature: Option<f32>,
1288
1289    /// Text format settings
1290    pub text: Option<TextConfig>,
1291
1292    /// Tool choice setting
1293    #[serde(default = "default_tool_choice")]
1294    pub tool_choice: String,
1295
1296    /// Available tools
1297    #[serde(default)]
1298    pub tools: Vec<ResponseTool>,
1299
1300    /// Top-p setting used
1301    pub top_p: Option<f32>,
1302
1303    /// Truncation strategy used
1304    pub truncation: Option<String>,
1305
1306    /// Usage information
1307    pub usage: Option<ResponsesUsage>,
1308
1309    /// User identifier
1310    pub user: Option<String>,
1311
1312    /// Safety identifier for content moderation
1313    pub safety_identifier: Option<String>,
1314
1315    /// Additional metadata
1316    #[serde(default)]
1317    pub metadata: HashMap<String, Value>,
1318}
1319
1320fn default_object_type() -> String {
1321    "response".to_string()
1322}
1323
1324fn default_tool_choice() -> String {
1325    "auto".to_string()
1326}
1327
1328impl ResponsesResponse {
1329    /// Create a builder for constructing a ResponsesResponse
1330    pub fn builder(id: impl Into<String>, model: impl Into<String>) -> ResponsesResponseBuilder {
1331        ResponsesResponseBuilder::new(id, model)
1332    }
1333
1334    /// Check if the response is complete
1335    pub fn is_complete(&self) -> bool {
1336        matches!(self.status, ResponseStatus::Completed)
1337    }
1338
1339    /// Check if the response is in progress
1340    pub fn is_in_progress(&self) -> bool {
1341        matches!(self.status, ResponseStatus::InProgress)
1342    }
1343
1344    /// Check if the response failed
1345    pub fn is_failed(&self) -> bool {
1346        matches!(self.status, ResponseStatus::Failed)
1347    }
1348}
1349
1350impl ResponseOutputItem {
1351    /// Create a new message output item
1352    pub fn new_message(
1353        id: String,
1354        role: String,
1355        content: Vec<ResponseContentPart>,
1356        status: String,
1357    ) -> Self {
1358        Self::Message {
1359            id,
1360            role,
1361            content,
1362            status,
1363        }
1364    }
1365
1366    /// Create a new reasoning output item
1367    pub fn new_reasoning(
1368        id: String,
1369        summary: Vec<String>,
1370        content: Vec<ResponseReasoningContent>,
1371        status: Option<String>,
1372    ) -> Self {
1373        Self::Reasoning {
1374            id,
1375            summary,
1376            content,
1377            status,
1378        }
1379    }
1380
1381    /// Create a new function tool call output item
1382    pub fn new_function_tool_call(
1383        id: String,
1384        call_id: String,
1385        name: String,
1386        arguments: String,
1387        output: Option<String>,
1388        status: String,
1389    ) -> Self {
1390        Self::FunctionToolCall {
1391            id,
1392            call_id,
1393            name,
1394            arguments,
1395            output,
1396            status,
1397        }
1398    }
1399}
1400
1401impl ResponseContentPart {
1402    /// Create a new text content part
1403    pub fn new_text(
1404        text: String,
1405        annotations: Vec<String>,
1406        logprobs: Option<ChatLogProbs>,
1407    ) -> Self {
1408        Self::OutputText {
1409            text,
1410            annotations,
1411            logprobs,
1412        }
1413    }
1414}
1415
1416impl ResponseReasoningContent {
1417    /// Create a new reasoning text content
1418    pub fn new_reasoning_text(text: String) -> Self {
1419        Self::ReasoningText { text }
1420    }
1421}