1use super::InputAudio;
17use crate::completion::CompletionError;
18use crate::completion::NormalizeCompletionResponse;
19use crate::http_client::HttpClientExt;
20use crate::json_utils;
21use crate::json_utils::string_or_vec;
22use crate::message::{
23 Document, DocumentMediaType, DocumentSourceKind, ImageDetail, MessageError, MimeType, Text,
24};
25use crate::providers::internal::completion_send::send_completion;
26use crate::telemetry::{CompletionOperation, CompletionSpanBuilder, SpanCombinator};
27
28use crate::wasm_compat::{WasmCompatSend, WasmCompatSync};
29use crate::{completion, message};
30use serde::{Deserialize, Deserializer, Serialize, Serializer};
31use serde_json::{Map, Value};
32use tracing::Instrument;
33
34use std::convert::Infallible;
35use std::ops::Add;
36use std::str::FromStr;
37
38pub mod streaming;
39#[cfg(all(not(target_family = "wasm"), feature = "websocket"))]
40pub mod websocket;
41
42#[derive(Debug, Deserialize, Serialize, Clone)]
45pub struct CompletionRequest {
46 pub input: Vec<InputItem>,
48 pub model: String,
50 #[serde(skip_serializing_if = "Option::is_none")]
52 pub instructions: Option<String>,
53 #[serde(skip_serializing_if = "Option::is_none")]
55 pub max_output_tokens: Option<u64>,
56 #[serde(skip_serializing_if = "Option::is_none")]
58 pub stream: Option<bool>,
59 #[serde(skip_serializing_if = "Option::is_none")]
61 pub temperature: Option<f64>,
62 #[serde(skip_serializing_if = "Option::is_none")]
65 tool_choice: Option<ToolChoice>,
66 #[serde(skip_serializing_if = "Vec::is_empty")]
69 pub tools: Vec<ResponsesToolDefinition>,
70 #[serde(flatten)]
72 pub additional_parameters: AdditionalParameters,
73}
74
75impl CompletionRequest {
76 pub fn with_structured_outputs<S>(mut self, schema_name: S, schema: serde_json::Value) -> Self
77 where
78 S: Into<String>,
79 {
80 self.additional_parameters.text = Some(TextConfig::structured_output(schema_name, schema));
81
82 self
83 }
84
85 pub fn with_reasoning(mut self, reasoning: Reasoning) -> Self {
86 self.additional_parameters.reasoning = Some(reasoning);
87
88 self
89 }
90
91 pub fn with_tool(mut self, tool: impl Into<ResponsesToolDefinition>) -> Self {
95 self.tools.push(tool.into());
96 self
97 }
98
99 pub fn with_tools<I, Tool>(mut self, tools: I) -> Self
102 where
103 I: IntoIterator<Item = Tool>,
104 Tool: Into<ResponsesToolDefinition>,
105 {
106 self.tools.extend(tools.into_iter().map(Into::into));
107 self
108 }
109}
110
111#[derive(Debug, Deserialize, Clone)]
113pub struct InputItem {
114 #[serde(skip_serializing_if = "Option::is_none")]
118 role: Option<Role>,
119 #[serde(flatten)]
121 input: InputContent,
122}
123
124impl Serialize for InputItem {
125 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
126 where
127 S: serde::Serializer,
128 {
129 let mut value = serde_json::to_value(&self.input).map_err(serde::ser::Error::custom)?;
130 let map = value.as_object_mut().ok_or_else(|| {
131 serde::ser::Error::custom("Input content must serialize to an object")
132 })?;
133
134 if let Some(role) = &self.role
135 && !map.contains_key("role")
136 {
137 map.insert(
138 "role".to_string(),
139 serde_json::to_value(role).map_err(serde::ser::Error::custom)?,
140 );
141 }
142
143 value.serialize(serializer)
144 }
145}
146
147impl InputItem {
148 pub fn system_message(content: impl Into<String>) -> Self {
149 Self {
150 role: Some(Role::System),
151 input: InputContent::Message(Message::System {
152 content: vec![SystemContent::InputText {
153 text: content.into(),
154 }],
155 name: None,
156 }),
157 }
158 }
159
160 fn user_content(content: UserContent) -> Self {
166 Self {
167 role: Some(Role::User),
168 input: InputContent::Message(Message::User {
169 content: vec![content],
170 name: None,
171 }),
172 }
173 }
174
175 pub(crate) fn system_text(&self) -> Option<String> {
176 match &self.input {
177 InputContent::Message(Message::System { content, .. }) => Some(
178 content
179 .iter()
180 .map(|item| match item {
181 SystemContent::InputText { text } => text.as_str(),
182 })
183 .collect::<Vec<_>>()
184 .join("\n"),
185 ),
186 _ => None,
187 }
188 }
189}
190
191#[derive(Debug, Deserialize, Serialize, Clone)]
193#[serde(rename_all = "lowercase")]
194pub enum Role {
195 User,
196 Assistant,
197 System,
198}
199
200#[derive(Debug, Deserialize, Serialize, Clone)]
202#[serde(tag = "type", rename_all = "snake_case")]
203pub enum InputContent {
204 Message(Message),
205 Reasoning(OpenAIReasoning),
206 FunctionCall(OutputFunctionCall),
207 FunctionCallOutput(ToolResult),
208}
209
210#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
211pub struct OpenAIReasoning {
212 id: String,
213 pub summary: Vec<ReasoningSummary>,
214 #[serde(
215 default,
216 deserialize_with = "deserialize_reasoning_text_content",
217 serialize_with = "serialize_reasoning_text_content",
218 skip_serializing_if = "Vec::is_empty"
219 )]
220 pub content: Vec<String>,
221 #[serde(skip_serializing_if = "Option::is_none")]
222 pub encrypted_content: Option<String>,
223 #[serde(skip_serializing_if = "Option::is_none")]
224 pub status: Option<ToolStatus>,
225}
226
227#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
228#[serde(tag = "type", rename_all = "snake_case")]
229pub enum ReasoningSummary {
230 SummaryText { text: String },
231}
232
233impl ReasoningSummary {
234 fn new(input: &str) -> Self {
235 Self::SummaryText {
236 text: input.to_string(),
237 }
238 }
239
240 pub fn text(&self) -> String {
241 let ReasoningSummary::SummaryText { text } = self;
242 text.clone()
243 }
244}
245
246fn reasoning_text_content_json(content: &[String]) -> Value {
247 Value::Array(
248 content
249 .iter()
250 .map(|text| {
251 serde_json::json!({
252 "type": "reasoning_text",
253 "text": text,
254 })
255 })
256 .collect(),
257 )
258}
259
260fn serialize_reasoning_text_content<S>(content: &[String], serializer: S) -> Result<S::Ok, S::Error>
261where
262 S: Serializer,
263{
264 reasoning_text_content_json(content).serialize(serializer)
265}
266
267fn deserialize_reasoning_text_content<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
268where
269 D: Deserializer<'de>,
270{
271 let value = Value::deserialize(deserializer)?;
272 Ok(match value {
273 Value::Array(items) => items
274 .into_iter()
275 .filter_map(|item| match item {
276 Value::Object(mut item) => item
277 .remove("text")
278 .and_then(|text| text.as_str().map(ToOwned::to_owned)),
279 Value::String(text) => Some(text),
280 _ => None,
281 })
282 .collect(),
283 Value::String(text) => vec![text],
284 _ => Vec::new(),
285 })
286}
287
288#[derive(Debug, Deserialize, Serialize, Clone)]
290pub struct ToolResult {
291 call_id: String,
293 output: ToolResultOutput,
295 status: ToolStatus,
297}
298
299#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
302#[serde(untagged)]
303pub enum ToolResultOutput {
304 Text(String),
306 Content(Vec<ToolResultOutputContent>),
308}
309
310#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
312#[serde(tag = "type", rename_all = "snake_case")]
313pub enum ToolResultOutputContent {
314 InputText {
316 text: String,
318 },
319 InputImage {
321 #[serde(skip_serializing_if = "Option::is_none")]
323 image_url: Option<String>,
324 #[serde(skip_serializing_if = "Option::is_none")]
327 file_id: Option<String>,
328 #[serde(default)]
330 detail: ImageDetail,
331 },
332}
333
334fn unsupported_document_source(source: DocumentSourceKind) -> CompletionError {
341 match source {
342 DocumentSourceKind::Raw(_) => CompletionError::RequestError(
343 "Raw file data not supported, encode as base64 first".into(),
344 ),
345 source => {
346 CompletionError::RequestError(format!("Unsupported document type: {source}").into())
347 }
348 }
349}
350
351fn responses_tool_result_output(
352 content: Vec<message::ToolResultContent>,
353) -> Result<ToolResultOutput, MessageError> {
354 let mut rich_output = Vec::new();
355
356 for content in content {
357 match content {
358 message::ToolResultContent::Text(Text { text, .. }) => {
359 rich_output.push(ToolResultOutputContent::InputText { text });
360 }
361 message::ToolResultContent::Json { value } => {
362 rich_output.push(ToolResultOutputContent::InputText {
363 text: value.to_string(),
364 });
365 }
366 message::ToolResultContent::Image(message::Image {
367 data,
368 media_type,
369 detail,
370 ..
371 }) => {
372 let (image_url, file_id) = match data {
373 DocumentSourceKind::Base64(data) => {
374 let media_type = media_type.ok_or_else(|| {
375 MessageError::ConversionError(
376 "A media type is required for base64 tool-result images".into(),
377 )
378 })?;
379 (
380 Some(format!(
381 "data:{media_type};base64,{data}",
382 media_type = media_type.to_mime_type()
383 )),
384 None,
385 )
386 }
387 DocumentSourceKind::Url(url) => (Some(url), None),
388 DocumentSourceKind::FileId(file_id) => (None, Some(file_id)),
389 unsupported => {
390 return Err(MessageError::ConversionError(format!(
391 "Unsupported tool-result image source: {unsupported}"
392 )));
393 }
394 };
395 rich_output.push(ToolResultOutputContent::InputImage {
396 image_url,
397 file_id,
398 detail: detail.unwrap_or_default(),
399 });
400 }
401 }
402 }
403
404 match rich_output.as_slice() {
405 [ToolResultOutputContent::InputText { text }] => Ok(ToolResultOutput::Text(text.clone())),
406
407 _ => Ok(ToolResultOutput::Content(rich_output)),
408 }
409}
410
411impl From<Message> for InputItem {
412 fn from(value: Message) -> Self {
413 match value {
414 Message::User { .. } => Self {
415 role: Some(Role::User),
416 input: InputContent::Message(value),
417 },
418 Message::Assistant { ref content, .. } => {
419 let role = if content
420 .iter()
421 .any(|x| matches!(x, AssistantContentType::Reasoning(_)))
422 {
423 None
424 } else {
425 Some(Role::Assistant)
426 };
427 Self {
428 role,
429 input: InputContent::Message(value),
430 }
431 }
432 Message::AssistantInput { .. } => Self {
433 role: Some(Role::Assistant),
434 input: InputContent::Message(value),
435 },
436 Message::System { .. } => Self {
437 role: Some(Role::System),
438 input: InputContent::Message(value),
439 },
440 Message::ToolResult {
441 tool_call_id,
442 output,
443 } => Self {
444 role: None,
445 input: InputContent::FunctionCallOutput(ToolResult {
446 call_id: tool_call_id,
447 output,
448 status: ToolStatus::Completed,
449 }),
450 },
451 }
452 }
453}
454
455impl TryFrom<crate::completion::Message> for Vec<InputItem> {
456 type Error = CompletionError;
457
458 fn try_from(value: crate::completion::Message) -> Result<Self, Self::Error> {
459 match value {
460 crate::completion::Message::System { content } => Ok(vec![InputItem {
461 role: Some(Role::System),
462 input: InputContent::Message(Message::System {
463 content: vec![content.into()],
464 name: None,
465 }),
466 }]),
467 crate::completion::Message::User { content } => {
468 let mut items = Vec::new();
469
470 for user_content in content {
471 match user_content {
472 crate::message::UserContent::Text(Text { text, .. }) => {
473 items.push(InputItem::user_content(UserContent::InputText { text }));
474 }
475 crate::message::UserContent::ToolResult(tool_result) => {
476 let call_id = tool_result.wire_call_id().to_owned();
480 let output = responses_tool_result_output(tool_result.content)
481 .map_err(|error| {
482 CompletionError::ProviderError(error.to_string())
483 })?;
484 items.push(InputItem {
485 role: None,
486 input: InputContent::FunctionCallOutput(ToolResult {
487 call_id,
488 output,
489 status: ToolStatus::Completed,
490 }),
491 });
492 }
493 crate::message::UserContent::Document(Document {
494 data: DocumentSourceKind::FileId(file_id),
495 ..
496 }) => items.push(InputItem::user_content(UserContent::InputFile {
497 file_id: Some(file_id),
498 file_data: None,
499 file_url: None,
500 filename: None,
501 })),
502 crate::message::UserContent::Document(Document {
503 data,
504 media_type: Some(DocumentMediaType::PDF),
505 ..
506 }) => {
507 let (file_data, file_url, filename) = match data {
508 DocumentSourceKind::Base64(data) => (
509 Some(format!("data:application/pdf;base64,{data}")),
510 None,
511 Some("document.pdf".to_string()),
512 ),
513 DocumentSourceKind::Url(url) => (None, Some(url), None),
514 source => return Err(unsupported_document_source(source)),
515 };
516
517 items.push(InputItem::user_content(UserContent::InputFile {
518 file_id: None,
519 file_data,
520 file_url,
521 filename,
522 }))
523 }
524 crate::message::UserContent::Document(Document {
525 data:
526 DocumentSourceKind::Base64(text) | DocumentSourceKind::String(text),
527 ..
528 }) => items.push(InputItem::user_content(UserContent::InputText { text })),
529 crate::message::UserContent::Image(crate::message::Image {
530 data,
531 media_type,
532 detail,
533 ..
534 }) => {
535 let url = match data {
536 DocumentSourceKind::Base64(data) => {
537 let media_type = if let Some(media_type) = media_type {
538 media_type.to_mime_type().to_string()
539 } else {
540 String::new()
541 };
542 format!("data:{media_type};base64,{data}")
543 }
544 DocumentSourceKind::Url(url) => url,
545 source => return Err(unsupported_document_source(source)),
546 };
547 items.push(InputItem::user_content(UserContent::InputImage {
548 image_url: url,
549 detail: detail.unwrap_or_default(),
550 }));
551 }
552 message => {
553 return Err(CompletionError::ProviderError(format!(
554 "Unsupported message: {message:?}"
555 )));
556 }
557 }
558 }
559
560 Ok(items)
561 }
562 crate::completion::Message::Assistant { id, content } => {
563 let mut reasoning_items = Vec::new();
564 let mut other_items = Vec::new();
565
566 for assistant_content in content {
567 match assistant_content {
568 crate::message::AssistantContent::Text(Text {
569 text,
570 additional_params,
571 }) => {
572 let Some(message) =
576 assistant_text_replay_message(id.clone(), text, additional_params)
577 else {
578 continue;
579 };
580
581 other_items.push(InputItem {
582 role: Some(Role::Assistant),
583 input: InputContent::Message(message),
584 });
585 }
586 crate::message::AssistantContent::ToolCall(crate::message::ToolCall {
587 id,
588 provider,
589 function,
590 ..
591 }) => {
592 let (call_id, item_id) = match provider {
593 Some(provider) => {
594 let item_id = provider.item_id.clone().unwrap_or_default();
595 (provider.call_id, item_id)
596 }
597 None => (id.into_string(), String::new()),
598 };
599 other_items.push(InputItem {
600 role: None,
601 input: InputContent::FunctionCall(OutputFunctionCall {
602 arguments: function.arguments.into(),
603 call_id,
604 id: item_id,
605 name: function.name,
606 status: ToolStatus::Completed,
607 }),
608 });
609 }
610 crate::message::AssistantContent::Reasoning(reasoning) => {
611 let openai_reasoning = openai_reasoning_from_core(&reasoning)
612 .map_err(|err| CompletionError::ProviderError(err.to_string()))?;
613 if let Some(openai_reasoning) = openai_reasoning {
614 reasoning_items.push(InputItem {
615 role: None,
616 input: InputContent::Reasoning(openai_reasoning),
617 });
618 }
619 }
620 crate::message::AssistantContent::Image(_) => {
621 return Err(CompletionError::ProviderError(
622 "Assistant image content is not supported in OpenAI Responses API"
623 .to_string(),
624 ));
625 }
626 }
627 }
628
629 let mut items = reasoning_items;
630 items.extend(other_items);
631 Ok(items)
632 }
633 }
634 }
635}
636
637pub fn reasoning_summaries(value: Vec<String>) -> Vec<ReasoningSummary> {
643 value
644 .into_iter()
645 .map(|text| ReasoningSummary::SummaryText { text })
646 .collect()
647}
648
649pub(crate) fn reasoning_content_blocks(
659 summary: Vec<ReasoningSummary>,
660 content: Vec<String>,
661 encrypted_content: Option<String>,
662) -> Vec<message::ReasoningContent> {
663 let mut blocks = summary
664 .into_iter()
665 .map(|summary| match summary {
666 ReasoningSummary::SummaryText { text } => message::ReasoningContent::Summary(text),
667 })
668 .collect::<Vec<_>>();
669
670 blocks.extend(
671 content
672 .into_iter()
673 .map(|text| message::ReasoningContent::Text {
674 text,
675 signature: None,
676 }),
677 );
678
679 if let Some(encrypted_content) = encrypted_content.filter(|content| !content.is_empty()) {
680 blocks.push(message::ReasoningContent::Encrypted(encrypted_content));
681 }
682
683 blocks
684}
685
686fn openai_reasoning_from_core(
687 reasoning: &crate::message::Reasoning,
688) -> Result<Option<OpenAIReasoning>, MessageError> {
689 let Some(id) = reasoning.id.clone() else {
695 return Ok(None);
696 };
697
698 let mut summary = Vec::new();
699 let mut reasoning_content = Vec::new();
700 let mut encrypted_content = None;
701 for content in &reasoning.content {
702 match content {
703 crate::message::ReasoningContent::Text { text, .. } => {
704 reasoning_content.push(text.clone());
705 }
706 crate::message::ReasoningContent::Summary(text) => {
707 summary.push(ReasoningSummary::new(text));
708 }
709 crate::message::ReasoningContent::Encrypted(data)
712 | crate::message::ReasoningContent::Redacted { data } => {
713 encrypted_content.get_or_insert_with(|| data.clone());
714 }
715 }
716 }
717
718 Ok(Some(OpenAIReasoning {
719 id,
720 summary,
721 content: reasoning_content,
722 encrypted_content,
723 status: None,
724 }))
725}
726
727#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
729pub struct ResponsesToolDefinition {
730 #[serde(rename = "type")]
732 pub kind: String,
733 #[serde(default, skip_serializing_if = "String::is_empty")]
735 pub name: String,
736 #[serde(default, skip_serializing_if = "is_json_null")]
738 pub parameters: serde_json::Value,
739 #[serde(
742 default,
743 skip_serializing_if = "is_false",
744 deserialize_with = "json_utils::null_or_default"
745 )]
746 pub strict: bool,
747 #[serde(default, skip_serializing_if = "String::is_empty")]
749 pub description: String,
750 #[serde(flatten, default, skip_serializing_if = "Map::is_empty")]
752 pub config: Map<String, Value>,
753}
754
755fn is_json_null(value: &Value) -> bool {
756 value.is_null()
757}
758
759fn is_false(value: &bool) -> bool {
760 !value
761}
762
763impl ResponsesToolDefinition {
764 pub fn function(
766 name: impl Into<String>,
767 description: impl Into<String>,
768 parameters: serde_json::Value,
769 ) -> Self {
770 Self {
771 kind: "function".to_string(),
772 name: name.into(),
773 parameters,
774 strict: false,
775 description: description.into(),
776 config: Map::new(),
777 }
778 }
779
780 pub fn strict_function(
785 name: impl Into<String>,
786 description: impl Into<String>,
787 parameters: serde_json::Value,
788 ) -> Self {
789 Self::function(name, description, parameters).with_strict()
790 }
791
792 pub fn with_strict(mut self) -> Self {
797 if self.kind == "function" {
798 super::sanitize_schema(&mut self.parameters);
799 self.strict = true;
800 }
801 self
802 }
803
804 pub fn hosted(kind: impl Into<String>) -> Self {
806 Self {
807 kind: kind.into(),
808 name: String::new(),
809 parameters: Value::Null,
810 strict: false,
811 description: String::new(),
812 config: Map::new(),
813 }
814 }
815
816 pub fn web_search() -> Self {
818 Self::hosted("web_search")
819 }
820
821 pub fn file_search() -> Self {
823 Self::hosted("file_search")
824 }
825
826 pub fn computer_use() -> Self {
828 Self::hosted("computer_use")
829 }
830
831 pub fn with_config(mut self, key: impl Into<String>, value: Value) -> Self {
833 self.config.insert(key.into(), value);
834 self
835 }
836
837 fn normalize(self) -> Self {
838 self.with_strict()
839 }
840}
841
842impl From<completion::ToolDefinition> for ResponsesToolDefinition {
843 fn from(value: completion::ToolDefinition) -> Self {
844 let completion::ToolDefinition {
845 name,
846 parameters,
847 description,
848 } = value;
849
850 Self::function(name, description, parameters)
851 }
852}
853
854#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
862#[serde(untagged)]
863pub enum ToolChoice {
864 Mode(super::completion::ToolChoice),
869 Definition(ToolChoiceDefinition),
871}
872
873#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
875#[serde(tag = "type", rename_all = "snake_case")]
876pub enum ToolChoiceDefinition {
877 Function {
879 name: String,
881 },
882 AllowedTools {
884 mode: AllowedToolsMode,
887 tools: Vec<AllowedTool>,
889 },
890}
891
892#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq)]
895#[serde(rename_all = "snake_case")]
896pub enum AllowedToolsMode {
897 Auto,
899 Required,
901}
902
903#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
905#[serde(tag = "type", rename_all = "snake_case")]
906pub enum AllowedTool {
907 Function {
909 name: String,
911 },
912}
913
914impl TryFrom<message::ToolChoice> for ToolChoice {
915 type Error = CompletionError;
916
917 fn try_from(value: message::ToolChoice) -> Result<Self, Self::Error> {
918 let choice = match value {
919 message::ToolChoice::Auto => Self::Mode(super::completion::ToolChoice::Auto),
920 message::ToolChoice::None => Self::Mode(super::completion::ToolChoice::None),
921 message::ToolChoice::Required => Self::Mode(super::completion::ToolChoice::Required),
922 message::ToolChoice::Specific { function_names } => {
923 let mut names = function_names.into_iter();
924 let Some(first) = names.next() else {
925 return Err(CompletionError::RequestError(
926 "ToolChoice::Specific requires at least one function name".into(),
927 ));
928 };
929
930 match names.next() {
931 None => Self::Definition(ToolChoiceDefinition::Function { name: first }),
932 Some(second) => {
933 let tools = std::iter::once(first)
934 .chain(std::iter::once(second))
935 .chain(names)
936 .map(|name| AllowedTool::Function { name })
937 .collect();
938 Self::Definition(ToolChoiceDefinition::AllowedTools {
939 mode: AllowedToolsMode::Required,
940 tools,
941 })
942 }
943 }
944 }
945 };
946
947 Ok(choice)
948 }
949}
950
951#[derive(Clone, Debug, Serialize, Deserialize)]
954pub struct ResponsesUsage {
955 pub input_tokens: u64,
957 #[serde(skip_serializing_if = "Option::is_none")]
959 pub input_tokens_details: Option<InputTokensDetails>,
960 pub output_tokens: u64,
962 #[serde(skip_serializing_if = "Option::is_none")]
964 pub output_tokens_details: Option<OutputTokensDetails>,
965 pub total_tokens: u64,
967}
968
969impl ResponsesUsage {
970 pub(crate) fn new() -> Self {
972 Self {
973 input_tokens: 0,
974 input_tokens_details: Some(InputTokensDetails::new()),
975 output_tokens: 0,
976 output_tokens_details: Some(OutputTokensDetails::new()),
977 total_tokens: 0,
978 }
979 }
980}
981
982impl From<&ResponsesUsage> for crate::completion::Usage {
983 fn from(usage: &ResponsesUsage) -> Self {
984 crate::completion::Usage {
985 input_tokens: usage.input_tokens,
986 output_tokens: usage.output_tokens,
987 total_tokens: usage.total_tokens,
988 cached_input_tokens: usage
989 .input_tokens_details
990 .as_ref()
991 .map(|details| details.cached_tokens)
992 .unwrap_or(0),
993 cache_creation_input_tokens: 0,
994 tool_use_prompt_tokens: 0,
995 reasoning_tokens: usage
996 .output_tokens_details
997 .as_ref()
998 .map(|details| details.reasoning_tokens)
999 .unwrap_or(0),
1000 }
1001 }
1002}
1003
1004impl From<ResponsesUsage> for crate::completion::Usage {
1005 fn from(usage: ResponsesUsage) -> Self {
1006 Self::from(&usage)
1007 }
1008}
1009
1010fn add_optional_details<T: Add<Output = T>>(lhs: Option<T>, rhs: Option<T>) -> Option<T> {
1014 match (lhs, rhs) {
1015 (Some(lhs), Some(rhs)) => Some(lhs + rhs),
1016 (lhs, rhs) => lhs.or(rhs),
1017 }
1018}
1019
1020impl Add for ResponsesUsage {
1021 type Output = Self;
1022
1023 fn add(self, rhs: Self) -> Self::Output {
1024 Self {
1025 input_tokens: self.input_tokens + rhs.input_tokens,
1026 input_tokens_details: add_optional_details(
1027 self.input_tokens_details,
1028 rhs.input_tokens_details,
1029 ),
1030 output_tokens: self.output_tokens + rhs.output_tokens,
1031 output_tokens_details: add_optional_details(
1032 self.output_tokens_details,
1033 rhs.output_tokens_details,
1034 ),
1035 total_tokens: self.total_tokens + rhs.total_tokens,
1036 }
1037 }
1038}
1039
1040#[derive(Clone, Debug, Serialize, Deserialize)]
1042pub struct InputTokensDetails {
1043 pub cached_tokens: u64,
1045}
1046
1047impl InputTokensDetails {
1048 pub(crate) fn new() -> Self {
1049 Self { cached_tokens: 0 }
1050 }
1051}
1052
1053impl Add for InputTokensDetails {
1054 type Output = Self;
1055 fn add(self, rhs: Self) -> Self::Output {
1056 Self {
1057 cached_tokens: self.cached_tokens + rhs.cached_tokens,
1058 }
1059 }
1060}
1061
1062#[derive(Clone, Debug, Serialize, Deserialize)]
1064pub struct OutputTokensDetails {
1065 pub reasoning_tokens: u64,
1067}
1068
1069impl OutputTokensDetails {
1070 pub(crate) fn new() -> Self {
1071 Self {
1072 reasoning_tokens: 0,
1073 }
1074 }
1075}
1076
1077impl Add for OutputTokensDetails {
1078 type Output = Self;
1079 fn add(self, rhs: Self) -> Self::Output {
1080 Self {
1081 reasoning_tokens: self.reasoning_tokens + rhs.reasoning_tokens,
1082 }
1083 }
1084}
1085
1086#[derive(Clone, Debug, Default, Serialize, Deserialize)]
1088pub struct IncompleteDetailsReason {
1089 pub reason: String,
1091}
1092
1093#[derive(Clone, Debug, Default, Serialize, Deserialize)]
1095pub struct ResponseError {
1096 pub code: String,
1098 pub message: String,
1100}
1101
1102#[derive(Clone, Debug, Deserialize, Serialize)]
1104#[serde(rename_all = "snake_case")]
1105pub enum ResponseObject {
1106 Response,
1107}
1108
1109#[derive(Clone, Debug, PartialEq)]
1111pub enum ResponseStatus {
1112 InProgress,
1113 Completed,
1114 Failed,
1115 Cancelled,
1116 Queued,
1117 Incomplete,
1118 Other(String),
1120}
1121
1122fn response_status_wire_name(status: &ResponseStatus) -> &str {
1128 match status {
1129 ResponseStatus::InProgress => "in_progress",
1130 ResponseStatus::Completed => "completed",
1131 ResponseStatus::Failed => "failed",
1132 ResponseStatus::Cancelled => "cancelled",
1133 ResponseStatus::Queued => "queued",
1134 ResponseStatus::Incomplete => "incomplete",
1135 ResponseStatus::Other(status) => status,
1136 }
1137}
1138
1139impl Serialize for ResponseStatus {
1140 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1141 where
1142 S: Serializer,
1143 {
1144 serializer.serialize_str(response_status_wire_name(self))
1145 }
1146}
1147
1148impl<'de> Deserialize<'de> for ResponseStatus {
1149 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1150 where
1151 D: Deserializer<'de>,
1152 {
1153 Ok(match String::deserialize(deserializer)?.as_str() {
1154 "in_progress" => Self::InProgress,
1155 "completed" => Self::Completed,
1156 "failed" => Self::Failed,
1157 "cancelled" => Self::Cancelled,
1158 "queued" => Self::Queued,
1159 "incomplete" => Self::Incomplete,
1160 other => Self::Other(other.to_owned()),
1161 })
1162 }
1163}
1164
1165pub(crate) fn map_finish_reason(
1183 status: &ResponseStatus,
1184 incomplete_details: Option<&IncompleteDetailsReason>,
1185) -> Option<completion::FinishReason> {
1186 match status {
1187 ResponseStatus::Completed => Some(completion::FinishReason::Stop),
1188 ResponseStatus::Incomplete => Some(
1189 match incomplete_details
1190 .map(|details| details.reason.as_str())
1191 .filter(|reason| !reason.is_empty())
1192 {
1193 Some("max_output_tokens") => completion::FinishReason::Length,
1194 Some("content_filter") => completion::FinishReason::ContentFilter,
1195 Some(other) => completion::FinishReason::Other(other.to_owned()),
1196 None => {
1199 completion::FinishReason::Other(response_status_wire_name(status).to_owned())
1200 }
1201 },
1202 ),
1203 ResponseStatus::Other(status) if status.is_empty() => None,
1204 ResponseStatus::Failed | ResponseStatus::Cancelled | ResponseStatus::Other(_) => Some(
1205 completion::FinishReason::Other(response_status_wire_name(status).to_owned()),
1206 ),
1207 ResponseStatus::InProgress | ResponseStatus::Queued => None,
1209 }
1210}
1211
1212#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
1214pub enum SystemInstructionsPlacement {
1215 #[default]
1220 Instructions,
1221 AllInstructions,
1226 InputSystemMessages,
1231}
1232
1233pub trait ResponsesProviderExt {
1239 const PROVIDER_NAME: &'static str = "openai";
1246
1247 const REQUEST_ID_HEADER: Option<&'static str> = Some("x-request-id");
1252
1253 const RESPONSES_PATH: &'static str = "/responses";
1255
1256 const EMITS_COMPLETE_TOOL_CALLS_IMMEDIATELY: bool = false;
1260
1261 const USES_2XX_ERROR_ENVELOPE: bool = false;
1264
1265 const COMPOSES_NATIVE_OUTPUT_WITH_TOOLS: bool = true;
1267
1268 fn system_instructions_placement(&self) -> SystemInstructionsPlacement;
1275
1276 #[doc(hidden)]
1282 fn create_responses_request(
1283 &self,
1284 model: String,
1285 request: crate::completion::CompletionRequest,
1286 default_tools: &[ResponsesToolDefinition],
1287 strict_tools: bool,
1288 system_instructions_placement: SystemInstructionsPlacement,
1289 stream: bool,
1290 ) -> Result<(String, Value), CompletionError> {
1291 let mut request = CompletionRequest::try_from(ResponsesRequestParams {
1292 model,
1293 request,
1294 system_instructions_placement,
1295 })?;
1296 request.tools.extend(default_tools.iter().cloned());
1297 if strict_tools {
1298 request.tools = request
1299 .tools
1300 .into_iter()
1301 .map(ResponsesToolDefinition::normalize)
1302 .collect();
1303 }
1304 if stream {
1305 request.stream = Some(true);
1306 }
1307 Ok((request.model.clone(), serde_json::to_value(request)?))
1308 }
1309}
1310
1311#[doc(hidden)]
1317pub trait ConfigurableSystemInstructionsPlacement: ResponsesProviderExt {}
1318
1319impl TryFrom<(String, crate::completion::CompletionRequest)> for CompletionRequest {
1321 type Error = CompletionError;
1322 fn try_from(
1323 (model, request): (String, crate::completion::CompletionRequest),
1324 ) -> Result<Self, Self::Error> {
1325 Self::try_from(ResponsesRequestParams {
1326 model,
1327 request,
1328 system_instructions_placement: SystemInstructionsPlacement::default(),
1329 })
1330 }
1331}
1332
1333pub struct ResponsesRequestParams {
1336 pub model: String,
1337 pub request: crate::completion::CompletionRequest,
1338 pub system_instructions_placement: SystemInstructionsPlacement,
1339}
1340
1341impl TryFrom<ResponsesRequestParams> for CompletionRequest {
1342 type Error = CompletionError;
1343
1344 fn try_from(params: ResponsesRequestParams) -> Result<Self, Self::Error> {
1345 let ResponsesRequestParams {
1346 model,
1347 request: mut req,
1348 system_instructions_placement,
1349 } = params;
1350 let chat_history = req.chat_history_with_documents();
1351 let model = req.model.clone().unwrap_or(model);
1352 let preamble = req.preamble.take();
1353 let mut instruction_parts = Vec::new();
1354 let mut input = {
1355 let mut partial_history = vec![];
1356 partial_history.extend(chat_history);
1357
1358 let mut full_history: Vec<InputItem> = preamble
1359 .map(InputItem::system_message)
1360 .into_iter()
1361 .collect();
1362
1363 for history_item in partial_history {
1364 full_history.extend(<Vec<InputItem>>::try_from(history_item)?);
1365 }
1366
1367 full_history
1368 };
1369
1370 let mut lift_system_text = |text: String| {
1371 let text = text.trim();
1372 if !text.is_empty() {
1373 instruction_parts.push(text.to_string());
1374 }
1375 };
1376 let items_before_lift = input.len();
1377 match system_instructions_placement {
1378 SystemInstructionsPlacement::Instructions => {
1379 let leading_system_texts: Vec<String> =
1385 input.iter().map_while(InputItem::system_text).collect();
1386 if leading_system_texts.len() < input.len() {
1387 input.drain(..leading_system_texts.len());
1388 leading_system_texts
1389 .into_iter()
1390 .for_each(&mut lift_system_text);
1391 }
1392 }
1393 SystemInstructionsPlacement::AllInstructions => {
1394 let mut remaining = Vec::with_capacity(input.len());
1397 for item in input {
1398 match item.system_text() {
1399 Some(text) => lift_system_text(text),
1400 None => remaining.push(item),
1401 }
1402 }
1403 input = remaining;
1404 }
1405 SystemInstructionsPlacement::InputSystemMessages => {}
1406 }
1407 let instructions = (!instruction_parts.is_empty()).then(|| instruction_parts.join("\n\n"));
1408 let lifted_system_items = input.len() < items_before_lift;
1409
1410 let input = crate::message::require_non_empty(input, || {
1411 CompletionError::RequestError(if lifted_system_items {
1412 "OpenAI Responses request input must contain at least one non-system item \
1413 (system messages were lifted into the top-level `instructions` field)"
1414 .into()
1415 } else {
1416 "OpenAI Responses request input must contain at least one item".into()
1417 })
1418 })?;
1419
1420 let mut additional_params_payload = req.additional_params.take().unwrap_or(Value::Null);
1421 let stream = match &additional_params_payload {
1422 Value::Bool(stream) => Some(*stream),
1423 Value::Object(map) => map.get("stream").and_then(Value::as_bool),
1424 _ => None,
1425 };
1426
1427 let mut additional_tools = Vec::new();
1428 if let Some(additional_params_map) = additional_params_payload.as_object_mut() {
1429 if let Some(raw_tools) = additional_params_map.remove("tools") {
1430 additional_tools = serde_json::from_value::<Vec<ResponsesToolDefinition>>(
1431 raw_tools,
1432 )
1433 .map_err(|err| {
1434 CompletionError::RequestError(
1435 format!(
1436 "Invalid OpenAI Responses tools payload in additional_params: {err}"
1437 )
1438 .into(),
1439 )
1440 })?;
1441 }
1442 additional_params_map.remove("stream");
1443 }
1444
1445 if additional_params_payload.is_boolean() {
1446 additional_params_payload = Value::Null;
1447 }
1448
1449 let mut additional_parameters = if additional_params_payload.is_null() {
1450 AdditionalParameters::default()
1452 } else {
1453 serde_json::from_value::<AdditionalParameters>(additional_params_payload).map_err(
1454 |err| {
1455 CompletionError::RequestError(
1456 format!("Invalid OpenAI Responses additional_params payload: {err}").into(),
1457 )
1458 },
1459 )?
1460 };
1461 if additional_parameters.reasoning.is_some() {
1462 let include = additional_parameters.include.get_or_insert_with(Vec::new);
1463 if !include
1464 .iter()
1465 .any(|item| matches!(item, Include::ReasoningEncryptedContent))
1466 {
1467 include.push(Include::ReasoningEncryptedContent);
1468 }
1469 }
1470
1471 if additional_parameters.text.is_none()
1473 && let Some(schema) = req.output_schema
1474 {
1475 let (name, schema_value) = super::structured_output_schema(schema);
1476 additional_parameters.text = Some(TextConfig::structured_output(name, schema_value));
1477 }
1478
1479 let tool_choice = req.tool_choice.map(ToolChoice::try_from).transpose()?;
1480 let mut tools: Vec<ResponsesToolDefinition> = req
1481 .tools
1482 .into_iter()
1483 .map(ResponsesToolDefinition::from)
1484 .collect();
1485 tools.append(&mut additional_tools);
1486
1487 Ok(Self {
1488 input,
1489 model,
1490 instructions,
1491 max_output_tokens: req.max_tokens,
1492 stream,
1493 tool_choice,
1494 tools,
1495 temperature: req.temperature,
1496 additional_parameters,
1497 })
1498 }
1499}
1500
1501#[doc(hidden)]
1503#[derive(Clone)]
1504pub struct GenericResponsesCompletionModel<Ext = super::OpenAIResponsesExt, H = reqwest::Client> {
1505 pub(crate) client: crate::client::Client<Ext, H>,
1507 pub model: String,
1509 pub tools: Vec<ResponsesToolDefinition>,
1511 pub strict_tools: bool,
1514 system_instructions_placement: SystemInstructionsPlacement,
1515}
1516
1517pub type ResponsesCompletionModel<H = reqwest::Client> =
1522 GenericResponsesCompletionModel<super::OpenAIResponsesExt, H>;
1523
1524impl<Ext, H> GenericResponsesCompletionModel<Ext, H>
1525where
1526 Ext: crate::client::Provider + ResponsesProviderExt,
1527{
1528 pub fn new(client: crate::client::Client<Ext, H>, model: impl Into<String>) -> Self {
1530 let system_instructions_placement = client.ext().system_instructions_placement();
1531 Self {
1532 client,
1533 model: model.into(),
1534 tools: Vec::new(),
1535 strict_tools: false,
1536 system_instructions_placement,
1537 }
1538 }
1539
1540 pub fn with_model(client: crate::client::Client<Ext, H>, model: &str) -> Self {
1541 Self::new(client, model)
1542 }
1543
1544 pub fn provider_name(&self) -> &'static str {
1547 Ext::PROVIDER_NAME
1548 }
1549
1550 pub fn with_strict_tools(mut self) -> Self {
1555 self.strict_tools = true;
1556 self
1557 }
1558
1559 pub fn with_tool(mut self, tool: impl Into<ResponsesToolDefinition>) -> Self {
1561 self.tools.push(tool.into());
1562 self
1563 }
1564
1565 pub fn with_tools<I, Tool>(mut self, tools: I) -> Self
1567 where
1568 I: IntoIterator<Item = Tool>,
1569 Tool: Into<ResponsesToolDefinition>,
1570 {
1571 self.tools.extend(tools.into_iter().map(Into::into));
1572 self
1573 }
1574
1575 pub(crate) fn create_completion_request(
1577 &self,
1578 completion_request: crate::completion::CompletionRequest,
1579 ) -> Result<CompletionRequest, CompletionError> {
1580 let mut req = CompletionRequest::try_from(ResponsesRequestParams {
1581 model: self.model.clone(),
1582 request: completion_request,
1583 system_instructions_placement: self.system_instructions_placement,
1584 })?;
1585 req.tools.extend(self.tools.clone());
1586
1587 if self.strict_tools {
1588 req.tools = req
1589 .tools
1590 .into_iter()
1591 .map(ResponsesToolDefinition::normalize)
1592 .collect();
1593 }
1594
1595 Ok(req)
1596 }
1597
1598 fn create_provider_request(
1599 &self,
1600 request: crate::completion::CompletionRequest,
1601 stream: bool,
1602 ) -> Result<(String, Value), CompletionError> {
1603 self.client.ext().create_responses_request(
1604 self.model.clone(),
1605 request,
1606 &self.tools,
1607 self.strict_tools,
1608 self.system_instructions_placement,
1609 stream,
1610 )
1611 }
1612}
1613
1614impl<Ext, H> GenericResponsesCompletionModel<Ext, H>
1615where
1616 Ext: crate::client::Provider + ResponsesProviderExt + ConfigurableSystemInstructionsPlacement,
1617{
1618 pub fn with_system_instructions_placement(
1622 mut self,
1623 placement: SystemInstructionsPlacement,
1624 ) -> Self {
1625 self.system_instructions_placement = placement;
1626 self
1627 }
1628
1629 pub fn with_system_instructions_as_messages(self) -> Self {
1636 self.with_system_instructions_placement(SystemInstructionsPlacement::InputSystemMessages)
1637 }
1638}
1639
1640impl<T> GenericResponsesCompletionModel<super::OpenAIResponsesExt, T>
1641where
1642 T: HttpClientExt + Clone + Default + std::fmt::Debug + 'static,
1643{
1644 pub fn completions_api(self) -> crate::providers::openai::completion::CompletionModel<T> {
1646 super::completion::CompletionModel::new(self.client.completions_api(), &self.model)
1647 }
1648}
1649
1650#[derive(Clone, Debug)]
1652pub struct CompletionResponse {
1653 pub id: String,
1655 pub object: ResponseObject,
1657 pub created_at: u64,
1659 pub status: ResponseStatus,
1661 pub error: Option<ResponseError>,
1663 pub incomplete_details: Option<IncompleteDetailsReason>,
1665 pub instructions: Option<String>,
1667 pub max_output_tokens: Option<u64>,
1669 pub model: String,
1671 pub provider_reasoning: Option<String>,
1674 pub provider_request_id: Option<String>,
1679 pub reasoning_metadata: Option<Map<String, Value>>,
1688 pub reasoning_context: Option<String>,
1694 pub usage: Option<ResponsesUsage>,
1696 pub output: Vec<Output>,
1698 pub tools: Vec<ResponsesToolDefinition>,
1700 pub additional_parameters: AdditionalParameters,
1702}
1703
1704#[derive(Serialize)]
1705#[serde(untagged)]
1706enum CompletionResponseReasoningRef<'a> {
1707 Text(&'a str),
1708 Metadata(&'a Map<String, Value>),
1709 Context { context: &'a str },
1710}
1711
1712#[derive(Serialize)]
1713struct CompletionResponseWireRef<'a> {
1714 id: &'a str,
1715 object: &'a ResponseObject,
1716 created_at: u64,
1717 status: &'a ResponseStatus,
1718 error: &'a Option<ResponseError>,
1719 incomplete_details: &'a Option<IncompleteDetailsReason>,
1720 instructions: &'a Option<String>,
1721 max_output_tokens: &'a Option<u64>,
1722 model: &'a str,
1723 #[serde(skip_serializing_if = "Option::is_none")]
1724 reasoning: Option<CompletionResponseReasoningRef<'a>>,
1725 usage: &'a Option<ResponsesUsage>,
1726 output: &'a Vec<Output>,
1727 tools: &'a Vec<ResponsesToolDefinition>,
1728 #[serde(flatten)]
1729 additional_parameters: &'a AdditionalParameters,
1730}
1731
1732#[derive(Deserialize)]
1733struct CompletionResponseWire {
1734 id: String,
1735 object: ResponseObject,
1736 created_at: u64,
1737 status: ResponseStatus,
1738 error: Option<ResponseError>,
1739 incomplete_details: Option<IncompleteDetailsReason>,
1740 instructions: Option<String>,
1741 max_output_tokens: Option<u64>,
1742 model: String,
1743 #[serde(default)]
1744 reasoning: Option<Value>,
1745 usage: Option<ResponsesUsage>,
1746 #[serde(default)]
1747 output: Vec<Output>,
1748 #[serde(default)]
1749 tools: Vec<ResponsesToolDefinition>,
1750 #[serde(flatten)]
1751 additional_parameters: AdditionalParameters,
1752}
1753
1754impl Serialize for CompletionResponse {
1755 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1756 where
1757 S: Serializer,
1758 {
1759 let mut additional_parameters = self.additional_parameters.clone();
1764 additional_parameters.reasoning = None;
1765
1766 let reasoning = self
1767 .provider_reasoning
1768 .as_deref()
1769 .map(CompletionResponseReasoningRef::Text)
1770 .or_else(|| {
1771 self.reasoning_metadata
1772 .as_ref()
1773 .map(CompletionResponseReasoningRef::Metadata)
1774 })
1775 .or_else(|| {
1776 self.reasoning_context
1777 .as_deref()
1778 .map(|context| CompletionResponseReasoningRef::Context { context })
1779 });
1780
1781 CompletionResponseWireRef {
1782 id: &self.id,
1783 object: &self.object,
1784 created_at: self.created_at,
1785 status: &self.status,
1786 error: &self.error,
1787 incomplete_details: &self.incomplete_details,
1788 instructions: &self.instructions,
1789 max_output_tokens: &self.max_output_tokens,
1790 model: &self.model,
1791 reasoning,
1792 usage: &self.usage,
1793 output: &self.output,
1794 tools: &self.tools,
1795 additional_parameters: &additional_parameters,
1796 }
1797 .serialize(serializer)
1798 }
1799}
1800
1801impl<'de> Deserialize<'de> for CompletionResponse {
1802 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1803 where
1804 D: Deserializer<'de>,
1805 {
1806 let response = CompletionResponseWire::deserialize(deserializer)?;
1807 let (provider_reasoning, reasoning_metadata) = match response.reasoning {
1808 Some(Value::String(reasoning)) => (Some(reasoning), None),
1809 Some(Value::Object(metadata)) => (None, Some(metadata)),
1810 _ => (None, None),
1814 };
1815 let reasoning_context = reasoning_metadata
1816 .as_ref()
1817 .and_then(|reasoning| reasoning.get("context"))
1818 .and_then(Value::as_str)
1819 .map(ToOwned::to_owned);
1820
1821 Ok(Self {
1822 id: response.id,
1823 object: response.object,
1824 created_at: response.created_at,
1825 status: response.status,
1826 error: response.error,
1827 incomplete_details: response.incomplete_details,
1828 instructions: response.instructions,
1829 max_output_tokens: response.max_output_tokens,
1830 model: response.model,
1831 provider_reasoning,
1832 provider_request_id: None,
1833 reasoning_metadata,
1834 reasoning_context,
1835 usage: response.usage,
1836 output: response.output,
1837 tools: response.tools,
1838 additional_parameters: response.additional_parameters,
1839 })
1840 }
1841}
1842
1843#[derive(Clone, Debug, Deserialize, Serialize, Default)]
1846pub struct AdditionalParameters {
1847 #[serde(skip_serializing_if = "Option::is_none")]
1849 pub background: Option<bool>,
1850 #[serde(skip_serializing_if = "Option::is_none")]
1852 pub text: Option<TextConfig>,
1853 #[serde(skip_serializing_if = "Option::is_none")]
1855 pub include: Option<Vec<Include>>,
1856 #[serde(skip_serializing_if = "Option::is_none")]
1858 pub top_p: Option<f64>,
1859 #[serde(skip_serializing_if = "Option::is_none")]
1861 pub truncation: Option<TruncationStrategy>,
1862 #[serde(skip_serializing_if = "Option::is_none")]
1864 pub user: Option<String>,
1865 #[serde(skip_serializing_if = "Option::is_none")]
1867 pub prompt_cache_key: Option<String>,
1868 #[serde(skip_serializing_if = "Option::is_none")]
1870 pub prompt_cache_retention: Option<String>,
1871 #[serde(
1873 skip_serializing_if = "Map::is_empty",
1874 default,
1875 deserialize_with = "deserialize_metadata"
1876 )]
1877 pub metadata: serde_json::Map<String, serde_json::Value>,
1878 #[serde(skip_serializing_if = "Option::is_none")]
1880 pub parallel_tool_calls: Option<bool>,
1881 #[serde(skip_serializing_if = "Option::is_none")]
1883 pub previous_response_id: Option<String>,
1884 #[serde(skip_serializing_if = "Option::is_none")]
1886 pub reasoning: Option<Reasoning>,
1887 #[serde(skip_serializing_if = "Option::is_none")]
1889 pub service_tier: Option<OpenAIServiceTier>,
1890 #[serde(skip_serializing_if = "Option::is_none")]
1892 pub store: Option<bool>,
1893}
1894
1895fn deserialize_metadata<'de, D>(
1896 deserializer: D,
1897) -> Result<serde_json::Map<String, serde_json::Value>, D::Error>
1898where
1899 D: Deserializer<'de>,
1900{
1901 Ok(
1902 Option::<serde_json::Map<String, serde_json::Value>>::deserialize(deserializer)?
1903 .unwrap_or_default(),
1904 )
1905}
1906
1907impl AdditionalParameters {
1908 pub fn to_json(self) -> serde_json::Value {
1909 serde_json::to_value(self).unwrap_or_else(|_| serde_json::Value::Object(Map::new()))
1910 }
1911}
1912
1913#[derive(Clone, Debug, Default, Serialize, Deserialize)]
1917#[serde(rename_all = "snake_case")]
1918pub enum TruncationStrategy {
1919 Auto,
1920 #[default]
1921 Disabled,
1922}
1923
1924#[derive(Clone, Debug, Serialize, Deserialize)]
1927pub struct TextConfig {
1928 pub format: TextFormat,
1929}
1930
1931impl TextConfig {
1932 pub(crate) fn structured_output<S>(name: S, schema: serde_json::Value) -> Self
1933 where
1934 S: Into<String>,
1935 {
1936 Self {
1937 format: TextFormat::JsonSchema(StructuredOutputsInput {
1938 name: name.into(),
1939 schema,
1940 strict: true,
1941 }),
1942 }
1943 }
1944}
1945
1946#[derive(Clone, Debug, Serialize, Deserialize, Default)]
1949#[serde(tag = "type")]
1950#[serde(rename_all = "snake_case")]
1951pub enum TextFormat {
1952 JsonSchema(StructuredOutputsInput),
1953 #[default]
1954 Text,
1955}
1956
1957#[derive(Clone, Debug, Serialize, Deserialize)]
1959pub struct StructuredOutputsInput {
1960 #[serde(default)]
1964 pub name: String,
1965 pub schema: serde_json::Value,
1967 #[serde(default)]
1969 pub strict: bool,
1970}
1971
1972#[derive(Clone, Debug, Default, Serialize, Deserialize)]
1987pub struct Reasoning {
1988 #[serde(skip_serializing_if = "Option::is_none")]
1990 pub effort: Option<ReasoningEffort>,
1991 #[serde(skip_serializing_if = "Option::is_none")]
1993 pub summary: Option<ReasoningSummaryLevel>,
1994 #[serde(skip_serializing_if = "Option::is_none")]
1997 pub mode: Option<ReasoningMode>,
1998 #[serde(skip_serializing_if = "Option::is_none")]
2001 pub context: Option<ReasoningContext>,
2002}
2003
2004impl Reasoning {
2005 pub fn new() -> Self {
2007 Self::default()
2008 }
2009
2010 pub fn with_effort(mut self, reasoning_effort: ReasoningEffort) -> Self {
2012 self.effort = Some(reasoning_effort);
2013
2014 self
2015 }
2016
2017 pub fn with_summary_level(mut self, reasoning_summary_level: ReasoningSummaryLevel) -> Self {
2019 self.summary = Some(reasoning_summary_level);
2020
2021 self
2022 }
2023
2024 pub fn with_mode(mut self, reasoning_mode: ReasoningMode) -> Self {
2026 self.mode = Some(reasoning_mode);
2027
2028 self
2029 }
2030
2031 pub fn with_context(mut self, reasoning_context: ReasoningContext) -> Self {
2033 self.context = Some(reasoning_context);
2034
2035 self
2036 }
2037}
2038
2039#[derive(Clone, Debug, Default)]
2041pub enum OpenAIServiceTier {
2042 #[default]
2044 Auto,
2045 Default,
2047 Flex,
2049 Priority,
2051 Standard,
2053 Other(String),
2055}
2056
2057impl Serialize for OpenAIServiceTier {
2058 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
2059 where
2060 S: Serializer,
2061 {
2062 serializer.serialize_str(match self {
2063 Self::Auto => "auto",
2064 Self::Default => "default",
2065 Self::Flex => "flex",
2066 Self::Priority => "priority",
2067 Self::Standard => "standard",
2068 Self::Other(value) => value,
2069 })
2070 }
2071}
2072
2073impl<'de> Deserialize<'de> for OpenAIServiceTier {
2074 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2075 where
2076 D: Deserializer<'de>,
2077 {
2078 let value = String::deserialize(deserializer)?;
2079 Ok(match value.as_str() {
2080 "auto" => Self::Auto,
2081 "default" => Self::Default,
2082 "flex" => Self::Flex,
2083 "priority" => Self::Priority,
2084 "standard" => Self::Standard,
2085 _ => Self::Other(value),
2086 })
2087 }
2088}
2089
2090#[derive(Clone, Debug, Default, Serialize, Deserialize)]
2092#[serde(rename_all = "snake_case")]
2093pub enum ReasoningEffort {
2094 None,
2095 Minimal,
2096 Low,
2097 #[default]
2098 Medium,
2099 High,
2100 Xhigh,
2101 Max,
2103}
2104
2105#[derive(Clone, Debug, Serialize, Deserialize)]
2110#[serde(rename_all = "snake_case")]
2111pub enum ReasoningMode {
2112 Pro,
2114}
2115
2116#[derive(Clone, Debug, Default, Serialize, Deserialize)]
2119#[serde(rename_all = "snake_case")]
2120pub enum ReasoningContext {
2121 #[default]
2123 Auto,
2124 AllTurns,
2126 CurrentTurn,
2128}
2129
2130#[derive(Clone, Debug, Default, Serialize, Deserialize)]
2132#[serde(rename_all = "snake_case")]
2133pub enum ReasoningSummaryLevel {
2134 #[default]
2135 Auto,
2136 Concise,
2137 Detailed,
2138}
2139
2140#[derive(Clone, Debug, Deserialize, Serialize)]
2143pub enum Include {
2144 #[serde(rename = "file_search_call.results")]
2145 FileSearchCallResults,
2146 #[serde(rename = "message.input_image.image_url")]
2147 MessageInputImageImageUrl,
2148 #[serde(rename = "computer_call.output.image_url")]
2149 ComputerCallOutputOutputImageUrl,
2150 #[serde(rename = "reasoning.encrypted_content")]
2151 ReasoningEncryptedContent,
2152 #[serde(rename = "code_interpreter_call.outputs")]
2153 CodeInterpreterCallOutputs,
2154}
2155
2156#[derive(Clone, Debug, PartialEq)]
2166pub enum Output {
2167 Message(OutputMessage),
2168 FunctionCall(OutputFunctionCall),
2169 Reasoning {
2170 id: String,
2171 summary: Vec<ReasoningSummary>,
2172 content: Vec<String>,
2173 encrypted_content: Option<String>,
2174 status: Option<ToolStatus>,
2175 },
2176 Unknown(Value),
2180}
2181
2182#[derive(Deserialize)]
2191struct ReasoningFields {
2192 id: String,
2193 #[serde(default)]
2194 summary: Vec<ReasoningSummary>,
2195 #[serde(default, deserialize_with = "deserialize_reasoning_text_content")]
2196 content: Vec<String>,
2197 #[serde(default)]
2198 encrypted_content: Option<String>,
2199 #[serde(default)]
2200 status: Option<ToolStatus>,
2201}
2202
2203impl From<ReasoningFields> for Output {
2204 fn from(fields: ReasoningFields) -> Self {
2205 Output::Reasoning {
2206 id: fields.id,
2207 summary: fields.summary,
2208 content: fields.content,
2209 encrypted_content: fields.encrypted_content,
2210 status: fields.status,
2211 }
2212 }
2213}
2214
2215fn tagged_output_object<T>(tag: &str, payload: &T) -> Result<Value, serde_json::Error>
2219where
2220 T: Serialize,
2221{
2222 let mut value = serde_json::to_value(payload)?;
2223 let map = value.as_object_mut().ok_or_else(|| {
2224 <serde_json::Error as serde::ser::Error>::custom(
2225 "output payload must serialize to a JSON object",
2226 )
2227 })?;
2228 map.insert("type".to_string(), Value::String(tag.to_string()));
2229 Ok(value)
2230}
2231
2232impl Serialize for Output {
2233 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
2234 where
2235 S: Serializer,
2236 {
2237 let value = match self {
2243 Output::Message(message) => tagged_output_object("message", message),
2244 Output::FunctionCall(call) => tagged_output_object("function_call", call),
2245 Output::Reasoning {
2246 id,
2247 summary,
2248 content,
2249 encrypted_content,
2250 status,
2251 } => {
2252 let mut value = serde_json::json!({
2253 "type": "reasoning",
2254 "id": id,
2255 "summary": summary,
2256 "encrypted_content": encrypted_content,
2257 "status": status,
2258 });
2259 if !content.is_empty() {
2260 let map = value.as_object_mut().ok_or_else(|| {
2261 serde::ser::Error::custom("reasoning output must serialize to an object")
2262 })?;
2263 map.insert("content".to_string(), reasoning_text_content_json(content));
2264 }
2265 Ok(value)
2266 }
2267 Output::Unknown(value) => return value.serialize(serializer),
2268 };
2269 value
2270 .map_err(serde::ser::Error::custom)?
2271 .serialize(serializer)
2272 }
2273}
2274
2275impl<'de> Deserialize<'de> for Output {
2276 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2277 where
2278 D: Deserializer<'de>,
2279 {
2280 let value = Value::deserialize(deserializer)?;
2285 let Some(tag) = value.get("type").and_then(Value::as_str) else {
2286 return Ok(Output::Unknown(value));
2287 };
2288 match tag {
2289 "message" => serde_json::from_value(value)
2290 .map(Output::Message)
2291 .map_err(serde::de::Error::custom),
2292 "function_call" => serde_json::from_value(value)
2293 .map(Output::FunctionCall)
2294 .map_err(serde::de::Error::custom),
2295 "reasoning" => serde_json::from_value::<ReasoningFields>(value)
2296 .map(Output::from)
2297 .map_err(serde::de::Error::custom),
2298 _ => Ok(Output::Unknown(value)),
2299 }
2300 }
2301}
2302
2303impl From<Output> for Vec<completion::AssistantContent> {
2304 fn from(value: Output) -> Self {
2305 let res: Vec<completion::AssistantContent> = match value {
2306 Output::Message(OutputMessage { content, .. }) => content
2307 .into_iter()
2308 .map(completion::AssistantContent::from)
2309 .collect(),
2310 Output::FunctionCall(OutputFunctionCall {
2311 id,
2312 arguments,
2313 call_id,
2314 name,
2315 ..
2316 }) => match arguments.parse() {
2317 Ok(arguments) => vec![completion::AssistantContent::tool_call_with_call_id(
2318 id, call_id, name, arguments,
2319 )],
2320 Err(_) => {
2324 tracing::warn!(
2328 tool = %name,
2329 "dropping tool call whose arguments never fully arrived"
2330 );
2331 Vec::new()
2332 }
2333 },
2334 Output::Reasoning {
2335 id,
2336 summary,
2337 content,
2338 encrypted_content,
2339 ..
2340 } => vec![completion::AssistantContent::Reasoning(
2341 message::Reasoning {
2342 id: Some(id),
2343 content: reasoning_content_blocks(summary, content, encrypted_content),
2344 },
2345 )],
2346 Output::Unknown(_) => Vec::new(),
2347 };
2348
2349 res
2350 }
2351}
2352
2353#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
2355pub struct OutputFunctionCall {
2356 #[serde(default, skip_serializing_if = "is_not_function_call_item_id")]
2362 pub id: String,
2363 pub arguments: FunctionCallArguments,
2364 pub call_id: String,
2365 pub name: String,
2366 pub status: ToolStatus,
2367}
2368
2369#[derive(Clone, Debug, PartialEq)]
2385pub struct FunctionCallArguments(String);
2386
2387impl FunctionCallArguments {
2388 pub fn parse(&self) -> serde_json::Result<serde_json::Value> {
2391 json_utils::parse_tool_arguments(&self.0)
2392 }
2393
2394 pub fn as_str(&self) -> &str {
2396 &self.0
2397 }
2398}
2399
2400impl From<serde_json::Value> for FunctionCallArguments {
2401 fn from(value: serde_json::Value) -> Self {
2404 Self(value.to_string())
2405 }
2406}
2407
2408impl Serialize for FunctionCallArguments {
2409 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
2410 where
2411 S: Serializer,
2412 {
2413 serializer.serialize_str(&self.0)
2414 }
2415}
2416
2417impl<'de> Deserialize<'de> for FunctionCallArguments {
2418 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2419 where
2420 D: Deserializer<'de>,
2421 {
2422 String::deserialize(deserializer).map(Self)
2425 }
2426}
2427
2428fn is_not_function_call_item_id(id: &str) -> bool {
2431 !id.starts_with("fc_")
2432}
2433
2434#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
2436#[serde(rename_all = "snake_case")]
2437pub enum ToolStatus {
2438 InProgress,
2439 Completed,
2440 Incomplete,
2441}
2442
2443#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
2445pub struct OutputMessage {
2446 pub id: String,
2448 pub role: OutputRole,
2450 pub status: ResponseStatus,
2452 pub content: Vec<AssistantContent>,
2454}
2455
2456#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
2458#[serde(rename_all = "snake_case")]
2459pub enum OutputRole {
2460 Assistant,
2461}
2462
2463impl crate::telemetry::ProviderResponseExt for CompletionResponse {
2464 type Usage = ResponsesUsage;
2465
2466 fn get_response_id(&self) -> Option<String> {
2469 Some(self.id.clone())
2470 }
2471
2472 fn get_response_model_name(&self) -> Option<String> {
2473 Some(self.model.clone())
2474 }
2475
2476 fn get_text_response(&self) -> Option<String> {
2477 output_text_response(&self.output)
2478 }
2479
2480 fn get_usage(&self) -> Option<Self::Usage> {
2481 self.usage.clone()
2482 }
2483}
2484
2485pub(crate) fn output_text_response(output: &[Output]) -> Option<String> {
2488 let text = output
2489 .iter()
2490 .filter_map(|item| match item {
2491 Output::Message(message) => {
2492 Some(message.content.iter().filter_map(|content| match content {
2493 AssistantContent::OutputText(output) => {
2494 (!output.text.is_empty()).then(|| output.text.clone())
2495 }
2496 AssistantContent::Refusal { refusal } => {
2497 (!refusal.is_empty()).then(|| refusal.clone())
2498 }
2499 }))
2500 }
2501 _ => None,
2502 })
2503 .flatten()
2504 .collect::<Vec<_>>()
2505 .join("\n");
2506
2507 if text.is_empty() { None } else { Some(text) }
2508}
2509
2510impl<Ext, H> GenericResponsesCompletionModel<Ext, H>
2511where
2512 crate::client::Client<Ext, H>:
2513 HttpClientExt + Clone + WasmCompatSend + WasmCompatSync + 'static,
2514 Ext: crate::client::Provider
2515 + ResponsesProviderExt
2516 + crate::client::DebugExt
2517 + Clone
2518 + WasmCompatSend
2519 + WasmCompatSync
2520 + 'static,
2521 H: Clone + Default + std::fmt::Debug + WasmCompatSend + WasmCompatSync + 'static,
2522{
2523 pub async fn raw_completion(
2532 &self,
2533 completion_request: crate::completion::CompletionRequest,
2534 ) -> Result<CompletionResponse, CompletionError> {
2535 let system_instructions = completion_request.preamble.clone();
2536 let record_telemetry_content = completion_request.record_telemetry_content;
2537 let (request_model, request) = self.create_provider_request(completion_request, false)?;
2538 let span = CompletionSpanBuilder::new(
2539 Ext::PROVIDER_NAME,
2540 &request_model,
2541 CompletionOperation::Chat,
2542 )
2543 .system_instructions(system_instructions.as_deref(), record_telemetry_content)
2544 .build();
2545 let body = serde_json::to_vec(&request)?;
2546
2547 crate::providers::internal::trace_json(
2548 crate::providers::internal::LogTarget::Completions,
2549 "Responses completion request",
2550 &request,
2551 );
2552
2553 let req = self
2554 .client
2555 .post(Ext::RESPONSES_PATH)?
2556 .body(body)
2557 .map_err(|e| CompletionError::HttpError(e.into()))?;
2558
2559 fn record_response(response: &CompletionResponse) {
2560 let span = tracing::Span::current();
2561 span.record_response_metadata(response);
2562 let usage = response
2563 .usage
2564 .as_ref()
2565 .map(crate::completion::Usage::from)
2566 .unwrap_or_default();
2567 span.record_token_usage(&usage);
2568 }
2569
2570 let (mut response, provider_request_id) = if Ext::USES_2XX_ERROR_ENVELOPE {
2571 send_completion::<
2572 _,
2573 crate::providers::openai::client::ApiResponse<CompletionResponse>,
2574 _,
2575 >(
2576 &self.client,
2577 req,
2578 "Responses completion",
2579 Ext::REQUEST_ID_HEADER,
2580 record_response,
2581 )
2582 .instrument(span)
2583 .await?
2584 } else {
2585 send_completion::<
2586 _,
2587 crate::providers::internal::envelope::DirectPayload<CompletionResponse>,
2588 _,
2589 >(
2590 &self.client,
2591 req,
2592 "Responses completion",
2593 Ext::REQUEST_ID_HEADER,
2594 record_response,
2595 )
2596 .instrument(span)
2597 .await?
2598 };
2599 response.provider_request_id = provider_request_id;
2600 Ok(response)
2601 }
2602}
2603
2604impl<Ext, H> completion::CompletionModel for GenericResponsesCompletionModel<Ext, H>
2605where
2606 crate::client::Client<Ext, H>:
2607 HttpClientExt + Clone + WasmCompatSend + WasmCompatSync + 'static,
2608 Ext: crate::client::Provider
2609 + ResponsesProviderExt
2610 + crate::client::DebugExt
2611 + Clone
2612 + WasmCompatSend
2613 + WasmCompatSync
2614 + 'static,
2615 H: Clone + Default + std::fmt::Debug + WasmCompatSend + WasmCompatSync + 'static,
2616{
2617 fn capabilities(&self) -> completion::ProviderCapabilities {
2618 completion::ProviderCapabilities::default()
2622 .with_native_output_tool_composition(Ext::COMPOSES_NATIVE_OUTPUT_WITH_TOOLS)
2623 }
2624
2625 async fn completion(
2626 &self,
2627 completion_request: crate::completion::CompletionRequest,
2628 ) -> Result<completion::CompletionResponse, CompletionError> {
2629 let response = self.raw_completion(completion_request).await?;
2631 let captured = serde_json::to_value(&response)?;
2632 Ok(response.normalize(Ext::PROVIDER_NAME)?.with_raw(captured))
2633 }
2634
2635 async fn stream(
2636 &self,
2637 request: crate::completion::CompletionRequest,
2638 ) -> Result<crate::streaming::StreamingCompletionResponse, CompletionError> {
2639 GenericResponsesCompletionModel::stream(self, request).await
2640 }
2641}
2642
2643impl<Ext, H> crate::client::ConstructCompletionModel<crate::client::Client<Ext, H>>
2644 for GenericResponsesCompletionModel<Ext, H>
2645where
2646 Ext: crate::client::Provider + ResponsesProviderExt + Clone,
2647 H: Clone,
2648{
2649 fn construct(client: &crate::client::Client<Ext, H>, model: String) -> Self {
2650 Self::new(client.clone(), model)
2651 }
2652}
2653
2654impl crate::completion::NormalizeCompletionResponse for CompletionResponse {
2661 fn normalize(self, provider: &str) -> Result<completion::CompletionResponse, CompletionError> {
2662 let response = self;
2663 let message_id = response.output.iter().find_map(|item| match item {
2668 Output::Message(msg) => Some(msg.id.clone()),
2669 _ => None,
2670 });
2671
2672 let output_content: Vec<completion::AssistantContent> = response
2673 .output
2674 .iter()
2675 .cloned()
2676 .flat_map(<Vec<completion::AssistantContent>>::from)
2677 .collect();
2678 let has_structured_reasoning = response
2679 .output
2680 .iter()
2681 .any(|item| matches!(item, Output::Reasoning { .. }));
2682 let content = response
2683 .provider_reasoning
2684 .as_ref()
2685 .filter(|reasoning| !has_structured_reasoning && !reasoning.is_empty())
2686 .map(|reasoning| {
2687 let mut content = Vec::with_capacity(output_content.len() + 1);
2688 content.push(completion::AssistantContent::Reasoning(
2689 message::Reasoning::new(reasoning),
2690 ));
2691 content.extend(output_content.clone());
2692 content
2693 })
2694 .unwrap_or(output_content);
2695
2696 let finish_reason =
2697 map_finish_reason(&response.status, response.incomplete_details.as_ref());
2698
2699 let choice = if matches!(response.status, ResponseStatus::Incomplete) {
2707 content
2708 } else {
2709 crate::message::require_non_empty_response(content)?
2710 };
2711
2712 let usage = response
2713 .usage
2714 .as_ref()
2715 .map(crate::completion::Usage::from)
2716 .unwrap_or_default();
2717
2718 Ok(completion::CompletionResponse::new(choice, usage, provider)
2719 .with_optional_message_id(message_id)
2720 .with_optional_response_id(Some(response.id.as_str()).filter(|id| !id.is_empty()))
2721 .with_optional_provider_request_id(response.provider_request_id.clone())
2722 .with_optional_model(Some(response.model.as_str()).filter(|model| !model.is_empty()))
2723 .with_optional_finish_reason(finish_reason))
2724 }
2725}
2726
2727#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
2729#[serde(tag = "role", rename_all = "lowercase")]
2730pub enum Message {
2731 #[serde(alias = "developer")]
2732 System {
2733 #[serde(deserialize_with = "string_or_vec")]
2734 content: Vec<SystemContent>,
2735 #[serde(skip_serializing_if = "Option::is_none")]
2736 name: Option<String>,
2737 },
2738 User {
2739 #[serde(deserialize_with = "string_or_vec")]
2740 content: Vec<UserContent>,
2741 #[serde(skip_serializing_if = "Option::is_none")]
2742 name: Option<String>,
2743 },
2744 Assistant {
2745 content: Vec<AssistantContentType>,
2746 #[serde(skip_serializing_if = "String::is_empty")]
2747 id: String,
2748 #[serde(skip_serializing_if = "Option::is_none")]
2749 name: Option<String>,
2750 status: ToolStatus,
2751 },
2752 #[serde(rename = "assistant", skip_deserializing)]
2753 AssistantInput {
2754 content: String,
2755 #[serde(skip_serializing_if = "Option::is_none")]
2756 name: Option<String>,
2757 },
2758 #[serde(rename = "tool")]
2759 ToolResult {
2760 tool_call_id: String,
2761 output: ToolResultOutput,
2762 },
2763}
2764
2765#[derive(Default, Debug, Serialize, Deserialize, PartialEq, Clone)]
2767#[serde(rename_all = "lowercase")]
2768pub enum ToolResultContentType {
2769 #[default]
2770 Text,
2771}
2772
2773impl Message {
2774 pub fn system(content: &str) -> Self {
2775 Message::System {
2776 content: vec![content.to_owned().into()],
2777 name: None,
2778 }
2779 }
2780}
2781
2782#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
2785#[serde(tag = "type", rename_all = "snake_case")]
2786pub enum AssistantContent {
2787 OutputText(OutputText),
2788 Refusal { refusal: String },
2789}
2790
2791#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
2796pub struct OutputText {
2797 pub text: String,
2798 #[serde(flatten, default, skip_serializing_if = "Map::is_empty")]
2802 pub extras: Map<String, Value>,
2803}
2804
2805impl OutputText {
2806 pub fn new(text: impl Into<String>) -> Self {
2808 Self {
2809 text: text.into(),
2810 extras: Map::new(),
2811 }
2812 }
2813
2814 fn from_message_text(
2819 text: impl Into<String>,
2820 additional_params: Option<crate::message::AdditionalParams>,
2821 ) -> Self {
2822 let Some(params) = additional_params else {
2823 return Self::new(text);
2824 };
2825 let extras = params
2829 .into_wire_extras(OPENAI_RESPONSES_EXTRAS_KEY)
2830 .map(|map| {
2831 map.into_iter()
2832 .filter(|(key, _)| key != "text" && key != "type")
2839 .collect()
2840 })
2841 .unwrap_or_default();
2842 Self {
2843 text: text.into(),
2844 extras,
2845 }
2846 }
2847}
2848
2849fn assistant_text_replay_message(
2864 id: Option<String>,
2865 text: String,
2866 additional_params: Option<crate::message::AdditionalParams>,
2867) -> Option<Message> {
2868 if let Some(non_object) = additional_params
2873 .as_ref()
2874 .and_then(|params| params.get(OPENAI_RESPONSES_EXTRAS_KEY))
2875 .filter(|value| !value.is_object())
2876 {
2877 tracing::warn!(
2878 %non_object,
2879 "`additional_params[\"{OPENAI_RESPONSES_EXTRAS_KEY}\"]` must be a JSON \
2880 object — replaying without these extras"
2881 );
2882 }
2883 let own_extras = additional_params
2884 .as_ref()
2885 .and_then(|params| params.wire_extras(OPENAI_RESPONSES_EXTRAS_KEY))
2886 .is_some();
2887 if text.is_empty() && !(own_extras && id.is_some()) {
2888 return None;
2889 }
2890 match id {
2891 Some(id) => Some(Message::Assistant {
2892 content: vec![AssistantContentType::Text(AssistantContent::OutputText(
2893 OutputText::from_message_text(text, additional_params),
2894 ))],
2895 id,
2896 name: None,
2897 status: ToolStatus::Completed,
2898 }),
2899 None => {
2900 if own_extras {
2901 tracing::warn!(
2902 "own-wire extras cannot ride the id-less assistant form — \
2903 replaying the text without them"
2904 );
2905 }
2906 Some(Message::AssistantInput {
2907 content: text,
2908 name: None,
2909 })
2910 }
2911 }
2912}
2913
2914pub(crate) const OPENAI_RESPONSES_EXTRAS_KEY: &str = "openai_responses";
2922
2923impl From<AssistantContent> for completion::AssistantContent {
2924 fn from(value: AssistantContent) -> Self {
2925 match value {
2926 AssistantContent::Refusal { refusal } => {
2927 completion::AssistantContent::Text(Text::new(refusal))
2928 }
2929 AssistantContent::OutputText(OutputText { text, extras }) => {
2932 let extras: Map<String, Value> = extras
2937 .into_iter()
2938 .filter(|(_, value)| {
2939 !(value.is_null()
2940 || value.as_array().is_some_and(Vec::is_empty)
2941 || value.as_object().is_some_and(Map::is_empty))
2942 })
2943 .collect();
2944 completion::AssistantContent::Text(Text {
2945 text,
2946 additional_params: crate::message::AdditionalParams::from_entries(
2947 (!extras.is_empty())
2948 .then_some((OPENAI_RESPONSES_EXTRAS_KEY, Value::Object(extras))),
2949 ),
2950 })
2951 }
2952 }
2953 }
2954}
2955
2956#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
2958#[serde(untagged)]
2959pub enum AssistantContentType {
2960 Text(AssistantContent),
2961 ToolCall(OutputFunctionCall),
2962 Reasoning(OpenAIReasoning),
2963}
2964
2965#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
2968#[serde(tag = "type", rename_all = "snake_case")]
2969pub enum SystemContent {
2970 InputText { text: String },
2971}
2972
2973impl From<String> for SystemContent {
2974 fn from(s: String) -> Self {
2975 SystemContent::InputText { text: s }
2976 }
2977}
2978
2979impl std::str::FromStr for SystemContent {
2980 type Err = std::convert::Infallible;
2981
2982 fn from_str(s: &str) -> Result<Self, Self::Err> {
2983 Ok(SystemContent::InputText {
2984 text: s.to_string(),
2985 })
2986 }
2987}
2988
2989#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
2991#[serde(tag = "type", rename_all = "snake_case")]
2992pub enum UserContent {
2993 InputText {
2994 text: String,
2995 },
2996 InputImage {
2997 image_url: String,
2998 #[serde(default)]
2999 detail: ImageDetail,
3000 },
3001 InputFile {
3002 #[serde(skip_serializing_if = "Option::is_none")]
3003 file_id: Option<String>,
3004 #[serde(skip_serializing_if = "Option::is_none")]
3005 file_url: Option<String>,
3006 #[serde(skip_serializing_if = "Option::is_none")]
3007 file_data: Option<String>,
3008 #[serde(skip_serializing_if = "Option::is_none")]
3009 filename: Option<String>,
3010 },
3011 Audio {
3012 input_audio: InputAudio,
3013 },
3014 #[serde(rename = "tool")]
3015 ToolResult {
3016 tool_call_id: String,
3017 output: String,
3018 },
3019}
3020
3021impl FromStr for UserContent {
3022 type Err = Infallible;
3023
3024 fn from_str(s: &str) -> Result<Self, Self::Err> {
3025 Ok(UserContent::InputText {
3026 text: s.to_string(),
3027 })
3028 }
3029}
3030
3031#[cfg(test)]
3032mod tests {
3033 use super::*;
3034 use crate::completion::CompletionRequestBuilder;
3035 use crate::message;
3036 use crate::test_utils::MockCompletionModel;
3037 use serde_json::json;
3038 use std::collections::HashMap;
3039
3040 #[test]
3041 fn output_text_extras_survive_generic_conversion_and_replay() {
3042 let mut extras = Map::new();
3046 extras.insert(
3047 "annotations".to_string(),
3048 json!([{"type": "url_citation", "url": "https://example.com"}]),
3049 );
3050 let wire = OutputText {
3051 text: "cited".to_string(),
3052 extras: extras.clone(),
3053 };
3054 let generic: completion::AssistantContent = AssistantContent::OutputText(wire).into();
3055 let completion::AssistantContent::Text(text) = &generic else {
3056 panic!("expected a text block, got: {generic:?}");
3057 };
3058 assert_eq!(
3059 text.additional_params
3060 .as_ref()
3061 .and_then(|params| params.get(OPENAI_RESPONSES_EXTRAS_KEY)),
3062 Some(&Value::Object(extras.clone()))
3063 );
3064
3065 let replayed =
3066 OutputText::from_message_text(text.text.clone(), text.additional_params.clone());
3067 assert_eq!(replayed.text, "cited");
3068 assert_eq!(replayed.extras, extras);
3069
3070 let hostile = message::AdditionalParams::try_from_value(json!({
3074 OPENAI_RESPONSES_EXTRAS_KEY: {
3075 "text": "evil",
3076 "type": "evil_type",
3077 "annotations": ["kept"],
3078 }
3079 }))
3080 .expect("object params");
3081 let replayed = OutputText::from_message_text("real", hostile.clone());
3082 assert_eq!(replayed.text, "real");
3083 assert!(replayed.extras.get("text").is_none());
3084 assert!(replayed.extras.get("type").is_none());
3085 assert_eq!(replayed.extras.get("annotations"), Some(&json!(["kept"])));
3086 let wire = serde_json::to_value(&replayed).expect("serialize");
3087 assert_eq!(wire.get("text"), Some(&json!("real")));
3088
3089 let foreign = message::Message::Assistant {
3096 id: None,
3097 content: vec![completion::AssistantContent::Text(message::Text {
3098 text: String::new(),
3099 additional_params: message::AdditionalParams::try_from_value(json!({
3100 "anthropic_content": {"type": "server_tool_use", "id": "srv_1"}
3101 }))
3102 .expect("object params"),
3103 })],
3104 };
3105 let items: Vec<InputItem> = foreign.try_into().expect("convert");
3106 assert!(
3107 items.is_empty(),
3108 "foreign-annotated empty block must produce no Responses item: {items:?}"
3109 );
3110
3111 let own_annotated_empty = |id: Option<String>| message::Message::Assistant {
3112 id,
3113 content: vec![completion::AssistantContent::Text(message::Text {
3114 text: String::new(),
3115 additional_params: message::AdditionalParams::try_from_value(json!({
3116 OPENAI_RESPONSES_EXTRAS_KEY: {"annotations": ["kept"]}
3117 }))
3118 .expect("object params"),
3119 })],
3120 };
3121 let items: Vec<InputItem> = own_annotated_empty(Some("msg_1".to_string()))
3123 .try_into()
3124 .expect("convert");
3125 assert_eq!(
3126 items.len(),
3127 1,
3128 "own-wire-annotated empty block must replay when deliverable: {items:?}"
3129 );
3130 let serialized = serde_json::to_value(&items).expect("serialize");
3131 assert_eq!(
3132 serialized[0]["content"][0]["annotations"],
3133 json!(["kept"]),
3134 "the replayed item must carry the extras: {serialized}"
3135 );
3136 let items: Vec<InputItem> = own_annotated_empty(None).try_into().expect("convert");
3140 assert!(
3141 items.is_empty(),
3142 "undeliverable annotated empty block must be skipped: {items:?}"
3143 );
3144
3145 let bare: completion::AssistantContent =
3147 AssistantContent::OutputText(OutputText::new("plain")).into();
3148 let completion::AssistantContent::Text(text) = &bare else {
3149 panic!("expected a text block, got: {bare:?}");
3150 };
3151 assert_eq!(text.additional_params, None);
3152 assert!(
3153 OutputText::from_message_text("plain", None)
3154 .extras
3155 .is_empty(),
3156 "no params, no extras"
3157 );
3158 }
3159
3160 fn test_document(id: &str, text: &str) -> crate::completion::Document {
3161 crate::completion::Document {
3162 id: id.to_string(),
3163 text: text.to_string(),
3164 additional_props: HashMap::new(),
3165 }
3166 }
3167
3168 fn weather_tool_definition() -> completion::ToolDefinition {
3169 completion::ToolDefinition {
3170 name: "get_weather".to_string(),
3171 description: "Get the weather".to_string(),
3172 parameters: json!({
3173 "type": "object",
3174 "properties": {
3175 "location": {"type": "string"},
3176 "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}
3177 },
3178 "required": ["location"]
3179 }),
3180 }
3181 }
3182
3183 fn rig_tool_result(content: message::ToolResultContent) -> message::Message {
3184 message::Message::User {
3185 content: vec![message::UserContent::ToolResult(message::ToolResult {
3186 call: message::ToolCallId::new_or_mint("call-id"),
3187 provider: message::ProviderCallId::new("call-id")
3188 .map(|provider| provider.with_item_id("result-id")),
3189 name: "tool".to_string(),
3190 content: vec![content],
3191 })],
3192 }
3193 }
3194
3195 #[test]
3196 fn mixed_user_content_preserves_order_around_tool_results() {
3197 let input = message::Message::User {
3198 content: vec![
3199 message::UserContent::text("before"),
3200 message::UserContent::tool_result_with_call_id(
3201 "result-id",
3202 "call-id".to_string(),
3203 "tool",
3204 vec![message::ToolResultContent::text("tool output")],
3205 ),
3206 message::UserContent::text("after"),
3207 ],
3208 };
3209
3210 let items = Vec::<InputItem>::try_from(input).expect("input item conversion");
3211
3212 assert!(matches!(
3213 items.as_slice(),
3214 [
3215 InputItem {
3216 input: InputContent::Message(Message::User { content: before, .. }),
3217 ..
3218 },
3219 InputItem {
3220 input: InputContent::FunctionCallOutput(ToolResult { call_id, .. }),
3221 ..
3222 },
3223 InputItem {
3224 input: InputContent::Message(Message::User { content: after, .. }),
3225 ..
3226 },
3227 ] if matches!(before.first(), Some(UserContent::InputText { text }) if text == "before")
3228 && call_id == "call-id"
3229 && matches!(after.first(), Some(UserContent::InputText { text }) if text == "after")
3230 ));
3231 }
3232
3233 fn reasoning_input_items(items: &[InputItem]) -> Vec<serde_json::Value> {
3234 items
3235 .iter()
3236 .map(|item| serde_json::to_value(item).expect("input item should serialize"))
3237 .filter(|value| value["type"] == "reasoning")
3238 .collect()
3239 }
3240
3241 #[tokio::test]
3247 async fn cross_provider_minted_reasoning_ids_are_not_serialized_upstream() {
3248 use crate::completion::CompletionModel as _;
3249 use crate::test_utils::MockStreamEvent;
3250 use futures::StreamExt as _;
3251
3252 let model = MockCompletionModel::from_stream_turns([vec![
3255 MockStreamEvent::reasoning_delta("thinking hard"),
3256 MockStreamEvent::text("answer"),
3257 MockStreamEvent::final_response_with_default_usage(),
3258 ]]);
3259 let request = CompletionRequestBuilder::new(model.clone(), "hi").build();
3260 let mut stream = model.stream(request).await.expect("mock stream");
3261 while stream.next().await.is_some() {}
3262 let choice = stream.choice.clone();
3263 assert!(
3268 choice.iter().any(
3269 |content| matches!(content, message::AssistantContent::Reasoning(reasoning)
3270 if reasoning.id.is_none())
3271 ),
3272 "a minted stream identity must aggregate as an id-less reasoning part"
3273 );
3274
3275 let items = Vec::<InputItem>::try_from(crate::completion::Message::Assistant {
3276 id: None,
3277 content: choice,
3278 })
3279 .expect("history should convert");
3280 assert!(
3281 reasoning_input_items(&items).is_empty(),
3282 "an id-less reasoning part must not reach the request input"
3283 );
3284
3285 let items = Vec::<InputItem>::try_from(crate::completion::Message::Assistant {
3287 id: None,
3288 content: vec![message::AssistantContent::Reasoning(message::Reasoning {
3289 id: Some("rs_0123".to_string()),
3290 content: vec![message::ReasoningContent::Text {
3291 text: "real item".to_string(),
3292 signature: None,
3293 }],
3294 })],
3295 })
3296 .expect("history should convert");
3297 let reasoning = reasoning_input_items(&items);
3298 assert_eq!(reasoning.len(), 1);
3299 assert_eq!(reasoning[0]["id"], "rs_0123");
3300 }
3301
3302 #[tokio::test]
3307 async fn delta_only_stream_minted_output_ids_are_not_serialized_upstream() {
3308 use crate::test_utils::streaming_conformance::{fixtures, ok_chunks};
3309 use bytes::Bytes;
3310
3311 let sse = |frame: &serde_json::Value| Bytes::from(format!("data: {frame}\n\n"));
3312 let frames = vec![
3313 sse(&json!({
3315 "type": "response.reasoning_text.delta",
3316 "output_index": 0,
3317 "content_index": 0,
3318 "sequence_number": 1,
3319 "delta": "unattributed thought",
3320 })),
3321 sse(&json!({
3322 "type": "response.completed",
3323 "sequence_number": 2,
3324 "response": {
3325 "id": "resp_1",
3326 "object": "response",
3327 "created_at": 0,
3328 "status": "completed",
3329 "model": "gpt-5.4",
3330 "output": [],
3331 "tools": [],
3332 "usage": null,
3333 },
3334 })),
3335 ];
3336 let drained = fixtures::openai_responses::driver()
3337 .drive(ok_chunks(frames))
3338 .await
3339 .expect("stream should complete");
3340 assert!(
3343 drained.choice.iter().any(
3344 |content| matches!(content, message::AssistantContent::Reasoning(reasoning)
3345 if reasoning.id.is_none())
3346 ),
3347 "an id-less delta stream must aggregate as an id-less reasoning part"
3348 );
3349
3350 let items = Vec::<InputItem>::try_from(crate::completion::Message::Assistant {
3351 id: None,
3352 content: drained.choice.clone(),
3353 })
3354 .expect("history should convert");
3355 assert!(
3356 reasoning_input_items(&items).is_empty(),
3357 "an id-less reasoning part must not reach the request input"
3358 );
3359 }
3360
3361 #[test]
3362 fn tool_result_literal_text_and_structured_json_render_without_reparsing() {
3363 let cases = [
3364 (
3365 message::ToolResultContent::text(r#"{"status":"ok"}"#),
3366 r#"{"status":"ok"}"#.to_string(),
3367 ),
3368 (
3369 message::ToolResultContent::json(json!({ "status": "ok" })),
3370 r#"{"status":"ok"}"#.to_string(),
3371 ),
3372 ];
3373
3374 for (content, expected) in cases {
3375 let input = rig_tool_result(content);
3376
3377 let items: Vec<InputItem> = input.try_into().expect("input item conversion");
3378 assert!(matches!(
3379 items.as_slice(),
3380 [InputItem {
3381 input: InputContent::FunctionCallOutput(ToolResult {
3382 output: ToolResultOutput::Text(output),
3383 ..
3384 }),
3385 ..
3386 }] if output == &expected
3387 ));
3388 }
3389 }
3390
3391 #[test]
3392 fn multiple_text_tool_result_blocks_preserve_order_as_rich_function_output() {
3393 let content = vec![
3394 message::ToolResultContent::text("first"),
3395 message::ToolResultContent::text("second"),
3396 ];
3397
3398 let input = message::Message::User {
3399 content: vec![message::UserContent::ToolResult(message::ToolResult {
3400 call: message::ToolCallId::new_or_mint("call-id"),
3401 provider: message::ProviderCallId::new("call-id")
3402 .map(|provider| provider.with_item_id("result-id")),
3403 name: "tool".to_string(),
3404 content,
3405 })],
3406 };
3407
3408 let expected = ToolResultOutput::Content(vec![
3409 ToolResultOutputContent::InputText {
3410 text: "first".to_string(),
3411 },
3412 ToolResultOutputContent::InputText {
3413 text: "second".to_string(),
3414 },
3415 ]);
3416
3417 let items: Vec<InputItem> = input.try_into().expect("input item conversion");
3418
3419 match items.as_slice() {
3420 [
3421 InputItem {
3422 input: InputContent::FunctionCallOutput(ToolResult { output, .. }),
3423 ..
3424 },
3425 ] => {
3426 assert_eq!(output, &expected);
3427 }
3428 other => panic!("expected one function-call output, got {other:?}"),
3429 }
3430
3431 let wire = serde_json::to_value(&items[0]).expect("input item should serialize");
3432
3433 assert_eq!(
3434 wire,
3435 json!({
3436 "type": "function_call_output",
3437 "call_id": "call-id",
3438 "output": [
3439 {
3440 "type": "input_text",
3441 "text": "first"
3442 },
3443 {
3444 "type": "input_text",
3445 "text": "second"
3446 }
3447 ],
3448 "status": "completed"
3449 })
3450 );
3451 }
3452
3453 #[test]
3454 fn multiple_text_and_json_tool_result_blocks_preserve_boundaries() {
3455 let content = vec![
3456 message::ToolResultContent::text("before"),
3457 message::ToolResultContent::json(json!({
3458 "status": "ok"
3459 })),
3460 message::ToolResultContent::text("after"),
3461 ];
3462
3463 let output =
3464 responses_tool_result_output(content).expect("tool-result conversion should succeed");
3465
3466 assert_eq!(
3467 output,
3468 ToolResultOutput::Content(vec![
3469 ToolResultOutputContent::InputText {
3470 text: "before".to_string(),
3471 },
3472 ToolResultOutputContent::InputText {
3473 text: r#"{"status":"ok"}"#.to_string(),
3474 },
3475 ToolResultOutputContent::InputText {
3476 text: "after".to_string(),
3477 },
3478 ])
3479 );
3480 }
3481
3482 #[test]
3483 fn tool_result_images_and_text_preserve_order_as_rich_function_output() {
3484 let content = vec![
3485 message::ToolResultContent::text("before"),
3486 message::ToolResultContent::image_base64(
3487 "aW1hZ2U=",
3488 Some(message::ImageMediaType::PNG),
3489 None,
3490 ),
3491 message::ToolResultContent::json(json!({ "after": true })),
3492 ];
3493 let input = message::Message::User {
3494 content: vec![message::UserContent::ToolResult(message::ToolResult {
3495 call: message::ToolCallId::new_or_mint("call-id"),
3496 provider: message::ProviderCallId::new("call-id")
3497 .map(|provider| provider.with_item_id("result-id")),
3498 name: "tool".to_string(),
3499 content,
3500 })],
3501 };
3502
3503 let assert_output = |output: &ToolResultOutput| {
3504 assert!(matches!(
3505 output,
3506 ToolResultOutput::Content(content)
3507 if matches!(content.as_slice(), [
3508 ToolResultOutputContent::InputText { text: before },
3509 ToolResultOutputContent::InputImage { image_url, .. },
3510 ToolResultOutputContent::InputText { text: after },
3511 ] if before == "before"
3512 && image_url.as_deref() == Some("data:image/png;base64,aW1hZ2U=")
3513 && after == r#"{"after":true}"#)
3514 ));
3515 };
3516
3517 let items: Vec<InputItem> = input.try_into().expect("input item conversion");
3518 match items.as_slice() {
3519 [
3520 InputItem {
3521 input: InputContent::FunctionCallOutput(ToolResult { output, .. }),
3522 ..
3523 },
3524 ] => assert_output(output),
3525 other => panic!("expected one rich function output, got {other:?}"),
3526 }
3527 }
3528
3529 #[test]
3530 fn tool_result_file_id_image_uses_the_native_wire_field() {
3531 let input = rig_tool_result(message::ToolResultContent::Image(message::Image {
3532 data: message::DocumentSourceKind::FileId("file-image-123".to_string()),
3533 media_type: None,
3534 detail: None,
3535 additional_params: None,
3536 }));
3537
3538 let items: Vec<InputItem> = input.try_into().expect("input item conversion");
3539 let wire = serde_json::to_value(&items[0]).expect("serialize input item");
3540 assert_eq!(
3541 wire,
3542 json!({
3543 "type": "function_call_output",
3544 "call_id": "call-id",
3545 "output": [{
3546 "type": "input_image",
3547 "file_id": "file-image-123",
3548 "detail": "auto"
3549 }],
3550 "status": "completed"
3551 })
3552 );
3553 }
3554
3555 fn weather_tool_request() -> completion::CompletionRequest {
3556 completion::CompletionRequest {
3557 model: None,
3558 preamble: None,
3559 chat_history: vec![message::Message::user("what's the weather?")],
3560 documents: Vec::new(),
3561 tools: vec![weather_tool_definition()],
3562 temperature: None,
3563 max_tokens: None,
3564 tool_choice: None,
3565 additional_params: None,
3566 output_schema: None,
3567 record_telemetry_content: false,
3568 }
3569 }
3570
3571 #[test]
3572 fn responses_tool_choice_modes_serialize_as_plain_strings() {
3573 for (choice, expected) in [
3574 (message::ToolChoice::Auto, json!("auto")),
3575 (message::ToolChoice::None, json!("none")),
3576 (message::ToolChoice::Required, json!("required")),
3577 ] {
3578 let converted = ToolChoice::try_from(choice).expect("mode should convert");
3579 assert_eq!(
3580 serde_json::to_value(&converted).expect("serialize tool choice"),
3581 expected
3582 );
3583 }
3584 }
3585
3586 #[test]
3587 fn responses_tool_choice_specific_single_name_serializes_as_named_function() {
3588 let converted = ToolChoice::try_from(message::ToolChoice::Specific {
3589 function_names: vec!["get_weather".to_string()],
3590 })
3591 .expect("single specific tool should convert");
3592
3593 assert_eq!(
3594 serde_json::to_value(&converted).expect("serialize tool choice"),
3595 json!({"type": "function", "name": "get_weather"})
3596 );
3597 }
3598
3599 #[test]
3600 fn responses_tool_choice_specific_multiple_names_serialize_as_allowed_tools() {
3601 let converted = ToolChoice::try_from(message::ToolChoice::Specific {
3602 function_names: vec!["add".to_string(), "subtract".to_string()],
3603 })
3604 .expect("multiple specific tools should convert");
3605
3606 assert_eq!(
3607 serde_json::to_value(&converted).expect("serialize tool choice"),
3608 json!({
3609 "type": "allowed_tools",
3610 "mode": "required",
3611 "tools": [
3612 {"type": "function", "name": "add"},
3613 {"type": "function", "name": "subtract"}
3614 ]
3615 })
3616 );
3617 }
3618
3619 #[test]
3620 fn responses_tool_choice_specific_empty_names_error() {
3621 let converted = ToolChoice::try_from(message::ToolChoice::Specific {
3622 function_names: vec![],
3623 });
3624
3625 assert!(matches!(
3626 converted,
3627 Err(CompletionError::RequestError(error))
3628 if error.to_string().contains("at least one function name")
3629 ));
3630 }
3631
3632 #[test]
3633 fn responses_request_with_specific_tool_choice_serializes_named_function() {
3634 let mut request = weather_tool_request();
3635 request.tool_choice = Some(message::ToolChoice::Specific {
3636 function_names: vec!["get_weather".to_string()],
3637 });
3638
3639 let request =
3640 CompletionRequest::try_from(("gpt-test".to_string(), request)).expect("convert");
3641 let request_json = serde_json::to_value(&request).expect("serialize request");
3642
3643 assert_eq!(
3644 request_json.get("tool_choice"),
3645 Some(&json!({"type": "function", "name": "get_weather"}))
3646 );
3647 }
3648
3649 #[test]
3650 fn responses_function_tools_are_non_strict_by_default() {
3651 let tool = ResponsesToolDefinition::function(
3652 "get_weather",
3653 "Get the weather",
3654 weather_tool_definition().parameters,
3655 );
3656
3657 assert!(!tool.strict);
3658 assert_eq!(tool.parameters["required"], json!(["location"]));
3659 assert!(tool.parameters.get("additionalProperties").is_none());
3660
3661 let serialized = serde_json::to_value(tool).expect("tool should serialize");
3662 assert!(serialized.get("strict").is_none());
3663 }
3664
3665 #[test]
3666 fn responses_tool_definitions_accept_nullable_strict() {
3667 let cases = [
3668 (
3669 json!({
3670 "type": "function",
3671 "name": "get_weather",
3672 "parameters": {}
3673 }),
3674 false,
3675 ),
3676 (
3677 json!({
3678 "type": "function",
3679 "name": "get_weather",
3680 "parameters": {},
3681 "strict": null
3682 }),
3683 false,
3684 ),
3685 (
3686 json!({
3687 "type": "function",
3688 "name": "get_weather",
3689 "parameters": {},
3690 "strict": false
3691 }),
3692 false,
3693 ),
3694 (
3695 json!({
3696 "type": "function",
3697 "name": "get_weather",
3698 "parameters": {},
3699 "strict": true
3700 }),
3701 true,
3702 ),
3703 ];
3704
3705 for (value, expected) in cases {
3706 let tool: ResponsesToolDefinition =
3707 serde_json::from_value(value).expect("tool definition should deserialize");
3708 assert_eq!(tool.strict, expected);
3709 }
3710 }
3711
3712 #[test]
3713 fn responses_strict_function_tools_sanitize_schema() {
3714 let tool = ResponsesToolDefinition::strict_function(
3715 "get_weather",
3716 "Get the weather",
3717 weather_tool_definition().parameters,
3718 );
3719
3720 assert!(tool.strict);
3721 assert_eq!(tool.parameters["additionalProperties"], json!(false));
3722 assert_eq!(tool.parameters["required"], json!(["location", "unit"]));
3723 }
3724
3725 fn request_with_preamble(preamble: &str) -> completion::CompletionRequest {
3726 completion::CompletionRequest {
3727 model: None,
3728 preamble: Some(preamble.to_string()),
3729 chat_history: vec![message::Message::user("Hello")],
3730 documents: Vec::new(),
3731 tools: Vec::new(),
3732 temperature: None,
3733 max_tokens: None,
3734 tool_choice: None,
3735 additional_params: None,
3736 output_schema: None,
3737 record_telemetry_content: false,
3738 }
3739 }
3740
3741 fn system_only_request(system_text: &str) -> completion::CompletionRequest {
3742 completion::CompletionRequest {
3743 model: None,
3744 preamble: None,
3745 chat_history: vec![completion::Message::system(system_text)],
3746 documents: Vec::new(),
3747 tools: Vec::new(),
3748 temperature: None,
3749 max_tokens: None,
3750 tool_choice: None,
3751 additional_params: None,
3752 output_schema: None,
3753 record_telemetry_content: false,
3754 }
3755 }
3756
3757 #[test]
3758 fn responses_request_uses_top_level_instructions_for_preamble_by_default() {
3759 let req = CompletionRequest::try_from((
3760 "gpt-4o-mini".to_string(),
3761 request_with_preamble("You are concise."),
3762 ))
3763 .expect("request should convert");
3764 let serialized = serde_json::to_value(&req).expect("request should serialize");
3765 let input = serialized["input"]
3766 .as_array()
3767 .expect("input should be array");
3768
3769 assert_eq!(serialized["instructions"], json!("You are concise."));
3770 assert_eq!(input.len(), 1);
3771 assert_eq!(input[0]["role"], "user");
3772 }
3773
3774 #[test]
3775 fn responses_request_drops_whitespace_only_preamble() {
3776 let req = CompletionRequest::try_from((
3777 "gpt-4o-mini".to_string(),
3778 request_with_preamble(" \n "),
3779 ))
3780 .expect("request should convert");
3781 let serialized = serde_json::to_value(&req).expect("request should serialize");
3782 let input = serialized["input"]
3783 .as_array()
3784 .expect("input should be array");
3785
3786 assert!(
3787 serialized.get("instructions").is_none(),
3788 "a whitespace-only preamble carries no content and is dropped"
3789 );
3790 assert_eq!(input.len(), 1);
3791 assert_eq!(input[0]["role"], "user");
3792 }
3793
3794 #[test]
3795 fn responses_request_lifts_system_messages_to_top_level_instructions_by_default() {
3796 let request = CompletionRequestBuilder::new(MockCompletionModel::default(), "Hello")
3797 .preamble("System one".to_string())
3798 .message(completion::Message::system("System two"))
3799 .build();
3800
3801 let req = CompletionRequest::try_from(("gpt-4o-mini".to_string(), request))
3802 .expect("request should convert");
3803 let serialized = serde_json::to_value(&req).expect("request should serialize");
3804 let input = serialized["input"]
3805 .as_array()
3806 .expect("input should be array");
3807
3808 assert_eq!(
3809 serialized["instructions"],
3810 json!("System one\n\nSystem two")
3811 );
3812 assert_eq!(input.len(), 1);
3813 assert_eq!(input[0]["role"], "user");
3814 }
3815
3816 #[test]
3817 fn responses_request_with_only_system_messages_keeps_them_in_input() {
3818 let req = CompletionRequest::try_from((
3819 "gpt-4o-mini".to_string(),
3820 system_only_request("System only"),
3821 ))
3822 .expect("request conversion should succeed");
3823 let serialized = serde_json::to_value(&req).expect("request should serialize");
3824 let input = serialized["input"]
3825 .as_array()
3826 .expect("input should be array");
3827
3828 assert!(
3829 serialized.get("instructions").is_none(),
3830 "lifting a system-only history would leave input empty, so it stays in input"
3831 );
3832 assert_eq!(input.len(), 1);
3833 assert_eq!(input[0]["role"], "system");
3834 assert!(input[0].to_string().contains("System only"));
3835 }
3836
3837 #[test]
3838 fn responses_model_can_fallback_to_system_messages_in_input() {
3839 let client = crate::providers::openai::Client::new("dummy-key").expect("client");
3840 let model = ResponsesCompletionModel::new(client, "gpt-4o-mini")
3841 .with_system_instructions_as_messages();
3842
3843 let req = model
3844 .create_completion_request(request_with_preamble("You are concise."))
3845 .expect("request should convert");
3846 let serialized = serde_json::to_value(&req).expect("request should serialize");
3847 let input = serialized["input"]
3848 .as_array()
3849 .expect("input should be array");
3850
3851 assert!(serialized.get("instructions").is_none());
3852 assert_eq!(input.len(), 2);
3853 assert_eq!(input[0]["role"], "system");
3854 assert!(input[0].to_string().contains("You are concise."));
3855 assert_eq!(input[1]["role"], "user");
3856 }
3857
3858 #[test]
3859 fn responses_client_can_fallback_to_system_messages_in_input() {
3860 use crate::prelude::CompletionClient;
3861
3862 let client = crate::providers::openai::Client::new("dummy-key")
3863 .expect("client")
3864 .with_system_instructions_as_messages();
3865 let model = client.completion_model("gpt-4o-mini");
3866
3867 let req = model
3868 .create_completion_request(request_with_preamble("You are concise."))
3869 .expect("request should convert");
3870 let serialized = serde_json::to_value(&req).expect("request should serialize");
3871 let input = serialized["input"]
3872 .as_array()
3873 .expect("input should be array");
3874
3875 assert!(serialized.get("instructions").is_none());
3876 assert_eq!(input.len(), 2);
3877 assert_eq!(input[0]["role"], "system");
3878 assert!(input[0].to_string().contains("You are concise."));
3879 assert_eq!(input[1]["role"], "user");
3880 }
3881
3882 #[test]
3883 fn responses_model_can_lift_all_system_messages_via_placement() {
3884 let client = crate::providers::openai::Client::new("dummy-key").expect("client");
3885 let model = ResponsesCompletionModel::new(client, "gpt-4o-mini")
3886 .with_system_instructions_placement(SystemInstructionsPlacement::AllInstructions);
3887
3888 let request = CompletionRequestBuilder::new(MockCompletionModel::default(), "again")
3889 .preamble("System one".to_string())
3890 .message(completion::Message::user("hi"))
3891 .message(completion::Message::system("Mid-conversation instruction"))
3892 .build();
3893
3894 let req = model
3895 .create_completion_request(request)
3896 .expect("request should convert");
3897 let serialized = serde_json::to_value(&req).expect("request should serialize");
3898 let input = serialized["input"]
3899 .as_array()
3900 .expect("input should be array");
3901
3902 assert_eq!(
3903 serialized["instructions"],
3904 json!("System one\n\nMid-conversation instruction")
3905 );
3906 assert!(
3907 input.iter().all(|item| item["role"] != "system"),
3908 "AllInstructions should leave no system items in input: {input:?}"
3909 );
3910 }
3911
3912 #[test]
3913 fn responses_client_placement_survives_completions_api_round_trip() {
3914 use crate::prelude::CompletionClient;
3915
3916 let client = crate::providers::openai::Client::new("dummy-key")
3917 .expect("client")
3918 .with_system_instructions_placement(SystemInstructionsPlacement::InputSystemMessages)
3919 .completions_api()
3920 .responses_api();
3921 let model = client.completion_model("gpt-4o-mini");
3922
3923 let req = model
3924 .create_completion_request(request_with_preamble("You are concise."))
3925 .expect("request should convert");
3926 let serialized = serde_json::to_value(&req).expect("request should serialize");
3927
3928 assert!(
3929 serialized.get("instructions").is_none(),
3930 "placement configured before completions_api() should survive responses_api()"
3931 );
3932 assert_eq!(serialized["input"][0]["role"], "system");
3933 }
3934
3935 #[test]
3936 fn all_instructions_system_only_input_reports_non_system_requirement() {
3937 let err = CompletionRequest::try_from(ResponsesRequestParams {
3938 model: "gpt-4o-mini".to_string(),
3939 request: system_only_request("System only"),
3940 system_instructions_placement: SystemInstructionsPlacement::AllInstructions,
3941 })
3942 .expect_err("system-only input should fail once every item is lifted");
3943
3944 assert!(
3945 err.to_string().contains("non-system item"),
3946 "error should explain that lifted system messages left input empty: {err}"
3947 );
3948 }
3949
3950 #[test]
3951 fn all_instructions_whitespace_only_system_input_reports_non_system_requirement() {
3952 let err = CompletionRequest::try_from(ResponsesRequestParams {
3953 model: "gpt-4o-mini".to_string(),
3954 request: system_only_request(" "),
3955 system_instructions_placement: SystemInstructionsPlacement::AllInstructions,
3956 })
3957 .expect_err("whitespace-only system input should fail once every item is lifted");
3958
3959 assert!(
3960 err.to_string().contains("non-system item"),
3961 "even when lifted system text is whitespace-only (so no `instructions` field is \
3962 produced), the error should explain that system messages were lifted: {err}"
3963 );
3964 }
3965
3966 #[test]
3967 fn responses_request_conversion_keeps_tools_non_strict_by_default() {
3968 let req = CompletionRequest::try_from(("gpt-4o-mini".to_string(), weather_tool_request()))
3969 .expect("request should convert");
3970
3971 let tool = &req.tools[0];
3972 assert!(!tool.strict);
3973 assert_eq!(tool.parameters["required"], json!(["location"]));
3974 assert!(tool.parameters.get("additionalProperties").is_none());
3975 }
3976
3977 #[test]
3978 fn responses_model_strict_tools_opt_in_sanitizes_all_function_tools() {
3979 let client = crate::providers::openai::Client::new("dummy-key").expect("client");
3980 let model = ResponsesCompletionModel::new(client, "gpt-4o-mini")
3981 .with_strict_tools()
3982 .with_tool(completion::ToolDefinition {
3983 name: "lookup".to_string(),
3984 description: "Look something up".to_string(),
3985 parameters: json!({
3986 "type": "object",
3987 "properties": {"q": {"type": "string"}}
3988 }),
3989 });
3990
3991 let mut request = weather_tool_request();
3992 request.additional_params = Some(json!({
3993 "tools": [{
3994 "type": "function",
3995 "name": "extra",
3996 "description": "An additional_params tool",
3997 "parameters": {"type": "object", "properties": {"x": {"type": "string"}}}
3998 }]
3999 }));
4000
4001 let req = model
4002 .create_completion_request(request)
4003 .expect("request should convert");
4004
4005 assert_eq!(req.tools.len(), 3);
4006 for tool in &req.tools {
4007 assert!(tool.strict, "{} should be strict", tool.name);
4008 assert_eq!(tool.parameters["additionalProperties"], json!(false));
4009 }
4010 }
4011
4012 #[test]
4013 fn responses_model_default_preserves_all_function_tools_as_constructed() {
4014 let client = crate::providers::openai::Client::new("dummy-key").expect("client");
4015 let model = ResponsesCompletionModel::new(client, "gpt-4o-mini")
4016 .with_tool(weather_tool_definition());
4017
4018 let mut request = weather_tool_request();
4019 request.additional_params = Some(json!({
4020 "tools": [{
4021 "type": "function",
4022 "name": "extra",
4023 "description": "An additional_params tool",
4024 "parameters": {"type": "object", "properties": {"x": {"type": "string"}}}
4025 }]
4026 }));
4027
4028 let req = model
4029 .create_completion_request(request)
4030 .expect("request should convert");
4031
4032 assert_eq!(req.tools.len(), 3);
4033 for tool in &req.tools {
4034 assert!(!tool.strict, "{} should not be strict", tool.name);
4035 assert!(tool.parameters.get("additionalProperties").is_none());
4036 }
4037 }
4038
4039 #[test]
4040 fn responses_explicit_strict_tool_stays_strict_on_default_model() {
4041 let client = crate::providers::openai::Client::new("dummy-key").expect("client");
4042 let model = ResponsesCompletionModel::new(client, "gpt-4o-mini").with_tool(
4043 ResponsesToolDefinition::strict_function(
4044 "lookup",
4045 "Look something up",
4046 json!({"type": "object", "properties": {"q": {"type": "string"}}}),
4047 ),
4048 );
4049
4050 let req = model
4051 .create_completion_request(weather_tool_request())
4052 .expect("request should convert");
4053
4054 assert!(!req.tools[0].strict);
4055 assert!(req.tools[1].strict);
4056 assert_eq!(
4057 req.tools[1].parameters["additionalProperties"],
4058 json!(false)
4059 );
4060 }
4061
4062 fn response_with_service_tier(service_tier: &str) -> Value {
4063 json!({
4064 "id": "resp_123",
4065 "object": "response",
4066 "created_at": 0,
4067 "status": "completed",
4068 "model": "gpt-5.4",
4069 "output": [],
4070 "service_tier": service_tier,
4071 })
4072 }
4073
4074 #[test]
4075 fn completion_response_deserializes_standard_service_tier() {
4076 let response: CompletionResponse =
4077 serde_json::from_value(response_with_service_tier("standard"))
4078 .expect("response should deserialize");
4079
4080 assert!(matches!(
4081 response.additional_parameters.service_tier,
4082 Some(OpenAIServiceTier::Standard)
4083 ));
4084 }
4085
4086 #[test]
4087 fn completion_response_deserializes_priority_service_tier() {
4088 let response: CompletionResponse =
4089 serde_json::from_value(response_with_service_tier("priority"))
4090 .expect("response should deserialize");
4091
4092 assert!(matches!(
4093 response.additional_parameters.service_tier,
4094 Some(OpenAIServiceTier::Priority)
4095 ));
4096 }
4097
4098 #[test]
4099 fn completion_response_preserves_unknown_service_tier() {
4100 let response: CompletionResponse =
4101 serde_json::from_value(response_with_service_tier("provider_experimental"))
4102 .expect("response should deserialize");
4103
4104 let Some(OpenAIServiceTier::Other(service_tier)) =
4105 response.additional_parameters.service_tier
4106 else {
4107 panic!("expected provider-specific service tier");
4108 };
4109
4110 assert_eq!(service_tier, "provider_experimental");
4111 }
4112
4113 #[test]
4114 fn responses_request_keeps_documents_after_lifted_system_messages() {
4115 let request = CompletionRequestBuilder::new(MockCompletionModel::default(), "Prompt")
4116 .message(completion::Message::system("System prompt"))
4117 .message(completion::Message::user("Earlier user turn"))
4118 .message(completion::Message::assistant("Earlier assistant turn"))
4119 .document(test_document("doc1", "Document text."))
4120 .build();
4121
4122 let responses_request = CompletionRequest::try_from(("gpt-4o-mini".to_string(), request))
4123 .expect("request conversion should succeed");
4124
4125 let serialized =
4126 serde_json::to_value(&responses_request).expect("request should serialize");
4127 let input = serialized["input"]
4128 .as_array()
4129 .expect("input should be an array");
4130
4131 assert_eq!(serialized["instructions"], json!("System prompt"));
4132 assert_eq!(input.len(), 4);
4133 assert_eq!(input[0]["role"], "user");
4134 assert!(
4135 input[0].to_string().contains("<file id: doc1>"),
4136 "document input should be first after system instructions are lifted: {input:?}"
4137 );
4138 assert_eq!(input[1]["role"], "user");
4139 assert!(
4140 input[1].to_string().contains("Earlier user turn"),
4141 "prior user history should follow document input: {input:?}"
4142 );
4143 assert_eq!(input[2]["role"], "assistant");
4144 assert!(
4145 input[2].to_string().contains("Earlier assistant turn"),
4146 "prior assistant history should follow prior user history: {input:?}"
4147 );
4148 assert_eq!(input[3]["role"], "user");
4149 assert!(
4150 input[3].to_string().contains("Prompt"),
4151 "prompt should remain last: {input:?}"
4152 );
4153 }
4154
4155 #[test]
4156 fn responses_direct_request_keeps_mid_conversation_system_messages_in_input() {
4157 let request = crate::completion::CompletionRequest {
4158 model: None,
4159 preamble: None,
4160 chat_history: vec![
4161 completion::Message::system("System prompt"),
4162 completion::Message::assistant("Earlier assistant turn"),
4163 completion::Message::system("Mid-conversation instruction"),
4164 completion::Message::user("Prompt"),
4165 ],
4166 documents: vec![test_document("doc1", "Document text.")],
4167 tools: vec![],
4168 temperature: None,
4169 max_tokens: None,
4170 tool_choice: None,
4171 additional_params: None,
4172 output_schema: None,
4173 record_telemetry_content: false,
4174 };
4175
4176 let responses_request = CompletionRequest::try_from(("gpt-4o-mini".to_string(), request))
4177 .expect("request conversion should succeed");
4178
4179 let serialized =
4180 serde_json::to_value(&responses_request).expect("request should serialize");
4181 let input = serialized["input"]
4182 .as_array()
4183 .expect("input should be an array");
4184
4185 assert_eq!(
4186 serialized["instructions"],
4187 json!("System prompt"),
4188 "only the leading run of system messages should be lifted"
4189 );
4190 assert_eq!(input.len(), 4);
4191 assert_eq!(input[0]["role"], "user");
4192 assert!(
4193 input[0].to_string().contains("<file id: doc1>"),
4194 "document input should follow lifted system instructions: {input:?}"
4195 );
4196 assert_eq!(input[1]["role"], "assistant");
4197 assert_eq!(input[2]["role"], "system");
4198 assert!(
4199 input[2]
4200 .to_string()
4201 .contains("Mid-conversation instruction"),
4202 "mid-conversation system messages should keep their position: {input:?}"
4203 );
4204 assert_eq!(input[3]["role"], "user");
4205 assert_eq!(
4206 input
4207 .iter()
4208 .filter(|message| message.to_string().contains("<file id: doc1>"))
4209 .count(),
4210 1,
4211 "document input should appear exactly once: {input:?}"
4212 );
4213 }
4214
4215 #[test]
4216 fn service_tier_serializes_expected_strings() {
4217 let cases = [
4218 (OpenAIServiceTier::Auto, "auto"),
4219 (OpenAIServiceTier::Default, "default"),
4220 (OpenAIServiceTier::Flex, "flex"),
4221 (OpenAIServiceTier::Priority, "priority"),
4222 (OpenAIServiceTier::Standard, "standard"),
4223 ];
4224
4225 for (service_tier, expected) in cases {
4226 assert_eq!(
4227 serde_json::to_value(service_tier).expect("service tier should serialize"),
4228 json!(expected)
4229 );
4230 }
4231
4232 assert_eq!(
4233 serde_json::to_value(OpenAIServiceTier::Other(
4234 "provider_experimental".to_string()
4235 ))
4236 .expect("provider-specific service tier should serialize"),
4237 json!("provider_experimental")
4238 );
4239 }
4240
4241 #[test]
4242 fn responses_usage_token_usage_preserves_reasoning_tokens() {
4243 let usage = ResponsesUsage {
4244 input_tokens: 100,
4245 input_tokens_details: Some(InputTokensDetails { cached_tokens: 25 }),
4246 output_tokens: 50,
4247 output_tokens_details: Some(OutputTokensDetails {
4248 reasoning_tokens: 15,
4249 }),
4250 total_tokens: 150,
4251 };
4252
4253 let token_usage = crate::completion::Usage::from(&usage);
4254
4255 assert_eq!(token_usage.input_tokens, 100);
4256 assert_eq!(token_usage.cached_input_tokens, 25);
4257 assert_eq!(token_usage.output_tokens, 50);
4258 assert_eq!(token_usage.reasoning_tokens, 15);
4259 assert_eq!(token_usage.total_tokens, 150);
4260 }
4261
4262 #[test]
4263 fn responses_usage_deserializes_without_output_token_details() {
4264 let usage: ResponsesUsage = serde_json::from_value(json!({
4265 "input_tokens": 100,
4266 "input_tokens_details": {
4267 "cached_tokens": 25
4268 },
4269 "output_tokens": 50,
4270 "total_tokens": 150
4271 }))
4272 .expect("usage should deserialize when output token details are omitted");
4273
4274 assert!(usage.output_tokens_details.is_none());
4275
4276 let token_usage = crate::completion::Usage::from(&usage);
4277
4278 assert_eq!(token_usage.input_tokens, 100);
4279 assert_eq!(token_usage.cached_input_tokens, 25);
4280 assert_eq!(token_usage.output_tokens, 50);
4281 assert_eq!(token_usage.reasoning_tokens, 0);
4282 assert_eq!(token_usage.total_tokens, 150);
4283 }
4284
4285 #[test]
4286 fn completion_response_accepts_top_level_reasoning_string() {
4287 let response: CompletionResponse = serde_json::from_value(json!({
4288 "id": "resp_123",
4289 "object": "response",
4290 "created_at": 0,
4291 "status": "completed",
4292 "model": "Qwen/Qwen3-4B",
4293 "reasoning": "thinking through the answer",
4294 "usage": {
4295 "input_tokens": 1,
4296 "output_tokens": 2,
4297 "total_tokens": 3
4298 },
4299 "output": [{
4300 "type": "message",
4301 "id": "msg_123",
4302 "status": "completed",
4303 "role": "assistant",
4304 "content": [{
4305 "type": "output_text",
4306 "annotations": [],
4307 "text": "done"
4308 }]
4309 }],
4310 "tools": []
4311 }))
4312 .expect("mistral.rs-style reasoning string should deserialize");
4313
4314 assert_eq!(
4315 response.provider_reasoning.as_deref(),
4316 Some("thinking through the answer")
4317 );
4318 assert_eq!(response.reasoning_metadata, None);
4319 assert_eq!(response.reasoning_context, None);
4320 assert_eq!(
4321 serde_json::to_value(&response).expect("response should serialize")["reasoning"],
4322 json!("thinking through the answer")
4323 );
4324
4325 let completion: completion::CompletionResponse = response
4326 .normalize("openai")
4327 .expect("response should convert");
4328 let items = completion.choice.iter().collect::<Vec<_>>();
4329 assert!(matches!(
4330 items[0],
4331 completion::AssistantContent::Reasoning(_)
4332 ));
4333 assert!(matches!(items[1], completion::AssistantContent::Text(_)));
4334 }
4335
4336 #[test]
4337 fn completion_response_accepts_null_metadata() {
4338 let response: CompletionResponse = serde_json::from_value(json!({
4339 "id": "resp_123",
4340 "object": "response",
4341 "created_at": 0,
4342 "status": "completed",
4343 "model": "openai-compatible-model",
4344 "metadata": null,
4345 "output": [{
4346 "type": "message",
4347 "id": "msg_123",
4348 "status": "completed",
4349 "role": "assistant",
4350 "content": [{
4351 "type": "output_text",
4352 "annotations": [],
4353 "text": "done"
4354 }]
4355 }],
4356 "tools": []
4357 }))
4358 .expect("response with null metadata should deserialize");
4359
4360 assert!(response.additional_parameters.metadata.is_empty());
4361 }
4362
4363 #[test]
4364 fn completion_response_accepts_reasoning_only_response() {
4365 let response: CompletionResponse = serde_json::from_value(json!({
4366 "id": "resp_123",
4367 "object": "response",
4368 "created_at": 0,
4369 "status": "completed",
4370 "model": "Qwen/Qwen3-4B",
4371 "reasoning": "thinking only",
4372 "usage": {
4373 "input_tokens": 1,
4374 "output_tokens": 2,
4375 "total_tokens": 3
4376 },
4377 "output": [],
4378 "tools": []
4379 }))
4380 .expect("reasoning-only response should deserialize");
4381
4382 let completion: completion::CompletionResponse = response
4383 .normalize("openai")
4384 .expect("reasoning-only response should convert");
4385 let items = completion.choice.iter().collect::<Vec<_>>();
4386
4387 assert_eq!(items.len(), 1);
4388 assert!(matches!(
4389 items[0],
4390 completion::AssistantContent::Reasoning(_)
4391 ));
4392 }
4393
4394 #[test]
4395 fn completion_response_rejects_empty_response_without_reasoning() {
4396 let response: CompletionResponse = serde_json::from_value(json!({
4397 "id": "resp_123",
4398 "object": "response",
4399 "created_at": 0,
4400 "status": "completed",
4401 "model": "Qwen/Qwen3-4B",
4402 "output": [],
4403 "tools": []
4404 }))
4405 .expect("empty response shape should deserialize");
4406
4407 let err = response
4408 .normalize("openai")
4409 .expect_err("empty response without reasoning should be rejected");
4410
4411 assert!(
4412 err.to_string()
4413 .contains(crate::message::EMPTY_RESPONSE_ERROR)
4414 );
4415 }
4416
4417 #[test]
4418 fn truncated_incomplete_response_surfaces_length_not_an_error() {
4419 let response: CompletionResponse = serde_json::from_value(json!({
4425 "id": "resp_123",
4426 "object": "response",
4427 "created_at": 0,
4428 "status": "incomplete",
4429 "incomplete_details": { "reason": "max_output_tokens" },
4430 "model": "gpt-test",
4431 "output": [],
4432 "tools": []
4433 }))
4434 .expect("incomplete response shape should deserialize");
4435
4436 let completion = response
4437 .normalize("openai")
4438 .expect("truncated incomplete response must not be an error");
4439
4440 assert!(completion.choice.is_empty());
4441 assert_eq!(
4442 completion.finish_reason(),
4443 Some(completion::FinishReason::Length)
4444 );
4445 }
4446
4447 fn incomplete_because(reason: &str) -> IncompleteDetailsReason {
4448 IncompleteDetailsReason {
4449 reason: reason.to_string(),
4450 }
4451 }
4452
4453 #[test]
4454 fn finish_reason_maps_every_documented_terminal_state() {
4455 assert_eq!(
4456 map_finish_reason(&ResponseStatus::Completed, None),
4457 Some(completion::FinishReason::Stop)
4458 );
4459 assert_eq!(
4460 map_finish_reason(
4461 &ResponseStatus::Incomplete,
4462 Some(&incomplete_because("max_output_tokens"))
4463 ),
4464 Some(completion::FinishReason::Length)
4465 );
4466 assert_eq!(
4467 map_finish_reason(
4468 &ResponseStatus::Incomplete,
4469 Some(&incomplete_because("content_filter"))
4470 ),
4471 Some(completion::FinishReason::ContentFilter)
4472 );
4473 assert_eq!(
4475 map_finish_reason(
4476 &ResponseStatus::Completed,
4477 Some(&incomplete_because("noise"))
4478 ),
4479 Some(completion::FinishReason::Stop)
4480 );
4481 assert_eq!(map_finish_reason(&ResponseStatus::InProgress, None), None);
4483 assert_eq!(map_finish_reason(&ResponseStatus::Queued, None), None);
4484 }
4485
4486 #[test]
4487 fn finish_reason_preserves_unknown_values_verbatim() {
4488 assert_eq!(
4491 map_finish_reason(
4492 &ResponseStatus::Incomplete,
4493 Some(&incomplete_because("MAX_TOOL_CALLS"))
4494 ),
4495 Some(completion::FinishReason::Other(
4496 "MAX_TOOL_CALLS".to_string()
4497 ))
4498 );
4499 assert_eq!(
4502 map_finish_reason(&ResponseStatus::Failed, None),
4503 Some(completion::FinishReason::Other("failed".to_string()))
4504 );
4505 assert_eq!(
4506 map_finish_reason(&ResponseStatus::Cancelled, None),
4507 Some(completion::FinishReason::Other("cancelled".to_string()))
4508 );
4509 let status: ResponseStatus = serde_json::from_str(r#""throttled""#)
4510 .expect("an unknown provider status should deserialize");
4511 assert_eq!(status, ResponseStatus::Other("throttled".to_string()));
4512 assert_eq!(
4513 map_finish_reason(&status, None),
4514 Some(completion::FinishReason::Other("throttled".to_string()))
4515 );
4516 assert_eq!(
4517 serde_json::to_string(&status).expect("unknown status should serialize"),
4518 r#""throttled""#
4519 );
4520 assert_eq!(
4521 map_finish_reason(&ResponseStatus::Incomplete, None),
4522 Some(completion::FinishReason::Other("incomplete".to_string()))
4523 );
4524 assert_eq!(
4525 map_finish_reason(&ResponseStatus::Incomplete, Some(&incomplete_because(""))),
4526 Some(completion::FinishReason::Other("incomplete".to_string()))
4527 );
4528 }
4529
4530 #[test]
4531 fn completion_response_carries_the_message_id_not_the_response_id() {
4532 let response: CompletionResponse = serde_json::from_value(json!({
4533 "id": "resp_123",
4534 "object": "response",
4535 "created_at": 0,
4536 "status": "completed",
4537 "model": "gpt-5.4",
4538 "output": [{
4539 "type": "message",
4540 "id": "msg_456",
4541 "status": "completed",
4542 "role": "assistant",
4543 "content": [{
4544 "type": "output_text",
4545 "annotations": [],
4546 "text": "done"
4547 }]
4548 }],
4549 "tools": []
4550 }))
4551 .expect("response should deserialize");
4552
4553 let completion: completion::CompletionResponse = response
4554 .normalize("openai")
4555 .expect("response should convert");
4556
4557 assert_eq!(completion.message_id.as_deref(), Some("msg_456"));
4560 assert_eq!(completion.provider, "openai");
4561 assert_eq!(completion.model.as_deref(), Some("gpt-5.4"));
4562 assert_eq!(
4563 completion.finish_reason(),
4564 Some(completion::FinishReason::Stop)
4565 );
4566 }
4567
4568 #[test]
4569 fn completion_response_provider_name_is_an_input() {
4570 let response: CompletionResponse = serde_json::from_value(json!({
4571 "id": "resp_123",
4572 "object": "response",
4573 "created_at": 0,
4574 "status": "completed",
4575 "model": "gpt-5.3-codex",
4576 "output": [{
4577 "type": "message",
4578 "id": "msg_456",
4579 "status": "completed",
4580 "role": "assistant",
4581 "content": [{ "type": "output_text", "annotations": [], "text": "done" }]
4582 }],
4583 "tools": []
4584 }))
4585 .expect("response should deserialize");
4586
4587 let completion: completion::CompletionResponse = response
4588 .normalize("chatgpt")
4589 .expect("response should convert");
4590
4591 assert_eq!(completion.provider, "chatgpt");
4592 }
4593
4594 #[test]
4595 fn completion_response_completed_with_tool_call_reports_tool_calls() {
4596 let response: CompletionResponse = serde_json::from_value(json!({
4597 "id": "resp_123",
4598 "object": "response",
4599 "created_at": 0,
4600 "status": "completed",
4601 "model": "gpt-5.4",
4602 "output": [{
4603 "type": "function_call",
4604 "id": "fc_1",
4605 "call_id": "call_1",
4606 "name": "get_weather",
4607 "arguments": "{\"city\":\"London\"}",
4608 "status": "completed"
4609 }],
4610 "tools": []
4611 }))
4612 .expect("response should deserialize");
4613
4614 let completion: completion::CompletionResponse = response
4615 .normalize("openai")
4616 .expect("response should convert");
4617
4618 assert_eq!(
4621 completion.finish_reason(),
4622 Some(completion::FinishReason::ToolCalls)
4623 );
4624 }
4625
4626 #[test]
4627 fn completion_response_incomplete_reports_the_truncation_reason() {
4628 let response: CompletionResponse = serde_json::from_value(json!({
4629 "id": "resp_123",
4630 "object": "response",
4631 "created_at": 0,
4632 "status": "incomplete",
4633 "incomplete_details": { "reason": "max_output_tokens" },
4634 "model": "gpt-5.4",
4635 "output": [{
4636 "type": "message",
4637 "id": "msg_456",
4638 "status": "incomplete",
4639 "role": "assistant",
4640 "content": [{ "type": "output_text", "annotations": [], "text": "half an ans" }]
4641 }],
4642 "tools": []
4643 }))
4644 .expect("response should deserialize");
4645
4646 let completion: completion::CompletionResponse = response
4647 .normalize("openai")
4648 .expect("response should convert");
4649
4650 assert_eq!(
4651 completion.finish_reason(),
4652 Some(completion::FinishReason::Length)
4653 );
4654 }
4655
4656 #[test]
4657 fn completion_response_preserves_context_without_treating_config_as_text() {
4658 let response: CompletionResponse = serde_json::from_value(json!({
4659 "id": "resp_123",
4660 "object": "response",
4661 "created_at": 0,
4662 "status": "completed",
4663 "model": "Qwen/Qwen3-4B",
4664 "reasoning": {
4665 "context": "all_turns",
4666 "effort": "high",
4667 "mode": "standard",
4668 "summary": null
4669 },
4670 "output": [{
4671 "type": "message",
4672 "id": "msg_123",
4673 "status": "completed",
4674 "role": "assistant",
4675 "content": [{
4676 "type": "output_text",
4677 "annotations": [],
4678 "text": "done"
4679 }]
4680 }],
4681 "tools": []
4682 }))
4683 .expect("object-shaped reasoning should be tolerated");
4684
4685 assert!(response.provider_reasoning.is_none());
4686 assert_eq!(response.reasoning_context.as_deref(), Some("all_turns"));
4687 assert_eq!(
4688 response.reasoning_metadata.as_ref(),
4689 json!({
4690 "context": "all_turns",
4691 "effort": "high",
4692 "mode": "standard",
4693 "summary": null
4694 })
4695 .as_object()
4696 );
4697 assert_eq!(
4698 serde_json::to_value(&response).expect("response should serialize")["reasoning"],
4699 json!({
4700 "context": "all_turns",
4701 "effort": "high",
4702 "mode": "standard",
4703 "summary": null
4704 })
4705 );
4706
4707 let completion: completion::CompletionResponse = response
4708 .normalize("openai")
4709 .expect("response should convert");
4710 let items = completion.choice.iter().collect::<Vec<_>>();
4711 assert_eq!(items.len(), 1);
4712 assert!(matches!(items[0], completion::AssistantContent::Text(_)));
4713 }
4714
4715 #[test]
4716 fn completion_response_preserves_unknown_reasoning_metadata_and_nulls() {
4717 let metadata = json!({
4718 "context": "future_context",
4719 "effort": "ultra",
4720 "summary": null,
4721 "future_control": { "depth": 3 }
4722 });
4723 let response: CompletionResponse = serde_json::from_value(json!({
4724 "id": "resp_123",
4725 "object": "response",
4726 "created_at": 0,
4727 "status": "completed",
4728 "model": "gpt-future",
4729 "reasoning": metadata.clone(),
4730 "output": [],
4731 "tools": []
4732 }))
4733 .expect("unknown reasoning metadata should deserialize");
4734
4735 assert_eq!(
4736 response.reasoning_context.as_deref(),
4737 Some("future_context")
4738 );
4739 assert_eq!(response.reasoning_metadata.as_ref(), metadata.as_object());
4740 assert_eq!(
4741 serde_json::to_value(&response).expect("response should serialize")["reasoning"],
4742 metadata
4743 );
4744 }
4745
4746 #[test]
4747 fn completion_response_ignores_unsupported_reasoning_shapes() {
4748 for reasoning in [Value::Null, json!(["unexpected"]), json!(42), json!(true)] {
4749 let response: CompletionResponse = serde_json::from_value(json!({
4750 "id": "resp_123",
4751 "object": "response",
4752 "created_at": 0,
4753 "status": "completed",
4754 "model": "openai-compatible-model",
4755 "reasoning": reasoning,
4756 "output": [],
4757 "tools": []
4758 }))
4759 .expect("unsupported reasoning shapes should remain non-fatal");
4760
4761 assert_eq!(response.provider_reasoning, None);
4762 assert_eq!(response.reasoning_metadata, None);
4763 assert_eq!(response.reasoning_context, None);
4764 let serialized = serde_json::to_value(&response).expect("response should serialize");
4765 assert!(
4766 !serialized
4767 .as_object()
4768 .expect("response should serialize as an object")
4769 .contains_key("reasoning")
4770 );
4771 }
4772 }
4773
4774 #[test]
4775 fn completion_response_reasoning_serialization_precedence_is_stable() {
4776 let mut response: CompletionResponse = serde_json::from_value(json!({
4777 "id": "resp_123",
4778 "object": "response",
4779 "created_at": 0,
4780 "status": "completed",
4781 "model": "gpt-5.6",
4782 "reasoning": { "context": "all_turns", "effort": "max" },
4783 "output": [],
4784 "tools": []
4785 }))
4786 .expect("reasoning metadata should deserialize");
4787
4788 response.reasoning_context = Some("current_turn".to_owned());
4789 response.additional_parameters.reasoning =
4790 Some(Reasoning::new().with_effort(ReasoningEffort::Low));
4791 let serialized = serde_json::to_string(&response).expect("response should serialize");
4792 assert_eq!(serialized.matches("\"reasoning\":").count(), 1);
4793 assert_eq!(
4794 serde_json::to_value(&response).expect("response should serialize")["reasoning"],
4795 json!({ "context": "all_turns", "effort": "max" })
4796 );
4797
4798 let metadata = response.reasoning_metadata.take();
4799 assert_eq!(
4800 serde_json::to_value(&response).expect("response should serialize")["reasoning"],
4801 json!({ "context": "current_turn" })
4802 );
4803
4804 response.reasoning_metadata = metadata;
4805 response.provider_reasoning = Some("compatible-provider text".to_owned());
4806 assert_eq!(
4807 serde_json::to_value(&response).expect("response should serialize")["reasoning"],
4808 json!("compatible-provider text")
4809 );
4810 }
4811
4812 fn request_with_reasoning_params(reasoning: Value) -> CompletionRequest {
4813 let mut request = request_with_preamble("You are concise.");
4814 request.additional_params = Some(json!({ "reasoning": reasoning }));
4815
4816 CompletionRequest::try_from(("gpt-5.6".to_string(), request))
4817 .expect("request with reasoning params should convert")
4818 }
4819
4820 #[test]
4821 fn reasoning_effort_max_survives_request_conversion() {
4822 let request = request_with_reasoning_params(json!({ "effort": "max" }));
4823 let serialized = serde_json::to_value(&request).expect("request should serialize");
4824
4825 assert_eq!(serialized["reasoning"], json!({ "effort": "max" }));
4826 }
4827
4828 #[test]
4829 fn reasoning_mode_pro_composes_with_independent_effort() {
4830 let request = request_with_reasoning_params(json!({ "effort": "high", "mode": "pro" }));
4831 let serialized = serde_json::to_value(&request).expect("request should serialize");
4832
4833 assert_eq!(
4834 serialized["reasoning"],
4835 json!({ "effort": "high", "mode": "pro" })
4836 );
4837 }
4838
4839 #[test]
4840 fn reasoning_context_values_survive_request_conversion() {
4841 for (context, wire_value) in [
4842 (ReasoningContext::Auto, "auto"),
4843 (ReasoningContext::AllTurns, "all_turns"),
4844 (ReasoningContext::CurrentTurn, "current_turn"),
4845 ] {
4846 let typed = serde_json::to_value(Reasoning::new().with_context(context))
4847 .expect("typed reasoning should serialize");
4848 assert_eq!(typed, json!({ "context": wire_value }));
4849
4850 let request = request_with_reasoning_params(json!({ "context": wire_value }));
4851 let serialized = serde_json::to_value(&request).expect("request should serialize");
4852 assert_eq!(serialized["reasoning"], json!({ "context": wire_value }));
4853 }
4854 }
4855
4856 #[test]
4857 fn reasoning_omits_unset_optional_fields() {
4858 let reasoning = serde_json::to_value(Reasoning::new().with_mode(ReasoningMode::Pro))
4859 .expect("reasoning should serialize");
4860
4861 assert_eq!(reasoning, json!({ "mode": "pro" }));
4862
4863 let reasoning = serde_json::to_value(
4864 Reasoning::new()
4865 .with_effort(ReasoningEffort::Max)
4866 .with_mode(ReasoningMode::Pro)
4867 .with_context(ReasoningContext::CurrentTurn)
4868 .with_summary_level(ReasoningSummaryLevel::Detailed),
4869 )
4870 .expect("reasoning should serialize");
4871
4872 assert_eq!(
4873 reasoning,
4874 json!({
4875 "effort": "max",
4876 "mode": "pro",
4877 "context": "current_turn",
4878 "summary": "detailed"
4879 })
4880 );
4881 }
4882
4883 #[test]
4884 fn completion_response_does_not_duplicate_structured_reasoning() {
4885 let response: CompletionResponse = serde_json::from_value(json!({
4886 "id": "resp_123",
4887 "object": "response",
4888 "created_at": 0,
4889 "status": "completed",
4890 "model": "gpt-5.4",
4891 "reasoning": "provider top-level text",
4892 "output": [{
4893 "type": "reasoning",
4894 "id": "rs_123",
4895 "summary": [{
4896 "type": "summary_text",
4897 "text": "structured summary"
4898 }]
4899 }, {
4900 "type": "message",
4901 "id": "msg_123",
4902 "status": "completed",
4903 "role": "assistant",
4904 "content": [{
4905 "type": "output_text",
4906 "annotations": [],
4907 "text": "done"
4908 }]
4909 }],
4910 "tools": []
4911 }))
4912 .expect("response should deserialize");
4913
4914 let completion: completion::CompletionResponse = response
4915 .normalize("openai")
4916 .expect("response should convert");
4917 let reasoning_count = completion
4918 .choice
4919 .iter()
4920 .filter(|item| matches!(item, completion::AssistantContent::Reasoning(_)))
4921 .count();
4922
4923 assert_eq!(reasoning_count, 1);
4924 }
4925
4926 #[test]
4927 fn idless_reasoning_only_is_skipped_without_empty_input_item() {
4928 let assistant = completion::Message::Assistant {
4929 id: None,
4930 content: vec![message::AssistantContent::Reasoning(
4931 message::Reasoning::new("provider reasoning"),
4932 )],
4933 };
4934
4935 let converted = Vec::<InputItem>::try_from(assistant)
4936 .expect("idless reasoning should degrade gracefully");
4937
4938 assert!(converted.is_empty());
4939 }
4940
4941 #[test]
4942 fn completion_history_idless_reasoning_plus_text_preserves_text_input_item() {
4943 let assistant = completion::Message::Assistant {
4944 id: Some("msg_123".to_string()),
4945 content: vec![
4946 message::AssistantContent::Reasoning(message::Reasoning::new("provider reasoning")),
4947 message::AssistantContent::Text(Text::new("final answer")),
4948 ],
4949 };
4950
4951 let converted =
4952 Vec::<InputItem>::try_from(assistant).expect("assistant history should convert");
4953
4954 assert_eq!(converted.len(), 1);
4955 assert!(matches!(converted[0].role, Some(Role::Assistant)));
4956 let InputContent::Message(Message::Assistant { content, .. }) = &converted[0].input else {
4957 panic!("expected assistant message input item");
4958 };
4959 assert!(matches!(
4960 content.first(),
4961 Some(AssistantContentType::Text(AssistantContent::OutputText(OutputText { text, .. }))) if text == "final answer"
4962 ));
4963 }
4964
4965 #[test]
4966 fn assistant_text_without_idless_reasoning_replays_as_output_text() {
4967 let assistant = completion::Message::Assistant {
4968 id: Some("msg_123".to_string()),
4969 content: vec![message::AssistantContent::Text(Text::new("final answer"))],
4970 };
4971
4972 let converted =
4973 Vec::<InputItem>::try_from(assistant).expect("assistant history should convert");
4974
4975 assert_eq!(converted.len(), 1);
4976 let InputContent::Message(Message::Assistant { content, .. }) = &converted[0].input else {
4977 panic!("expected assistant message input item");
4978 };
4979 assert!(matches!(
4980 content.first(),
4981 Some(AssistantContentType::Text(AssistantContent::OutputText(OutputText { text, .. }))) if text == "final answer"
4982 ));
4983 }
4984
4985 #[test]
4986 fn idless_completion_assistant_text_replays_as_easy_input_message() {
4987 let assistant = completion::Message::Assistant {
4988 id: None,
4989 content: vec![message::AssistantContent::Text(Text::new("final answer"))],
4990 };
4991
4992 let converted =
4993 Vec::<InputItem>::try_from(assistant).expect("assistant history should convert");
4994
4995 assert_eq!(converted.len(), 1);
4996 assert!(matches!(converted[0].role, Some(Role::Assistant)));
4997 let InputContent::Message(Message::AssistantInput { content, .. }) = &converted[0].input
4998 else {
4999 panic!("expected assistant input message item");
5000 };
5001 assert_eq!(content, "final answer");
5002
5003 let serialized =
5004 serde_json::to_value(&converted[0]).expect("input item should serialize to JSON");
5005 assert_eq!(serialized["type"], json!("message"));
5006 assert_eq!(serialized["role"], json!("assistant"));
5007 assert_eq!(serialized["content"], json!("final answer"));
5008 assert!(serialized.get("id").is_none());
5009 assert!(serialized.get("status").is_none());
5010 }
5011
5012 #[test]
5013 fn structured_reasoning_with_id_still_converts_to_input_item() {
5014 let assistant = completion::Message::Assistant {
5015 id: Some("msg_123".to_string()),
5016 content: vec![message::AssistantContent::Reasoning(message::Reasoning {
5017 id: Some("rs_123".to_string()),
5018 content: vec![message::ReasoningContent::Summary(
5019 "structured summary".to_string(),
5020 )],
5021 })],
5022 };
5023
5024 let converted =
5025 Vec::<InputItem>::try_from(assistant).expect("structured reasoning should convert");
5026
5027 assert_eq!(converted.len(), 1);
5028 assert!(converted[0].role.is_none());
5029 assert!(matches!(
5030 &converted[0].input,
5031 InputContent::Reasoning(OpenAIReasoning { id, .. }) if id == "rs_123"
5032 ));
5033 }
5034
5035 #[test]
5036 fn assistant_reasoning_text_tool_call_convert_in_responses_replay_order() {
5037 let assistant = completion::Message::Assistant {
5038 id: Some("msg_123".to_string()),
5039 content: vec![
5040 message::AssistantContent::Reasoning(message::Reasoning {
5041 id: Some("rs_123".to_string()),
5042 content: vec![message::ReasoningContent::Summary(
5043 "structured summary".to_string(),
5044 )],
5045 }),
5046 message::AssistantContent::Text(Text::new("final answer")),
5047 message::AssistantContent::tool_call_with_call_id(
5048 "fc_123",
5049 "call_123".to_string(),
5050 "lookup",
5051 json!({"query": "rig"}),
5052 ),
5053 ],
5054 };
5055
5056 let converted =
5057 Vec::<InputItem>::try_from(assistant).expect("assistant history should convert");
5058
5059 assert_eq!(converted.len(), 3);
5060 assert!(converted[0].role.is_none());
5061 assert!(matches!(
5062 &converted[0].input,
5063 InputContent::Reasoning(OpenAIReasoning { id, .. }) if id == "rs_123"
5064 ));
5065
5066 assert!(matches!(converted[1].role, Some(Role::Assistant)));
5067 let InputContent::Message(Message::Assistant { content, id, .. }) = &converted[1].input
5068 else {
5069 panic!("expected assistant output message");
5070 };
5071 assert_eq!(id, "msg_123");
5072 assert!(matches!(
5073 content.first(),
5074 Some(AssistantContentType::Text(AssistantContent::OutputText(OutputText { text, .. })))
5075 if text == "final answer"
5076 ));
5077
5078 assert!(converted[2].role.is_none());
5079 let InputContent::FunctionCall(OutputFunctionCall {
5080 id, call_id, name, ..
5081 }) = &converted[2].input
5082 else {
5083 panic!("expected function call input item");
5084 };
5085 assert_eq!(id, "fc_123");
5086 assert_eq!(call_id, "call_123");
5087 assert_eq!(name, "lookup");
5088 }
5089
5090 #[test]
5091 fn mocked_second_turn_request_omits_unreplayable_reasoning() {
5092 let request = crate::completion::CompletionRequest {
5093 model: None,
5094 preamble: Some("You are concise.".to_string()),
5095 chat_history: vec![
5096 completion::Message::User {
5097 content: vec![message::UserContent::Text(Text::new(
5098 "Think briefly, then answer.",
5099 ))],
5100 },
5101 completion::Message::Assistant {
5102 id: Some("msg_123".to_string()),
5103 content: vec![
5104 message::AssistantContent::Reasoning(message::Reasoning::new(
5105 "provider reasoning",
5106 )),
5107 message::AssistantContent::Text(Text::new("final answer")),
5108 ],
5109 },
5110 completion::Message::Assistant {
5111 id: None,
5112 content: vec![
5113 message::AssistantContent::Reasoning(message::Reasoning::new(
5114 "provider reasoning only",
5115 )),
5116 message::AssistantContent::Text(Text::new("")),
5117 ],
5118 },
5119 completion::Message::User {
5120 content: vec![message::UserContent::Text(Text::new(
5121 "/no_think Reply with exactly: OK",
5122 ))],
5123 },
5124 ],
5125 documents: Vec::new(),
5126 tools: Vec::new(),
5127 temperature: None,
5128 max_tokens: Some(64),
5129 tool_choice: None,
5130 additional_params: None,
5131 output_schema: None,
5132 record_telemetry_content: false,
5133 };
5134
5135 let request = CompletionRequest::try_from(("Qwen/Qwen3-4B".to_string(), request))
5136 .expect("request should convert");
5137 let value = serde_json::to_value(&request).expect("request should serialize");
5138 let input = value["input"]
5139 .as_array()
5140 .expect("mocked multi-turn request should serialize input as an array");
5141
5142 assert!(!input.iter().any(|item| {
5143 item.get("type") == Some(&json!("reasoning")) && item.get("id").is_none()
5144 }));
5145 assert!(!input.iter().any(|item| {
5146 item.get("role") == Some(&json!("assistant"))
5147 && item
5148 .get("content")
5149 .and_then(Value::as_array)
5150 .is_some_and(Vec::is_empty)
5151 }));
5152
5153 let assistant_items = input
5154 .iter()
5155 .filter(|item| item.get("role") == Some(&json!("assistant")))
5156 .collect::<Vec<_>>();
5157
5158 assert_eq!(assistant_items.len(), 1);
5159 assert_eq!(assistant_items[0]["content"][0]["type"], "output_text");
5160 assert_eq!(assistant_items[0]["content"][0]["text"], "final answer");
5161 }
5162
5163 #[test]
5164 fn responses_usage_add_preserves_rhs_details_when_lhs_details_are_absent() {
5165 let lhs = ResponsesUsage {
5166 input_tokens: 10,
5167 input_tokens_details: None,
5168 output_tokens: 20,
5169 output_tokens_details: None,
5170 total_tokens: 30,
5171 };
5172 let rhs = ResponsesUsage {
5173 input_tokens: 3,
5174 input_tokens_details: Some(InputTokensDetails { cached_tokens: 2 }),
5175 output_tokens: 5,
5176 output_tokens_details: Some(OutputTokensDetails {
5177 reasoning_tokens: 4,
5178 }),
5179 total_tokens: 8,
5180 };
5181
5182 let usage = lhs + rhs;
5183 let token_usage = crate::completion::Usage::from(&usage);
5184
5185 assert_eq!(token_usage.input_tokens, 13);
5186 assert_eq!(token_usage.cached_input_tokens, 2);
5187 assert_eq!(token_usage.output_tokens, 25);
5188 assert_eq!(token_usage.reasoning_tokens, 4);
5189 assert_eq!(token_usage.total_tokens, 38);
5190 }
5191
5192 #[test]
5193 fn file_id_document_serializes_as_input_item_content() {
5194 let message = completion::Message::User {
5195 content: vec![message::UserContent::Document(message::Document {
5196 data: DocumentSourceKind::FileId("file_abc".to_string()),
5197 media_type: None,
5198 additional_params: None,
5199 })],
5200 };
5201
5202 let converted: Vec<InputItem> = message.try_into().expect("conversion should succeed");
5203 let json = serde_json::to_value(&converted[0]).expect("serialize input item");
5204
5205 assert_eq!(json["type"], "message");
5206 assert_eq!(json["role"], "user");
5207 assert_eq!(json["content"][0]["type"], "input_file");
5208 assert_eq!(json["content"][0]["file_id"], "file_abc");
5209 assert!(json["content"][0].get("file_data").is_none());
5210 assert!(json["content"][0].get("file_url").is_none());
5211 }
5212
5213 #[tokio::test]
5214 async fn responses_completion_http_non_success_preserves_status_and_body() {
5215 use crate::client::CompletionClient;
5216 use crate::completion::CompletionModel;
5217 use crate::providers::openai::Client;
5218 use crate::test_utils::RecordingHttpClient;
5219
5220 let body = r#"{"error":{"message":"bad image","type":"invalid_request_error","code":"invalid_value"}}"#;
5221 let http_client =
5222 RecordingHttpClient::with_error_response(http::StatusCode::BAD_REQUEST, body);
5223 let client = Client::builder()
5224 .api_key("test-key")
5225 .http_client(http_client)
5226 .build()
5227 .expect("build client");
5228 let model = client.completion_model("gpt-4o-mini");
5229 let request = model.completion_request("hello").build();
5230
5231 let error = model
5232 .completion(request)
5233 .await
5234 .expect_err("completion should fail with non-success status");
5235
5236 assert!(matches!(error, CompletionError::ProviderResponse(_)));
5240 assert_eq!(error.provider_request_id(), None);
5241 assert_eq!(
5242 error.provider_response_status(),
5243 Some(http::StatusCode::BAD_REQUEST)
5244 );
5245 assert_eq!(error.provider_response_body(), Some(body));
5246 let json = error
5247 .provider_response_json()
5248 .expect("raw body should be valid JSON")
5249 .expect("parsed JSON should be present");
5250 assert_eq!(json["error"]["code"], "invalid_value");
5251 }
5252
5253 #[test]
5254 fn output_unknown_preserves_hosted_tool_payload() {
5255 let item = json!({
5256 "type": "web_search_call",
5257 "id": "ws_001",
5258 "status": "completed",
5259 "action": { "type": "search", "queries": ["rig framework"] },
5260 });
5261
5262 let output: Output =
5263 serde_json::from_value(item.clone()).expect("unknown output should deserialize");
5264
5265 let Output::Unknown(value) = output else {
5266 panic!("expected Output::Unknown for an unmodeled item type");
5267 };
5268 assert_eq!(value, item);
5269 }
5270
5271 #[test]
5272 fn output_unknown_round_trips_value_equal() {
5273 let item = json!({
5274 "type": "file_search_call",
5275 "id": "fs_007",
5276 "status": "in_progress",
5277 "queries": ["lifecycle"],
5278 });
5279
5280 let output: Output =
5281 serde_json::from_value(item.clone()).expect("unknown output should deserialize");
5282 let serialized = serde_json::to_value(&output).expect("unknown output should serialize");
5283
5284 assert_eq!(serialized, item);
5285 }
5286
5287 #[test]
5288 fn output_known_variant_with_bad_body_errors() {
5289 let malformed = json!({
5292 "type": "function_call",
5293 "id": "call_1",
5294 });
5296
5297 let result: Result<Output, _> = serde_json::from_value(malformed);
5298 assert!(result.is_err());
5299 }
5300
5301 #[test]
5302 fn completion_response_with_unknown_output_keeps_usage() {
5303 let response = json!({
5306 "id": "resp_123",
5307 "object": "response",
5308 "created_at": 0,
5309 "status": "completed",
5310 "model": "gpt-5.4",
5311 "output": [
5312 {
5313 "type": "web_search_call",
5314 "id": "ws_001",
5315 "status": "completed",
5316 },
5317 {
5318 "type": "message",
5319 "id": "msg_1",
5320 "role": "assistant",
5321 "status": "completed",
5322 "content": [ { "type": "output_text", "text": "hi", "annotations": [] } ],
5323 },
5324 ],
5325 "usage": {
5326 "input_tokens": 100,
5327 "input_tokens_details": { "cached_tokens": 25 },
5328 "output_tokens": 50,
5329 "output_tokens_details": { "reasoning_tokens": 15 },
5330 "total_tokens": 150,
5331 },
5332 });
5333
5334 let response: CompletionResponse =
5335 serde_json::from_value(response).expect("response should deserialize");
5336
5337 assert!(matches!(response.output.first(), Some(Output::Unknown(_))));
5338 let usage = response.usage.expect("usage should be present");
5339 assert_eq!(usage.total_tokens, 150);
5340 }
5341
5342 #[test]
5343 fn output_known_variant_round_trips_value_equal() {
5344 let item = json!({
5350 "type": "function_call",
5351 "id": "fc_1",
5352 "arguments": "{}",
5353 "call_id": "c1",
5354 "name": "search",
5355 "status": "completed",
5356 });
5357
5358 let output: Output =
5359 serde_json::from_value(item.clone()).expect("known output should deserialize");
5360 assert!(matches!(output, Output::FunctionCall(_)));
5361
5362 let serialized = serde_json::to_value(&output).expect("known output should serialize");
5363 assert_eq!(serialized, item);
5364 }
5365
5366 #[test]
5367 fn output_reasoning_round_trips_value_equal() {
5368 let original = Output::Reasoning {
5374 id: "reasoning_1".to_string(),
5375 summary: vec![ReasoningSummary::SummaryText {
5376 text: "weighing options".to_string(),
5377 }],
5378 content: vec!["private reasoning".to_string()],
5379 encrypted_content: Some("ENCRYPTED".to_string()),
5380 status: Some(ToolStatus::Completed),
5381 };
5382
5383 let value = serde_json::to_value(&original).expect("reasoning should serialize");
5384 let round_tripped: Output =
5385 serde_json::from_value(value).expect("reasoning should deserialize");
5386
5387 assert_eq!(round_tripped, original);
5388 }
5389
5390 #[test]
5391 fn output_reasoning_conversion_omits_empty_encrypted_content() {
5392 let output = Output::Reasoning {
5393 id: "reasoning_1".to_string(),
5394 summary: vec![],
5395 content: vec!["visible reasoning".to_string()],
5396 encrypted_content: Some(String::new()),
5397 status: Some(ToolStatus::Completed),
5398 };
5399
5400 let converted = Vec::<completion::AssistantContent>::from(output);
5401
5402 assert_eq!(converted.len(), 1);
5403 let completion::AssistantContent::Reasoning(reasoning) = &converted[0] else {
5404 panic!("expected reasoning output");
5405 };
5406 assert_eq!(reasoning.id.as_deref(), Some("reasoning_1"));
5407 assert_eq!(reasoning.content.len(), 1);
5408 assert!(matches!(
5409 reasoning.content.first(),
5410 Some(message::ReasoningContent::Text { text, .. })
5411 if text == "visible reasoning"
5412 ));
5413 }
5414
5415 #[test]
5416 fn output_reasoning_conversion_preserves_non_empty_encrypted_content() {
5417 let output = Output::Reasoning {
5418 id: "reasoning_1".to_string(),
5419 summary: vec![],
5420 content: vec![],
5421 encrypted_content: Some("ciphertext".to_string()),
5422 status: Some(ToolStatus::Completed),
5423 };
5424
5425 let converted = Vec::<completion::AssistantContent>::from(output);
5426
5427 assert_eq!(converted.len(), 1);
5428 let completion::AssistantContent::Reasoning(reasoning) = &converted[0] else {
5429 panic!("expected reasoning output");
5430 };
5431 assert_eq!(
5432 reasoning.content,
5433 vec![message::ReasoningContent::Encrypted(
5434 "ciphertext".to_string()
5435 )]
5436 );
5437 }
5438
5439 #[test]
5440 fn output_reasoning_none_optionals_serialize_as_explicit_null() {
5441 let value = serde_json::to_value(Output::Reasoning {
5447 id: "reasoning_1".to_string(),
5448 summary: vec![],
5449 content: vec![],
5450 encrypted_content: None,
5451 status: None,
5452 })
5453 .expect("reasoning should serialize");
5454
5455 assert_eq!(value["type"], "reasoning");
5456 assert_eq!(value["encrypted_content"], Value::Null);
5457 assert_eq!(value["status"], Value::Null);
5458 assert!(value.get("encrypted_content").is_some());
5459 assert!(value.get("status").is_some());
5460 }
5461
5462 #[test]
5463 fn output_message_round_trips_value_equal() {
5464 let item = json!({
5468 "type": "message",
5469 "id": "msg_1",
5470 "role": "assistant",
5471 "status": "completed",
5472 "content": [ { "type": "output_text", "text": "hello", "annotations": [] } ],
5473 });
5474
5475 let output: Output =
5476 serde_json::from_value(item.clone()).expect("message item should deserialize");
5477 assert!(matches!(output, Output::Message(_)));
5478
5479 let serialized = serde_json::to_value(&output).expect("message should serialize");
5480 assert_eq!(serialized, item);
5481 }
5482
5483 #[test]
5484 fn each_known_tag_decodes_to_its_modeled_variant() {
5485 let message: Output = serde_json::from_value(json!({
5490 "type": "message", "id": "msg_1", "role": "assistant", "status": "completed",
5491 "content": [ { "type": "output_text", "text": "hi", "annotations": [] } ],
5492 }))
5493 .expect("message item should decode");
5494 assert!(matches!(message, Output::Message(_)));
5495
5496 let function_call: Output = serde_json::from_value(json!({
5497 "type": "function_call", "id": "call_1", "arguments": "{}",
5498 "call_id": "c1", "name": "f", "status": "completed",
5499 }))
5500 .expect("function_call item should decode");
5501 assert!(matches!(function_call, Output::FunctionCall(_)));
5502
5503 let reasoning: Output =
5504 serde_json::from_value(json!({ "type": "reasoning", "id": "r1", "summary": [] }))
5505 .expect("reasoning item should decode");
5506 assert!(matches!(reasoning, Output::Reasoning { .. }));
5507 }
5508
5509 #[test]
5510 fn output_without_usable_type_tag_decodes_to_unknown() {
5511 for item in [
5514 json!({ "id": "x", "note": "no type field" }),
5515 json!({ "type": 7, "id": "x" }),
5516 ] {
5517 let output: Output =
5518 serde_json::from_value(item.clone()).expect("should decode to Unknown");
5519 assert_eq!(output, Output::Unknown(item));
5520 }
5521 }
5522
5523 const PDF_URL: &str = "https://example.com/resume.pdf";
5534
5535 fn url_pdf_message() -> message::Message {
5536 message::Message::User {
5537 content: vec![message::UserContent::document_url(
5538 PDF_URL,
5539 Some(message::DocumentMediaType::PDF),
5540 )],
5541 }
5542 }
5543
5544 fn find_input_files(value: &serde_json::Value, out: &mut Vec<serde_json::Value>) {
5546 match value {
5547 serde_json::Value::Object(map) => {
5548 if map.get("type").and_then(|t| t.as_str()) == Some("input_file") {
5549 out.push(value.clone());
5550 }
5551 map.values().for_each(|v| find_input_files(v, out));
5552 }
5553 serde_json::Value::Array(items) => {
5554 items.iter().for_each(|v| find_input_files(v, out));
5555 }
5556 _ => {}
5557 }
5558 }
5559
5560 fn sole_input_file(value: &serde_json::Value) -> serde_json::Value {
5561 let mut found = Vec::new();
5562 find_input_files(value, &mut found);
5563 assert_eq!(
5564 found.len(),
5565 1,
5566 "expected exactly one input_file item in {value:#}"
5567 );
5568 found.pop().unwrap()
5569 }
5570
5571 fn assert_url_only_input_file(input_file: &serde_json::Value) {
5572 assert_eq!(
5573 input_file.get("file_url").and_then(|v| v.as_str()),
5574 Some(PDF_URL),
5575 "URL PDF should carry file_url: {input_file:#}"
5576 );
5577 assert_eq!(
5578 input_file.get("filename"),
5579 None,
5580 "filename must be absent for URL PDFs (issue #1429): {input_file:#}"
5581 );
5582 assert_eq!(
5583 input_file.get("file_data"),
5584 None,
5585 "file_data must be absent for URL PDFs: {input_file:#}"
5586 );
5587 }
5588
5589 #[test]
5590 fn url_pdf_via_input_item_path_omits_filename() {
5591 let items = Vec::<InputItem>::try_from(url_pdf_message())
5592 .expect("URL PDF should convert to input items");
5593 let json = serde_json::to_value(&items).expect("input items should serialize");
5594 assert_url_only_input_file(&sole_input_file(&json));
5595 }
5596
5597 #[test]
5598 fn url_pdf_in_full_completion_request_omits_filename() {
5599 let core_request = crate::completion::CompletionRequest {
5600 model: None,
5601 preamble: None,
5602 chat_history: vec![url_pdf_message()],
5603 documents: Vec::new(),
5604 tools: Vec::new(),
5605 temperature: None,
5606 max_tokens: None,
5607 tool_choice: None,
5608 additional_params: None,
5609 output_schema: None,
5610 record_telemetry_content: false,
5611 };
5612
5613 let request = CompletionRequest::try_from(("gpt-4o".to_string(), core_request))
5614 .expect("request should convert");
5615 let json = serde_json::to_value(&request).expect("request should serialize");
5616 assert_url_only_input_file(&sole_input_file(&json));
5617 }
5618
5619 #[test]
5620 fn base64_pdf_via_input_item_path_keeps_filename() {
5621 let input = message::Message::User {
5622 content: vec![message::UserContent::Document(message::Document {
5623 data: DocumentSourceKind::base64("dGVzdA=="),
5624 media_type: Some(message::DocumentMediaType::PDF),
5625 additional_params: None,
5626 })],
5627 };
5628
5629 let items =
5630 Vec::<InputItem>::try_from(input).expect("base64 PDF should convert to input items");
5631 let json = serde_json::to_value(&items).expect("input items should serialize");
5632 let input_file = sole_input_file(&json);
5633
5634 assert_eq!(
5635 input_file.get("file_data").and_then(|v| v.as_str()),
5636 Some("data:application/pdf;base64,dGVzdA=="),
5637 "base64 PDF should carry file_data: {input_file:#}"
5638 );
5639 assert_eq!(
5640 input_file.get("filename").and_then(|v| v.as_str()),
5641 Some("document.pdf"),
5642 "base64 PDF should keep the default filename: {input_file:#}"
5643 );
5644 assert_eq!(
5645 input_file.get("file_url"),
5646 None,
5647 "base64 PDF should not carry file_url: {input_file:#}"
5648 );
5649 }
5650
5651 mod raw_capture {
5662 use super::*;
5663 use crate::client::CompletionClient;
5664 use crate::completion::CompletionModel as _;
5665 use crate::providers::openai::Client;
5666 use crate::test_utils::RecordingHttpClient;
5667
5668 const REQUEST_ID: &str = "req_unit_responses_0001";
5669
5670 const BODY: &str = r#"{
5673 "id": "resp_raw_1",
5674 "object": "response",
5675 "created_at": 1700000000,
5676 "status": "completed",
5677 "error": null,
5678 "incomplete_details": null,
5679 "instructions": null,
5680 "max_output_tokens": null,
5681 "model": "gpt-4o-mini-2024-07-18",
5682 "service_tier": "default",
5683 "usage": {
5684 "input_tokens": 4,
5685 "input_tokens_details": {"cached_tokens": 0},
5686 "output_tokens": 3,
5687 "output_tokens_details": {"reasoning_tokens": 0},
5688 "total_tokens": 7
5689 },
5690 "output": [{
5691 "type": "message",
5692 "id": "msg_raw_1",
5693 "role": "assistant",
5694 "status": "completed",
5695 "content": [{"type": "output_text", "text": "hello", "annotations": []}]
5696 }],
5697 "tools": []
5698 }"#;
5699
5700 fn model() -> ResponsesCompletionModel<RecordingHttpClient> {
5701 let mut headers = http::HeaderMap::new();
5702 headers.insert("x-request-id", http::HeaderValue::from_static(REQUEST_ID));
5703 let http_client = RecordingHttpClient::with_error_response_headers(
5704 http::StatusCode::OK,
5705 BODY,
5706 headers,
5707 );
5708 let client = Client::builder()
5709 .api_key("test-key")
5710 .http_client(http_client)
5711 .build()
5712 .expect("build client");
5713 client.completion_model("gpt-4o-mini")
5714 }
5715
5716 #[tokio::test]
5728 async fn completion_captures_raw_that_round_trips_into_the_wire_type() {
5729 let model = model();
5730
5731 let response = model
5732 .completion(model.completion_request("hello").build())
5733 .await
5734 .expect("completion");
5735
5736 let raw = &response.raw;
5737 let typed: CompletionResponse =
5738 serde_json::from_value(raw.clone()).expect("raw must deserialize");
5739 assert_eq!(
5740 serde_json::to_value(&typed).expect("re-serialize"),
5741 *raw,
5742 "the capture must be exactly what the wire type serializes to"
5743 );
5744 assert!(matches!(
5745 typed.additional_parameters.service_tier,
5746 Some(OpenAIServiceTier::Default)
5747 ));
5748 assert_eq!(raw["service_tier"], "default");
5749 assert!(raw.get("provider_request_id").is_none());
5750 assert_eq!(typed.provider_request_id, None);
5751
5752 let renormalized = typed
5753 .normalize(<crate::providers::openai::OpenAIResponsesExt as ResponsesProviderExt>::PROVIDER_NAME)
5754 .expect("re-normalize the capture")
5755 .with_optional_provider_request_id(Some(REQUEST_ID.to_string()));
5756 assert_eq!(response.identity(), renormalized.identity());
5757 assert_eq!(response.finish_reason(), renormalized.finish_reason());
5758 assert_eq!(response.model, renormalized.model);
5759 assert_eq!(response.usage, renormalized.usage);
5760 assert_eq!(response.choice, renormalized.choice);
5761 assert_eq!(response.provider_request_id.as_deref(), Some(REQUEST_ID));
5762 assert_eq!(response.identity().message_id.as_deref(), Some("msg_raw_1"));
5763 }
5764
5765 #[tokio::test]
5770 async fn raw_completion_then_normalize_reproduces_completion() {
5771 let model = model();
5772
5773 let raw = model
5774 .raw_completion(model.completion_request("hello").build())
5775 .await
5776 .expect("typed route");
5777 assert_eq!(raw.provider_request_id.as_deref(), Some(REQUEST_ID));
5778 let reassembled = raw
5779 .normalize(<crate::providers::openai::OpenAIResponsesExt as ResponsesProviderExt>::PROVIDER_NAME)
5780 .expect("normalize");
5781
5782 let normalized = model
5783 .completion(model.completion_request("hello").build())
5784 .await
5785 .expect("normalized route");
5786
5787 assert_eq!(reassembled.identity(), normalized.identity());
5788 assert_eq!(reassembled.finish_reason(), normalized.finish_reason());
5789 assert_eq!(reassembled.model, normalized.model);
5790 assert_eq!(reassembled.usage, normalized.usage);
5791 assert_eq!(reassembled.provider_request_id.as_deref(), Some(REQUEST_ID));
5792 assert_eq!(normalized.provider_request_id.as_deref(), Some(REQUEST_ID));
5793 }
5794 }
5795}