Skip to main content

openai_protocol/
responses.rs

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