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