1use super::InputAudio;
17use super::completion::ToolChoice;
18use super::responses_api::streaming::StreamingCompletionResponse;
19use crate::completion::{CompletionError, GetTokenUsage};
20use crate::http_client;
21use crate::http_client::HttpClientExt;
22use crate::json_utils;
23use crate::message::{
24 AudioMediaType, Document, DocumentMediaType, DocumentSourceKind, ImageDetail, MessageError,
25 MimeType, Text,
26};
27use crate::one_or_many::string_or_one_or_many;
28
29use crate::wasm_compat::{WasmCompatSend, WasmCompatSync};
30use crate::{OneOrMany, completion, message};
31use serde::{Deserialize, Deserializer, Serialize, Serializer};
32use serde_json::{Map, Value};
33use tracing::{Instrument, Level, enabled, info_span};
34
35use std::convert::Infallible;
36use std::ops::Add;
37use std::str::FromStr;
38
39pub mod streaming;
40#[cfg(all(not(target_family = "wasm"), feature = "websocket"))]
41pub mod websocket;
42
43#[derive(Debug, Deserialize, Serialize, Clone)]
46pub struct CompletionRequest {
47 pub input: OneOrMany<InputItem>,
49 pub model: String,
51 #[serde(skip_serializing_if = "Option::is_none")]
53 pub instructions: Option<String>,
54 #[serde(skip_serializing_if = "Option::is_none")]
56 pub max_output_tokens: Option<u64>,
57 #[serde(skip_serializing_if = "Option::is_none")]
59 pub stream: Option<bool>,
60 #[serde(skip_serializing_if = "Option::is_none")]
62 pub temperature: Option<f64>,
63 #[serde(skip_serializing_if = "Option::is_none")]
66 tool_choice: Option<ToolChoice>,
67 #[serde(skip_serializing_if = "Vec::is_empty")]
70 pub tools: Vec<ResponsesToolDefinition>,
71 #[serde(flatten)]
73 pub additional_parameters: AdditionalParameters,
74}
75
76impl CompletionRequest {
77 pub fn with_structured_outputs<S>(mut self, schema_name: S, schema: serde_json::Value) -> Self
78 where
79 S: Into<String>,
80 {
81 self.additional_parameters.text = Some(TextConfig::structured_output(schema_name, schema));
82
83 self
84 }
85
86 pub fn with_reasoning(mut self, reasoning: Reasoning) -> Self {
87 self.additional_parameters.reasoning = Some(reasoning);
88
89 self
90 }
91
92 pub fn with_tool(mut self, tool: impl Into<ResponsesToolDefinition>) -> Self {
96 self.tools.push(tool.into());
97 self
98 }
99
100 pub fn with_tools<I, Tool>(mut self, tools: I) -> Self
103 where
104 I: IntoIterator<Item = Tool>,
105 Tool: Into<ResponsesToolDefinition>,
106 {
107 self.tools.extend(tools.into_iter().map(Into::into));
108 self
109 }
110}
111
112#[derive(Debug, Deserialize, Clone)]
114pub struct InputItem {
115 #[serde(skip_serializing_if = "Option::is_none")]
119 role: Option<Role>,
120 #[serde(flatten)]
122 input: InputContent,
123}
124
125impl Serialize for InputItem {
126 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
127 where
128 S: serde::Serializer,
129 {
130 let mut value = serde_json::to_value(&self.input).map_err(serde::ser::Error::custom)?;
131 let map = value.as_object_mut().ok_or_else(|| {
132 serde::ser::Error::custom("Input content must serialize to an object")
133 })?;
134
135 if let Some(role) = &self.role
136 && !map.contains_key("role")
137 {
138 map.insert(
139 "role".to_string(),
140 serde_json::to_value(role).map_err(serde::ser::Error::custom)?,
141 );
142 }
143
144 value.serialize(serializer)
145 }
146}
147
148impl InputItem {
149 pub fn system_message(content: impl Into<String>) -> Self {
150 Self {
151 role: Some(Role::System),
152 input: InputContent::Message(Message::System {
153 content: OneOrMany::one(SystemContent::InputText {
154 text: content.into(),
155 }),
156 name: None,
157 }),
158 }
159 }
160
161 pub(crate) fn system_text(&self) -> Option<String> {
162 match &self.input {
163 InputContent::Message(Message::System { content, .. }) => Some(
164 content
165 .iter()
166 .map(|item| match item {
167 SystemContent::InputText { text } => text.as_str(),
168 })
169 .collect::<Vec<_>>()
170 .join("\n"),
171 ),
172 _ => None,
173 }
174 }
175}
176
177#[derive(Debug, Deserialize, Serialize, Clone)]
179#[serde(rename_all = "lowercase")]
180pub enum Role {
181 User,
182 Assistant,
183 System,
184}
185
186#[derive(Debug, Deserialize, Serialize, Clone)]
188#[serde(tag = "type", rename_all = "snake_case")]
189pub enum InputContent {
190 Message(Message),
191 Reasoning(OpenAIReasoning),
192 FunctionCall(OutputFunctionCall),
193 FunctionCallOutput(ToolResult),
194}
195
196#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
197pub struct OpenAIReasoning {
198 id: String,
199 pub summary: Vec<ReasoningSummary>,
200 #[serde(skip_serializing_if = "Option::is_none")]
201 pub encrypted_content: Option<String>,
202 #[serde(skip_serializing_if = "Option::is_none")]
203 pub status: Option<ToolStatus>,
204}
205
206#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
207#[serde(tag = "type", rename_all = "snake_case")]
208pub enum ReasoningSummary {
209 SummaryText { text: String },
210}
211
212impl ReasoningSummary {
213 fn new(input: &str) -> Self {
214 Self::SummaryText {
215 text: input.to_string(),
216 }
217 }
218
219 pub fn text(&self) -> String {
220 let ReasoningSummary::SummaryText { text } = self;
221 text.clone()
222 }
223}
224
225#[derive(Debug, Deserialize, Serialize, Clone)]
227pub struct ToolResult {
228 call_id: String,
230 output: String,
232 status: ToolStatus,
234}
235
236impl From<Message> for InputItem {
237 fn from(value: Message) -> Self {
238 match value {
239 Message::User { .. } => Self {
240 role: Some(Role::User),
241 input: InputContent::Message(value),
242 },
243 Message::Assistant { ref content, .. } => {
244 let role = if content
245 .iter()
246 .any(|x| matches!(x, AssistantContentType::Reasoning(_)))
247 {
248 None
249 } else {
250 Some(Role::Assistant)
251 };
252 Self {
253 role,
254 input: InputContent::Message(value),
255 }
256 }
257 Message::System { .. } => Self {
258 role: Some(Role::System),
259 input: InputContent::Message(value),
260 },
261 Message::ToolResult {
262 tool_call_id,
263 output,
264 } => Self {
265 role: None,
266 input: InputContent::FunctionCallOutput(ToolResult {
267 call_id: tool_call_id,
268 output,
269 status: ToolStatus::Completed,
270 }),
271 },
272 }
273 }
274}
275
276impl TryFrom<crate::completion::Message> for Vec<InputItem> {
277 type Error = CompletionError;
278
279 fn try_from(value: crate::completion::Message) -> Result<Self, Self::Error> {
280 match value {
281 crate::completion::Message::System { content } => Ok(vec![InputItem {
282 role: Some(Role::System),
283 input: InputContent::Message(Message::System {
284 content: OneOrMany::one(content.into()),
285 name: None,
286 }),
287 }]),
288 crate::completion::Message::User { content } => {
289 let mut items = Vec::new();
290
291 for user_content in content {
292 match user_content {
293 crate::message::UserContent::Text(Text { text, .. }) => {
294 items.push(InputItem {
295 role: Some(Role::User),
296 input: InputContent::Message(Message::User {
297 content: OneOrMany::one(UserContent::InputText { text }),
298 name: None,
299 }),
300 });
301 }
302 crate::message::UserContent::ToolResult(
303 crate::completion::message::ToolResult {
304 call_id,
305 content: tool_content,
306 ..
307 },
308 ) => {
309 for tool_result_content in tool_content {
310 let crate::completion::message::ToolResultContent::Text(Text {
311 text,
312 ..
313 }) = tool_result_content
314 else {
315 return Err(CompletionError::ProviderError(
316 "This thing only supports text!".to_string(),
317 ));
318 };
319 items.push(InputItem {
321 role: None,
322 input: InputContent::FunctionCallOutput(ToolResult {
323 call_id: require_call_id(call_id.clone(), "Tool result")?,
324 output: text,
325 status: ToolStatus::Completed,
326 }),
327 });
328 }
329 }
330 crate::message::UserContent::Document(Document {
331 data: DocumentSourceKind::FileId(file_id),
332 ..
333 }) => items.push(InputItem {
334 role: Some(Role::User),
335 input: InputContent::Message(Message::User {
336 content: OneOrMany::one(UserContent::InputFile {
337 file_id: Some(file_id),
338 file_data: None,
339 file_url: None,
340 filename: None,
341 }),
342 name: None,
343 }),
344 }),
345 crate::message::UserContent::Document(Document {
346 data,
347 media_type: Some(DocumentMediaType::PDF),
348 ..
349 }) => {
350 let (file_data, file_url) = match data {
351 DocumentSourceKind::Base64(data) => {
352 (Some(format!("data:application/pdf;base64,{data}")), None)
353 }
354 DocumentSourceKind::Url(url) => (None, Some(url)),
355 DocumentSourceKind::Raw(_) => {
356 return Err(CompletionError::RequestError(
357 "Raw file data not supported, encode as base64 first"
358 .into(),
359 ));
360 }
361 doc => {
362 return Err(CompletionError::RequestError(
363 format!("Unsupported document type: {doc}").into(),
364 ));
365 }
366 };
367
368 items.push(InputItem {
369 role: Some(Role::User),
370 input: InputContent::Message(Message::User {
371 content: OneOrMany::one(UserContent::InputFile {
372 file_id: None,
373 file_data,
374 file_url,
375 filename: Some("document.pdf".to_string()),
376 }),
377 name: None,
378 }),
379 })
380 }
381 crate::message::UserContent::Document(Document {
382 data:
383 DocumentSourceKind::Base64(text) | DocumentSourceKind::String(text),
384 ..
385 }) => items.push(InputItem {
386 role: Some(Role::User),
387 input: InputContent::Message(Message::User {
388 content: OneOrMany::one(UserContent::InputText { text }),
389 name: None,
390 }),
391 }),
392 crate::message::UserContent::Image(crate::message::Image {
393 data,
394 media_type,
395 detail,
396 ..
397 }) => {
398 let url = match data {
399 DocumentSourceKind::Base64(data) => {
400 let media_type = if let Some(media_type) = media_type {
401 media_type.to_mime_type().to_string()
402 } else {
403 String::new()
404 };
405 format!("data:{media_type};base64,{data}")
406 }
407 DocumentSourceKind::Url(url) => url,
408 DocumentSourceKind::Raw(_) => {
409 return Err(CompletionError::RequestError(
410 "Raw file data not supported, encode as base64 first"
411 .into(),
412 ));
413 }
414 doc => {
415 return Err(CompletionError::RequestError(
416 format!("Unsupported document type: {doc}").into(),
417 ));
418 }
419 };
420 items.push(InputItem {
421 role: Some(Role::User),
422 input: InputContent::Message(Message::User {
423 content: OneOrMany::one(UserContent::InputImage {
424 image_url: url,
425 detail: detail.unwrap_or_default(),
426 }),
427 name: None,
428 }),
429 });
430 }
431 message => {
432 return Err(CompletionError::ProviderError(format!(
433 "Unsupported message: {message:?}"
434 )));
435 }
436 }
437 }
438
439 Ok(items)
440 }
441 crate::completion::Message::Assistant { id, content } => {
442 let mut reasoning_items = Vec::new();
443 let mut other_items = Vec::new();
444 let content = content.into_iter().collect::<Vec<_>>();
445 let has_unreplayable_reasoning = content.iter().any(|assistant_content| {
446 matches!(
447 assistant_content,
448 crate::message::AssistantContent::Reasoning(reasoning)
449 if reasoning.id.is_none()
450 )
451 });
452 let cannot_replay_as_provider_output = id.is_none() || has_unreplayable_reasoning;
453
454 for assistant_content in content {
455 match assistant_content {
456 crate::message::AssistantContent::Text(Text { text, .. }) => {
457 if text.is_empty() {
458 continue;
459 }
460 let text = if cannot_replay_as_provider_output {
461 AssistantContent::InputText { text }
462 } else {
463 AssistantContent::OutputText(Text::new(text))
464 };
465 other_items.push(InputItem {
466 role: Some(Role::Assistant),
467 input: InputContent::Message(Message::Assistant {
468 content: OneOrMany::one(AssistantContentType::Text(text)),
469 id: id.clone().unwrap_or_default(),
470 name: None,
471 status: ToolStatus::Completed,
472 }),
473 });
474 }
475 crate::message::AssistantContent::ToolCall(crate::message::ToolCall {
476 id: tool_id,
477 call_id,
478 function,
479 ..
480 }) => {
481 other_items.push(InputItem {
482 role: None,
483 input: InputContent::FunctionCall(OutputFunctionCall {
484 arguments: function.arguments,
485 call_id: require_call_id(call_id, "Assistant tool call")?,
486 id: tool_id,
487 name: function.name,
488 status: ToolStatus::Completed,
489 }),
490 });
491 }
492 crate::message::AssistantContent::Reasoning(reasoning) => {
493 let openai_reasoning = openai_reasoning_from_core(&reasoning)
494 .map_err(|err| CompletionError::ProviderError(err.to_string()))?;
495 if let Some(openai_reasoning) = openai_reasoning {
496 reasoning_items.push(InputItem {
497 role: None,
498 input: InputContent::Reasoning(openai_reasoning),
499 });
500 }
501 }
502 crate::message::AssistantContent::Image(_) => {
503 return Err(CompletionError::ProviderError(
504 "Assistant image content is not supported in OpenAI Responses API"
505 .to_string(),
506 ));
507 }
508 }
509 }
510
511 let mut items = reasoning_items;
512 items.extend(other_items);
513 Ok(items)
514 }
515 }
516 }
517}
518
519impl From<OneOrMany<String>> for Vec<ReasoningSummary> {
520 fn from(value: OneOrMany<String>) -> Self {
521 value.iter().map(|x| ReasoningSummary::new(x)).collect()
522 }
523}
524
525fn require_call_id(call_id: Option<String>, context: &str) -> Result<String, CompletionError> {
526 call_id.ok_or_else(|| {
527 CompletionError::RequestError(
528 format!("{context} `call_id` is required for OpenAI Responses API").into(),
529 )
530 })
531}
532
533fn openai_reasoning_from_core(
534 reasoning: &crate::message::Reasoning,
535) -> Result<Option<OpenAIReasoning>, MessageError> {
536 let Some(id) = reasoning.id.clone() else {
537 return Ok(None);
538 };
539
540 let mut summary = Vec::new();
541 let mut encrypted_content = None;
542 for content in &reasoning.content {
543 match content {
544 crate::message::ReasoningContent::Text { text, .. }
545 | crate::message::ReasoningContent::Summary(text) => {
546 summary.push(ReasoningSummary::new(text));
547 }
548 crate::message::ReasoningContent::Encrypted(data)
551 | crate::message::ReasoningContent::Redacted { data } => {
552 encrypted_content.get_or_insert_with(|| data.clone());
553 }
554 }
555 }
556
557 Ok(Some(OpenAIReasoning {
558 id,
559 summary,
560 encrypted_content,
561 status: None,
562 }))
563}
564
565fn optional_reasoning_string<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
566where
567 D: Deserializer<'de>,
568{
569 Ok(
570 match Option::<serde_json::Value>::deserialize(deserializer)? {
571 Some(serde_json::Value::String(reasoning)) => Some(reasoning),
572 _ => None,
573 },
574 )
575}
576
577#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
579pub struct ResponsesToolDefinition {
580 #[serde(rename = "type")]
582 pub kind: String,
583 #[serde(default, skip_serializing_if = "String::is_empty")]
585 pub name: String,
586 #[serde(default, skip_serializing_if = "is_json_null")]
588 pub parameters: serde_json::Value,
589 #[serde(default, skip_serializing_if = "is_false")]
591 pub strict: bool,
592 #[serde(default, skip_serializing_if = "String::is_empty")]
594 pub description: String,
595 #[serde(flatten, default, skip_serializing_if = "Map::is_empty")]
597 pub config: Map<String, Value>,
598}
599
600fn is_json_null(value: &Value) -> bool {
601 value.is_null()
602}
603
604fn is_false(value: &bool) -> bool {
605 !value
606}
607
608impl ResponsesToolDefinition {
609 pub fn function(
611 name: impl Into<String>,
612 description: impl Into<String>,
613 mut parameters: serde_json::Value,
614 ) -> Self {
615 super::sanitize_schema(&mut parameters);
616
617 Self {
618 kind: "function".to_string(),
619 name: name.into(),
620 parameters,
621 strict: true,
622 description: description.into(),
623 config: Map::new(),
624 }
625 }
626
627 pub fn hosted(kind: impl Into<String>) -> Self {
629 Self {
630 kind: kind.into(),
631 name: String::new(),
632 parameters: Value::Null,
633 strict: false,
634 description: String::new(),
635 config: Map::new(),
636 }
637 }
638
639 pub fn web_search() -> Self {
641 Self::hosted("web_search")
642 }
643
644 pub fn file_search() -> Self {
646 Self::hosted("file_search")
647 }
648
649 pub fn computer_use() -> Self {
651 Self::hosted("computer_use")
652 }
653
654 pub fn with_config(mut self, key: impl Into<String>, value: Value) -> Self {
656 self.config.insert(key.into(), value);
657 self
658 }
659
660 fn normalize(mut self) -> Self {
661 if self.kind == "function" {
662 super::sanitize_schema(&mut self.parameters);
663 self.strict = true;
664 }
665 self
666 }
667}
668
669impl From<completion::ToolDefinition> for ResponsesToolDefinition {
670 fn from(value: completion::ToolDefinition) -> Self {
671 let completion::ToolDefinition {
672 name,
673 parameters,
674 description,
675 } = value;
676
677 Self::function(name, description, parameters)
678 }
679}
680
681#[derive(Clone, Debug, Serialize, Deserialize)]
684pub struct ResponsesUsage {
685 pub input_tokens: u64,
687 #[serde(skip_serializing_if = "Option::is_none")]
689 pub input_tokens_details: Option<InputTokensDetails>,
690 pub output_tokens: u64,
692 #[serde(skip_serializing_if = "Option::is_none")]
694 pub output_tokens_details: Option<OutputTokensDetails>,
695 pub total_tokens: u64,
697}
698
699impl ResponsesUsage {
700 pub(crate) fn new() -> Self {
702 Self {
703 input_tokens: 0,
704 input_tokens_details: Some(InputTokensDetails::new()),
705 output_tokens: 0,
706 output_tokens_details: Some(OutputTokensDetails::new()),
707 total_tokens: 0,
708 }
709 }
710}
711
712impl GetTokenUsage for ResponsesUsage {
713 fn token_usage(&self) -> Option<crate::completion::Usage> {
714 Some(crate::completion::Usage {
715 input_tokens: self.input_tokens,
716 output_tokens: self.output_tokens,
717 total_tokens: self.total_tokens,
718 cached_input_tokens: self
719 .input_tokens_details
720 .as_ref()
721 .map(|details| details.cached_tokens)
722 .unwrap_or(0),
723 cache_creation_input_tokens: 0,
724 tool_use_prompt_tokens: 0,
725 reasoning_tokens: self
726 .output_tokens_details
727 .as_ref()
728 .map(|details| details.reasoning_tokens)
729 .unwrap_or(0),
730 })
731 }
732}
733
734impl Add for ResponsesUsage {
735 type Output = Self;
736
737 fn add(self, rhs: Self) -> Self::Output {
738 let input_tokens = self.input_tokens + rhs.input_tokens;
739 let input_tokens_details = match (self.input_tokens_details, rhs.input_tokens_details) {
740 (Some(lhs), Some(rhs)) => Some(lhs + rhs),
741 (Some(lhs), None) => Some(lhs),
742 (None, Some(rhs)) => Some(rhs),
743 (None, None) => None,
744 };
745 let output_tokens = self.output_tokens + rhs.output_tokens;
746 let output_tokens_details = match (self.output_tokens_details, rhs.output_tokens_details) {
747 (Some(lhs), Some(rhs)) => Some(lhs + rhs),
748 (Some(lhs), None) => Some(lhs),
749 (None, Some(rhs)) => Some(rhs),
750 (None, None) => None,
751 };
752 let total_tokens = self.total_tokens + rhs.total_tokens;
753 Self {
754 input_tokens,
755 input_tokens_details,
756 output_tokens,
757 output_tokens_details,
758 total_tokens,
759 }
760 }
761}
762
763#[derive(Clone, Debug, Serialize, Deserialize)]
765pub struct InputTokensDetails {
766 pub cached_tokens: u64,
768}
769
770impl InputTokensDetails {
771 pub(crate) fn new() -> Self {
772 Self { cached_tokens: 0 }
773 }
774}
775
776impl Add for InputTokensDetails {
777 type Output = Self;
778 fn add(self, rhs: Self) -> Self::Output {
779 Self {
780 cached_tokens: self.cached_tokens + rhs.cached_tokens,
781 }
782 }
783}
784
785#[derive(Clone, Debug, Serialize, Deserialize)]
787pub struct OutputTokensDetails {
788 pub reasoning_tokens: u64,
790}
791
792impl OutputTokensDetails {
793 pub(crate) fn new() -> Self {
794 Self {
795 reasoning_tokens: 0,
796 }
797 }
798}
799
800impl Add for OutputTokensDetails {
801 type Output = Self;
802 fn add(self, rhs: Self) -> Self::Output {
803 Self {
804 reasoning_tokens: self.reasoning_tokens + rhs.reasoning_tokens,
805 }
806 }
807}
808
809#[derive(Clone, Debug, Default, Serialize, Deserialize)]
811pub struct IncompleteDetailsReason {
812 pub reason: String,
814}
815
816#[derive(Clone, Debug, Default, Serialize, Deserialize)]
818pub struct ResponseError {
819 pub code: String,
821 pub message: String,
823}
824
825#[derive(Clone, Debug, Deserialize, Serialize)]
827#[serde(rename_all = "snake_case")]
828pub enum ResponseObject {
829 Response,
830}
831
832#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
834#[serde(rename_all = "snake_case")]
835pub enum ResponseStatus {
836 InProgress,
837 Completed,
838 Failed,
839 Cancelled,
840 Queued,
841 Incomplete,
842}
843
844impl TryFrom<(String, crate::completion::CompletionRequest)> for CompletionRequest {
846 type Error = CompletionError;
847 fn try_from(
848 (model, mut req): (String, crate::completion::CompletionRequest),
849 ) -> Result<Self, Self::Error> {
850 let model = req.model.clone().unwrap_or(model);
851 let input = {
852 let mut partial_history = vec![];
853 if let Some(docs) = req.normalized_documents() {
854 partial_history.push(docs);
855 }
856 partial_history.extend(req.chat_history);
857
858 let mut full_history: Vec<InputItem> = if let Some(content) = req.preamble {
862 vec![InputItem::system_message(content)]
863 } else {
864 Vec::new()
865 };
866
867 for history_item in partial_history {
868 full_history.extend(<Vec<InputItem>>::try_from(history_item)?);
869 }
870
871 full_history
872 };
873
874 let input = OneOrMany::many(input).map_err(|_| {
875 CompletionError::RequestError(
876 "OpenAI Responses request input must contain at least one item".into(),
877 )
878 })?;
879
880 let mut additional_params_payload = req.additional_params.take().unwrap_or(Value::Null);
881 let stream = match &additional_params_payload {
882 Value::Bool(stream) => Some(*stream),
883 Value::Object(map) => map.get("stream").and_then(Value::as_bool),
884 _ => None,
885 };
886
887 let mut additional_tools = Vec::new();
888 if let Some(additional_params_map) = additional_params_payload.as_object_mut() {
889 if let Some(raw_tools) = additional_params_map.remove("tools") {
890 additional_tools = serde_json::from_value::<Vec<ResponsesToolDefinition>>(
891 raw_tools,
892 )
893 .map_err(|err| {
894 CompletionError::RequestError(
895 format!(
896 "Invalid OpenAI Responses tools payload in additional_params: {err}"
897 )
898 .into(),
899 )
900 })?;
901 }
902 additional_params_map.remove("stream");
903 }
904
905 if additional_params_payload.is_boolean() {
906 additional_params_payload = Value::Null;
907 }
908
909 additional_tools = additional_tools
910 .into_iter()
911 .map(ResponsesToolDefinition::normalize)
912 .collect();
913
914 let mut additional_parameters = if additional_params_payload.is_null() {
915 AdditionalParameters::default()
917 } else {
918 serde_json::from_value::<AdditionalParameters>(additional_params_payload).map_err(
919 |err| {
920 CompletionError::RequestError(
921 format!("Invalid OpenAI Responses additional_params payload: {err}").into(),
922 )
923 },
924 )?
925 };
926 if additional_parameters.reasoning.is_some() {
927 let include = additional_parameters.include.get_or_insert_with(Vec::new);
928 if !include
929 .iter()
930 .any(|item| matches!(item, Include::ReasoningEncryptedContent))
931 {
932 include.push(Include::ReasoningEncryptedContent);
933 }
934 }
935
936 if additional_parameters.text.is_none()
938 && let Some(schema) = req.output_schema
939 {
940 let name = schema
941 .as_object()
942 .and_then(|o| o.get("title"))
943 .and_then(|v| v.as_str())
944 .unwrap_or("response_schema")
945 .to_string();
946 let mut schema_value = schema.to_value();
947 super::sanitize_schema(&mut schema_value);
948 additional_parameters.text = Some(TextConfig::structured_output(name, schema_value));
949 }
950
951 let tool_choice = req.tool_choice.map(ToolChoice::try_from).transpose()?;
952 let mut tools: Vec<ResponsesToolDefinition> = req
953 .tools
954 .into_iter()
955 .map(ResponsesToolDefinition::from)
956 .collect();
957 tools.append(&mut additional_tools);
958
959 Ok(Self {
960 input,
961 model,
962 instructions: None, max_output_tokens: req.max_tokens,
964 stream,
965 tool_choice,
966 tools,
967 temperature: req.temperature,
968 additional_parameters,
969 })
970 }
971}
972
973#[doc(hidden)]
975#[derive(Clone)]
976pub struct GenericResponsesCompletionModel<Ext = super::OpenAIResponsesExt, H = reqwest::Client> {
977 pub(crate) client: crate::client::Client<Ext, H>,
979 pub model: String,
981 pub tools: Vec<ResponsesToolDefinition>,
983}
984
985pub type ResponsesCompletionModel<H = reqwest::Client> =
990 GenericResponsesCompletionModel<super::OpenAIResponsesExt, H>;
991
992impl<Ext, H> GenericResponsesCompletionModel<Ext, H>
993where
994 crate::client::Client<Ext, H>: HttpClientExt + Clone + std::fmt::Debug + 'static,
995 Ext: crate::client::Provider + Clone + 'static,
996 H: Clone + Default + std::fmt::Debug + 'static,
997{
998 pub fn new(client: crate::client::Client<Ext, H>, model: impl Into<String>) -> Self {
1000 Self {
1001 client,
1002 model: model.into(),
1003 tools: Vec::new(),
1004 }
1005 }
1006
1007 pub fn with_model(client: crate::client::Client<Ext, H>, model: &str) -> Self {
1008 Self {
1009 client,
1010 model: model.to_string(),
1011 tools: Vec::new(),
1012 }
1013 }
1014
1015 pub fn with_tool(mut self, tool: impl Into<ResponsesToolDefinition>) -> Self {
1017 self.tools.push(tool.into());
1018 self
1019 }
1020
1021 pub fn with_tools<I, Tool>(mut self, tools: I) -> Self
1023 where
1024 I: IntoIterator<Item = Tool>,
1025 Tool: Into<ResponsesToolDefinition>,
1026 {
1027 self.tools.extend(tools.into_iter().map(Into::into));
1028 self
1029 }
1030
1031 pub(crate) fn create_completion_request(
1033 &self,
1034 completion_request: crate::completion::CompletionRequest,
1035 ) -> Result<CompletionRequest, CompletionError> {
1036 let mut req = CompletionRequest::try_from((self.model.clone(), completion_request))?;
1037 req.tools.extend(self.tools.clone());
1038
1039 Ok(req)
1040 }
1041}
1042
1043impl<T> GenericResponsesCompletionModel<super::OpenAIResponsesExt, T>
1044where
1045 T: HttpClientExt + Clone + Default + std::fmt::Debug + 'static,
1046{
1047 pub fn completions_api(self) -> crate::providers::openai::completion::CompletionModel<T> {
1049 super::completion::CompletionModel::with_model(self.client.completions_api(), &self.model)
1050 }
1051}
1052
1053#[derive(Clone, Debug, Serialize, Deserialize)]
1055pub struct CompletionResponse {
1056 pub id: String,
1058 pub object: ResponseObject,
1060 pub created_at: u64,
1062 pub status: ResponseStatus,
1064 pub error: Option<ResponseError>,
1066 pub incomplete_details: Option<IncompleteDetailsReason>,
1068 pub instructions: Option<String>,
1070 pub max_output_tokens: Option<u64>,
1072 pub model: String,
1074 #[serde(
1077 default,
1078 rename = "reasoning",
1079 deserialize_with = "optional_reasoning_string",
1080 skip_serializing_if = "Option::is_none"
1081 )]
1082 pub provider_reasoning: Option<String>,
1083 pub usage: Option<ResponsesUsage>,
1085 #[serde(default)]
1087 pub output: Vec<Output>,
1088 #[serde(default)]
1090 pub tools: Vec<ResponsesToolDefinition>,
1091 #[serde(flatten)]
1093 pub additional_parameters: AdditionalParameters,
1094}
1095
1096#[derive(Clone, Debug, Deserialize, Serialize, Default)]
1099pub struct AdditionalParameters {
1100 #[serde(skip_serializing_if = "Option::is_none")]
1102 pub background: Option<bool>,
1103 #[serde(skip_serializing_if = "Option::is_none")]
1105 pub text: Option<TextConfig>,
1106 #[serde(skip_serializing_if = "Option::is_none")]
1108 pub include: Option<Vec<Include>>,
1109 #[serde(skip_serializing_if = "Option::is_none")]
1111 pub top_p: Option<f64>,
1112 #[serde(skip_serializing_if = "Option::is_none")]
1114 pub truncation: Option<TruncationStrategy>,
1115 #[serde(skip_serializing_if = "Option::is_none")]
1117 pub user: Option<String>,
1118 #[serde(skip_serializing_if = "Map::is_empty", default)]
1120 pub metadata: serde_json::Map<String, serde_json::Value>,
1121 #[serde(skip_serializing_if = "Option::is_none")]
1123 pub parallel_tool_calls: Option<bool>,
1124 #[serde(skip_serializing_if = "Option::is_none")]
1126 pub previous_response_id: Option<String>,
1127 #[serde(skip_serializing_if = "Option::is_none")]
1129 pub reasoning: Option<Reasoning>,
1130 #[serde(skip_serializing_if = "Option::is_none")]
1132 pub service_tier: Option<OpenAIServiceTier>,
1133 #[serde(skip_serializing_if = "Option::is_none")]
1135 pub store: Option<bool>,
1136}
1137
1138impl AdditionalParameters {
1139 pub fn to_json(self) -> serde_json::Value {
1140 serde_json::to_value(self).unwrap_or_else(|_| serde_json::Value::Object(Map::new()))
1141 }
1142}
1143
1144#[derive(Clone, Debug, Default, Serialize, Deserialize)]
1148#[serde(rename_all = "snake_case")]
1149pub enum TruncationStrategy {
1150 Auto,
1151 #[default]
1152 Disabled,
1153}
1154
1155#[derive(Clone, Debug, Serialize, Deserialize)]
1158pub struct TextConfig {
1159 pub format: TextFormat,
1160}
1161
1162impl TextConfig {
1163 pub(crate) fn structured_output<S>(name: S, schema: serde_json::Value) -> Self
1164 where
1165 S: Into<String>,
1166 {
1167 Self {
1168 format: TextFormat::JsonSchema(StructuredOutputsInput {
1169 name: name.into(),
1170 schema,
1171 strict: true,
1172 }),
1173 }
1174 }
1175}
1176
1177#[derive(Clone, Debug, Serialize, Deserialize, Default)]
1180#[serde(tag = "type")]
1181#[serde(rename_all = "snake_case")]
1182pub enum TextFormat {
1183 JsonSchema(StructuredOutputsInput),
1184 #[default]
1185 Text,
1186}
1187
1188#[derive(Clone, Debug, Serialize, Deserialize)]
1190pub struct StructuredOutputsInput {
1191 pub name: String,
1193 pub schema: serde_json::Value,
1195 #[serde(default)]
1197 pub strict: bool,
1198}
1199
1200#[derive(Clone, Debug, Default, Serialize, Deserialize)]
1202pub struct Reasoning {
1203 pub effort: Option<ReasoningEffort>,
1205 #[serde(skip_serializing_if = "Option::is_none")]
1207 pub summary: Option<ReasoningSummaryLevel>,
1208}
1209
1210impl Reasoning {
1211 pub fn new() -> Self {
1213 Self {
1214 effort: None,
1215 summary: None,
1216 }
1217 }
1218
1219 pub fn with_effort(mut self, reasoning_effort: ReasoningEffort) -> Self {
1221 self.effort = Some(reasoning_effort);
1222
1223 self
1224 }
1225
1226 pub fn with_summary_level(mut self, reasoning_summary_level: ReasoningSummaryLevel) -> Self {
1228 self.summary = Some(reasoning_summary_level);
1229
1230 self
1231 }
1232}
1233
1234#[derive(Clone, Debug, Default)]
1236pub enum OpenAIServiceTier {
1237 #[default]
1239 Auto,
1240 Default,
1242 Flex,
1244 Priority,
1246 Standard,
1248 Other(String),
1250}
1251
1252impl Serialize for OpenAIServiceTier {
1253 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1254 where
1255 S: Serializer,
1256 {
1257 serializer.serialize_str(match self {
1258 Self::Auto => "auto",
1259 Self::Default => "default",
1260 Self::Flex => "flex",
1261 Self::Priority => "priority",
1262 Self::Standard => "standard",
1263 Self::Other(value) => value,
1264 })
1265 }
1266}
1267
1268impl<'de> Deserialize<'de> for OpenAIServiceTier {
1269 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1270 where
1271 D: Deserializer<'de>,
1272 {
1273 let value = String::deserialize(deserializer)?;
1274 Ok(match value.as_str() {
1275 "auto" => Self::Auto,
1276 "default" => Self::Default,
1277 "flex" => Self::Flex,
1278 "priority" => Self::Priority,
1279 "standard" => Self::Standard,
1280 _ => Self::Other(value),
1281 })
1282 }
1283}
1284
1285#[derive(Clone, Debug, Default, Serialize, Deserialize)]
1287#[serde(rename_all = "snake_case")]
1288pub enum ReasoningEffort {
1289 None,
1290 Minimal,
1291 Low,
1292 #[default]
1293 Medium,
1294 High,
1295 Xhigh,
1296}
1297
1298#[derive(Clone, Debug, Default, Serialize, Deserialize)]
1300#[serde(rename_all = "snake_case")]
1301pub enum ReasoningSummaryLevel {
1302 #[default]
1303 Auto,
1304 Concise,
1305 Detailed,
1306}
1307
1308#[derive(Clone, Debug, Deserialize, Serialize)]
1311pub enum Include {
1312 #[serde(rename = "file_search_call.results")]
1313 FileSearchCallResults,
1314 #[serde(rename = "message.input_image.image_url")]
1315 MessageInputImageImageUrl,
1316 #[serde(rename = "computer_call.output.image_url")]
1317 ComputerCallOutputOutputImageUrl,
1318 #[serde(rename = "reasoning.encrypted_content")]
1319 ReasoningEncryptedContent,
1320 #[serde(rename = "code_interpreter_call.outputs")]
1321 CodeInterpreterCallOutputs,
1322}
1323
1324#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
1326#[serde(tag = "type")]
1327#[serde(rename_all = "snake_case")]
1328pub enum Output {
1329 Message(OutputMessage),
1330 #[serde(alias = "function_call")]
1331 FunctionCall(OutputFunctionCall),
1332 Reasoning {
1333 id: String,
1334 summary: Vec<ReasoningSummary>,
1335 #[serde(default)]
1336 encrypted_content: Option<String>,
1337 #[serde(default)]
1338 status: Option<ToolStatus>,
1339 },
1340 #[serde(other)]
1345 Unknown,
1346}
1347
1348impl From<Output> for Vec<completion::AssistantContent> {
1349 fn from(value: Output) -> Self {
1350 let res: Vec<completion::AssistantContent> = match value {
1351 Output::Message(OutputMessage { content, .. }) => content
1352 .into_iter()
1353 .map(completion::AssistantContent::from)
1354 .collect(),
1355 Output::FunctionCall(OutputFunctionCall {
1356 id,
1357 arguments,
1358 call_id,
1359 name,
1360 ..
1361 }) => vec![completion::AssistantContent::tool_call_with_call_id(
1362 id, call_id, name, arguments,
1363 )],
1364 Output::Reasoning {
1365 id,
1366 summary,
1367 encrypted_content,
1368 ..
1369 } => {
1370 let mut content = summary
1371 .into_iter()
1372 .map(|summary| match summary {
1373 ReasoningSummary::SummaryText { text } => {
1374 message::ReasoningContent::Summary(text)
1375 }
1376 })
1377 .collect::<Vec<_>>();
1378 if let Some(encrypted_content) = encrypted_content {
1379 content.push(message::ReasoningContent::Encrypted(encrypted_content));
1380 }
1381 vec![completion::AssistantContent::Reasoning(
1382 message::Reasoning {
1383 id: Some(id),
1384 content,
1385 },
1386 )]
1387 }
1388 Output::Unknown => Vec::new(),
1389 };
1390
1391 res
1392 }
1393}
1394
1395#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
1396pub struct OutputReasoning {
1397 id: String,
1398 summary: Vec<ReasoningSummary>,
1399 status: ToolStatus,
1400}
1401
1402#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
1404pub struct OutputFunctionCall {
1405 pub id: String,
1406 #[serde(with = "json_utils::stringified_json")]
1407 pub arguments: serde_json::Value,
1408 pub call_id: String,
1409 pub name: String,
1410 pub status: ToolStatus,
1411}
1412
1413#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
1415#[serde(rename_all = "snake_case")]
1416pub enum ToolStatus {
1417 InProgress,
1418 Completed,
1419 Incomplete,
1420}
1421
1422#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
1424pub struct OutputMessage {
1425 pub id: String,
1427 pub role: OutputRole,
1429 pub status: ResponseStatus,
1431 pub content: Vec<AssistantContent>,
1433}
1434
1435#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
1437#[serde(rename_all = "snake_case")]
1438pub enum OutputRole {
1439 Assistant,
1440}
1441
1442impl<Ext, H> completion::CompletionModel for GenericResponsesCompletionModel<Ext, H>
1443where
1444 crate::client::Client<Ext, H>:
1445 HttpClientExt + Clone + WasmCompatSend + WasmCompatSync + 'static,
1446 Ext: crate::client::Provider
1447 + crate::client::DebugExt
1448 + Clone
1449 + WasmCompatSend
1450 + WasmCompatSync
1451 + 'static,
1452 H: Clone + Default + std::fmt::Debug + WasmCompatSend + WasmCompatSync + 'static,
1453{
1454 type Response = CompletionResponse;
1455 type StreamingResponse = StreamingCompletionResponse;
1456
1457 type Client = crate::client::Client<Ext, H>;
1458
1459 fn make(client: &Self::Client, model: impl Into<String>) -> Self {
1460 Self::new(client.clone(), model)
1461 }
1462
1463 async fn completion(
1464 &self,
1465 completion_request: crate::completion::CompletionRequest,
1466 ) -> Result<completion::CompletionResponse<Self::Response>, CompletionError> {
1467 let span = if tracing::Span::current().is_disabled() {
1468 info_span!(
1469 target: "rig::completions",
1470 "chat",
1471 gen_ai.operation.name = "chat",
1472 gen_ai.provider.name = tracing::field::Empty,
1473 gen_ai.request.model = tracing::field::Empty,
1474 gen_ai.response.id = tracing::field::Empty,
1475 gen_ai.response.model = tracing::field::Empty,
1476 gen_ai.usage.output_tokens = tracing::field::Empty,
1477 gen_ai.usage.input_tokens = tracing::field::Empty,
1478 gen_ai.usage.cache_read.input_tokens = tracing::field::Empty,
1479 gen_ai.input.messages = tracing::field::Empty,
1480 gen_ai.output.messages = tracing::field::Empty,
1481 )
1482 } else {
1483 tracing::Span::current()
1484 };
1485
1486 span.record("gen_ai.provider.name", "openai");
1487 span.record("gen_ai.request.model", &self.model);
1488 let request = self.create_completion_request(completion_request)?;
1489 let body = serde_json::to_vec(&request)?;
1490
1491 if enabled!(Level::TRACE) {
1492 tracing::trace!(
1493 target: "rig::completions",
1494 "OpenAI Responses completion request: {request}",
1495 request = serde_json::to_string_pretty(&request)?
1496 );
1497 }
1498
1499 let req = self
1500 .client
1501 .post("/responses")?
1502 .body(body)
1503 .map_err(|e| CompletionError::HttpError(e.into()))?;
1504
1505 async move {
1506 let response = self.client.send(req).await?;
1507
1508 if response.status().is_success() {
1509 let t = http_client::text(response).await?;
1510 let response = serde_json::from_str::<Self::Response>(&t)?;
1511 let span = tracing::Span::current();
1512 span.record("gen_ai.response.id", &response.id);
1513 span.record("gen_ai.response.model", &response.model);
1514 if let Some(ref usage) = response.usage {
1515 span.record("gen_ai.usage.output_tokens", usage.output_tokens);
1516 span.record("gen_ai.usage.input_tokens", usage.input_tokens);
1517 let cached_tokens = usage
1518 .input_tokens_details
1519 .as_ref()
1520 .map(|d| d.cached_tokens)
1521 .unwrap_or(0);
1522 span.record("gen_ai.usage.cache_read.input_tokens", cached_tokens);
1523 }
1524 if enabled!(Level::TRACE) {
1525 tracing::trace!(
1526 target: "rig::completions",
1527 "OpenAI Responses completion response: {response}",
1528 response = serde_json::to_string_pretty(&response)?
1529 );
1530 }
1531 response.try_into()
1532 } else {
1533 let text = http_client::text(response).await?;
1534 Err(CompletionError::ProviderError(text))
1535 }
1536 }
1537 .instrument(span)
1538 .await
1539 }
1540
1541 async fn stream(
1542 &self,
1543 request: crate::completion::CompletionRequest,
1544 ) -> Result<
1545 crate::streaming::StreamingCompletionResponse<Self::StreamingResponse>,
1546 CompletionError,
1547 > {
1548 GenericResponsesCompletionModel::stream(self, request).await
1549 }
1550}
1551
1552impl TryFrom<CompletionResponse> for completion::CompletionResponse<CompletionResponse> {
1553 type Error = CompletionError;
1554
1555 fn try_from(response: CompletionResponse) -> Result<Self, Self::Error> {
1556 let message_id = response.output.iter().find_map(|item| match item {
1558 Output::Message(msg) => Some(msg.id.clone()),
1559 _ => None,
1560 });
1561
1562 let output_content: Vec<completion::AssistantContent> = response
1563 .output
1564 .iter()
1565 .cloned()
1566 .flat_map(<Vec<completion::AssistantContent>>::from)
1567 .collect();
1568 let has_structured_reasoning = response
1569 .output
1570 .iter()
1571 .any(|item| matches!(item, Output::Reasoning { .. }));
1572 let content = response
1573 .provider_reasoning
1574 .as_ref()
1575 .filter(|reasoning| !has_structured_reasoning && !reasoning.is_empty())
1576 .map(|reasoning| {
1577 let mut content = Vec::with_capacity(output_content.len() + 1);
1578 content.push(completion::AssistantContent::Reasoning(
1579 message::Reasoning::new(reasoning),
1580 ));
1581 content.extend(output_content.clone());
1582 content
1583 })
1584 .unwrap_or(output_content);
1585
1586 let choice = OneOrMany::many(content).map_err(|_| {
1587 CompletionError::ResponseError(
1588 "Response contained no message or tool call (empty)".to_owned(),
1589 )
1590 })?;
1591
1592 let usage = response
1593 .usage
1594 .as_ref()
1595 .and_then(GetTokenUsage::token_usage)
1596 .unwrap_or_default();
1597
1598 Ok(completion::CompletionResponse {
1599 choice,
1600 usage,
1601 raw_response: response,
1602 message_id,
1603 })
1604 }
1605}
1606
1607#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
1609#[serde(tag = "role", rename_all = "lowercase")]
1610pub enum Message {
1611 #[serde(alias = "developer")]
1612 System {
1613 #[serde(deserialize_with = "string_or_one_or_many")]
1614 content: OneOrMany<SystemContent>,
1615 #[serde(skip_serializing_if = "Option::is_none")]
1616 name: Option<String>,
1617 },
1618 User {
1619 #[serde(deserialize_with = "string_or_one_or_many")]
1620 content: OneOrMany<UserContent>,
1621 #[serde(skip_serializing_if = "Option::is_none")]
1622 name: Option<String>,
1623 },
1624 Assistant {
1625 content: OneOrMany<AssistantContentType>,
1626 #[serde(skip_serializing_if = "String::is_empty")]
1627 id: String,
1628 #[serde(skip_serializing_if = "Option::is_none")]
1629 name: Option<String>,
1630 status: ToolStatus,
1631 },
1632 #[serde(rename = "tool")]
1633 ToolResult {
1634 tool_call_id: String,
1635 output: String,
1636 },
1637}
1638
1639#[derive(Default, Debug, Serialize, Deserialize, PartialEq, Clone)]
1641#[serde(rename_all = "lowercase")]
1642pub enum ToolResultContentType {
1643 #[default]
1644 Text,
1645}
1646
1647impl Message {
1648 pub fn system(content: &str) -> Self {
1649 Message::System {
1650 content: OneOrMany::one(content.to_owned().into()),
1651 name: None,
1652 }
1653 }
1654}
1655
1656#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
1659#[serde(tag = "type", rename_all = "snake_case")]
1660pub enum AssistantContent {
1661 InputText { text: String },
1662 OutputText(Text),
1663 Refusal { refusal: String },
1664}
1665
1666impl From<AssistantContent> for completion::AssistantContent {
1667 fn from(value: AssistantContent) -> Self {
1668 match value {
1669 AssistantContent::InputText { text } => {
1670 completion::AssistantContent::Text(Text::new(text))
1671 }
1672 AssistantContent::Refusal { refusal } => {
1673 completion::AssistantContent::Text(Text::new(refusal))
1674 }
1675 AssistantContent::OutputText(Text { text, .. }) => {
1676 completion::AssistantContent::Text(Text::new(text))
1677 }
1678 }
1679 }
1680}
1681
1682#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
1684#[serde(untagged)]
1685pub enum AssistantContentType {
1686 Text(AssistantContent),
1687 ToolCall(OutputFunctionCall),
1688 Reasoning(OpenAIReasoning),
1689}
1690
1691#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
1694#[serde(tag = "type", rename_all = "snake_case")]
1695pub enum SystemContent {
1696 InputText { text: String },
1697}
1698
1699impl From<String> for SystemContent {
1700 fn from(s: String) -> Self {
1701 SystemContent::InputText { text: s }
1702 }
1703}
1704
1705impl std::str::FromStr for SystemContent {
1706 type Err = std::convert::Infallible;
1707
1708 fn from_str(s: &str) -> Result<Self, Self::Err> {
1709 Ok(SystemContent::InputText {
1710 text: s.to_string(),
1711 })
1712 }
1713}
1714
1715#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
1717#[serde(tag = "type", rename_all = "snake_case")]
1718pub enum UserContent {
1719 InputText {
1720 text: String,
1721 },
1722 InputImage {
1723 image_url: String,
1724 #[serde(default)]
1725 detail: ImageDetail,
1726 },
1727 InputFile {
1728 #[serde(skip_serializing_if = "Option::is_none")]
1729 file_id: Option<String>,
1730 #[serde(skip_serializing_if = "Option::is_none")]
1731 file_url: Option<String>,
1732 #[serde(skip_serializing_if = "Option::is_none")]
1733 file_data: Option<String>,
1734 #[serde(skip_serializing_if = "Option::is_none")]
1735 filename: Option<String>,
1736 },
1737 Audio {
1738 input_audio: InputAudio,
1739 },
1740 #[serde(rename = "tool")]
1741 ToolResult {
1742 tool_call_id: String,
1743 output: String,
1744 },
1745}
1746
1747impl TryFrom<message::Message> for Vec<Message> {
1748 type Error = message::MessageError;
1749
1750 fn try_from(message: message::Message) -> Result<Self, Self::Error> {
1751 match message {
1752 message::Message::System { content } => Ok(vec![Message::System {
1753 content: OneOrMany::one(content.into()),
1754 name: None,
1755 }]),
1756 message::Message::User { content } => {
1757 let (tool_results, other_content): (Vec<_>, Vec<_>) = content
1758 .into_iter()
1759 .partition(|content| matches!(content, message::UserContent::ToolResult(_)));
1760
1761 if !tool_results.is_empty() {
1764 tool_results
1765 .into_iter()
1766 .map(|content| match content {
1767 message::UserContent::ToolResult(message::ToolResult {
1768 call_id,
1769 content,
1770 ..
1771 }) => Ok::<_, message::MessageError>(Message::ToolResult {
1772 tool_call_id: call_id.ok_or_else(|| {
1773 MessageError::ConversionError(
1774 "Tool result `call_id` is required for OpenAI Responses API"
1775 .into(),
1776 )
1777 })?,
1778 output: {
1779 let res = content.first();
1780 match res {
1781 completion::message::ToolResultContent::Text(Text {
1782 text,
1783 ..
1784 }) => text,
1785 _ => return Err(MessageError::ConversionError("This API only currently supports text tool results".into()))
1786 }
1787 },
1788 }),
1789 _ => Err(MessageError::ConversionError(
1790 "expected tool result content while converting Responses API input"
1791 .into(),
1792 )),
1793 })
1794 .collect::<Result<Vec<_>, _>>()
1795 } else {
1796 let other_content = other_content
1797 .into_iter()
1798 .map(|content| match content {
1799 message::UserContent::Text(message::Text { text, .. }) => {
1800 Ok(UserContent::InputText { text })
1801 }
1802 message::UserContent::Image(message::Image {
1803 data,
1804 detail,
1805 media_type,
1806 ..
1807 }) => {
1808 let url = match data {
1809 DocumentSourceKind::Base64(data) => {
1810 let media_type = if let Some(media_type) = media_type {
1811 media_type.to_mime_type().to_string()
1812 } else {
1813 String::new()
1814 };
1815 format!("data:{media_type};base64,{data}")
1816 }
1817 DocumentSourceKind::Url(url) => url,
1818 DocumentSourceKind::Raw(_) => {
1819 return Err(MessageError::ConversionError(
1820 "Raw files not supported, encode as base64 first"
1821 .into(),
1822 ));
1823 }
1824 doc => {
1825 return Err(MessageError::ConversionError(format!(
1826 "Unsupported document type: {doc}"
1827 )));
1828 }
1829 };
1830
1831 Ok(UserContent::InputImage {
1832 image_url: url,
1833 detail: detail.unwrap_or_default(),
1834 })
1835 }
1836 message::UserContent::Document(message::Document {
1837 data: DocumentSourceKind::FileId(file_id),
1838 ..
1839 }) => Ok(UserContent::InputFile {
1840 file_id: Some(file_id),
1841 file_url: None,
1842 file_data: None,
1843 filename: None,
1844 }),
1845 message::UserContent::Document(message::Document {
1846 media_type: Some(DocumentMediaType::PDF),
1847 data,
1848 ..
1849 }) => {
1850 let (file_data, file_url, filename) = match data {
1851 DocumentSourceKind::Base64(data) => (
1852 Some(format!("data:application/pdf;base64,{data}")),
1853 None,
1854 Some("document.pdf".to_string()),
1855 ),
1856 DocumentSourceKind::Url(url) => (None, Some(url), None),
1857 DocumentSourceKind::Raw(_) => {
1858 return Err(MessageError::ConversionError(
1859 "Raw files not supported, encode as base64 first"
1860 .into(),
1861 ));
1862 }
1863 doc => {
1864 return Err(MessageError::ConversionError(format!(
1865 "Unsupported document type: {doc}"
1866 )));
1867 }
1868 };
1869
1870 Ok(UserContent::InputFile {
1871 file_id: None,
1872 file_url,
1873 file_data,
1874 filename,
1875 })
1876 }
1877 message::UserContent::Document(message::Document {
1878 data: DocumentSourceKind::Base64(text),
1879 ..
1880 }) => Ok(UserContent::InputText { text }),
1881 message::UserContent::Audio(message::Audio {
1882 data: DocumentSourceKind::Base64(data),
1883 media_type,
1884 ..
1885 }) => Ok(UserContent::Audio {
1886 input_audio: InputAudio {
1887 data,
1888 format: match media_type {
1889 Some(media_type) => media_type,
1890 None => AudioMediaType::MP3,
1891 },
1892 },
1893 }),
1894 message::UserContent::Audio(_) => Err(MessageError::ConversionError(
1895 "Audio must be base64 encoded data".into(),
1896 )),
1897 _ => Err(MessageError::ConversionError(
1898 "Unsupported user content for OpenAI Responses API".into(),
1899 )),
1900 })
1901 .collect::<Result<Vec<_>, _>>()?;
1902
1903 let other_content = OneOrMany::many(other_content).map_err(|_| {
1904 MessageError::ConversionError(
1905 "User message did not contain OpenAI Responses-compatible content"
1906 .to_string(),
1907 )
1908 })?;
1909
1910 Ok(vec![Message::User {
1911 content: other_content,
1912 name: None,
1913 }])
1914 }
1915 }
1916 message::Message::Assistant { content, id } => {
1917 let cannot_replay_without_provider_id = id.is_none();
1918 let assistant_message_id = id.unwrap_or_default();
1919 let mut messages = Vec::new();
1920 let content = content.into_iter().collect::<Vec<_>>();
1921 let has_unreplayable_reasoning = content.iter().any(|assistant_content| {
1922 matches!(
1923 assistant_content,
1924 crate::message::AssistantContent::Reasoning(reasoning)
1925 if reasoning.id.is_none()
1926 )
1927 });
1928 let cannot_replay_as_provider_output =
1929 cannot_replay_without_provider_id || has_unreplayable_reasoning;
1930
1931 for assistant_content in content {
1932 match assistant_content {
1933 crate::message::AssistantContent::Text(Text { text, .. }) => {
1934 if text.is_empty() {
1935 continue;
1936 }
1937 let text = if cannot_replay_as_provider_output {
1938 AssistantContent::InputText { text }
1939 } else {
1940 AssistantContent::OutputText(Text::new(text))
1941 };
1942 messages.push(Message::Assistant {
1943 id: assistant_message_id.clone(),
1944 status: ToolStatus::Completed,
1945 content: OneOrMany::one(AssistantContentType::Text(text)),
1946 name: None,
1947 });
1948 }
1949 crate::message::AssistantContent::ToolCall(crate::message::ToolCall {
1950 id,
1951 call_id,
1952 function,
1953 ..
1954 }) => {
1955 messages.push(Message::Assistant {
1956 content: OneOrMany::one(AssistantContentType::ToolCall(
1957 OutputFunctionCall {
1958 call_id: call_id.ok_or_else(|| {
1959 MessageError::ConversionError(
1960 "Tool call `call_id` is required for OpenAI Responses API"
1961 .into(),
1962 )
1963 })?,
1964 arguments: function.arguments,
1965 id,
1966 name: function.name,
1967 status: ToolStatus::Completed,
1968 },
1969 )),
1970 id: assistant_message_id.clone(),
1971 name: None,
1972 status: ToolStatus::Completed,
1973 });
1974 }
1975 crate::message::AssistantContent::Reasoning(reasoning) => {
1976 if let Some(openai_reasoning) = openai_reasoning_from_core(&reasoning)?
1977 {
1978 messages.push(Message::Assistant {
1979 content: OneOrMany::one(AssistantContentType::Reasoning(
1980 openai_reasoning,
1981 )),
1982 id: assistant_message_id.clone(),
1983 name: None,
1984 status: ToolStatus::Completed,
1985 });
1986 }
1987 }
1988 crate::message::AssistantContent::Image(_) => {
1989 return Err(MessageError::ConversionError(
1990 "Assistant image content is not supported in OpenAI Responses API"
1991 .into(),
1992 ));
1993 }
1994 }
1995 }
1996
1997 Ok(messages)
1998 }
1999 }
2000 }
2001}
2002
2003impl FromStr for UserContent {
2004 type Err = Infallible;
2005
2006 fn from_str(s: &str) -> Result<Self, Self::Err> {
2007 Ok(UserContent::InputText {
2008 text: s.to_string(),
2009 })
2010 }
2011}
2012
2013#[cfg(test)]
2014mod tests {
2015 use super::*;
2016 use crate::message;
2017 use serde_json::json;
2018
2019 fn response_with_service_tier(service_tier: &str) -> Value {
2020 json!({
2021 "id": "resp_123",
2022 "object": "response",
2023 "created_at": 0,
2024 "status": "completed",
2025 "model": "gpt-5.4",
2026 "output": [],
2027 "service_tier": service_tier,
2028 })
2029 }
2030
2031 #[test]
2032 fn completion_response_deserializes_standard_service_tier() {
2033 let response: CompletionResponse =
2034 serde_json::from_value(response_with_service_tier("standard"))
2035 .expect("response should deserialize");
2036
2037 assert!(matches!(
2038 response.additional_parameters.service_tier,
2039 Some(OpenAIServiceTier::Standard)
2040 ));
2041 }
2042
2043 #[test]
2044 fn completion_response_deserializes_priority_service_tier() {
2045 let response: CompletionResponse =
2046 serde_json::from_value(response_with_service_tier("priority"))
2047 .expect("response should deserialize");
2048
2049 assert!(matches!(
2050 response.additional_parameters.service_tier,
2051 Some(OpenAIServiceTier::Priority)
2052 ));
2053 }
2054
2055 #[test]
2056 fn completion_response_preserves_unknown_service_tier() {
2057 let response: CompletionResponse =
2058 serde_json::from_value(response_with_service_tier("provider_experimental"))
2059 .expect("response should deserialize");
2060
2061 let Some(OpenAIServiceTier::Other(service_tier)) =
2062 response.additional_parameters.service_tier
2063 else {
2064 panic!("expected provider-specific service tier");
2065 };
2066
2067 assert_eq!(service_tier, "provider_experimental");
2068 }
2069
2070 #[test]
2071 fn service_tier_serializes_expected_strings() {
2072 let cases = [
2073 (OpenAIServiceTier::Auto, "auto"),
2074 (OpenAIServiceTier::Default, "default"),
2075 (OpenAIServiceTier::Flex, "flex"),
2076 (OpenAIServiceTier::Priority, "priority"),
2077 (OpenAIServiceTier::Standard, "standard"),
2078 ];
2079
2080 for (service_tier, expected) in cases {
2081 assert_eq!(
2082 serde_json::to_value(service_tier).expect("service tier should serialize"),
2083 json!(expected)
2084 );
2085 }
2086
2087 assert_eq!(
2088 serde_json::to_value(OpenAIServiceTier::Other(
2089 "provider_experimental".to_string()
2090 ))
2091 .expect("provider-specific service tier should serialize"),
2092 json!("provider_experimental")
2093 );
2094 }
2095
2096 #[test]
2097 fn responses_usage_token_usage_preserves_reasoning_tokens() {
2098 let usage = ResponsesUsage {
2099 input_tokens: 100,
2100 input_tokens_details: Some(InputTokensDetails { cached_tokens: 25 }),
2101 output_tokens: 50,
2102 output_tokens_details: Some(OutputTokensDetails {
2103 reasoning_tokens: 15,
2104 }),
2105 total_tokens: 150,
2106 };
2107
2108 let token_usage = usage.token_usage().expect("usage should be present");
2109
2110 assert_eq!(token_usage.input_tokens, 100);
2111 assert_eq!(token_usage.cached_input_tokens, 25);
2112 assert_eq!(token_usage.output_tokens, 50);
2113 assert_eq!(token_usage.reasoning_tokens, 15);
2114 assert_eq!(token_usage.total_tokens, 150);
2115 }
2116
2117 #[test]
2118 fn responses_usage_deserializes_without_output_token_details() {
2119 let usage: ResponsesUsage = serde_json::from_value(json!({
2120 "input_tokens": 100,
2121 "input_tokens_details": {
2122 "cached_tokens": 25
2123 },
2124 "output_tokens": 50,
2125 "total_tokens": 150
2126 }))
2127 .expect("usage should deserialize when output token details are omitted");
2128
2129 assert!(usage.output_tokens_details.is_none());
2130
2131 let token_usage = usage.token_usage().expect("usage should be present");
2132
2133 assert_eq!(token_usage.input_tokens, 100);
2134 assert_eq!(token_usage.cached_input_tokens, 25);
2135 assert_eq!(token_usage.output_tokens, 50);
2136 assert_eq!(token_usage.reasoning_tokens, 0);
2137 assert_eq!(token_usage.total_tokens, 150);
2138 }
2139
2140 #[test]
2141 fn completion_response_accepts_top_level_reasoning_string() {
2142 let response: CompletionResponse = serde_json::from_value(json!({
2143 "id": "resp_123",
2144 "object": "response",
2145 "created_at": 0,
2146 "status": "completed",
2147 "model": "Qwen/Qwen3-4B",
2148 "reasoning": "thinking through the answer",
2149 "usage": {
2150 "input_tokens": 1,
2151 "output_tokens": 2,
2152 "total_tokens": 3
2153 },
2154 "output": [{
2155 "type": "message",
2156 "id": "msg_123",
2157 "status": "completed",
2158 "role": "assistant",
2159 "content": [{
2160 "type": "output_text",
2161 "annotations": [],
2162 "text": "done"
2163 }]
2164 }],
2165 "tools": []
2166 }))
2167 .expect("mistral.rs-style reasoning string should deserialize");
2168
2169 assert_eq!(
2170 response.provider_reasoning.as_deref(),
2171 Some("thinking through the answer")
2172 );
2173
2174 let completion: completion::CompletionResponse<CompletionResponse> =
2175 response.try_into().expect("response should convert");
2176 let items = completion.choice.iter().collect::<Vec<_>>();
2177 assert!(matches!(
2178 items[0],
2179 completion::AssistantContent::Reasoning(_)
2180 ));
2181 assert!(matches!(items[1], completion::AssistantContent::Text(_)));
2182 }
2183
2184 #[test]
2185 fn completion_response_accepts_reasoning_only_response() {
2186 let response: CompletionResponse = serde_json::from_value(json!({
2187 "id": "resp_123",
2188 "object": "response",
2189 "created_at": 0,
2190 "status": "completed",
2191 "model": "Qwen/Qwen3-4B",
2192 "reasoning": "thinking only",
2193 "usage": {
2194 "input_tokens": 1,
2195 "output_tokens": 2,
2196 "total_tokens": 3
2197 },
2198 "output": [],
2199 "tools": []
2200 }))
2201 .expect("reasoning-only response should deserialize");
2202
2203 let completion: completion::CompletionResponse<CompletionResponse> = response
2204 .try_into()
2205 .expect("reasoning-only response should convert");
2206 let items = completion.choice.iter().collect::<Vec<_>>();
2207
2208 assert_eq!(items.len(), 1);
2209 assert!(matches!(
2210 items[0],
2211 completion::AssistantContent::Reasoning(_)
2212 ));
2213 }
2214
2215 #[test]
2216 fn completion_response_rejects_empty_response_without_reasoning() {
2217 let response: CompletionResponse = serde_json::from_value(json!({
2218 "id": "resp_123",
2219 "object": "response",
2220 "created_at": 0,
2221 "status": "completed",
2222 "model": "Qwen/Qwen3-4B",
2223 "output": [],
2224 "tools": []
2225 }))
2226 .expect("empty response shape should deserialize");
2227
2228 let err = completion::CompletionResponse::<CompletionResponse>::try_from(response)
2229 .expect_err("empty response without reasoning should be rejected");
2230
2231 assert!(
2232 err.to_string()
2233 .contains("Response contained no message or tool call")
2234 );
2235 }
2236
2237 #[test]
2238 fn completion_response_ignores_top_level_reasoning_object_as_text() {
2239 let response: CompletionResponse = serde_json::from_value(json!({
2240 "id": "resp_123",
2241 "object": "response",
2242 "created_at": 0,
2243 "status": "completed",
2244 "model": "Qwen/Qwen3-4B",
2245 "reasoning": {
2246 "effort": "high"
2247 },
2248 "output": [{
2249 "type": "message",
2250 "id": "msg_123",
2251 "status": "completed",
2252 "role": "assistant",
2253 "content": [{
2254 "type": "output_text",
2255 "annotations": [],
2256 "text": "done"
2257 }]
2258 }],
2259 "tools": []
2260 }))
2261 .expect("object-shaped reasoning should be tolerated");
2262
2263 assert!(response.provider_reasoning.is_none());
2264
2265 let completion: completion::CompletionResponse<CompletionResponse> =
2266 response.try_into().expect("response should convert");
2267 let items = completion.choice.iter().collect::<Vec<_>>();
2268 assert_eq!(items.len(), 1);
2269 assert!(matches!(items[0], completion::AssistantContent::Text(_)));
2270 }
2271
2272 #[test]
2273 fn completion_response_does_not_duplicate_structured_reasoning() {
2274 let response: CompletionResponse = serde_json::from_value(json!({
2275 "id": "resp_123",
2276 "object": "response",
2277 "created_at": 0,
2278 "status": "completed",
2279 "model": "gpt-5.4",
2280 "reasoning": "provider top-level text",
2281 "output": [{
2282 "type": "reasoning",
2283 "id": "rs_123",
2284 "summary": [{
2285 "type": "summary_text",
2286 "text": "structured summary"
2287 }]
2288 }, {
2289 "type": "message",
2290 "id": "msg_123",
2291 "status": "completed",
2292 "role": "assistant",
2293 "content": [{
2294 "type": "output_text",
2295 "annotations": [],
2296 "text": "done"
2297 }]
2298 }],
2299 "tools": []
2300 }))
2301 .expect("response should deserialize");
2302
2303 let completion: completion::CompletionResponse<CompletionResponse> =
2304 response.try_into().expect("response should convert");
2305 let reasoning_count = completion
2306 .choice
2307 .iter()
2308 .filter(|item| matches!(item, completion::AssistantContent::Reasoning(_)))
2309 .count();
2310
2311 assert_eq!(reasoning_count, 1);
2312 }
2313
2314 #[test]
2315 fn idless_reasoning_is_skipped_when_converting_responses_history() {
2316 let assistant = message::Message::Assistant {
2317 id: Some("msg_123".to_string()),
2318 content: OneOrMany::one(message::AssistantContent::Reasoning(
2319 message::Reasoning::new("provider reasoning"),
2320 )),
2321 };
2322
2323 let converted = Vec::<Message>::try_from(assistant)
2324 .expect("idless reasoning should degrade gracefully");
2325
2326 assert!(converted.is_empty());
2327 }
2328
2329 #[test]
2330 fn idless_reasoning_only_is_skipped_without_empty_input_item() {
2331 let assistant = completion::Message::Assistant {
2332 id: None,
2333 content: OneOrMany::one(message::AssistantContent::Reasoning(
2334 message::Reasoning::new("provider reasoning"),
2335 )),
2336 };
2337
2338 let converted = Vec::<InputItem>::try_from(assistant)
2339 .expect("idless reasoning should degrade gracefully");
2340
2341 assert!(converted.is_empty());
2342 }
2343
2344 #[test]
2345 fn idless_reasoning_plus_text_preserves_text_for_responses_history() {
2346 let assistant = message::Message::Assistant {
2347 id: Some("msg_123".to_string()),
2348 content: OneOrMany::many(vec![
2349 message::AssistantContent::Reasoning(message::Reasoning::new("provider reasoning")),
2350 message::AssistantContent::Text(Text::new("final answer")),
2351 ])
2352 .expect("assistant content should be non-empty"),
2353 };
2354
2355 let converted =
2356 Vec::<Message>::try_from(assistant).expect("assistant history should convert");
2357
2358 assert_eq!(converted.len(), 1);
2359 let Message::Assistant { content, .. } = &converted[0] else {
2360 panic!("expected assistant message");
2361 };
2362 assert!(matches!(
2363 content.first_ref(),
2364 AssistantContentType::Text(AssistantContent::InputText { text }) if text == "final answer"
2365 ));
2366 }
2367
2368 #[test]
2369 fn completion_history_idless_reasoning_plus_text_preserves_text_input_item() {
2370 let assistant = completion::Message::Assistant {
2371 id: Some("msg_123".to_string()),
2372 content: OneOrMany::many(vec![
2373 message::AssistantContent::Reasoning(message::Reasoning::new("provider reasoning")),
2374 message::AssistantContent::Text(Text::new("final answer")),
2375 ])
2376 .expect("assistant content should be non-empty"),
2377 };
2378
2379 let converted =
2380 Vec::<InputItem>::try_from(assistant).expect("assistant history should convert");
2381
2382 assert_eq!(converted.len(), 1);
2383 assert!(matches!(converted[0].role, Some(Role::Assistant)));
2384 let InputContent::Message(Message::Assistant { content, .. }) = &converted[0].input else {
2385 panic!("expected assistant message input item");
2386 };
2387 assert!(matches!(
2388 content.first_ref(),
2389 AssistantContentType::Text(AssistantContent::InputText { text }) if text == "final answer"
2390 ));
2391 }
2392
2393 #[test]
2394 fn assistant_text_without_idless_reasoning_replays_as_output_text() {
2395 let assistant = completion::Message::Assistant {
2396 id: Some("msg_123".to_string()),
2397 content: OneOrMany::one(message::AssistantContent::Text(Text::new("final answer"))),
2398 };
2399
2400 let converted =
2401 Vec::<InputItem>::try_from(assistant).expect("assistant history should convert");
2402
2403 assert_eq!(converted.len(), 1);
2404 let InputContent::Message(Message::Assistant { content, .. }) = &converted[0].input else {
2405 panic!("expected assistant message input item");
2406 };
2407 assert!(matches!(
2408 content.first_ref(),
2409 AssistantContentType::Text(AssistantContent::OutputText(Text { text, .. })) if text == "final answer"
2410 ));
2411 }
2412
2413 #[test]
2414 fn idless_completion_assistant_text_replays_as_input_text() {
2415 let assistant = completion::Message::Assistant {
2416 id: None,
2417 content: OneOrMany::one(message::AssistantContent::Text(Text::new("final answer"))),
2418 };
2419
2420 let converted =
2421 Vec::<InputItem>::try_from(assistant).expect("assistant history should convert");
2422
2423 assert_eq!(converted.len(), 1);
2424 assert!(matches!(converted[0].role, Some(Role::Assistant)));
2425 let InputContent::Message(Message::Assistant { content, id, .. }) = &converted[0].input
2426 else {
2427 panic!("expected assistant message input item");
2428 };
2429 assert!(id.is_empty());
2430 assert!(matches!(
2431 content.first_ref(),
2432 AssistantContentType::Text(AssistantContent::InputText { text }) if text == "final answer"
2433 ));
2434
2435 let serialized =
2436 serde_json::to_value(&converted[0]).expect("input item should serialize to JSON");
2437 assert_eq!(serialized["content"][0]["type"], json!("input_text"));
2438 assert!(serialized.get("id").is_none());
2439 }
2440
2441 #[test]
2442 fn idless_message_assistant_text_replays_as_input_text() {
2443 let assistant = message::Message::Assistant {
2444 id: None,
2445 content: OneOrMany::one(message::AssistantContent::Text(Text::new("final answer"))),
2446 };
2447
2448 let converted =
2449 Vec::<Message>::try_from(assistant).expect("assistant history should convert");
2450
2451 assert_eq!(converted.len(), 1);
2452 let Message::Assistant { content, id, .. } = &converted[0] else {
2453 panic!("expected assistant message");
2454 };
2455 assert!(id.is_empty());
2456 assert!(matches!(
2457 content.first_ref(),
2458 AssistantContentType::Text(AssistantContent::InputText { text }) if text == "final answer"
2459 ));
2460
2461 let serialized = serde_json::to_value(&converted[0])
2462 .expect("assistant message should serialize to JSON");
2463 assert_eq!(serialized["content"][0]["type"], json!("input_text"));
2464 assert!(serialized.get("id").is_none());
2465 }
2466
2467 #[test]
2468 fn structured_reasoning_with_id_still_converts_for_responses_history() {
2469 let assistant = message::Message::Assistant {
2470 id: Some("msg_123".to_string()),
2471 content: OneOrMany::one(message::AssistantContent::Reasoning(message::Reasoning {
2472 id: Some("rs_123".to_string()),
2473 content: vec![message::ReasoningContent::Summary(
2474 "structured summary".to_string(),
2475 )],
2476 })),
2477 };
2478
2479 let converted =
2480 Vec::<Message>::try_from(assistant).expect("structured reasoning should still convert");
2481
2482 assert_eq!(converted.len(), 1);
2483 let Message::Assistant { content, .. } = &converted[0] else {
2484 panic!("expected assistant message");
2485 };
2486 assert!(matches!(
2487 content.first_ref(),
2488 AssistantContentType::Reasoning(OpenAIReasoning { id, .. }) if id == "rs_123"
2489 ));
2490 }
2491
2492 #[test]
2493 fn structured_reasoning_with_id_still_converts_to_input_item() {
2494 let assistant = completion::Message::Assistant {
2495 id: Some("msg_123".to_string()),
2496 content: OneOrMany::one(message::AssistantContent::Reasoning(message::Reasoning {
2497 id: Some("rs_123".to_string()),
2498 content: vec![message::ReasoningContent::Summary(
2499 "structured summary".to_string(),
2500 )],
2501 })),
2502 };
2503
2504 let converted =
2505 Vec::<InputItem>::try_from(assistant).expect("structured reasoning should convert");
2506
2507 assert_eq!(converted.len(), 1);
2508 assert!(converted[0].role.is_none());
2509 assert!(matches!(
2510 &converted[0].input,
2511 InputContent::Reasoning(OpenAIReasoning { id, .. }) if id == "rs_123"
2512 ));
2513 }
2514
2515 #[test]
2516 fn mocked_second_turn_request_omits_unreplayable_reasoning() {
2517 let request = crate::completion::CompletionRequest {
2518 model: None,
2519 preamble: Some("You are concise.".to_string()),
2520 chat_history: OneOrMany::many(vec![
2521 completion::Message::User {
2522 content: OneOrMany::one(message::UserContent::Text(Text::new(
2523 "Think briefly, then answer.",
2524 ))),
2525 },
2526 completion::Message::Assistant {
2527 id: Some("msg_123".to_string()),
2528 content: OneOrMany::many(vec![
2529 message::AssistantContent::Reasoning(message::Reasoning::new(
2530 "provider reasoning",
2531 )),
2532 message::AssistantContent::Text(Text::new("final answer")),
2533 ])
2534 .expect("assistant content should be non-empty"),
2535 },
2536 completion::Message::Assistant {
2537 id: None,
2538 content: OneOrMany::many(vec![
2539 message::AssistantContent::Reasoning(message::Reasoning::new(
2540 "provider reasoning only",
2541 )),
2542 message::AssistantContent::Text(Text::new("")),
2543 ])
2544 .expect("assistant content should be non-empty"),
2545 },
2546 completion::Message::User {
2547 content: OneOrMany::one(message::UserContent::Text(Text::new(
2548 "/no_think Reply with exactly: OK",
2549 ))),
2550 },
2551 ])
2552 .expect("history should be non-empty"),
2553 documents: Vec::new(),
2554 tools: Vec::new(),
2555 temperature: None,
2556 max_tokens: Some(64),
2557 tool_choice: None,
2558 additional_params: None,
2559 output_schema: None,
2560 };
2561
2562 let request = CompletionRequest::try_from(("Qwen/Qwen3-4B".to_string(), request))
2563 .expect("request should convert");
2564 let value = serde_json::to_value(&request).expect("request should serialize");
2565 let input = value["input"]
2566 .as_array()
2567 .expect("mocked multi-turn request should serialize input as an array");
2568
2569 assert!(!input.iter().any(|item| {
2570 item.get("type") == Some(&json!("reasoning")) && item.get("id").is_none()
2571 }));
2572 assert!(!input.iter().any(|item| {
2573 item.get("role") == Some(&json!("assistant"))
2574 && item
2575 .get("content")
2576 .and_then(Value::as_array)
2577 .is_some_and(Vec::is_empty)
2578 }));
2579
2580 let assistant_items = input
2581 .iter()
2582 .filter(|item| item.get("role") == Some(&json!("assistant")))
2583 .collect::<Vec<_>>();
2584
2585 assert_eq!(assistant_items.len(), 1);
2586 assert_eq!(assistant_items[0]["content"][0]["type"], "input_text");
2587 assert_eq!(assistant_items[0]["content"][0]["text"], "final answer");
2588 }
2589
2590 #[test]
2591 fn responses_usage_add_preserves_rhs_details_when_lhs_details_are_absent() {
2592 let lhs = ResponsesUsage {
2593 input_tokens: 10,
2594 input_tokens_details: None,
2595 output_tokens: 20,
2596 output_tokens_details: None,
2597 total_tokens: 30,
2598 };
2599 let rhs = ResponsesUsage {
2600 input_tokens: 3,
2601 input_tokens_details: Some(InputTokensDetails { cached_tokens: 2 }),
2602 output_tokens: 5,
2603 output_tokens_details: Some(OutputTokensDetails {
2604 reasoning_tokens: 4,
2605 }),
2606 total_tokens: 8,
2607 };
2608
2609 let usage = lhs + rhs;
2610 let token_usage = usage.token_usage().expect("usage should be present");
2611
2612 assert_eq!(token_usage.input_tokens, 13);
2613 assert_eq!(token_usage.cached_input_tokens, 2);
2614 assert_eq!(token_usage.output_tokens, 25);
2615 assert_eq!(token_usage.reasoning_tokens, 4);
2616 assert_eq!(token_usage.total_tokens, 38);
2617 }
2618
2619 #[test]
2620 fn file_id_document_serializes_as_input_file_content() {
2621 let message = message::Message::User {
2622 content: OneOrMany::one(message::UserContent::Document(message::Document {
2623 data: DocumentSourceKind::FileId("file_abc".to_string()),
2624 media_type: None,
2625 additional_params: None,
2626 })),
2627 };
2628
2629 let converted: Vec<Message> = message.try_into().expect("conversion should succeed");
2630 let Message::User { content, .. } = &converted[0] else {
2631 panic!("expected user message");
2632 };
2633
2634 let json = serde_json::to_value(content.first_ref()).expect("serialize content");
2635
2636 assert_eq!(json["type"], "input_file");
2637 assert_eq!(json["file_id"], "file_abc");
2638 assert!(json.get("file_data").is_none());
2639 assert!(json.get("file_url").is_none());
2640 }
2641
2642 #[test]
2643 fn file_id_document_serializes_as_input_item_content() {
2644 let message = completion::Message::User {
2645 content: OneOrMany::one(message::UserContent::Document(message::Document {
2646 data: DocumentSourceKind::FileId("file_abc".to_string()),
2647 media_type: None,
2648 additional_params: None,
2649 })),
2650 };
2651
2652 let converted: Vec<InputItem> = message.try_into().expect("conversion should succeed");
2653 let json = serde_json::to_value(&converted[0]).expect("serialize input item");
2654
2655 assert_eq!(json["type"], "message");
2656 assert_eq!(json["role"], "user");
2657 assert_eq!(json["content"][0]["type"], "input_file");
2658 assert_eq!(json["content"][0]["file_id"], "file_abc");
2659 assert!(json["content"][0].get("file_data").is_none());
2660 assert!(json["content"][0].get("file_url").is_none());
2661 }
2662}