1use 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#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
24#[serde(tag = "type")]
25#[serde(rename_all = "snake_case")]
26pub enum ResponseTool {
27 #[serde(rename = "function")]
29 Function(FunctionTool),
30
31 #[serde(rename = "web_search_preview")]
33 WebSearchPreview(WebSearchPreviewTool),
34
35 #[serde(rename = "code_interpreter")]
37 CodeInterpreter(CodeInterpreterTool),
38
39 #[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 #[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 pub headers: Option<HashMap<String, String>>,
61 pub server_label: String,
62 pub server_description: Option<String>,
63 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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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 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 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
592fn 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#[derive(Debug, Clone, Deserialize, Serialize, Validate, schemars::JsonSchema)]
617#[validate(schema(function = "validate_responses_cross_parameters"))]
618pub struct ResponsesRequest {
619 #[serde(skip_serializing_if = "Option::is_none")]
621 pub background: Option<bool>,
622
623 #[serde(skip_serializing_if = "Option::is_none")]
625 pub include: Option<Vec<IncludeField>>,
626
627 #[validate(custom(function = "validate_response_input"))]
629 pub input: ResponseInput,
630
631 #[serde(skip_serializing_if = "Option::is_none")]
633 pub instructions: Option<String>,
634
635 #[serde(skip_serializing_if = "Option::is_none")]
637 #[validate(range(min = 1))]
638 pub max_output_tokens: Option<u32>,
639
640 #[serde(skip_serializing_if = "Option::is_none")]
642 #[validate(range(min = 1))]
643 pub max_tool_calls: Option<u32>,
644
645 #[serde(skip_serializing_if = "Option::is_none")]
647 pub metadata: Option<HashMap<String, Value>>,
648
649 pub model: String,
651
652 #[serde(skip_serializing_if = "Option::is_none")]
654 #[validate(custom(function = "validate_conversation_id"))]
655 pub conversation: Option<String>,
656
657 #[serde(skip_serializing_if = "Option::is_none")]
659 pub parallel_tool_calls: Option<bool>,
660
661 #[serde(skip_serializing_if = "Option::is_none")]
663 pub previous_response_id: Option<String>,
664
665 #[serde(skip_serializing_if = "Option::is_none")]
667 pub reasoning: Option<ResponseReasoningParam>,
668
669 #[serde(skip_serializing_if = "Option::is_none")]
671 pub service_tier: Option<ServiceTier>,
672
673 #[serde(skip_serializing_if = "Option::is_none")]
675 pub store: Option<bool>,
676
677 #[serde(default)]
679 pub stream: Option<bool>,
680
681 #[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 #[serde(skip_serializing_if = "Option::is_none")]
691 pub tool_choice: Option<ToolChoice>,
692
693 #[serde(skip_serializing_if = "Option::is_none")]
695 #[validate(custom(function = "validate_response_tools"))]
696 pub tools: Option<Vec<ResponseTool>>,
697
698 #[serde(skip_serializing_if = "Option::is_none")]
700 #[validate(range(min = 0, max = 20))]
701 pub top_logprobs: Option<u32>,
702
703 #[serde(skip_serializing_if = "Option::is_none")]
705 #[validate(custom(function = "validate_top_p_value"))]
706 pub top_p: Option<f32>,
707
708 #[serde(skip_serializing_if = "Option::is_none")]
710 pub truncation: Option<Truncation>,
711
712 #[serde(skip_serializing_if = "Option::is_none")]
714 #[validate(custom(function = "validate_text_format"))]
715 pub text: Option<TextConfig>,
716
717 #[serde(skip_serializing_if = "Option::is_none")]
719 pub user: Option<String>,
720
721 #[serde(skip_serializing_if = "Option::is_none")]
723 pub request_id: Option<String>,
724
725 #[serde(default)]
727 pub priority: i32,
728
729 #[serde(skip_serializing_if = "Option::is_none")]
731 #[validate(range(min = -2.0, max = 2.0))]
732 pub frequency_penalty: Option<f32>,
733
734 #[serde(skip_serializing_if = "Option::is_none")]
736 #[validate(range(min = -2.0, max = 2.0))]
737 pub presence_penalty: Option<f32>,
738
739 #[serde(skip_serializing_if = "Option::is_none")]
741 #[validate(custom(function = "validate_stop"))]
742 pub stop: Option<StringOrArray>,
743
744 #[serde(default = "default_top_k")]
746 #[validate(custom(function = "validate_top_k_value"))]
747 pub top_k: i32,
748
749 #[serde(default)]
751 #[validate(range(min = 0.0, max = 1.0))]
752 pub min_p: f32,
753
754 #[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 fn normalize(&mut self) {
811 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 }
823
824 if self.parallel_tool_calls.is_none() && self.tools.is_some() {
826 self.parallel_tool_calls = Some(true);
827 }
828
829 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
921pub 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 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
946fn 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 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 if !has_tools {
964 return Ok(());
965 }
966
967 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 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 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 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 }
1029 }
1030 ToolChoice::Value(_) => {}
1031 }
1032
1033 Ok(())
1034}
1035
1036fn validate_responses_cross_parameters(request: &ResponsesRequest) -> Result<(), ValidationError> {
1038 validate_tool_choice_with_tools(request)?;
1040
1041 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 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 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 if let ResponseInput::Items(items) = &request.input {
1073 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 Ok(())
1094}
1095
1096fn 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 for item in items {
1118 validate_input_item(item)?;
1119 }
1120 }
1121 }
1122 Ok(())
1123}
1124
1125fn 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 }
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
1165fn validate_response_tools(tools: &[ResponseTool]) -> Result<(), ValidationError> {
1167 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 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
1212fn 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
1224pub 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 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 pub id: String,
1273
1274 #[serde(default = "default_object_type")]
1276 pub object: String,
1277
1278 pub created_at: i64,
1280
1281 pub status: ResponseStatus,
1283
1284 pub error: Option<Value>,
1286
1287 pub incomplete_details: Option<Value>,
1289
1290 pub instructions: Option<String>,
1292
1293 pub max_output_tokens: Option<u32>,
1295
1296 pub model: String,
1298
1299 #[serde(default)]
1301 pub output: Vec<ResponseOutputItem>,
1302
1303 #[serde(default = "default_true")]
1305 pub parallel_tool_calls: bool,
1306
1307 pub previous_response_id: Option<String>,
1309
1310 pub reasoning: Option<ReasoningInfo>,
1312
1313 #[serde(default = "default_true")]
1315 pub store: bool,
1316
1317 pub temperature: Option<f32>,
1319
1320 pub text: Option<TextConfig>,
1322
1323 #[serde(default = "default_tool_choice")]
1325 pub tool_choice: String,
1326
1327 #[serde(default)]
1329 pub tools: Vec<ResponseTool>,
1330
1331 pub top_p: Option<f32>,
1333
1334 pub truncation: Option<String>,
1336
1337 pub usage: Option<ResponsesUsage>,
1339
1340 pub user: Option<String>,
1342
1343 pub safety_identifier: Option<String>,
1345
1346 #[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 pub fn builder(id: impl Into<String>, model: impl Into<String>) -> ResponsesResponseBuilder {
1362 ResponsesResponseBuilder::new(id, model)
1363 }
1364
1365 pub fn is_complete(&self) -> bool {
1367 matches!(self.status, ResponseStatus::Completed)
1368 }
1369
1370 pub fn is_in_progress(&self) -> bool {
1372 matches!(self.status, ResponseStatus::InProgress)
1373 }
1374
1375 pub fn is_failed(&self) -> bool {
1377 matches!(self.status, ResponseStatus::Failed)
1378 }
1379}
1380
1381impl ResponseOutputItem {
1382 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 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 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 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 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}