1use crate::error::EncodeError;
15use crate::json_utils;
16use crate::json_utils::string_or_vec;
17use crate::message::{
18 Document, DocumentMediaType, DocumentSourceKind, ImageDetail, MessageError, MimeType, Text,
19};
20use crate::{completion, message};
21use serde::{Deserialize, Deserializer, Serialize, Serializer};
22use serde_json::{Map, Value};
23
24use std::convert::Infallible;
25use std::ops::Add;
26use std::str::FromStr;
27
28pub mod streaming;
29#[cfg(feature = "websocket")]
30#[cfg_attr(docsrs, doc(cfg(feature = "websocket")))]
31pub mod websocket;
32pub mod wire;
33
34#[derive(Debug, Deserialize, Serialize, Clone)]
37pub struct CompletionRequest {
38 pub input: Vec<InputItem>,
40 pub model: String,
42 #[serde(skip_serializing_if = "Option::is_none")]
44 pub instructions: Option<String>,
45 #[serde(skip_serializing_if = "Option::is_none")]
47 pub max_output_tokens: Option<u64>,
48 #[serde(skip_serializing_if = "Option::is_none")]
50 pub stream: Option<bool>,
51 #[serde(skip_serializing_if = "Option::is_none")]
53 pub temperature: Option<f64>,
54 #[serde(skip_serializing_if = "Option::is_none")]
57 tool_choice: Option<ToolChoice>,
58 #[serde(skip_serializing_if = "Vec::is_empty")]
61 pub tools: Vec<ResponsesToolDefinition>,
62 #[serde(flatten)]
64 pub additional_parameters: AdditionalParameters,
65}
66
67impl CompletionRequest {
68 pub fn with_tool(mut self, tool: impl Into<ResponsesToolDefinition>) -> Self {
70 self.tools.push(tool.into());
71 self
72 }
73
74 pub fn with_tools<I, Tool>(mut self, tools: I) -> Self
76 where
77 I: IntoIterator<Item = Tool>,
78 Tool: Into<ResponsesToolDefinition>,
79 {
80 self.tools.extend(tools.into_iter().map(Into::into));
81 self
82 }
83}
84
85#[derive(Debug, Deserialize, Clone)]
87pub struct InputItem {
88 #[serde(skip_serializing_if = "Option::is_none")]
92 role: Option<Role>,
93 #[serde(flatten)]
95 input: InputContent,
96}
97
98impl Serialize for InputItem {
99 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
100 where
101 S: serde::Serializer,
102 {
103 let mut value = serde_json::to_value(&self.input).map_err(serde::ser::Error::custom)?;
104 let map = value.as_object_mut().ok_or_else(|| {
105 serde::ser::Error::custom("Input content must serialize to an object")
106 })?;
107
108 if let Some(role) = &self.role
109 && !map.contains_key("role")
110 {
111 map.insert(
112 "role".to_string(),
113 serde_json::to_value(role).map_err(serde::ser::Error::custom)?,
114 );
115 }
116
117 value.serialize(serializer)
118 }
119}
120
121impl InputItem {
122 pub fn system_message(content: impl Into<String>) -> Self {
123 Self {
124 role: Some(Role::System),
125 input: InputContent::Message(Message::System {
126 content: vec![SystemContent::InputText {
127 text: content.into(),
128 }],
129 name: None,
130 }),
131 }
132 }
133
134 fn user_content(content: UserContent) -> Self {
136 Self {
137 role: Some(Role::User),
138 input: InputContent::Message(Message::User {
139 content: vec![content],
140 name: None,
141 }),
142 }
143 }
144
145 pub(crate) fn system_text(&self) -> Option<String> {
146 match &self.input {
147 InputContent::Message(Message::System { content, .. }) => Some(
148 content
149 .iter()
150 .map(|item| match item {
151 SystemContent::InputText { text } => text.as_str(),
152 })
153 .collect::<Vec<_>>()
154 .join("\n"),
155 ),
156 _ => None,
157 }
158 }
159}
160
161#[derive(Debug, Deserialize, Serialize, Clone)]
163#[serde(rename_all = "lowercase")]
164pub enum Role {
165 User,
166 Assistant,
167 System,
168}
169
170#[derive(Debug, Deserialize, Serialize, Clone)]
172#[serde(tag = "type", rename_all = "snake_case")]
173pub enum InputContent {
174 Message(Message),
175 Reasoning(OpenAIReasoning),
176 FunctionCall(OutputFunctionCall),
177 FunctionCallOutput(ToolResult),
178 Compaction(Map<String, Value>),
181}
182
183#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
184pub struct OpenAIReasoning {
185 id: String,
186 pub summary: Vec<ReasoningSummary>,
187 #[serde(
188 default,
189 deserialize_with = "deserialize_reasoning_text_content",
190 serialize_with = "serialize_reasoning_text_content",
191 skip_serializing_if = "Vec::is_empty"
192 )]
193 pub content: Vec<String>,
194 #[serde(skip_serializing_if = "Option::is_none")]
195 pub encrypted_content: Option<String>,
196 #[serde(default, skip_serializing_if = "Option::is_none")]
199 pub signature: Option<String>,
200 #[serde(skip_serializing_if = "Option::is_none")]
201 pub status: Option<ToolStatus>,
202}
203
204#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
205#[serde(tag = "type", rename_all = "snake_case")]
206pub enum ReasoningSummary {
207 SummaryText { text: String },
208}
209
210impl ReasoningSummary {
211 fn new(input: &str) -> Self {
212 Self::SummaryText {
213 text: input.to_string(),
214 }
215 }
216
217 pub fn text(&self) -> &str {
218 let ReasoningSummary::SummaryText { text } = self;
219 text
220 }
221}
222
223fn reasoning_text_content_json(content: &[String]) -> Value {
224 Value::Array(
225 content
226 .iter()
227 .map(|text| {
228 serde_json::json!({
229 "type": "reasoning_text",
230 "text": text,
231 })
232 })
233 .collect(),
234 )
235}
236
237fn serialize_reasoning_text_content<S>(content: &[String], serializer: S) -> Result<S::Ok, S::Error>
238where
239 S: Serializer,
240{
241 reasoning_text_content_json(content).serialize(serializer)
242}
243
244fn deserialize_reasoning_text_content<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
245where
246 D: Deserializer<'de>,
247{
248 let value = Value::deserialize(deserializer)?;
249 Ok(match value {
250 Value::Array(items) => items
251 .into_iter()
252 .filter_map(|item| match item {
253 Value::Object(mut item) => item
254 .remove("text")
255 .and_then(|text| text.as_str().map(ToOwned::to_owned)),
256 Value::String(text) => Some(text),
257 _ => None,
258 })
259 .collect(),
260 Value::String(text) => vec![text],
261 _ => Vec::new(),
262 })
263}
264
265#[derive(Debug, Deserialize, Serialize, Clone)]
267pub struct ToolResult {
268 call_id: String,
270 output: ToolResultOutput,
272 status: ToolStatus,
274}
275
276#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
279#[serde(untagged)]
280pub enum ToolResultOutput {
281 Text(String),
283 Content(Vec<ToolResultOutputContent>),
285}
286
287#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
289#[serde(tag = "type", rename_all = "snake_case")]
290pub enum ToolResultOutputContent {
291 InputText {
293 text: String,
295 },
296 InputImage {
298 #[serde(skip_serializing_if = "Option::is_none")]
300 image_url: Option<String>,
301 #[serde(skip_serializing_if = "Option::is_none")]
304 file_id: Option<String>,
305 #[serde(default)]
307 detail: ImageDetail,
308 },
309}
310
311fn unsupported_document_source(source: DocumentSourceKind) -> EncodeError {
314 match source {
315 DocumentSourceKind::Raw(_) => {
316 EncodeError::request("Raw file data not supported, encode as base64 first")
317 }
318 source => EncodeError::request(format!("Unsupported document type: {source}")),
319 }
320}
321
322fn responses_tool_result_output(
323 content: Vec<message::ToolResultContent>,
324) -> Result<ToolResultOutput, MessageError> {
325 let mut rich_output = Vec::new();
326
327 for content in content {
328 match content {
329 message::ToolResultContent::Text(Text { text, .. }) => {
330 rich_output.push(ToolResultOutputContent::InputText { text });
331 }
332 message::ToolResultContent::Json { value } => {
333 rich_output.push(ToolResultOutputContent::InputText {
334 text: value.to_string(),
335 });
336 }
337 message::ToolResultContent::Image(message::Image {
338 data,
339 media_type,
340 detail,
341 ..
342 }) => {
343 let (image_url, file_id) = match data {
344 DocumentSourceKind::Base64(data) => {
345 let media_type = media_type.ok_or_else(|| {
346 MessageError::ConversionError(
347 "A media type is required for base64 tool-result images".into(),
348 )
349 })?;
350 (
351 Some(format!(
352 "data:{media_type};base64,{data}",
353 media_type = media_type.to_mime_type()
354 )),
355 None,
356 )
357 }
358 DocumentSourceKind::Url(url) => (Some(url), None),
359 DocumentSourceKind::FileId(file_id) => (None, Some(file_id)),
360 unsupported => {
361 return Err(MessageError::ConversionError(format!(
362 "Unsupported tool-result image source: {unsupported}"
363 )));
364 }
365 };
366 rich_output.push(ToolResultOutputContent::InputImage {
367 image_url,
368 file_id,
369 detail: detail.unwrap_or_default(),
370 });
371 }
372 }
373 }
374
375 match rich_output.as_slice() {
376 [ToolResultOutputContent::InputText { text }] => Ok(ToolResultOutput::Text(text.clone())),
377
378 _ => Ok(ToolResultOutput::Content(rich_output)),
379 }
380}
381
382impl TryFrom<crate::completion::Message> for Vec<InputItem> {
386 type Error = EncodeError;
387
388 fn try_from(value: crate::completion::Message) -> Result<Self, Self::Error> {
389 input_items(value, &[])
390 }
391}
392
393fn input_items(
395 value: crate::completion::Message,
396 issuers: &[crate::message::Issuer],
397) -> Result<Vec<InputItem>, EncodeError> {
398 {
399 match value {
400 crate::completion::Message::System { content } => Ok(vec![InputItem {
401 role: Some(Role::System),
402 input: InputContent::Message(Message::System {
403 content: vec![content.into()],
404 name: None,
405 }),
406 }]),
407 crate::completion::Message::User { content } => {
408 let mut items = Vec::new();
409
410 for user_content in content {
411 match user_content {
412 crate::message::UserContent::Text(Text { text, .. }) => {
413 items.push(InputItem::user_content(UserContent::InputText { text }));
414 }
415 crate::message::UserContent::ToolResult(tool_result) => {
416 let call_id = tool_result.call.wire().into_owned();
418 let output = responses_tool_result_output(tool_result.content)?;
419 items.push(InputItem {
420 role: None,
421 input: InputContent::FunctionCallOutput(ToolResult {
422 call_id,
423 output,
424 status: ToolStatus::Completed,
425 }),
426 });
427 }
428 crate::message::UserContent::Document(Document {
429 data: DocumentSourceKind::FileId(file_id),
430 ..
431 }) => items.push(InputItem::user_content(UserContent::InputFile {
432 file_id: Some(file_id),
433 file_data: None,
434 file_url: None,
435 filename: None,
436 })),
437 crate::message::UserContent::Document(Document {
438 data,
439 media_type: Some(DocumentMediaType::PDF),
440 ..
441 }) => {
442 let (file_data, file_url, filename) = match data {
443 DocumentSourceKind::Base64(data) => (
444 Some(format!("data:application/pdf;base64,{data}")),
445 None,
446 Some("document.pdf".to_string()),
447 ),
448 DocumentSourceKind::Url(url) => (None, Some(url), None),
449 source => return Err(unsupported_document_source(source)),
450 };
451
452 items.push(InputItem::user_content(UserContent::InputFile {
453 file_id: None,
454 file_data,
455 file_url,
456 filename,
457 }));
458 }
459 crate::message::UserContent::Document(Document {
462 data: DocumentSourceKind::Url(url),
463 media_type: None,
464 ..
465 }) => items.push(InputItem::user_content(UserContent::InputFile {
466 file_id: None,
467 file_data: None,
468 file_url: Some(url),
469 filename: None,
470 })),
471 crate::message::UserContent::Document(Document {
472 data:
473 DocumentSourceKind::Base64(text) | DocumentSourceKind::String(text),
474 ..
475 }) => items.push(InputItem::user_content(UserContent::InputText { text })),
476 crate::message::UserContent::Image(crate::message::Image {
477 data,
478 media_type,
479 detail,
480 ..
481 }) => {
482 let url = match data {
483 DocumentSourceKind::Base64(data) => {
484 let media_type = media_type
485 .map(|media_type| media_type.to_mime_type().to_string())
486 .unwrap_or_default();
487 format!("data:{media_type};base64,{data}")
488 }
489 DocumentSourceKind::Url(url) => url,
490 source => return Err(unsupported_document_source(source)),
491 };
492 items.push(InputItem::user_content(UserContent::InputImage {
493 image_url: url,
494 detail: detail.unwrap_or_default(),
495 }));
496 }
497 message => {
498 return Err(EncodeError::request(format!(
499 "Unsupported message: {message:?}"
500 )));
501 }
502 }
503 }
504
505 Ok(items)
506 }
507 crate::completion::Message::Assistant { id, content } => {
508 let mut reasoning_items = Vec::new();
509 let mut other_items: Vec<InputItem> = Vec::new();
510 let mut message_items: Vec<(String, usize)> = Vec::new();
515
516 for assistant_content in content {
517 match assistant_content {
518 crate::message::AssistantContent::Text(Text {
519 text,
520 additional_params,
521 }) => {
522 let Some(message) = assistant_text_replay_message(
523 id.as_deref(),
524 text,
525 additional_params,
526 ) else {
527 continue;
528 };
529 let joined = match &message {
530 Message::Assistant { id: item_id, .. } if !item_id.is_empty() => {
531 message_items
532 .iter()
533 .find(|(seen, _)| seen == item_id)
534 .map(|(_, at)| *at)
535 }
536 _ => None,
537 };
538 match (message, joined) {
539 (Message::Assistant { content: more, .. }, Some(at)) => {
540 if let Some(InputItem {
541 input:
542 InputContent::Message(Message::Assistant {
543 content, ..
544 }),
545 ..
546 }) = other_items.get_mut(at)
547 {
548 content.extend(more);
549 }
550 }
551 (message, _) => {
552 if let Message::Assistant { id: item_id, .. } = &message
553 && !item_id.is_empty()
554 {
555 message_items.push((item_id.clone(), other_items.len()));
556 }
557 other_items.push(InputItem {
558 role: Some(Role::Assistant),
559 input: InputContent::Message(message),
560 });
561 }
562 }
563 }
564 crate::message::AssistantContent::ToolCall(crate::message::ToolCall {
565 id,
566 function,
567 ..
568 }) => {
569 let (call_id, item_id) = match id {
570 crate::message::CallId::Provider(provider) => {
571 (provider.call_id, provider.item_id.unwrap_or_default())
572 }
573 local => (local.wire().into_owned(), String::new()),
574 };
575 other_items.push(InputItem {
576 role: None,
577 input: InputContent::FunctionCall(OutputFunctionCall {
578 arguments: function.arguments.into(),
579 call_id,
580 id: item_id,
581 name: function.name.into(),
582 status: ToolStatus::Completed,
583 }),
584 });
585 }
586 crate::message::AssistantContent::Reasoning(reasoning) => {
587 if let Some(openai_reasoning) = reasoning
589 .open_for(issuers)
590 .and_then(openai_reasoning_from_core)
591 {
592 reasoning_items.push(InputItem {
593 role: None,
594 input: InputContent::Reasoning(openai_reasoning),
595 });
596 }
597 }
598 crate::message::AssistantContent::Image(_) => {
599 return Err(EncodeError::request(
600 "Assistant image content is not supported in OpenAI Responses API"
601 .to_string(),
602 ));
603 }
604 }
605 }
606
607 let mut items = reasoning_items;
608 items.extend(other_items);
609 Ok(items)
610 }
611 }
612 }
613}
614
615pub(crate) fn reasoning_content_blocks(
620 summary: Vec<ReasoningSummary>,
621 content: Vec<String>,
622 encrypted_content: Option<String>,
623 signature: Option<String>,
624) -> Vec<message::ReasoningContent> {
625 let mut blocks = summary
626 .into_iter()
627 .map(|summary| match summary {
628 ReasoningSummary::SummaryText { text } => message::ReasoningContent::Summary(text),
629 })
630 .collect::<Vec<_>>();
631
632 blocks.extend(
633 content
634 .into_iter()
635 .map(|text| message::ReasoningContent::Text {
636 text,
637 signature: None,
638 }),
639 );
640 if let Some(signature) = signature {
641 match blocks.iter_mut().rev().find_map(|block| match block {
642 message::ReasoningContent::Text { signature, .. } => Some(signature),
643 _ => None,
644 }) {
645 Some(slot) => *slot = Some(signature),
646 None => blocks.push(message::ReasoningContent::Text {
647 text: String::new(),
648 signature: Some(signature),
649 }),
650 }
651 }
652
653 if let Some(encrypted_content) = encrypted_content.filter(|content| !content.is_empty()) {
654 blocks.push(message::ReasoningContent::Encrypted(encrypted_content));
655 }
656
657 blocks
658}
659
660fn openai_reasoning_from_core(reasoning: &crate::message::Reasoning) -> Option<OpenAIReasoning> {
661 let id = reasoning.id.clone()?;
663
664 let mut summary = Vec::new();
665 let mut reasoning_content = Vec::new();
666 let mut encrypted_content = None;
667 let mut text_signature = None;
668 for content in &reasoning.content {
669 match content {
670 crate::message::ReasoningContent::Text { text, signature } => {
671 if !(text.is_empty() && signature.is_some()) {
673 reasoning_content.push(text.clone());
674 }
675 if let Some(signature) = signature {
678 text_signature = Some(signature.clone());
679 }
680 }
681 crate::message::ReasoningContent::Summary(text) => {
682 summary.push(ReasoningSummary::new(text));
683 }
684 crate::message::ReasoningContent::Encrypted(data)
687 | crate::message::ReasoningContent::Redacted { data } => {
688 encrypted_content.get_or_insert_with(|| data.clone());
689 }
690 }
691 }
692
693 Some(OpenAIReasoning {
694 id,
695 summary,
696 content: reasoning_content,
697 encrypted_content,
698 signature: text_signature,
699 status: None,
700 })
701}
702
703#[derive(Debug, Deserialize, Clone, PartialEq)]
705pub struct ResponsesToolDefinition {
706 #[serde(rename = "type")]
708 pub kind: String,
709 #[serde(default)]
711 pub name: String,
712 #[serde(default)]
714 pub parameters: serde_json::Value,
715 #[serde(default, deserialize_with = "json_utils::null_or_default")]
723 pub strict: bool,
724 #[serde(default)]
726 pub description: String,
727 #[serde(flatten, default)]
729 pub config: Map<String, Value>,
730}
731
732impl Serialize for ResponsesToolDefinition {
733 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
734 where
735 S: Serializer,
736 {
737 use serde::ser::SerializeMap;
738
739 let mut map = serializer.serialize_map(None)?;
740 map.serialize_entry("type", &self.kind)?;
741 if !self.name.is_empty() {
742 map.serialize_entry("name", &self.name)?;
743 }
744 if !self.parameters.is_null() {
745 map.serialize_entry("parameters", &self.parameters)?;
746 }
747 if self.kind == "function" {
748 map.serialize_entry("strict", &self.strict)?;
749 }
750 if !self.description.is_empty() {
751 map.serialize_entry("description", &self.description)?;
752 }
753 for (key, value) in &self.config {
754 map.serialize_entry(key, value)?;
755 }
756 map.end()
757 }
758}
759
760impl ResponsesToolDefinition {
761 pub fn function(
763 name: impl Into<String>,
764 description: impl Into<String>,
765 parameters: serde_json::Value,
766 ) -> Self {
767 Self {
768 kind: "function".to_string(),
769 name: name.into(),
770 parameters,
771 strict: false,
772 description: description.into(),
773 config: Map::new(),
774 }
775 }
776
777 pub fn strict_function(
782 name: impl Into<String>,
783 description: impl Into<String>,
784 parameters: serde_json::Value,
785 ) -> Self {
786 Self::function(name, description, parameters).with_strict()
787 }
788
789 pub fn with_strict(mut self) -> Self {
794 if self.kind == "function" {
795 super::sanitize_schema(&mut self.parameters);
796 self.strict = true;
797 }
798 self
799 }
800
801 pub fn hosted(kind: impl Into<String>) -> Self {
803 Self {
804 kind: kind.into(),
805 name: String::new(),
806 parameters: Value::Null,
807 strict: false,
808 description: String::new(),
809 config: Map::new(),
810 }
811 }
812
813 pub fn web_search() -> Self {
815 Self::hosted("web_search")
816 }
817
818 pub fn file_search() -> Self {
820 Self::hosted("file_search")
821 }
822
823 pub fn computer_use() -> Self {
825 Self::hosted("computer_use")
826 }
827
828 pub fn with_config(mut self, key: impl Into<String>, value: Value) -> Self {
830 self.config.insert(key.into(), value);
831 self
832 }
833
834 fn normalize(self) -> Self {
835 self.with_strict()
836 }
837}
838
839impl From<completion::ToolDefinition> for ResponsesToolDefinition {
840 fn from(value: completion::ToolDefinition) -> Self {
841 let completion::ToolDefinition {
842 name,
843 parameters,
844 description,
845 } = value;
846
847 Self::function(name, description, parameters)
848 }
849}
850
851#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
853#[serde(untagged)]
854pub enum ToolChoice {
855 Mode(super::completion::ToolChoice),
858 Definition(ToolChoiceDefinition),
860}
861
862#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
864#[serde(tag = "type", rename_all = "snake_case")]
865pub enum ToolChoiceDefinition {
866 Function {
868 name: String,
870 },
871 AllowedTools {
873 mode: AllowedToolsMode,
876 tools: Vec<AllowedTool>,
878 },
879}
880
881#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq)]
884#[serde(rename_all = "snake_case")]
885pub enum AllowedToolsMode {
886 Auto,
888 Required,
890}
891
892#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
894#[serde(tag = "type", rename_all = "snake_case")]
895pub enum AllowedTool {
896 Function {
898 name: String,
900 },
901}
902
903impl TryFrom<message::ToolChoice> for ToolChoice {
904 type Error = EncodeError;
905
906 fn try_from(value: message::ToolChoice) -> Result<Self, Self::Error> {
907 let choice = match value {
908 message::ToolChoice::Auto => Self::Mode(super::completion::ToolChoice::Auto),
909 message::ToolChoice::None => Self::Mode(super::completion::ToolChoice::None),
910 message::ToolChoice::Required => Self::Mode(super::completion::ToolChoice::Required),
911 message::ToolChoice::Specific { function_names } => {
912 let mut names = function_names.into_iter();
913 let Some(first) = names.next() else {
914 return Err(EncodeError::request(
915 "ToolChoice::Specific requires at least one function name",
916 ));
917 };
918
919 match names.next() {
920 None => Self::Definition(ToolChoiceDefinition::Function { name: first }),
921 Some(second) => {
922 let tools = std::iter::once(first)
923 .chain(std::iter::once(second))
924 .chain(names)
925 .map(|name| AllowedTool::Function { name })
926 .collect();
927 Self::Definition(ToolChoiceDefinition::AllowedTools {
928 mode: AllowedToolsMode::Required,
929 tools,
930 })
931 }
932 }
933 }
934 };
935
936 Ok(choice)
937 }
938}
939
940#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
942pub struct ResponsesUsage {
943 pub input_tokens: u64,
945 #[serde(skip_serializing_if = "Option::is_none")]
947 pub input_tokens_details: Option<InputTokensDetails>,
948 pub output_tokens: u64,
950 #[serde(skip_serializing_if = "Option::is_none")]
952 pub output_tokens_details: Option<OutputTokensDetails>,
953 pub total_tokens: u64,
955}
956
957impl From<&ResponsesUsage> for crate::completion::Usage {
958 fn from(usage: &ResponsesUsage) -> Self {
959 crate::completion::Usage {
960 input_tokens: Some(usage.input_tokens),
961 output_tokens: Some(usage.output_tokens),
962 total_tokens: Some(usage.total_tokens),
963 cached_input_tokens: usage
964 .input_tokens_details
965 .as_ref()
966 .map(|details| details.cached_tokens),
967 cache_creation_input_tokens: usage
968 .input_tokens_details
969 .as_ref()
970 .and_then(|details| details.cache_write_tokens),
971 reasoning_tokens: usage
972 .output_tokens_details
973 .as_ref()
974 .map(|details| details.reasoning_tokens),
975 ..Default::default()
976 }
977 }
978}
979
980impl From<ResponsesUsage> for crate::completion::Usage {
981 fn from(usage: ResponsesUsage) -> Self {
982 Self::from(&usage)
983 }
984}
985
986fn add_optional_details<T: Add<Output = T>>(lhs: Option<T>, rhs: Option<T>) -> Option<T> {
988 match (lhs, rhs) {
989 (Some(lhs), Some(rhs)) => Some(lhs + rhs),
990 (lhs, rhs) => lhs.or(rhs),
991 }
992}
993
994impl Add for ResponsesUsage {
995 type Output = Self;
996
997 fn add(self, rhs: Self) -> Self::Output {
998 Self {
999 input_tokens: self.input_tokens + rhs.input_tokens,
1000 input_tokens_details: add_optional_details(
1001 self.input_tokens_details,
1002 rhs.input_tokens_details,
1003 ),
1004 output_tokens: self.output_tokens + rhs.output_tokens,
1005 output_tokens_details: add_optional_details(
1006 self.output_tokens_details,
1007 rhs.output_tokens_details,
1008 ),
1009 total_tokens: self.total_tokens + rhs.total_tokens,
1010 }
1011 }
1012}
1013
1014#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
1016pub struct InputTokensDetails {
1017 pub cached_tokens: u64,
1019 #[serde(default, skip_serializing_if = "Option::is_none")]
1022 pub cache_write_tokens: Option<u64>,
1023}
1024
1025impl Add for InputTokensDetails {
1026 type Output = Self;
1027 fn add(self, rhs: Self) -> Self::Output {
1028 Self {
1029 cached_tokens: self.cached_tokens + rhs.cached_tokens,
1030 cache_write_tokens: add_optional_details(
1031 self.cache_write_tokens,
1032 rhs.cache_write_tokens,
1033 ),
1034 }
1035 }
1036}
1037
1038#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
1040pub struct OutputTokensDetails {
1041 pub reasoning_tokens: u64,
1043}
1044
1045impl Add for OutputTokensDetails {
1046 type Output = Self;
1047 fn add(self, rhs: Self) -> Self::Output {
1048 Self {
1049 reasoning_tokens: self.reasoning_tokens + rhs.reasoning_tokens,
1050 }
1051 }
1052}
1053
1054#[derive(Clone, Debug, Default, Serialize, Deserialize)]
1056pub struct IncompleteDetailsReason {
1057 pub reason: String,
1059}
1060
1061#[derive(Clone, Debug, Default, Serialize, Deserialize)]
1063pub struct ResponseError {
1064 pub code: String,
1066 pub message: String,
1068}
1069
1070#[derive(Clone, Debug, Deserialize, Serialize)]
1072#[serde(rename_all = "snake_case")]
1073pub enum ResponseObject {
1074 Response,
1075}
1076
1077#[derive(Clone, Debug, PartialEq)]
1079pub enum ResponseStatus {
1080 InProgress,
1081 Completed,
1082 Failed,
1083 Cancelled,
1084 Queued,
1085 Incomplete,
1086 Other(String),
1088}
1089
1090fn response_status_wire_name(status: &ResponseStatus) -> &str {
1096 match status {
1097 ResponseStatus::InProgress => "in_progress",
1098 ResponseStatus::Completed => "completed",
1099 ResponseStatus::Failed => "failed",
1100 ResponseStatus::Cancelled => "cancelled",
1101 ResponseStatus::Queued => "queued",
1102 ResponseStatus::Incomplete => "incomplete",
1103 ResponseStatus::Other(status) => status,
1104 }
1105}
1106
1107impl Serialize for ResponseStatus {
1108 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1109 where
1110 S: Serializer,
1111 {
1112 serializer.serialize_str(response_status_wire_name(self))
1113 }
1114}
1115
1116impl<'de> Deserialize<'de> for ResponseStatus {
1117 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1118 where
1119 D: Deserializer<'de>,
1120 {
1121 Ok(match String::deserialize(deserializer)?.as_str() {
1122 "in_progress" => Self::InProgress,
1123 "completed" => Self::Completed,
1124 "failed" => Self::Failed,
1125 "cancelled" => Self::Cancelled,
1126 "queued" => Self::Queued,
1127 "incomplete" => Self::Incomplete,
1128 other => Self::Other(other.to_owned()),
1129 })
1130 }
1131}
1132
1133pub(crate) fn map_finish_reason(
1137 status: &ResponseStatus,
1138 incomplete_details: Option<&IncompleteDetailsReason>,
1139) -> Option<completion::FinishReason> {
1140 match status {
1141 ResponseStatus::Completed => Some(completion::FinishReason::Stop),
1142 ResponseStatus::Incomplete => Some(
1143 match incomplete_details
1144 .map(|details| details.reason.as_str())
1145 .filter(|reason| !reason.is_empty())
1146 {
1147 Some("max_output_tokens") => completion::FinishReason::Length,
1148 Some("content_filter") => completion::FinishReason::ContentFilter,
1149 Some(other) => completion::FinishReason::Other(other.to_owned()),
1150 None => {
1153 completion::FinishReason::Other(response_status_wire_name(status).to_owned())
1154 }
1155 },
1156 ),
1157 ResponseStatus::Other(status) if status.is_empty() => None,
1158 ResponseStatus::Failed | ResponseStatus::Cancelled | ResponseStatus::Other(_) => Some(
1159 completion::FinishReason::Other(response_status_wire_name(status).to_owned()),
1160 ),
1161 ResponseStatus::InProgress | ResponseStatus::Queued => None,
1163 }
1164}
1165
1166#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1171#[serde(rename_all = "snake_case")]
1172pub enum SystemInstructionsPlacement {
1173 #[default]
1178 Instructions,
1179 AllInstructions,
1184 InputSystemMessages,
1189}
1190
1191impl TryFrom<(String, crate::completion::CompletionRequest)> for CompletionRequest {
1193 type Error = EncodeError;
1194 fn try_from(
1195 (model, request): (String, crate::completion::CompletionRequest),
1196 ) -> Result<Self, Self::Error> {
1197 Self::try_from(ResponsesRequestParams {
1198 model,
1199 request,
1200 system_instructions_placement: SystemInstructionsPlacement::default(),
1201 issuers: Vec::new(),
1202 })
1203 }
1204}
1205
1206pub struct ResponsesRequestParams {
1209 pub model: String,
1210 pub request: crate::completion::CompletionRequest,
1211 pub system_instructions_placement: SystemInstructionsPlacement,
1212 pub issuers: Vec<crate::message::Issuer>,
1214}
1215
1216impl TryFrom<ResponsesRequestParams> for CompletionRequest {
1217 type Error = EncodeError;
1218
1219 fn try_from(params: ResponsesRequestParams) -> Result<Self, Self::Error> {
1220 let ResponsesRequestParams {
1221 model,
1222 request: mut req,
1223 system_instructions_placement,
1224 issuers,
1225 } = params;
1226 let chat_history = req.chat_history_with_documents();
1227 let model = req.model.clone().unwrap_or(model);
1228 let mut instruction_parts = Vec::new();
1229 let mut input = {
1230 let mut full_history: Vec<InputItem> = Vec::new();
1231 let tool_ids = crate::providers::internal::wire_ids::WireIds::new(&chat_history);
1232 for (position, history_item) in chat_history.into_iter().enumerate() {
1233 let mut items = input_items(history_item, &issuers)?;
1234 tool_ids
1235 .apply(
1236 position,
1237 items.iter_mut().filter_map(|item| match &mut item.input {
1238 InputContent::FunctionCall(call) => Some(&mut call.call_id),
1239 InputContent::FunctionCallOutput(result) => Some(&mut result.call_id),
1240 _ => None,
1241 }),
1242 )
1243 .map_err(EncodeError::request)?;
1244 full_history.extend(items);
1245 }
1246 full_history
1247 };
1248
1249 let mut lift_system_text = |text: String| {
1250 let text = text.trim();
1251 if !text.is_empty() {
1252 instruction_parts.push(text.to_string());
1253 }
1254 };
1255 let items_before_lift = input.len();
1256 match system_instructions_placement {
1257 SystemInstructionsPlacement::Instructions => {
1258 let leading_system_texts: Vec<String> =
1264 input.iter().map_while(InputItem::system_text).collect();
1265 if leading_system_texts.len() < input.len() {
1266 input.drain(..leading_system_texts.len());
1267 leading_system_texts
1268 .into_iter()
1269 .for_each(&mut lift_system_text);
1270 }
1271 }
1272 SystemInstructionsPlacement::AllInstructions => {
1273 let mut remaining = Vec::with_capacity(input.len());
1276 for item in input {
1277 match item.system_text() {
1278 Some(text) => lift_system_text(text),
1279 None => remaining.push(item),
1280 }
1281 }
1282 input = remaining;
1283 }
1284 SystemInstructionsPlacement::InputSystemMessages => {}
1285 }
1286 let instructions = (!instruction_parts.is_empty()).then(|| instruction_parts.join("\n\n"));
1287 let lifted_system_items = input.len() < items_before_lift;
1288
1289 let input = crate::message::require_non_empty(input, || {
1290 EncodeError::request(if lifted_system_items {
1291 "OpenAI Responses request input must contain at least one non-system item \
1292 (system messages were lifted into the top-level `instructions` field)"
1293 } else {
1294 "OpenAI Responses request input must contain at least one item"
1295 })
1296 })?;
1297
1298 let mut additional_params_payload = req.additional_params.take().unwrap_or(Value::Null);
1299 let stream = match &additional_params_payload {
1300 Value::Bool(stream) => Some(*stream),
1301 Value::Object(map) => map.get("stream").and_then(Value::as_bool),
1302 _ => None,
1303 };
1304
1305 let mut additional_tools = Vec::new();
1306 if let Some(additional_params_map) = additional_params_payload.as_object_mut() {
1307 if let Some(raw_tools) = additional_params_map.remove("tools") {
1308 additional_tools = serde_json::from_value::<Vec<ResponsesToolDefinition>>(
1309 raw_tools,
1310 )
1311 .map_err(|err| {
1312 EncodeError::request(format!(
1313 "Invalid OpenAI Responses tools payload in additional_params: {err}"
1314 ))
1315 })?;
1316 }
1317 additional_params_map.remove("stream");
1318 }
1319
1320 if additional_params_payload.is_boolean() {
1321 additional_params_payload = Value::Null;
1322 }
1323
1324 let mut additional_parameters = if additional_params_payload.is_null() {
1325 AdditionalParameters::default()
1327 } else {
1328 serde_json::from_value::<AdditionalParameters>(additional_params_payload).map_err(
1329 |err| {
1330 EncodeError::request(format!(
1331 "Invalid OpenAI Responses additional_params payload: {err}"
1332 ))
1333 },
1334 )?
1335 };
1336 if additional_parameters.reasoning.is_some() {
1337 let include = additional_parameters.include.get_or_insert_with(Vec::new);
1338 if !include
1339 .iter()
1340 .any(|item| matches!(item, Include::ReasoningEncryptedContent))
1341 {
1342 include.push(Include::ReasoningEncryptedContent);
1343 }
1344 }
1345
1346 if additional_parameters.text.is_none()
1348 && let Some(schema) = req.output_schema
1349 {
1350 let (name, schema_value) = super::structured_output_schema(schema);
1351 additional_parameters.text = Some(TextConfig::structured_output(name, schema_value));
1352 }
1353
1354 let tool_choice = req.tool_choice.map(ToolChoice::try_from).transpose()?;
1355 let mut tools: Vec<ResponsesToolDefinition> = req
1356 .tools
1357 .into_iter()
1358 .map(ResponsesToolDefinition::from)
1359 .collect();
1360 tools.append(&mut additional_tools);
1361
1362 Ok(Self {
1363 input,
1364 model,
1365 instructions,
1366 max_output_tokens: req.max_tokens,
1367 stream,
1368 tool_choice,
1369 tools,
1370 temperature: req.temperature,
1371 additional_parameters,
1372 })
1373 }
1374}
1375
1376#[derive(Clone, Debug)]
1378pub struct CompletionResponse {
1379 pub id: String,
1381 pub object: ResponseObject,
1383 pub created_at: u64,
1385 pub status: ResponseStatus,
1387 pub error: Option<ResponseError>,
1389 pub incomplete_details: Option<IncompleteDetailsReason>,
1391 pub instructions: Option<String>,
1393 pub max_output_tokens: Option<u64>,
1395 pub model: String,
1397 pub provider_reasoning: Option<String>,
1400 pub reasoning_metadata: Option<Map<String, Value>>,
1409 pub reasoning_context: Option<String>,
1415 pub usage: Option<ResponsesUsage>,
1417 pub output: Vec<Output>,
1419 pub tools: Vec<ResponsesToolDefinition>,
1421 pub additional_parameters: AdditionalParameters,
1423}
1424
1425#[derive(Serialize)]
1426#[serde(untagged)]
1427enum CompletionResponseReasoningRef<'a> {
1428 Text(&'a str),
1429 Metadata(&'a Map<String, Value>),
1430 Context { context: &'a str },
1431}
1432
1433#[derive(Serialize)]
1434struct CompletionResponseWireRef<'a> {
1435 id: &'a str,
1436 object: &'a ResponseObject,
1437 created_at: u64,
1438 status: &'a ResponseStatus,
1439 error: &'a Option<ResponseError>,
1440 incomplete_details: &'a Option<IncompleteDetailsReason>,
1441 instructions: &'a Option<String>,
1442 max_output_tokens: &'a Option<u64>,
1443 model: &'a str,
1444 #[serde(skip_serializing_if = "Option::is_none")]
1445 reasoning: Option<CompletionResponseReasoningRef<'a>>,
1446 usage: &'a Option<ResponsesUsage>,
1447 output: &'a Vec<Output>,
1448 tools: &'a Vec<ResponsesToolDefinition>,
1449 #[serde(flatten)]
1450 additional_parameters: &'a AdditionalParameters,
1451}
1452
1453#[derive(Deserialize)]
1456struct CompletionResponseWire {
1457 id: String,
1458 object: ResponseObject,
1459 created_at: u64,
1460 status: ResponseStatus,
1461 error: Option<ResponseError>,
1462 incomplete_details: Option<IncompleteDetailsReason>,
1463 instructions: Option<String>,
1464 max_output_tokens: Option<u64>,
1465 model: String,
1466 #[serde(default)]
1467 reasoning: Option<Value>,
1468 usage: Option<ResponsesUsage>,
1469 #[serde(default)]
1470 output: Vec<Output>,
1471 #[serde(default)]
1472 tools: Vec<ResponsesToolDefinition>,
1473 #[serde(flatten)]
1474 metadata: Map<String, Value>,
1475}
1476
1477impl Serialize for CompletionResponse {
1478 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1479 where
1480 S: Serializer,
1481 {
1482 let mut additional_parameters = self.additional_parameters.clone();
1484 additional_parameters.reasoning = None;
1485
1486 let reasoning = self
1487 .provider_reasoning
1488 .as_deref()
1489 .map(CompletionResponseReasoningRef::Text)
1490 .or_else(|| {
1491 self.reasoning_metadata
1492 .as_ref()
1493 .map(CompletionResponseReasoningRef::Metadata)
1494 })
1495 .or_else(|| {
1496 self.reasoning_context
1497 .as_deref()
1498 .map(|context| CompletionResponseReasoningRef::Context { context })
1499 });
1500
1501 CompletionResponseWireRef {
1502 id: &self.id,
1503 object: &self.object,
1504 created_at: self.created_at,
1505 status: &self.status,
1506 error: &self.error,
1507 incomplete_details: &self.incomplete_details,
1508 instructions: &self.instructions,
1509 max_output_tokens: &self.max_output_tokens,
1510 model: &self.model,
1511 reasoning,
1512 usage: &self.usage,
1513 output: &self.output,
1514 tools: &self.tools,
1515 additional_parameters: &additional_parameters,
1516 }
1517 .serialize(serializer)
1518 }
1519}
1520
1521impl<'de> Deserialize<'de> for CompletionResponse {
1522 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1523 where
1524 D: Deserializer<'de>,
1525 {
1526 let response = CompletionResponseWire::deserialize(deserializer)?;
1527 let (provider_reasoning, reasoning_metadata) = match response.reasoning {
1528 Some(Value::String(reasoning)) => (Some(reasoning), None),
1529 Some(Value::Object(metadata)) => (None, Some(metadata)),
1530 _ => (None, None),
1532 };
1533 let reasoning_context = reasoning_metadata
1534 .as_ref()
1535 .and_then(|reasoning| reasoning.get("context"))
1536 .and_then(Value::as_str)
1537 .map(ToOwned::to_owned);
1538
1539 Ok(Self {
1540 id: response.id,
1541 object: response.object,
1542 created_at: response.created_at,
1543 status: response.status,
1544 error: response.error,
1545 incomplete_details: response.incomplete_details,
1546 instructions: response.instructions,
1547 max_output_tokens: response.max_output_tokens,
1548 model: response.model,
1549 provider_reasoning,
1550 reasoning_metadata,
1551 reasoning_context,
1552 usage: response.usage,
1553 output: response.output,
1554 tools: response.tools,
1555 additional_parameters: AdditionalParameters::from_response_metadata(response.metadata),
1556 })
1557 }
1558}
1559
1560#[derive(Clone, Debug, Deserialize, Serialize, Default)]
1563pub struct AdditionalParameters {
1564 #[serde(skip_serializing_if = "Option::is_none")]
1566 pub background: Option<bool>,
1567 #[serde(skip_serializing_if = "Option::is_none")]
1569 pub text: Option<TextConfig>,
1570 #[serde(skip_serializing_if = "Option::is_none")]
1572 pub include: Option<Vec<Include>>,
1573 #[serde(skip_serializing_if = "Option::is_none")]
1575 pub top_p: Option<f64>,
1576 #[serde(skip_serializing_if = "Option::is_none")]
1578 pub truncation: Option<TruncationStrategy>,
1579 #[serde(skip_serializing_if = "Option::is_none")]
1581 pub user: Option<String>,
1582 #[serde(skip_serializing_if = "Option::is_none")]
1584 pub prompt_cache_key: Option<String>,
1585 #[serde(skip_serializing_if = "Option::is_none")]
1587 pub prompt_cache_retention: Option<String>,
1588 #[serde(
1590 skip_serializing_if = "Map::is_empty",
1591 default,
1592 deserialize_with = "deserialize_metadata"
1593 )]
1594 pub metadata: serde_json::Map<String, serde_json::Value>,
1595 #[serde(skip_serializing_if = "Option::is_none")]
1597 pub parallel_tool_calls: Option<bool>,
1598 #[serde(skip_serializing_if = "Option::is_none")]
1600 pub previous_response_id: Option<String>,
1601 #[serde(skip_serializing_if = "Option::is_none")]
1603 pub reasoning: Option<Reasoning>,
1604 #[serde(skip_serializing_if = "Option::is_none")]
1606 pub service_tier: Option<OpenAIServiceTier>,
1607 #[serde(skip_serializing_if = "Option::is_none")]
1609 pub store: Option<bool>,
1610}
1611
1612fn deserialize_metadata<'de, D>(
1613 deserializer: D,
1614) -> Result<serde_json::Map<String, serde_json::Value>, D::Error>
1615where
1616 D: Deserializer<'de>,
1617{
1618 Ok(
1619 Option::<serde_json::Map<String, serde_json::Value>>::deserialize(deserializer)?
1620 .unwrap_or_default(),
1621 )
1622}
1623
1624impl AdditionalParameters {
1625 fn from_response_metadata(metadata: Map<String, Value>) -> Self {
1631 let mut accepted = Map::with_capacity(metadata.len());
1632 for (key, value) in metadata {
1633 let probe = Value::Object(Map::from_iter([(key.clone(), value.clone())]));
1634 if serde_json::from_value::<Self>(probe).is_ok() {
1635 accepted.insert(key, value);
1636 } else {
1637 tracing::debug!(
1638 target: "rig::providers::openai",
1639 field = %key,
1640 "ignoring response metadata field that does not match its expected type"
1641 );
1642 }
1643 }
1644 serde_json::from_value(Value::Object(accepted)).unwrap_or_default()
1647 }
1648
1649 pub fn to_json(self) -> serde_json::Value {
1650 serde_json::to_value(self).unwrap_or_else(|_| serde_json::Value::Object(Map::new()))
1651 }
1652}
1653
1654#[derive(Clone, Debug, Default, Serialize, Deserialize)]
1658#[serde(rename_all = "snake_case")]
1659pub enum TruncationStrategy {
1660 Auto,
1661 #[default]
1662 Disabled,
1663}
1664
1665#[derive(Clone, Debug, Serialize, Deserialize)]
1668pub struct TextConfig {
1669 pub format: TextFormat,
1670}
1671
1672impl TextConfig {
1673 pub(crate) fn structured_output<S>(name: S, schema: serde_json::Value) -> Self
1674 where
1675 S: Into<String>,
1676 {
1677 Self {
1678 format: TextFormat::JsonSchema(StructuredOutputsInput {
1679 name: name.into(),
1680 schema,
1681 strict: true,
1682 }),
1683 }
1684 }
1685}
1686
1687#[derive(Clone, Debug, Serialize, Deserialize, Default)]
1690#[serde(tag = "type")]
1691#[serde(rename_all = "snake_case")]
1692pub enum TextFormat {
1693 JsonSchema(StructuredOutputsInput),
1694 #[default]
1695 Text,
1696}
1697
1698#[derive(Clone, Debug, Serialize, Deserialize)]
1700pub struct StructuredOutputsInput {
1701 #[serde(default)]
1705 pub name: String,
1706 pub schema: serde_json::Value,
1708 #[serde(default)]
1710 pub strict: bool,
1711}
1712
1713#[derive(Clone, Debug, Default, Serialize, Deserialize)]
1728pub struct Reasoning {
1729 #[serde(skip_serializing_if = "Option::is_none")]
1731 pub effort: Option<ReasoningEffort>,
1732 #[serde(skip_serializing_if = "Option::is_none")]
1734 pub summary: Option<ReasoningSummaryLevel>,
1735 #[serde(skip_serializing_if = "Option::is_none")]
1738 pub mode: Option<ReasoningMode>,
1739 #[serde(skip_serializing_if = "Option::is_none")]
1742 pub context: Option<ReasoningContext>,
1743}
1744
1745impl Reasoning {
1746 pub fn new() -> Self {
1748 Self::default()
1749 }
1750
1751 pub fn with_effort(mut self, reasoning_effort: ReasoningEffort) -> Self {
1753 self.effort = Some(reasoning_effort);
1754
1755 self
1756 }
1757
1758 pub fn with_summary_level(mut self, reasoning_summary_level: ReasoningSummaryLevel) -> Self {
1760 self.summary = Some(reasoning_summary_level);
1761
1762 self
1763 }
1764
1765 pub fn with_mode(mut self, reasoning_mode: ReasoningMode) -> Self {
1767 self.mode = Some(reasoning_mode);
1768
1769 self
1770 }
1771
1772 pub fn with_context(mut self, reasoning_context: ReasoningContext) -> Self {
1774 self.context = Some(reasoning_context);
1775
1776 self
1777 }
1778}
1779
1780#[derive(Clone, Debug, Default)]
1782pub enum OpenAIServiceTier {
1783 #[default]
1785 Auto,
1786 Default,
1788 Flex,
1790 Priority,
1792 Standard,
1794 Other(String),
1796}
1797
1798impl Serialize for OpenAIServiceTier {
1799 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1800 where
1801 S: Serializer,
1802 {
1803 serializer.serialize_str(match self {
1804 Self::Auto => "auto",
1805 Self::Default => "default",
1806 Self::Flex => "flex",
1807 Self::Priority => "priority",
1808 Self::Standard => "standard",
1809 Self::Other(value) => value,
1810 })
1811 }
1812}
1813
1814impl<'de> Deserialize<'de> for OpenAIServiceTier {
1815 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1816 where
1817 D: Deserializer<'de>,
1818 {
1819 let value = String::deserialize(deserializer)?;
1820 Ok(match value.as_str() {
1821 "auto" => Self::Auto,
1822 "default" => Self::Default,
1823 "flex" => Self::Flex,
1824 "priority" => Self::Priority,
1825 "standard" => Self::Standard,
1826 _ => Self::Other(value),
1827 })
1828 }
1829}
1830
1831#[derive(Clone, Debug, Default, Serialize, Deserialize)]
1833#[serde(rename_all = "snake_case")]
1834pub enum ReasoningEffort {
1835 None,
1836 Minimal,
1837 Low,
1838 #[default]
1839 Medium,
1840 High,
1841 Xhigh,
1842 Max,
1844}
1845
1846#[derive(Clone, Debug, Serialize, Deserialize)]
1851#[serde(rename_all = "snake_case")]
1852pub enum ReasoningMode {
1853 Pro,
1855}
1856
1857#[derive(Clone, Debug, Default, Serialize, Deserialize)]
1860#[serde(rename_all = "snake_case")]
1861pub enum ReasoningContext {
1862 #[default]
1864 Auto,
1865 AllTurns,
1867 CurrentTurn,
1869}
1870
1871#[derive(Clone, Debug, Default, Serialize, Deserialize)]
1873#[serde(rename_all = "snake_case")]
1874pub enum ReasoningSummaryLevel {
1875 #[default]
1876 Auto,
1877 Concise,
1878 Detailed,
1879}
1880
1881#[derive(Clone, Debug, Deserialize, Serialize)]
1883pub enum Include {
1884 #[serde(rename = "file_search_call.results")]
1885 FileSearchCallResults,
1886 #[serde(rename = "message.input_image.image_url")]
1887 MessageInputImageImageUrl,
1888 #[serde(rename = "computer_call.output.image_url")]
1889 ComputerCallOutputOutputImageUrl,
1890 #[serde(rename = "reasoning.encrypted_content")]
1891 ReasoningEncryptedContent,
1892 #[serde(rename = "code_interpreter_call.outputs")]
1893 CodeInterpreterCallOutputs,
1894}
1895
1896#[derive(Clone, Debug, PartialEq)]
1900pub enum Output {
1901 Message(OutputMessage),
1902 FunctionCall(OutputFunctionCall),
1903 Reasoning {
1904 id: String,
1905 summary: Vec<ReasoningSummary>,
1906 content: Vec<String>,
1907 encrypted_content: Option<String>,
1908 signature: Option<String>,
1911 status: Option<ToolStatus>,
1912 },
1913 Compaction(Map<String, Value>),
1918 Unknown(Value),
1922}
1923
1924#[derive(Deserialize)]
1926struct ReasoningFields {
1927 id: String,
1928 #[serde(default)]
1929 summary: Vec<ReasoningSummary>,
1930 #[serde(default, deserialize_with = "deserialize_reasoning_text_content")]
1931 content: Vec<String>,
1932 #[serde(default)]
1933 encrypted_content: Option<String>,
1934 #[serde(default)]
1935 signature: Option<String>,
1936 #[serde(default)]
1937 status: Option<ToolStatus>,
1938}
1939
1940impl From<ReasoningFields> for Output {
1941 fn from(fields: ReasoningFields) -> Self {
1942 Output::Reasoning {
1943 id: fields.id,
1944 summary: fields.summary,
1945 content: fields.content,
1946 encrypted_content: fields.encrypted_content,
1947 signature: fields.signature,
1948 status: fields.status,
1949 }
1950 }
1951}
1952
1953fn tagged_output_object<T>(tag: &str, payload: &T) -> Result<Value, serde_json::Error>
1956where
1957 T: Serialize,
1958{
1959 let mut value = serde_json::to_value(payload)?;
1960 let map = value.as_object_mut().ok_or_else(|| {
1961 <serde_json::Error as serde::ser::Error>::custom(
1962 "output payload must serialize to a JSON object",
1963 )
1964 })?;
1965 map.insert("type".to_string(), Value::String(tag.to_string()));
1966 Ok(value)
1967}
1968
1969impl Serialize for Output {
1970 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1971 where
1972 S: Serializer,
1973 {
1974 let value = match self {
1975 Output::Message(message) => tagged_output_object("message", message),
1976 Output::FunctionCall(call) => tagged_output_object("function_call", call),
1977 Output::Reasoning {
1978 id,
1979 summary,
1980 content,
1981 encrypted_content,
1982 signature,
1983 status,
1984 } => {
1985 let mut value = serde_json::json!({
1986 "type": "reasoning",
1987 "id": id,
1988 "summary": summary,
1989 "encrypted_content": encrypted_content,
1990 "status": status,
1991 });
1992 let map = value.as_object_mut().ok_or_else(|| {
1993 serde::ser::Error::custom("reasoning output must serialize to an object")
1994 })?;
1995 if !content.is_empty() {
1996 map.insert("content".to_string(), reasoning_text_content_json(content));
1997 }
1998 if let Some(signature) = signature {
1999 map.insert("signature".to_string(), Value::String(signature.clone()));
2000 }
2001 Ok(value)
2002 }
2003 Output::Compaction(fields) => {
2004 let mut map = fields.clone();
2005 map.insert("type".to_string(), Value::String("compaction".to_string()));
2006 return Value::Object(map).serialize(serializer);
2007 }
2008 Output::Unknown(value) => return value.serialize(serializer),
2009 };
2010 value
2011 .map_err(serde::ser::Error::custom)?
2012 .serialize(serializer)
2013 }
2014}
2015
2016impl<'de> Deserialize<'de> for Output {
2017 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2018 where
2019 D: Deserializer<'de>,
2020 {
2021 let value = Value::deserialize(deserializer)?;
2024 let Some(tag) = value.get("type").and_then(Value::as_str) else {
2025 return Ok(Output::Unknown(value));
2026 };
2027 match tag {
2028 "message" => serde_json::from_value(value)
2029 .map(Output::Message)
2030 .map_err(serde::de::Error::custom),
2031 "function_call" => serde_json::from_value(value)
2032 .map(Output::FunctionCall)
2033 .map_err(serde::de::Error::custom),
2034 "reasoning" => serde_json::from_value::<ReasoningFields>(value)
2035 .map(Output::from)
2036 .map_err(serde::de::Error::custom),
2037 "compaction" => {
2038 let Value::Object(mut map) = value else {
2039 return Ok(Output::Unknown(value));
2040 };
2041 map.remove("type");
2042 Ok(Output::Compaction(map))
2043 }
2044 _ => Ok(Output::Unknown(value)),
2045 }
2046 }
2047}
2048
2049#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
2051pub struct OutputFunctionCall {
2052 #[serde(default, skip_serializing_if = "is_not_function_call_item_id")]
2058 pub id: String,
2059 pub arguments: FunctionCallArguments,
2060 pub call_id: String,
2061 pub name: String,
2062 pub status: ToolStatus,
2063}
2064
2065#[derive(Clone, Debug, PartialEq)]
2069pub struct FunctionCallArguments(String);
2070
2071impl FunctionCallArguments {
2072 pub fn parse(&self) -> serde_json::Result<serde_json::Value> {
2075 json_utils::parse_tool_arguments(&self.0)
2076 }
2077
2078 pub fn as_str(&self) -> &str {
2080 &self.0
2081 }
2082}
2083
2084impl From<serde_json::Value> for FunctionCallArguments {
2085 fn from(value: serde_json::Value) -> Self {
2088 Self(value.to_string())
2089 }
2090}
2091
2092impl Serialize for FunctionCallArguments {
2093 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
2094 where
2095 S: Serializer,
2096 {
2097 serializer.serialize_str(&self.0)
2098 }
2099}
2100
2101impl<'de> Deserialize<'de> for FunctionCallArguments {
2102 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2103 where
2104 D: Deserializer<'de>,
2105 {
2106 String::deserialize(deserializer).map(Self)
2109 }
2110}
2111
2112fn is_not_function_call_item_id(id: &str) -> bool {
2115 !id.starts_with("fc_")
2116}
2117
2118#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
2120#[serde(rename_all = "snake_case")]
2121pub enum ToolStatus {
2122 InProgress,
2123 Completed,
2124 Incomplete,
2125}
2126
2127#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
2129pub struct OutputMessage {
2130 pub id: String,
2132 pub role: OutputRole,
2134 pub status: ResponseStatus,
2136 pub content: Vec<AssistantContent>,
2138 #[serde(default, skip_serializing_if = "Option::is_none")]
2140 pub phase: Option<String>,
2141}
2142
2143#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
2145#[serde(rename_all = "snake_case")]
2146pub enum OutputRole {
2147 Assistant,
2148}
2149
2150#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
2152#[serde(tag = "role", rename_all = "lowercase")]
2153pub enum Message {
2154 #[serde(alias = "developer")]
2155 System {
2156 #[serde(deserialize_with = "string_or_vec")]
2157 content: Vec<SystemContent>,
2158 #[serde(skip_serializing_if = "Option::is_none")]
2159 name: Option<String>,
2160 },
2161 User {
2162 #[serde(deserialize_with = "string_or_vec")]
2163 content: Vec<UserContent>,
2164 #[serde(skip_serializing_if = "Option::is_none")]
2165 name: Option<String>,
2166 },
2167 Assistant {
2168 content: Vec<AssistantContentType>,
2169 #[serde(skip_serializing_if = "String::is_empty")]
2170 id: String,
2171 #[serde(skip_serializing_if = "Option::is_none")]
2172 name: Option<String>,
2173 status: ToolStatus,
2174 #[serde(default, skip_serializing_if = "Option::is_none")]
2176 phase: Option<String>,
2177 },
2178 #[serde(rename = "assistant", skip_deserializing)]
2179 AssistantInput {
2180 content: String,
2181 #[serde(skip_serializing_if = "Option::is_none")]
2182 name: Option<String>,
2183 },
2184}
2185
2186impl Message {
2187 pub fn system(content: &str) -> Self {
2188 Message::System {
2189 content: vec![content.to_owned().into()],
2190 name: None,
2191 }
2192 }
2193}
2194
2195#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
2198#[serde(tag = "type", rename_all = "snake_case")]
2199pub enum AssistantContent {
2200 OutputText(OutputText),
2201 Refusal { refusal: String },
2202}
2203
2204#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
2206pub struct OutputText {
2207 pub text: String,
2208 #[serde(flatten, default, skip_serializing_if = "Map::is_empty")]
2212 pub extras: Map<String, Value>,
2213}
2214
2215impl OutputText {
2216 pub fn new(text: impl Into<String>) -> Self {
2218 Self {
2219 text: text.into(),
2220 extras: Map::new(),
2221 }
2222 }
2223
2224 fn from_message_text(
2229 text: impl Into<String>,
2230 additional_params: Option<crate::message::AdditionalParams>,
2231 ) -> Self {
2232 let Some(params) = additional_params else {
2233 return Self::new(text);
2234 };
2235 let extras = params
2237 .into_wire_extras(OPENAI_RESPONSES_EXTRAS_KEY)
2238 .map(|map| {
2239 map.into_iter()
2240 .filter(|(key, _)| {
2244 key != "text"
2245 && key != "type"
2246 && key != OPENAI_RESPONSES_PHASE_KEY
2247 && key != OPENAI_RESPONSES_MESSAGE_ID_KEY
2248 })
2249 .collect()
2250 })
2251 .unwrap_or_default();
2252 Self {
2253 text: text.into(),
2254 extras,
2255 }
2256 }
2257}
2258
2259fn assistant_text_replay_message(
2266 id: Option<&str>,
2267 text: String,
2268 additional_params: Option<crate::message::AdditionalParams>,
2269) -> Option<Message> {
2270 if let Some(non_object) = additional_params
2273 .as_ref()
2274 .and_then(|params| params.get(OPENAI_RESPONSES_EXTRAS_KEY))
2275 .filter(|value| !value.is_object())
2276 {
2277 tracing::warn!(
2278 %non_object,
2279 "`additional_params[\"{OPENAI_RESPONSES_EXTRAS_KEY}\"]` must be a JSON \
2280 object — replaying without these extras"
2281 );
2282 }
2283 let own_extras = additional_params
2284 .as_ref()
2285 .and_then(|params| params.wire_extras(OPENAI_RESPONSES_EXTRAS_KEY));
2286 let message_field = |key: &str| {
2290 own_extras
2291 .and_then(|extras| extras.get(key))
2292 .and_then(Value::as_str)
2293 .filter(|value| !value.is_empty())
2294 .map(str::to_owned)
2295 };
2296 let phase = message_field(OPENAI_RESPONSES_PHASE_KEY);
2297 let id = message_field(OPENAI_RESPONSES_MESSAGE_ID_KEY).or_else(|| id.map(str::to_owned));
2298 let content_extras = own_extras.is_some_and(|extras| {
2299 extras
2300 .keys()
2301 .any(|key| key != OPENAI_RESPONSES_PHASE_KEY && key != OPENAI_RESPONSES_MESSAGE_ID_KEY)
2302 });
2303 if text.is_empty() && !(own_extras.is_some() && id.is_some()) {
2304 return None;
2305 }
2306 if id.is_none() && content_extras {
2307 tracing::warn!(
2308 "own-wire extras cannot ride the id-less assistant form — \
2309 replaying the text without them"
2310 );
2311 }
2312 match (id, phase) {
2313 (Some(id), phase) => Some(Message::Assistant {
2314 content: vec![AssistantContentType::Text(AssistantContent::OutputText(
2315 OutputText::from_message_text(text, additional_params),
2316 ))],
2317 id,
2318 name: None,
2319 status: ToolStatus::Completed,
2320 phase,
2321 }),
2322 (None, Some(phase)) => Some(Message::Assistant {
2325 content: vec![AssistantContentType::Text(AssistantContent::OutputText(
2326 OutputText::new(text),
2327 ))],
2328 id: String::new(),
2329 name: None,
2330 status: ToolStatus::Completed,
2331 phase: Some(phase),
2332 }),
2333 (None, None) => Some(Message::AssistantInput {
2334 content: text,
2335 name: None,
2336 }),
2337 }
2338}
2339
2340pub(crate) const OPENAI_RESPONSES_EXTRAS_KEY: &str = "openai_responses";
2344
2345pub(crate) const OPENAI_RESPONSES_PHASE_KEY: &str = "phase";
2350
2351pub(crate) const OPENAI_RESPONSES_MESSAGE_ID_KEY: &str = "message_id";
2356
2357pub(crate) fn text_block(value: AssistantContent) -> Text {
2360 match value {
2361 AssistantContent::Refusal { refusal } => Text::new(refusal),
2362 AssistantContent::OutputText(OutputText { text, extras }) => {
2365 let extras: Map<String, Value> = extras
2367 .into_iter()
2368 .filter(|(_, value)| {
2369 !(value.is_null()
2370 || value.as_array().is_some_and(Vec::is_empty)
2371 || value.as_object().is_some_and(Map::is_empty))
2372 })
2373 .collect();
2374 Text {
2375 text,
2376 additional_params: crate::message::AdditionalParams::from_entries(
2377 (!extras.is_empty())
2378 .then_some((OPENAI_RESPONSES_EXTRAS_KEY, Value::Object(extras))),
2379 ),
2380 }
2381 }
2382 }
2383}
2384
2385impl From<AssistantContent> for completion::AssistantContent {
2386 fn from(value: AssistantContent) -> Self {
2387 completion::AssistantContent::Text(text_block(value))
2388 }
2389}
2390
2391#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
2393#[serde(untagged)]
2394pub enum AssistantContentType {
2395 Text(AssistantContent),
2396 ToolCall(OutputFunctionCall),
2397 Reasoning(OpenAIReasoning),
2398}
2399
2400#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
2403#[serde(tag = "type", rename_all = "snake_case")]
2404pub enum SystemContent {
2405 InputText { text: String },
2406}
2407
2408impl From<String> for SystemContent {
2409 fn from(s: String) -> Self {
2410 SystemContent::InputText { text: s }
2411 }
2412}
2413
2414impl std::str::FromStr for SystemContent {
2415 type Err = std::convert::Infallible;
2416
2417 fn from_str(s: &str) -> Result<Self, Self::Err> {
2418 Ok(SystemContent::InputText {
2419 text: s.to_string(),
2420 })
2421 }
2422}
2423
2424#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
2426#[serde(tag = "type", rename_all = "snake_case")]
2427pub enum UserContent {
2428 InputText {
2429 text: String,
2430 },
2431 InputImage {
2432 image_url: String,
2433 #[serde(default)]
2434 detail: ImageDetail,
2435 },
2436 InputFile {
2437 #[serde(skip_serializing_if = "Option::is_none")]
2438 file_id: Option<String>,
2439 #[serde(skip_serializing_if = "Option::is_none")]
2440 file_url: Option<String>,
2441 #[serde(skip_serializing_if = "Option::is_none")]
2442 file_data: Option<String>,
2443 #[serde(skip_serializing_if = "Option::is_none")]
2444 filename: Option<String>,
2445 },
2446}
2447
2448impl FromStr for UserContent {
2449 type Err = Infallible;
2450
2451 fn from_str(s: &str) -> Result<Self, Self::Err> {
2452 Ok(UserContent::InputText {
2453 text: s.to_string(),
2454 })
2455 }
2456}
2457
2458#[cfg(test)]
2459mod stateless_replay_tests;
2460#[cfg(test)]
2461mod tests;