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_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#[serde_with::skip_serializing_none]
24#[derive(Debug, Clone, Deserialize, Serialize)]
25pub struct ResponseTool {
26 #[serde(rename = "type")]
27 pub r#type: ResponseToolType,
28 #[serde(flatten)]
31 pub function: Option<Function>,
32 pub server_url: Option<String>,
34 pub authorization: Option<String>,
35 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#[serde_with::skip_serializing_none]
73#[derive(Debug, Clone, Deserialize, Serialize)]
74pub struct ResponseReasoningParam {
75 #[serde(default = "default_reasoning_effort")]
76 pub effort: Option<ReasoningEffort>,
77 pub summary: Option<ReasoningSummary>,
78}
79
80#[expect(
81 clippy::unnecessary_wraps,
82 reason = "serde default function must match field type Option<T>"
83)]
84fn default_reasoning_effort() -> Option<ReasoningEffort> {
85 Some(ReasoningEffort::Medium)
86}
87
88#[derive(Debug, Clone, Deserialize, Serialize)]
89#[serde(rename_all = "snake_case")]
90pub enum ReasoningEffort {
91 Minimal,
92 Low,
93 Medium,
94 High,
95}
96
97#[derive(Debug, Clone, Deserialize, Serialize)]
98#[serde(rename_all = "snake_case")]
99pub enum ReasoningSummary {
100 Auto,
101 Concise,
102 Detailed,
103}
104
105#[derive(Debug, Clone, Deserialize, Serialize)]
111#[serde(untagged)]
112pub enum StringOrContentParts {
113 String(String),
114 Array(Vec<ResponseContentPart>),
115}
116
117#[derive(Debug, Clone, Deserialize, Serialize)]
118#[serde(tag = "type")]
119#[serde(rename_all = "snake_case")]
120pub enum ResponseInputOutputItem {
121 #[serde(rename = "message")]
122 Message {
123 id: String,
124 role: String,
125 content: Vec<ResponseContentPart>,
126 #[serde(skip_serializing_if = "Option::is_none")]
127 status: Option<String>,
128 },
129 #[serde(rename = "reasoning")]
130 Reasoning {
131 id: String,
132 summary: Vec<String>,
133 #[serde(skip_serializing_if = "Vec::is_empty")]
134 #[serde(default)]
135 content: Vec<ResponseReasoningContent>,
136 #[serde(skip_serializing_if = "Option::is_none")]
137 status: Option<String>,
138 },
139 #[serde(rename = "function_call")]
140 FunctionToolCall {
141 id: String,
142 call_id: String,
143 name: String,
144 arguments: String,
145 #[serde(skip_serializing_if = "Option::is_none")]
146 output: Option<String>,
147 #[serde(skip_serializing_if = "Option::is_none")]
148 status: Option<String>,
149 },
150 #[serde(rename = "function_call_output")]
151 FunctionCallOutput {
152 id: Option<String>,
153 call_id: String,
154 output: String,
155 #[serde(skip_serializing_if = "Option::is_none")]
156 status: Option<String>,
157 },
158 #[serde(untagged)]
159 SimpleInputMessage {
160 content: StringOrContentParts,
161 role: String,
162 #[serde(skip_serializing_if = "Option::is_none")]
163 #[serde(rename = "type")]
164 r#type: Option<String>,
165 },
166}
167
168#[derive(Debug, Clone, Deserialize, Serialize)]
169#[serde(tag = "type")]
170#[serde(rename_all = "snake_case")]
171pub enum ResponseContentPart {
172 #[serde(rename = "output_text")]
173 OutputText {
174 text: String,
175 #[serde(default)]
176 #[serde(skip_serializing_if = "Vec::is_empty")]
177 annotations: Vec<String>,
178 #[serde(skip_serializing_if = "Option::is_none")]
179 logprobs: Option<ChatLogProbs>,
180 },
181 #[serde(rename = "input_text")]
182 InputText { text: String },
183 #[serde(other)]
184 Unknown,
185}
186
187#[derive(Debug, Clone, Deserialize, Serialize)]
188#[serde(tag = "type")]
189#[serde(rename_all = "snake_case")]
190pub enum ResponseReasoningContent {
191 #[serde(rename = "reasoning_text")]
192 ReasoningText { text: String },
193}
194
195#[serde_with::skip_serializing_none]
197#[derive(Debug, Clone, Deserialize, Serialize)]
198pub struct McpToolInfo {
199 pub name: String,
200 pub description: Option<String>,
201 pub input_schema: Value,
202 pub annotations: Option<Value>,
203}
204
205#[serde_with::skip_serializing_none]
206#[derive(Debug, Clone, Deserialize, Serialize)]
207#[serde(tag = "type")]
208#[serde(rename_all = "snake_case")]
209pub enum ResponseOutputItem {
210 #[serde(rename = "message")]
211 Message {
212 id: String,
213 role: String,
214 content: Vec<ResponseContentPart>,
215 status: String,
216 },
217 #[serde(rename = "reasoning")]
218 Reasoning {
219 id: String,
220 summary: Vec<String>,
221 content: Vec<ResponseReasoningContent>,
222 status: Option<String>,
223 },
224 #[serde(rename = "function_call")]
225 FunctionToolCall {
226 id: String,
227 call_id: String,
228 name: String,
229 arguments: String,
230 output: Option<String>,
231 status: String,
232 },
233 #[serde(rename = "mcp_list_tools")]
234 McpListTools {
235 id: String,
236 server_label: String,
237 tools: Vec<McpToolInfo>,
238 },
239 #[serde(rename = "mcp_call")]
240 McpCall {
241 id: String,
242 status: String,
243 approval_request_id: Option<String>,
244 arguments: String,
245 error: Option<String>,
246 name: String,
247 output: String,
248 server_label: String,
249 },
250 #[serde(rename = "web_search_call")]
251 WebSearchCall {
252 id: String,
253 status: WebSearchCallStatus,
254 action: WebSearchAction,
255 },
256 #[serde(rename = "code_interpreter_call")]
257 CodeInterpreterCall {
258 id: String,
259 status: CodeInterpreterCallStatus,
260 container_id: String,
261 code: Option<String>,
262 outputs: Option<Vec<CodeInterpreterOutput>>,
263 },
264 #[serde(rename = "file_search_call")]
265 FileSearchCall {
266 id: String,
267 status: FileSearchCallStatus,
268 queries: Vec<String>,
269 results: Option<Vec<FileSearchResult>>,
270 },
271}
272
273#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
279#[serde(rename_all = "snake_case")]
280pub enum WebSearchCallStatus {
281 InProgress,
282 Searching,
283 Completed,
284 Failed,
285}
286
287#[derive(Debug, Clone, Deserialize, Serialize)]
289#[serde(tag = "type", rename_all = "snake_case")]
290pub enum WebSearchAction {
291 Search {
292 #[serde(skip_serializing_if = "Option::is_none")]
293 query: Option<String>,
294 #[serde(default, skip_serializing_if = "Vec::is_empty")]
295 queries: Vec<String>,
296 #[serde(default, skip_serializing_if = "Vec::is_empty")]
297 sources: Vec<WebSearchSource>,
298 },
299 OpenPage {
300 url: String,
301 },
302 Find {
303 url: String,
304 pattern: String,
305 },
306}
307
308#[derive(Debug, Clone, Deserialize, Serialize)]
310pub struct WebSearchSource {
311 #[serde(rename = "type")]
312 pub source_type: String,
313 pub url: String,
314}
315
316#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
318#[serde(rename_all = "snake_case")]
319pub enum CodeInterpreterCallStatus {
320 InProgress,
321 Completed,
322 Incomplete,
323 Interpreting,
324 Failed,
325}
326
327#[derive(Debug, Clone, Deserialize, Serialize)]
329#[serde(tag = "type", rename_all = "snake_case")]
330pub enum CodeInterpreterOutput {
331 Logs { logs: String },
332 Image { url: String },
333}
334
335#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
337#[serde(rename_all = "snake_case")]
338pub enum FileSearchCallStatus {
339 InProgress,
340 Searching,
341 Completed,
342 Incomplete,
343 Failed,
344}
345
346#[serde_with::skip_serializing_none]
348#[derive(Debug, Clone, Deserialize, Serialize)]
349pub struct FileSearchResult {
350 pub file_id: String,
351 pub filename: String,
352 pub text: Option<String>,
353 pub score: Option<f32>,
354 pub attributes: Option<Value>,
355}
356
357#[derive(Debug, Clone, Deserialize, Serialize, Default)]
362#[serde(rename_all = "snake_case")]
363pub enum ServiceTier {
364 #[default]
365 Auto,
366 Default,
367 Flex,
368 Scale,
369 Priority,
370}
371
372#[derive(Debug, Clone, Deserialize, Serialize, Default)]
373#[serde(rename_all = "snake_case")]
374pub enum Truncation {
375 Auto,
376 #[default]
377 Disabled,
378}
379
380#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
381#[serde(rename_all = "snake_case")]
382pub enum ResponseStatus {
383 Queued,
384 InProgress,
385 Completed,
386 Failed,
387 Cancelled,
388}
389
390#[serde_with::skip_serializing_none]
391#[derive(Debug, Clone, Deserialize, Serialize)]
392pub struct ReasoningInfo {
393 pub effort: Option<String>,
394 pub summary: Option<String>,
395}
396
397#[derive(Debug, Clone, Deserialize, Serialize)]
403pub struct TextConfig {
404 #[serde(skip_serializing_if = "Option::is_none")]
405 pub format: Option<TextFormat>,
406}
407
408#[serde_with::skip_serializing_none]
410#[derive(Debug, Clone, Deserialize, Serialize)]
411#[serde(tag = "type")]
412pub enum TextFormat {
413 #[serde(rename = "text")]
414 Text,
415
416 #[serde(rename = "json_object")]
417 JsonObject,
418
419 #[serde(rename = "json_schema")]
420 JsonSchema {
421 name: String,
422 schema: Value,
423 description: Option<String>,
424 strict: Option<bool>,
425 },
426}
427
428#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
429#[serde(rename_all = "snake_case")]
430pub enum IncludeField {
431 #[serde(rename = "code_interpreter_call.outputs")]
432 CodeInterpreterCallOutputs,
433 #[serde(rename = "computer_call_output.output.image_url")]
434 ComputerCallOutputImageUrl,
435 #[serde(rename = "file_search_call.results")]
436 FileSearchCallResults,
437 #[serde(rename = "message.input_image.image_url")]
438 MessageInputImageUrl,
439 #[serde(rename = "message.output_text.logprobs")]
440 MessageOutputTextLogprobs,
441 #[serde(rename = "reasoning.encrypted_content")]
442 ReasoningEncryptedContent,
443}
444
445#[serde_with::skip_serializing_none]
451#[derive(Debug, Clone, Deserialize, Serialize)]
452pub struct ResponseUsage {
453 pub input_tokens: u32,
454 pub output_tokens: u32,
455 pub total_tokens: u32,
456 pub input_tokens_details: Option<InputTokensDetails>,
457 pub output_tokens_details: Option<OutputTokensDetails>,
458}
459
460#[derive(Debug, Clone, Deserialize, Serialize)]
461#[serde(untagged)]
462pub enum ResponsesUsage {
463 Classic(UsageInfo),
464 Modern(ResponseUsage),
465}
466
467#[derive(Debug, Clone, Deserialize, Serialize)]
468pub struct InputTokensDetails {
469 pub cached_tokens: u32,
470}
471
472#[derive(Debug, Clone, Deserialize, Serialize)]
473pub struct OutputTokensDetails {
474 pub reasoning_tokens: u32,
475}
476
477impl UsageInfo {
478 pub fn to_response_usage(&self) -> ResponseUsage {
480 ResponseUsage {
481 input_tokens: self.prompt_tokens,
482 output_tokens: self.completion_tokens,
483 total_tokens: self.total_tokens,
484 input_tokens_details: self.prompt_tokens_details.as_ref().map(|details| {
485 InputTokensDetails {
486 cached_tokens: details.cached_tokens,
487 }
488 }),
489 output_tokens_details: self.reasoning_tokens.map(|tokens| OutputTokensDetails {
490 reasoning_tokens: tokens,
491 }),
492 }
493 }
494}
495
496impl From<UsageInfo> for ResponseUsage {
497 fn from(usage: UsageInfo) -> Self {
498 usage.to_response_usage()
499 }
500}
501
502impl ResponseUsage {
503 pub fn to_usage_info(&self) -> UsageInfo {
505 UsageInfo {
506 prompt_tokens: self.input_tokens,
507 completion_tokens: self.output_tokens,
508 total_tokens: self.total_tokens,
509 reasoning_tokens: self
510 .output_tokens_details
511 .as_ref()
512 .map(|details| details.reasoning_tokens),
513 prompt_tokens_details: self.input_tokens_details.as_ref().map(|details| {
514 PromptTokenUsageInfo {
515 cached_tokens: details.cached_tokens,
516 }
517 }),
518 }
519 }
520}
521
522#[derive(Debug, Clone, Default, Deserialize, Serialize)]
523pub struct ResponsesGetParams {
524 #[serde(default)]
525 pub include: Vec<String>,
526 #[serde(default)]
527 pub include_obfuscation: Option<bool>,
528 #[serde(default)]
529 pub starting_after: Option<i64>,
530 #[serde(default)]
531 pub stream: Option<bool>,
532}
533
534impl ResponsesUsage {
535 pub fn to_response_usage(&self) -> ResponseUsage {
536 match self {
537 ResponsesUsage::Classic(usage) => usage.to_response_usage(),
538 ResponsesUsage::Modern(usage) => usage.clone(),
539 }
540 }
541
542 pub fn to_usage_info(&self) -> UsageInfo {
543 match self {
544 ResponsesUsage::Classic(usage) => usage.clone(),
545 ResponsesUsage::Modern(usage) => usage.to_usage_info(),
546 }
547 }
548}
549
550fn default_top_k() -> i32 {
555 -1
556}
557
558fn default_repetition_penalty() -> f32 {
559 1.0
560}
561
562#[expect(
563 clippy::unnecessary_wraps,
564 reason = "serde default function must match field type Option<T>"
565)]
566fn default_temperature() -> Option<f32> {
567 Some(1.0)
568}
569
570#[expect(
571 clippy::unnecessary_wraps,
572 reason = "serde default function must match field type Option<T>"
573)]
574fn default_top_p() -> Option<f32> {
575 Some(1.0)
576}
577
578#[derive(Debug, Clone, Deserialize, Serialize, Validate)]
583#[validate(schema(function = "validate_responses_cross_parameters"))]
584pub struct ResponsesRequest {
585 #[serde(skip_serializing_if = "Option::is_none")]
587 pub background: Option<bool>,
588
589 #[serde(skip_serializing_if = "Option::is_none")]
591 pub include: Option<Vec<IncludeField>>,
592
593 #[validate(custom(function = "validate_response_input"))]
595 pub input: ResponseInput,
596
597 #[serde(skip_serializing_if = "Option::is_none")]
599 pub instructions: Option<String>,
600
601 #[serde(skip_serializing_if = "Option::is_none")]
603 #[validate(range(min = 1))]
604 pub max_output_tokens: Option<u32>,
605
606 #[serde(skip_serializing_if = "Option::is_none")]
608 #[validate(range(min = 1))]
609 pub max_tool_calls: Option<u32>,
610
611 #[serde(skip_serializing_if = "Option::is_none")]
613 pub metadata: Option<HashMap<String, Value>>,
614
615 #[serde(default = "default_model")]
617 pub model: String,
618
619 #[serde(skip_serializing_if = "Option::is_none")]
621 #[validate(custom(function = "validate_conversation_id"))]
622 pub conversation: Option<String>,
623
624 #[serde(skip_serializing_if = "Option::is_none")]
626 pub parallel_tool_calls: Option<bool>,
627
628 #[serde(skip_serializing_if = "Option::is_none")]
630 pub previous_response_id: Option<String>,
631
632 #[serde(skip_serializing_if = "Option::is_none")]
634 pub reasoning: Option<ResponseReasoningParam>,
635
636 #[serde(skip_serializing_if = "Option::is_none")]
638 pub service_tier: Option<ServiceTier>,
639
640 #[serde(skip_serializing_if = "Option::is_none")]
642 pub store: Option<bool>,
643
644 #[serde(default)]
646 pub stream: Option<bool>,
647
648 #[serde(
650 default = "default_temperature",
651 skip_serializing_if = "Option::is_none"
652 )]
653 #[validate(range(min = 0.0, max = 2.0))]
654 pub temperature: Option<f32>,
655
656 #[serde(skip_serializing_if = "Option::is_none")]
658 pub tool_choice: Option<ToolChoice>,
659
660 #[serde(skip_serializing_if = "Option::is_none")]
662 #[validate(custom(function = "validate_response_tools"))]
663 pub tools: Option<Vec<ResponseTool>>,
664
665 #[serde(skip_serializing_if = "Option::is_none")]
667 #[validate(range(min = 0, max = 20))]
668 pub top_logprobs: Option<u32>,
669
670 #[serde(default = "default_top_p", skip_serializing_if = "Option::is_none")]
672 #[validate(custom(function = "validate_top_p_value"))]
673 pub top_p: Option<f32>,
674
675 #[serde(skip_serializing_if = "Option::is_none")]
677 pub truncation: Option<Truncation>,
678
679 #[serde(skip_serializing_if = "Option::is_none")]
681 #[validate(custom(function = "validate_text_format"))]
682 pub text: Option<TextConfig>,
683
684 #[serde(skip_serializing_if = "Option::is_none")]
686 pub user: Option<String>,
687
688 #[serde(skip_serializing_if = "Option::is_none")]
690 pub request_id: Option<String>,
691
692 #[serde(default)]
694 pub priority: i32,
695
696 #[serde(skip_serializing_if = "Option::is_none")]
698 #[validate(range(min = -2.0, max = 2.0))]
699 pub frequency_penalty: Option<f32>,
700
701 #[serde(skip_serializing_if = "Option::is_none")]
703 #[validate(range(min = -2.0, max = 2.0))]
704 pub presence_penalty: Option<f32>,
705
706 #[serde(skip_serializing_if = "Option::is_none")]
708 #[validate(custom(function = "validate_stop"))]
709 pub stop: Option<StringOrArray>,
710
711 #[serde(default = "default_top_k")]
713 #[validate(custom(function = "validate_top_k_value"))]
714 pub top_k: i32,
715
716 #[serde(default)]
718 #[validate(range(min = 0.0, max = 1.0))]
719 pub min_p: f32,
720
721 #[serde(default = "default_repetition_penalty")]
723 #[validate(range(min = 0.0, max = 2.0))]
724 pub repetition_penalty: f32,
725}
726
727#[derive(Debug, Clone, Deserialize, Serialize)]
728#[serde(untagged)]
729pub enum ResponseInput {
730 Items(Vec<ResponseInputOutputItem>),
731 Text(String),
732}
733
734impl Default for ResponsesRequest {
735 fn default() -> Self {
736 Self {
737 background: None,
738 include: None,
739 input: ResponseInput::Text(String::new()),
740 instructions: None,
741 max_output_tokens: None,
742 max_tool_calls: None,
743 metadata: None,
744 model: default_model(),
745 conversation: None,
746 parallel_tool_calls: None,
747 previous_response_id: None,
748 reasoning: None,
749 service_tier: None,
750 store: None,
751 stream: None,
752 temperature: None,
753 tool_choice: None,
754 tools: None,
755 top_logprobs: None,
756 top_p: None,
757 truncation: None,
758 text: None,
759 user: None,
760 request_id: None,
761 priority: 0,
762 frequency_penalty: None,
763 presence_penalty: None,
764 stop: None,
765 top_k: default_top_k(),
766 min_p: 0.0,
767 repetition_penalty: default_repetition_penalty(),
768 }
769 }
770}
771
772impl Normalizable for ResponsesRequest {
773 fn normalize(&mut self) {
778 if self.tool_choice.is_none() {
780 if let Some(tools) = &self.tools {
781 let choice_value = if tools.is_empty() {
782 ToolChoiceValue::None
783 } else {
784 ToolChoiceValue::Auto
785 };
786 self.tool_choice = Some(ToolChoice::Value(choice_value));
787 }
788 }
790
791 if self.parallel_tool_calls.is_none() && self.tools.is_some() {
793 self.parallel_tool_calls = Some(true);
794 }
795
796 if self.store.is_none() {
798 self.store = Some(true);
799 }
800 }
801}
802
803impl GenerationRequest for ResponsesRequest {
804 fn is_stream(&self) -> bool {
805 self.stream.unwrap_or(false)
806 }
807
808 fn get_model(&self) -> Option<&str> {
809 Some(self.model.as_str())
810 }
811
812 fn extract_text_for_routing(&self) -> String {
813 match &self.input {
814 ResponseInput::Text(text) => text.clone(),
815 ResponseInput::Items(items) => items
816 .iter()
817 .filter_map(|item| match item {
818 ResponseInputOutputItem::Message { content, .. } => {
819 let texts: Vec<String> = content
820 .iter()
821 .filter_map(|part| match part {
822 ResponseContentPart::OutputText { text, .. } => Some(text.clone()),
823 ResponseContentPart::InputText { text } => Some(text.clone()),
824 ResponseContentPart::Unknown => None,
825 })
826 .collect();
827 if texts.is_empty() {
828 None
829 } else {
830 Some(texts.join(" "))
831 }
832 }
833 ResponseInputOutputItem::SimpleInputMessage { content, .. } => {
834 match content {
835 StringOrContentParts::String(s) => Some(s.clone()),
836 StringOrContentParts::Array(parts) => {
837 let texts: Vec<String> = parts
839 .iter()
840 .filter_map(|part| match part {
841 ResponseContentPart::InputText { text } => {
842 Some(text.clone())
843 }
844 _ => None,
845 })
846 .collect();
847 if texts.is_empty() {
848 None
849 } else {
850 Some(texts.join(" "))
851 }
852 }
853 }
854 }
855 ResponseInputOutputItem::Reasoning { content, .. } => {
856 let texts: Vec<String> = content
857 .iter()
858 .map(|part| match part {
859 ResponseReasoningContent::ReasoningText { text } => text.clone(),
860 })
861 .collect();
862 if texts.is_empty() {
863 None
864 } else {
865 Some(texts.join(" "))
866 }
867 }
868 ResponseInputOutputItem::FunctionToolCall { arguments, .. } => {
869 Some(arguments.clone())
870 }
871 ResponseInputOutputItem::FunctionCallOutput { output, .. } => {
872 Some(output.clone())
873 }
874 })
875 .collect::<Vec<String>>()
876 .join(" "),
877 }
878 }
879}
880
881pub fn validate_conversation_id(conv_id: &str) -> Result<(), ValidationError> {
883 if !conv_id.starts_with("conv_") {
884 let mut error = ValidationError::new("invalid_conversation_id");
885 error.message = Some(std::borrow::Cow::Owned(format!(
886 "Invalid 'conversation': '{conv_id}'. Expected an ID that begins with 'conv_'."
887 )));
888 return Err(error);
889 }
890
891 let is_valid = conv_id
893 .chars()
894 .all(|c| c.is_alphanumeric() || c == '_' || c == '-');
895
896 if !is_valid {
897 let mut error = ValidationError::new("invalid_conversation_id");
898 error.message = Some(std::borrow::Cow::Owned(format!(
899 "Invalid 'conversation': '{conv_id}'. Expected an ID that contains letters, numbers, underscores, or dashes, but this value contained additional characters."
900 )));
901 return Err(error);
902 }
903 Ok(())
904}
905
906fn validate_tool_choice_with_tools(request: &ResponsesRequest) -> Result<(), ValidationError> {
908 let Some(tool_choice) = &request.tool_choice else {
909 return Ok(());
910 };
911
912 let has_tools = request.tools.as_ref().is_some_and(|t| !t.is_empty());
913 let is_some_choice = !matches!(tool_choice, ToolChoice::Value(ToolChoiceValue::None));
914
915 if is_some_choice && !has_tools {
917 let mut e = ValidationError::new("tool_choice_requires_tools");
918 e.message = Some("Invalid value for 'tool_choice': 'tool_choice' is only allowed when 'tools' are specified.".into());
919 return Err(e);
920 }
921
922 if !has_tools {
924 return Ok(());
925 }
926
927 let Some(tools) = request.tools.as_ref() else {
930 return Ok(());
931 };
932 let function_tool_names: Vec<&str> = tools
933 .iter()
934 .filter_map(|t| match t.r#type {
935 ResponseToolType::Function => t.function.as_ref().map(|f| f.name.as_str()),
936 _ => None,
937 })
938 .collect();
939
940 match tool_choice {
942 ToolChoice::Function { function, .. } => {
943 if !function_tool_names.contains(&function.name.as_str()) {
944 let mut e = ValidationError::new("tool_choice_function_not_found");
945 e.message = Some(
946 format!(
947 "Invalid value for 'tool_choice': function '{}' not found in 'tools'.",
948 function.name
949 )
950 .into(),
951 );
952 return Err(e);
953 }
954 }
955 ToolChoice::AllowedTools {
956 mode,
957 tools: allowed_tools,
958 ..
959 } => {
960 if mode != "auto" && mode != "required" {
962 let mut e = ValidationError::new("tool_choice_invalid_mode");
963 e.message = Some(
964 format!(
965 "Invalid value for 'tool_choice.mode': must be 'auto' or 'required', got '{mode}'."
966 )
967 .into(),
968 );
969 return Err(e);
970 }
971
972 for tool_ref in allowed_tools {
974 if let ToolReference::Function { name } = tool_ref {
975 if !function_tool_names.contains(&name.as_str()) {
976 let mut e = ValidationError::new("tool_choice_tool_not_found");
977 e.message = Some(
978 format!(
979 "Invalid value for 'tool_choice.tools': tool '{name}' not found in 'tools'."
980 )
981 .into(),
982 );
983 return Err(e);
984 }
985 }
986 }
989 }
990 ToolChoice::Value(_) => {}
991 }
992
993 Ok(())
994}
995
996fn validate_responses_cross_parameters(request: &ResponsesRequest) -> Result<(), ValidationError> {
998 validate_tool_choice_with_tools(request)?;
1000
1001 if request.top_logprobs.is_some() {
1003 let has_logprobs_include = request
1004 .include
1005 .as_ref()
1006 .is_some_and(|inc| inc.contains(&IncludeField::MessageOutputTextLogprobs));
1007
1008 if !has_logprobs_include {
1009 let mut e = ValidationError::new("top_logprobs_requires_include");
1010 e.message = Some(
1011 "top_logprobs requires include field with 'message.output_text.logprobs'".into(),
1012 );
1013 return Err(e);
1014 }
1015 }
1016
1017 if request.background == Some(true) && request.stream == Some(true) {
1019 let mut e = ValidationError::new("background_conflicts_with_stream");
1020 e.message = Some("Cannot use background mode with streaming".into());
1021 return Err(e);
1022 }
1023
1024 if request.conversation.is_some() && request.previous_response_id.is_some() {
1026 let mut e = ValidationError::new("mutually_exclusive_parameters");
1027 e.message = Some("Mutually exclusive parameters. Ensure you are only providing one of: 'previous_response_id' or 'conversation'.".into());
1028 return Err(e);
1029 }
1030
1031 if let ResponseInput::Items(items) = &request.input {
1033 let has_valid_input = items.iter().any(|item| {
1035 matches!(
1036 item,
1037 ResponseInputOutputItem::Message { .. }
1038 | ResponseInputOutputItem::SimpleInputMessage { .. }
1039 )
1040 });
1041
1042 if !has_valid_input {
1043 let mut e = ValidationError::new("input_missing_user_message");
1044 e.message = Some("Input items must contain at least one message".into());
1045 return Err(e);
1046 }
1047 }
1048
1049 Ok(())
1054}
1055
1056fn validate_response_input(input: &ResponseInput) -> Result<(), ValidationError> {
1062 match input {
1063 ResponseInput::Text(text) => {
1064 if text.is_empty() {
1065 let mut e = ValidationError::new("input_text_empty");
1066 e.message = Some("Input text cannot be empty".into());
1067 return Err(e);
1068 }
1069 }
1070 ResponseInput::Items(items) => {
1071 if items.is_empty() {
1072 let mut e = ValidationError::new("input_items_empty");
1073 e.message = Some("Input items cannot be empty".into());
1074 return Err(e);
1075 }
1076 for item in items {
1078 validate_input_item(item)?;
1079 }
1080 }
1081 }
1082 Ok(())
1083}
1084
1085fn validate_input_item(item: &ResponseInputOutputItem) -> Result<(), ValidationError> {
1087 match item {
1088 ResponseInputOutputItem::Message { content, .. } => {
1089 if content.is_empty() {
1090 let mut e = ValidationError::new("message_content_empty");
1091 e.message = Some("Message content cannot be empty".into());
1092 return Err(e);
1093 }
1094 }
1095 ResponseInputOutputItem::SimpleInputMessage { content, .. } => match content {
1096 StringOrContentParts::String(s) if s.is_empty() => {
1097 let mut e = ValidationError::new("message_content_empty");
1098 e.message = Some("Message content cannot be empty".into());
1099 return Err(e);
1100 }
1101 StringOrContentParts::Array(parts) if parts.is_empty() => {
1102 let mut e = ValidationError::new("message_content_empty");
1103 e.message = Some("Message content parts cannot be empty".into());
1104 return Err(e);
1105 }
1106 _ => {}
1107 },
1108 ResponseInputOutputItem::Reasoning { .. } => {
1109 }
1111 ResponseInputOutputItem::FunctionCallOutput { output, .. } => {
1112 if output.is_empty() {
1113 let mut e = ValidationError::new("function_output_empty");
1114 e.message = Some("Function call output cannot be empty".into());
1115 return Err(e);
1116 }
1117 }
1118 ResponseInputOutputItem::FunctionToolCall { .. } => {}
1119 }
1120 Ok(())
1121}
1122
1123fn validate_response_tools(tools: &[ResponseTool]) -> Result<(), ValidationError> {
1125 let mut seen_mcp_labels: HashSet<String> = HashSet::new();
1127
1128 for (idx, tool) in tools.iter().enumerate() {
1129 match tool.r#type {
1130 ResponseToolType::Function => {
1131 if tool.function.is_none() {
1132 let mut e = ValidationError::new("function_tool_missing_function");
1133 e.message = Some("Function tool must have a function definition".into());
1134 return Err(e);
1135 }
1136 }
1137 ResponseToolType::Mcp => {
1138 let Some(raw_label) = tool.server_label.as_deref().filter(|s| !s.is_empty()) else {
1139 let mut e = ValidationError::new("missing_required_parameter");
1140 e.message = Some(
1141 format!("Missing required parameter: 'tools[{idx}].server_label'.").into(),
1142 );
1143 return Err(e);
1144 };
1145
1146 let valid = raw_label.starts_with(|c: char| c.is_ascii_alphabetic())
1149 && raw_label
1150 .chars()
1151 .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_');
1152 if !valid {
1153 let mut e = ValidationError::new("invalid_server_label");
1154 e.message = Some(
1155 format!(
1156 "Invalid input {raw_label}: 'server_label' must start with a letter and consist of only letters, digits, '-' and '_'"
1157 )
1158 .into(),
1159 );
1160 return Err(e);
1161 }
1162
1163 let normalized = raw_label.to_lowercase();
1164 if !seen_mcp_labels.insert(normalized) {
1165 let mut e = ValidationError::new("mcp_tool_duplicate_server_label");
1166 e.message = Some(
1167 format!(
1168 "Duplicate MCP server_label '{raw_label}' found in 'tools' parameter."
1169 )
1170 .into(),
1171 );
1172 return Err(e);
1173 }
1174 }
1175 _ => {}
1176 }
1177 }
1178 Ok(())
1179}
1180
1181fn validate_text_format(text: &TextConfig) -> Result<(), ValidationError> {
1183 if let Some(TextFormat::JsonSchema { name, .. }) = &text.format {
1184 if name.is_empty() {
1185 let mut e = ValidationError::new("json_schema_name_empty");
1186 e.message = Some("JSON schema name cannot be empty".into());
1187 return Err(e);
1188 }
1189 }
1190 Ok(())
1191}
1192
1193pub fn normalize_input_item(item: &ResponseInputOutputItem) -> ResponseInputOutputItem {
1207 match item {
1208 ResponseInputOutputItem::SimpleInputMessage { content, role, .. } => {
1209 let content_vec = match content {
1210 StringOrContentParts::String(s) => {
1211 vec![ResponseContentPart::InputText { text: s.clone() }]
1212 }
1213 StringOrContentParts::Array(parts) => parts.clone(),
1214 };
1215
1216 ResponseInputOutputItem::Message {
1217 id: generate_id("msg"),
1218 role: role.clone(),
1219 content: content_vec,
1220 status: Some("completed".to_string()),
1221 }
1222 }
1223 _ => item.clone(),
1224 }
1225}
1226
1227pub fn generate_id(prefix: &str) -> String {
1228 use rand::RngCore;
1229 let mut rng = rand::rng();
1230 let mut bytes = [0u8; 25];
1232 rng.fill_bytes(&mut bytes);
1233 let hex_string: String = bytes.iter().map(|b| format!("{b:02x}")).collect();
1234 format!("{prefix}_{hex_string}")
1235}
1236
1237#[serde_with::skip_serializing_none]
1238#[derive(Debug, Clone, Deserialize, Serialize)]
1239pub struct ResponsesResponse {
1240 pub id: String,
1242
1243 #[serde(default = "default_object_type")]
1245 pub object: String,
1246
1247 pub created_at: i64,
1249
1250 pub status: ResponseStatus,
1252
1253 pub error: Option<Value>,
1255
1256 pub incomplete_details: Option<Value>,
1258
1259 pub instructions: Option<String>,
1261
1262 pub max_output_tokens: Option<u32>,
1264
1265 pub model: String,
1267
1268 #[serde(default)]
1270 pub output: Vec<ResponseOutputItem>,
1271
1272 #[serde(default = "default_true")]
1274 pub parallel_tool_calls: bool,
1275
1276 pub previous_response_id: Option<String>,
1278
1279 pub reasoning: Option<ReasoningInfo>,
1281
1282 #[serde(default = "default_true")]
1284 pub store: bool,
1285
1286 pub temperature: Option<f32>,
1288
1289 pub text: Option<TextConfig>,
1291
1292 #[serde(default = "default_tool_choice")]
1294 pub tool_choice: String,
1295
1296 #[serde(default)]
1298 pub tools: Vec<ResponseTool>,
1299
1300 pub top_p: Option<f32>,
1302
1303 pub truncation: Option<String>,
1305
1306 pub usage: Option<ResponsesUsage>,
1308
1309 pub user: Option<String>,
1311
1312 pub safety_identifier: Option<String>,
1314
1315 #[serde(default)]
1317 pub metadata: HashMap<String, Value>,
1318}
1319
1320fn default_object_type() -> String {
1321 "response".to_string()
1322}
1323
1324fn default_tool_choice() -> String {
1325 "auto".to_string()
1326}
1327
1328impl ResponsesResponse {
1329 pub fn builder(id: impl Into<String>, model: impl Into<String>) -> ResponsesResponseBuilder {
1331 ResponsesResponseBuilder::new(id, model)
1332 }
1333
1334 pub fn is_complete(&self) -> bool {
1336 matches!(self.status, ResponseStatus::Completed)
1337 }
1338
1339 pub fn is_in_progress(&self) -> bool {
1341 matches!(self.status, ResponseStatus::InProgress)
1342 }
1343
1344 pub fn is_failed(&self) -> bool {
1346 matches!(self.status, ResponseStatus::Failed)
1347 }
1348}
1349
1350impl ResponseOutputItem {
1351 pub fn new_message(
1353 id: String,
1354 role: String,
1355 content: Vec<ResponseContentPart>,
1356 status: String,
1357 ) -> Self {
1358 Self::Message {
1359 id,
1360 role,
1361 content,
1362 status,
1363 }
1364 }
1365
1366 pub fn new_reasoning(
1368 id: String,
1369 summary: Vec<String>,
1370 content: Vec<ResponseReasoningContent>,
1371 status: Option<String>,
1372 ) -> Self {
1373 Self::Reasoning {
1374 id,
1375 summary,
1376 content,
1377 status,
1378 }
1379 }
1380
1381 pub fn new_function_tool_call(
1383 id: String,
1384 call_id: String,
1385 name: String,
1386 arguments: String,
1387 output: Option<String>,
1388 status: String,
1389 ) -> Self {
1390 Self::FunctionToolCall {
1391 id,
1392 call_id,
1393 name,
1394 arguments,
1395 output,
1396 status,
1397 }
1398 }
1399}
1400
1401impl ResponseContentPart {
1402 pub fn new_text(
1404 text: String,
1405 annotations: Vec<String>,
1406 logprobs: Option<ChatLogProbs>,
1407 ) -> Self {
1408 Self::OutputText {
1409 text,
1410 annotations,
1411 logprobs,
1412 }
1413 }
1414}
1415
1416impl ResponseReasoningContent {
1417 pub fn new_reasoning_text(text: String) -> Self {
1419 Self::ReasoningText { text }
1420 }
1421}