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