openai_protocol/
responses.rs

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