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 }) = tool_result_content
313 else {
314 return Err(CompletionError::ProviderError(
315 "This thing only supports text!".to_string(),
316 ));
317 };
318 items.push(InputItem {
320 role: None,
321 input: InputContent::FunctionCallOutput(ToolResult {
322 call_id: require_call_id(call_id.clone(), "Tool result")?,
323 output: text,
324 status: ToolStatus::Completed,
325 }),
326 });
327 }
328 }
329 crate::message::UserContent::Document(Document {
330 data: DocumentSourceKind::FileId(file_id),
331 ..
332 }) => items.push(InputItem {
333 role: Some(Role::User),
334 input: InputContent::Message(Message::User {
335 content: OneOrMany::one(UserContent::InputFile {
336 file_id: Some(file_id),
337 file_data: None,
338 file_url: None,
339 filename: None,
340 }),
341 name: None,
342 }),
343 }),
344 crate::message::UserContent::Document(Document {
345 data,
346 media_type: Some(DocumentMediaType::PDF),
347 ..
348 }) => {
349 let (file_data, file_url) = match data {
350 DocumentSourceKind::Base64(data) => {
351 (Some(format!("data:application/pdf;base64,{data}")), None)
352 }
353 DocumentSourceKind::Url(url) => (None, Some(url)),
354 DocumentSourceKind::Raw(_) => {
355 return Err(CompletionError::RequestError(
356 "Raw file data not supported, encode as base64 first"
357 .into(),
358 ));
359 }
360 doc => {
361 return Err(CompletionError::RequestError(
362 format!("Unsupported document type: {doc}").into(),
363 ));
364 }
365 };
366
367 items.push(InputItem {
368 role: Some(Role::User),
369 input: InputContent::Message(Message::User {
370 content: OneOrMany::one(UserContent::InputFile {
371 file_id: None,
372 file_data,
373 file_url,
374 filename: Some("document.pdf".to_string()),
375 }),
376 name: None,
377 }),
378 })
379 }
380 crate::message::UserContent::Document(Document {
381 data:
382 DocumentSourceKind::Base64(text) | DocumentSourceKind::String(text),
383 ..
384 }) => items.push(InputItem {
385 role: Some(Role::User),
386 input: InputContent::Message(Message::User {
387 content: OneOrMany::one(UserContent::InputText { text }),
388 name: None,
389 }),
390 }),
391 crate::message::UserContent::Image(crate::message::Image {
392 data,
393 media_type,
394 detail,
395 ..
396 }) => {
397 let url = match data {
398 DocumentSourceKind::Base64(data) => {
399 let media_type = if let Some(media_type) = media_type {
400 media_type.to_mime_type().to_string()
401 } else {
402 String::new()
403 };
404 format!("data:{media_type};base64,{data}")
405 }
406 DocumentSourceKind::Url(url) => url,
407 DocumentSourceKind::Raw(_) => {
408 return Err(CompletionError::RequestError(
409 "Raw file data not supported, encode as base64 first"
410 .into(),
411 ));
412 }
413 doc => {
414 return Err(CompletionError::RequestError(
415 format!("Unsupported document type: {doc}").into(),
416 ));
417 }
418 };
419 items.push(InputItem {
420 role: Some(Role::User),
421 input: InputContent::Message(Message::User {
422 content: OneOrMany::one(UserContent::InputImage {
423 image_url: url,
424 detail: detail.unwrap_or_default(),
425 }),
426 name: None,
427 }),
428 });
429 }
430 message => {
431 return Err(CompletionError::ProviderError(format!(
432 "Unsupported message: {message:?}"
433 )));
434 }
435 }
436 }
437
438 Ok(items)
439 }
440 crate::completion::Message::Assistant { id, content } => {
441 let mut reasoning_items = Vec::new();
442 let mut other_items = Vec::new();
443
444 for assistant_content in content {
445 match assistant_content {
446 crate::message::AssistantContent::Text(Text { text }) => {
447 let id = id.as_ref().unwrap_or(&String::default()).clone();
448 other_items.push(InputItem {
449 role: Some(Role::Assistant),
450 input: InputContent::Message(Message::Assistant {
451 content: OneOrMany::one(AssistantContentType::Text(
452 AssistantContent::OutputText(Text { text }),
453 )),
454 id,
455 name: None,
456 status: ToolStatus::Completed,
457 }),
458 });
459 }
460 crate::message::AssistantContent::ToolCall(crate::message::ToolCall {
461 id: tool_id,
462 call_id,
463 function,
464 ..
465 }) => {
466 other_items.push(InputItem {
467 role: None,
468 input: InputContent::FunctionCall(OutputFunctionCall {
469 arguments: function.arguments,
470 call_id: require_call_id(call_id, "Assistant tool call")?,
471 id: tool_id,
472 name: function.name,
473 status: ToolStatus::Completed,
474 }),
475 });
476 }
477 crate::message::AssistantContent::Reasoning(reasoning) => {
478 let openai_reasoning = openai_reasoning_from_core(&reasoning)
479 .map_err(|err| CompletionError::ProviderError(err.to_string()))?;
480 reasoning_items.push(InputItem {
481 role: None,
482 input: InputContent::Reasoning(openai_reasoning),
483 });
484 }
485 crate::message::AssistantContent::Image(_) => {
486 return Err(CompletionError::ProviderError(
487 "Assistant image content is not supported in OpenAI Responses API"
488 .to_string(),
489 ));
490 }
491 }
492 }
493
494 let mut items = reasoning_items;
495 items.extend(other_items);
496 Ok(items)
497 }
498 }
499 }
500}
501
502impl From<OneOrMany<String>> for Vec<ReasoningSummary> {
503 fn from(value: OneOrMany<String>) -> Self {
504 value.iter().map(|x| ReasoningSummary::new(x)).collect()
505 }
506}
507
508fn require_call_id(call_id: Option<String>, context: &str) -> Result<String, CompletionError> {
509 call_id.ok_or_else(|| {
510 CompletionError::RequestError(
511 format!("{context} `call_id` is required for OpenAI Responses API").into(),
512 )
513 })
514}
515
516fn openai_reasoning_from_core(
517 reasoning: &crate::message::Reasoning,
518) -> Result<OpenAIReasoning, MessageError> {
519 let id = reasoning.id.clone().ok_or_else(|| {
520 MessageError::ConversionError(
521 "An OpenAI-generated ID is required when using OpenAI reasoning items".to_string(),
522 )
523 })?;
524 let mut summary = Vec::new();
525 let mut encrypted_content = None;
526 for content in &reasoning.content {
527 match content {
528 crate::message::ReasoningContent::Text { text, .. }
529 | crate::message::ReasoningContent::Summary(text) => {
530 summary.push(ReasoningSummary::new(text));
531 }
532 crate::message::ReasoningContent::Encrypted(data)
535 | crate::message::ReasoningContent::Redacted { data } => {
536 encrypted_content.get_or_insert_with(|| data.clone());
537 }
538 }
539 }
540
541 Ok(OpenAIReasoning {
542 id,
543 summary,
544 encrypted_content,
545 status: None,
546 })
547}
548
549#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
551pub struct ResponsesToolDefinition {
552 #[serde(rename = "type")]
554 pub kind: String,
555 #[serde(default, skip_serializing_if = "String::is_empty")]
557 pub name: String,
558 #[serde(default, skip_serializing_if = "is_json_null")]
560 pub parameters: serde_json::Value,
561 #[serde(default, skip_serializing_if = "is_false")]
563 pub strict: bool,
564 #[serde(default, skip_serializing_if = "String::is_empty")]
566 pub description: String,
567 #[serde(flatten, default, skip_serializing_if = "Map::is_empty")]
569 pub config: Map<String, Value>,
570}
571
572fn is_json_null(value: &Value) -> bool {
573 value.is_null()
574}
575
576fn is_false(value: &bool) -> bool {
577 !value
578}
579
580impl ResponsesToolDefinition {
581 pub fn function(
583 name: impl Into<String>,
584 description: impl Into<String>,
585 mut parameters: serde_json::Value,
586 ) -> Self {
587 super::sanitize_schema(&mut parameters);
588
589 Self {
590 kind: "function".to_string(),
591 name: name.into(),
592 parameters,
593 strict: true,
594 description: description.into(),
595 config: Map::new(),
596 }
597 }
598
599 pub fn hosted(kind: impl Into<String>) -> Self {
601 Self {
602 kind: kind.into(),
603 name: String::new(),
604 parameters: Value::Null,
605 strict: false,
606 description: String::new(),
607 config: Map::new(),
608 }
609 }
610
611 pub fn web_search() -> Self {
613 Self::hosted("web_search")
614 }
615
616 pub fn file_search() -> Self {
618 Self::hosted("file_search")
619 }
620
621 pub fn computer_use() -> Self {
623 Self::hosted("computer_use")
624 }
625
626 pub fn with_config(mut self, key: impl Into<String>, value: Value) -> Self {
628 self.config.insert(key.into(), value);
629 self
630 }
631
632 fn normalize(mut self) -> Self {
633 if self.kind == "function" {
634 super::sanitize_schema(&mut self.parameters);
635 self.strict = true;
636 }
637 self
638 }
639}
640
641impl From<completion::ToolDefinition> for ResponsesToolDefinition {
642 fn from(value: completion::ToolDefinition) -> Self {
643 let completion::ToolDefinition {
644 name,
645 parameters,
646 description,
647 } = value;
648
649 Self::function(name, description, parameters)
650 }
651}
652
653#[derive(Clone, Debug, Serialize, Deserialize)]
656pub struct ResponsesUsage {
657 pub input_tokens: u64,
659 #[serde(skip_serializing_if = "Option::is_none")]
661 pub input_tokens_details: Option<InputTokensDetails>,
662 pub output_tokens: u64,
664 pub output_tokens_details: OutputTokensDetails,
666 pub total_tokens: u64,
668}
669
670impl ResponsesUsage {
671 pub(crate) fn new() -> Self {
673 Self {
674 input_tokens: 0,
675 input_tokens_details: Some(InputTokensDetails::new()),
676 output_tokens: 0,
677 output_tokens_details: OutputTokensDetails::new(),
678 total_tokens: 0,
679 }
680 }
681}
682
683impl GetTokenUsage for ResponsesUsage {
684 fn token_usage(&self) -> Option<crate::completion::Usage> {
685 Some(crate::completion::Usage {
686 input_tokens: self.input_tokens,
687 output_tokens: self.output_tokens,
688 total_tokens: self.total_tokens,
689 cached_input_tokens: self
690 .input_tokens_details
691 .as_ref()
692 .map(|details| details.cached_tokens)
693 .unwrap_or(0),
694 cache_creation_input_tokens: 0,
695 reasoning_tokens: self.output_tokens_details.reasoning_tokens,
696 })
697 }
698}
699
700impl Add for ResponsesUsage {
701 type Output = Self;
702
703 fn add(self, rhs: Self) -> Self::Output {
704 let input_tokens = self.input_tokens + rhs.input_tokens;
705 let input_tokens_details = self.input_tokens_details.map(|lhs| {
706 if let Some(tokens) = rhs.input_tokens_details {
707 lhs + tokens
708 } else {
709 lhs
710 }
711 });
712 let output_tokens = self.output_tokens + rhs.output_tokens;
713 let output_tokens_details = self.output_tokens_details + rhs.output_tokens_details;
714 let total_tokens = self.total_tokens + rhs.total_tokens;
715 Self {
716 input_tokens,
717 input_tokens_details,
718 output_tokens,
719 output_tokens_details,
720 total_tokens,
721 }
722 }
723}
724
725#[derive(Clone, Debug, Serialize, Deserialize)]
727pub struct InputTokensDetails {
728 pub cached_tokens: u64,
730}
731
732impl InputTokensDetails {
733 pub(crate) fn new() -> Self {
734 Self { cached_tokens: 0 }
735 }
736}
737
738impl Add for InputTokensDetails {
739 type Output = Self;
740 fn add(self, rhs: Self) -> Self::Output {
741 Self {
742 cached_tokens: self.cached_tokens + rhs.cached_tokens,
743 }
744 }
745}
746
747#[derive(Clone, Debug, Serialize, Deserialize)]
749pub struct OutputTokensDetails {
750 pub reasoning_tokens: u64,
752}
753
754impl OutputTokensDetails {
755 pub(crate) fn new() -> Self {
756 Self {
757 reasoning_tokens: 0,
758 }
759 }
760}
761
762impl Add for OutputTokensDetails {
763 type Output = Self;
764 fn add(self, rhs: Self) -> Self::Output {
765 Self {
766 reasoning_tokens: self.reasoning_tokens + rhs.reasoning_tokens,
767 }
768 }
769}
770
771#[derive(Clone, Debug, Default, Serialize, Deserialize)]
773pub struct IncompleteDetailsReason {
774 pub reason: String,
776}
777
778#[derive(Clone, Debug, Default, Serialize, Deserialize)]
780pub struct ResponseError {
781 pub code: String,
783 pub message: String,
785}
786
787#[derive(Clone, Debug, Deserialize, Serialize)]
789#[serde(rename_all = "snake_case")]
790pub enum ResponseObject {
791 Response,
792}
793
794#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
796#[serde(rename_all = "snake_case")]
797pub enum ResponseStatus {
798 InProgress,
799 Completed,
800 Failed,
801 Cancelled,
802 Queued,
803 Incomplete,
804}
805
806impl TryFrom<(String, crate::completion::CompletionRequest)> for CompletionRequest {
808 type Error = CompletionError;
809 fn try_from(
810 (model, mut req): (String, crate::completion::CompletionRequest),
811 ) -> Result<Self, Self::Error> {
812 let model = req.model.clone().unwrap_or(model);
813 let input = {
814 let mut partial_history = vec![];
815 if let Some(docs) = req.normalized_documents() {
816 partial_history.push(docs);
817 }
818 partial_history.extend(req.chat_history);
819
820 let mut full_history: Vec<InputItem> = if let Some(content) = req.preamble {
824 vec![InputItem::system_message(content)]
825 } else {
826 Vec::new()
827 };
828
829 for history_item in partial_history {
830 full_history.extend(<Vec<InputItem>>::try_from(history_item)?);
831 }
832
833 full_history
834 };
835
836 let input = OneOrMany::many(input).map_err(|_| {
837 CompletionError::RequestError(
838 "OpenAI Responses request input must contain at least one item".into(),
839 )
840 })?;
841
842 let mut additional_params_payload = req.additional_params.take().unwrap_or(Value::Null);
843 let stream = match &additional_params_payload {
844 Value::Bool(stream) => Some(*stream),
845 Value::Object(map) => map.get("stream").and_then(Value::as_bool),
846 _ => None,
847 };
848
849 let mut additional_tools = Vec::new();
850 if let Some(additional_params_map) = additional_params_payload.as_object_mut() {
851 if let Some(raw_tools) = additional_params_map.remove("tools") {
852 additional_tools = serde_json::from_value::<Vec<ResponsesToolDefinition>>(
853 raw_tools,
854 )
855 .map_err(|err| {
856 CompletionError::RequestError(
857 format!(
858 "Invalid OpenAI Responses tools payload in additional_params: {err}"
859 )
860 .into(),
861 )
862 })?;
863 }
864 additional_params_map.remove("stream");
865 }
866
867 if additional_params_payload.is_boolean() {
868 additional_params_payload = Value::Null;
869 }
870
871 additional_tools = additional_tools
872 .into_iter()
873 .map(ResponsesToolDefinition::normalize)
874 .collect();
875
876 let mut additional_parameters = if additional_params_payload.is_null() {
877 AdditionalParameters::default()
879 } else {
880 serde_json::from_value::<AdditionalParameters>(additional_params_payload).map_err(
881 |err| {
882 CompletionError::RequestError(
883 format!("Invalid OpenAI Responses additional_params payload: {err}").into(),
884 )
885 },
886 )?
887 };
888 if additional_parameters.reasoning.is_some() {
889 let include = additional_parameters.include.get_or_insert_with(Vec::new);
890 if !include
891 .iter()
892 .any(|item| matches!(item, Include::ReasoningEncryptedContent))
893 {
894 include.push(Include::ReasoningEncryptedContent);
895 }
896 }
897
898 if additional_parameters.text.is_none()
900 && let Some(schema) = req.output_schema
901 {
902 let name = schema
903 .as_object()
904 .and_then(|o| o.get("title"))
905 .and_then(|v| v.as_str())
906 .unwrap_or("response_schema")
907 .to_string();
908 let mut schema_value = schema.to_value();
909 super::sanitize_schema(&mut schema_value);
910 additional_parameters.text = Some(TextConfig::structured_output(name, schema_value));
911 }
912
913 let tool_choice = req.tool_choice.map(ToolChoice::try_from).transpose()?;
914 let mut tools: Vec<ResponsesToolDefinition> = req
915 .tools
916 .into_iter()
917 .map(ResponsesToolDefinition::from)
918 .collect();
919 tools.append(&mut additional_tools);
920
921 Ok(Self {
922 input,
923 model,
924 instructions: None, max_output_tokens: req.max_tokens,
926 stream,
927 tool_choice,
928 tools,
929 temperature: req.temperature,
930 additional_parameters,
931 })
932 }
933}
934
935#[doc(hidden)]
937#[derive(Clone)]
938pub struct GenericResponsesCompletionModel<Ext = super::OpenAIResponsesExt, H = reqwest::Client> {
939 pub(crate) client: crate::client::Client<Ext, H>,
941 pub model: String,
943 pub tools: Vec<ResponsesToolDefinition>,
945}
946
947pub type ResponsesCompletionModel<H = reqwest::Client> =
952 GenericResponsesCompletionModel<super::OpenAIResponsesExt, H>;
953
954impl<Ext, H> GenericResponsesCompletionModel<Ext, H>
955where
956 crate::client::Client<Ext, H>: HttpClientExt + Clone + std::fmt::Debug + 'static,
957 Ext: crate::client::Provider + Clone + 'static,
958 H: Clone + Default + std::fmt::Debug + 'static,
959{
960 pub fn new(client: crate::client::Client<Ext, H>, model: impl Into<String>) -> Self {
962 Self {
963 client,
964 model: model.into(),
965 tools: Vec::new(),
966 }
967 }
968
969 pub fn with_model(client: crate::client::Client<Ext, H>, model: &str) -> Self {
970 Self {
971 client,
972 model: model.to_string(),
973 tools: Vec::new(),
974 }
975 }
976
977 pub fn with_tool(mut self, tool: impl Into<ResponsesToolDefinition>) -> Self {
979 self.tools.push(tool.into());
980 self
981 }
982
983 pub fn with_tools<I, Tool>(mut self, tools: I) -> Self
985 where
986 I: IntoIterator<Item = Tool>,
987 Tool: Into<ResponsesToolDefinition>,
988 {
989 self.tools.extend(tools.into_iter().map(Into::into));
990 self
991 }
992
993 pub(crate) fn create_completion_request(
995 &self,
996 completion_request: crate::completion::CompletionRequest,
997 ) -> Result<CompletionRequest, CompletionError> {
998 let mut req = CompletionRequest::try_from((self.model.clone(), completion_request))?;
999 req.tools.extend(self.tools.clone());
1000
1001 Ok(req)
1002 }
1003}
1004
1005impl<T> GenericResponsesCompletionModel<super::OpenAIResponsesExt, T>
1006where
1007 T: HttpClientExt + Clone + Default + std::fmt::Debug + 'static,
1008{
1009 pub fn completions_api(self) -> crate::providers::openai::completion::CompletionModel<T> {
1011 super::completion::CompletionModel::with_model(self.client.completions_api(), &self.model)
1012 }
1013}
1014
1015#[derive(Clone, Debug, Serialize, Deserialize)]
1017pub struct CompletionResponse {
1018 pub id: String,
1020 pub object: ResponseObject,
1022 pub created_at: u64,
1024 pub status: ResponseStatus,
1026 pub error: Option<ResponseError>,
1028 pub incomplete_details: Option<IncompleteDetailsReason>,
1030 pub instructions: Option<String>,
1032 pub max_output_tokens: Option<u64>,
1034 pub model: String,
1036 pub usage: Option<ResponsesUsage>,
1038 pub output: Vec<Output>,
1040 #[serde(default)]
1042 pub tools: Vec<ResponsesToolDefinition>,
1043 #[serde(flatten)]
1045 pub additional_parameters: AdditionalParameters,
1046}
1047
1048#[derive(Clone, Debug, Deserialize, Serialize, Default)]
1051pub struct AdditionalParameters {
1052 #[serde(skip_serializing_if = "Option::is_none")]
1054 pub background: Option<bool>,
1055 #[serde(skip_serializing_if = "Option::is_none")]
1057 pub text: Option<TextConfig>,
1058 #[serde(skip_serializing_if = "Option::is_none")]
1060 pub include: Option<Vec<Include>>,
1061 #[serde(skip_serializing_if = "Option::is_none")]
1063 pub top_p: Option<f64>,
1064 #[serde(skip_serializing_if = "Option::is_none")]
1066 pub truncation: Option<TruncationStrategy>,
1067 #[serde(skip_serializing_if = "Option::is_none")]
1069 pub user: Option<String>,
1070 #[serde(skip_serializing_if = "Map::is_empty", default)]
1072 pub metadata: serde_json::Map<String, serde_json::Value>,
1073 #[serde(skip_serializing_if = "Option::is_none")]
1075 pub parallel_tool_calls: Option<bool>,
1076 #[serde(skip_serializing_if = "Option::is_none")]
1078 pub previous_response_id: Option<String>,
1079 #[serde(skip_serializing_if = "Option::is_none")]
1081 pub reasoning: Option<Reasoning>,
1082 #[serde(skip_serializing_if = "Option::is_none")]
1084 pub service_tier: Option<OpenAIServiceTier>,
1085 #[serde(skip_serializing_if = "Option::is_none")]
1087 pub store: Option<bool>,
1088}
1089
1090impl AdditionalParameters {
1091 pub fn to_json(self) -> serde_json::Value {
1092 serde_json::to_value(self).unwrap_or_else(|_| serde_json::Value::Object(Map::new()))
1093 }
1094}
1095
1096#[derive(Clone, Debug, Default, Serialize, Deserialize)]
1100#[serde(rename_all = "snake_case")]
1101pub enum TruncationStrategy {
1102 Auto,
1103 #[default]
1104 Disabled,
1105}
1106
1107#[derive(Clone, Debug, Serialize, Deserialize)]
1110pub struct TextConfig {
1111 pub format: TextFormat,
1112}
1113
1114impl TextConfig {
1115 pub(crate) fn structured_output<S>(name: S, schema: serde_json::Value) -> Self
1116 where
1117 S: Into<String>,
1118 {
1119 Self {
1120 format: TextFormat::JsonSchema(StructuredOutputsInput {
1121 name: name.into(),
1122 schema,
1123 strict: true,
1124 }),
1125 }
1126 }
1127}
1128
1129#[derive(Clone, Debug, Serialize, Deserialize, Default)]
1132#[serde(tag = "type")]
1133#[serde(rename_all = "snake_case")]
1134pub enum TextFormat {
1135 JsonSchema(StructuredOutputsInput),
1136 #[default]
1137 Text,
1138}
1139
1140#[derive(Clone, Debug, Serialize, Deserialize)]
1142pub struct StructuredOutputsInput {
1143 pub name: String,
1145 pub schema: serde_json::Value,
1147 #[serde(default)]
1149 pub strict: bool,
1150}
1151
1152#[derive(Clone, Debug, Default, Serialize, Deserialize)]
1154pub struct Reasoning {
1155 pub effort: Option<ReasoningEffort>,
1157 #[serde(skip_serializing_if = "Option::is_none")]
1159 pub summary: Option<ReasoningSummaryLevel>,
1160}
1161
1162impl Reasoning {
1163 pub fn new() -> Self {
1165 Self {
1166 effort: None,
1167 summary: None,
1168 }
1169 }
1170
1171 pub fn with_effort(mut self, reasoning_effort: ReasoningEffort) -> Self {
1173 self.effort = Some(reasoning_effort);
1174
1175 self
1176 }
1177
1178 pub fn with_summary_level(mut self, reasoning_summary_level: ReasoningSummaryLevel) -> Self {
1180 self.summary = Some(reasoning_summary_level);
1181
1182 self
1183 }
1184}
1185
1186#[derive(Clone, Debug, Default)]
1188pub enum OpenAIServiceTier {
1189 #[default]
1191 Auto,
1192 Default,
1194 Flex,
1196 Priority,
1198 Standard,
1200 Other(String),
1202}
1203
1204impl Serialize for OpenAIServiceTier {
1205 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1206 where
1207 S: Serializer,
1208 {
1209 serializer.serialize_str(match self {
1210 Self::Auto => "auto",
1211 Self::Default => "default",
1212 Self::Flex => "flex",
1213 Self::Priority => "priority",
1214 Self::Standard => "standard",
1215 Self::Other(value) => value,
1216 })
1217 }
1218}
1219
1220impl<'de> Deserialize<'de> for OpenAIServiceTier {
1221 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1222 where
1223 D: Deserializer<'de>,
1224 {
1225 let value = String::deserialize(deserializer)?;
1226 Ok(match value.as_str() {
1227 "auto" => Self::Auto,
1228 "default" => Self::Default,
1229 "flex" => Self::Flex,
1230 "priority" => Self::Priority,
1231 "standard" => Self::Standard,
1232 _ => Self::Other(value),
1233 })
1234 }
1235}
1236
1237#[derive(Clone, Debug, Default, Serialize, Deserialize)]
1239#[serde(rename_all = "snake_case")]
1240pub enum ReasoningEffort {
1241 None,
1242 Minimal,
1243 Low,
1244 #[default]
1245 Medium,
1246 High,
1247 Xhigh,
1248}
1249
1250#[derive(Clone, Debug, Default, Serialize, Deserialize)]
1252#[serde(rename_all = "snake_case")]
1253pub enum ReasoningSummaryLevel {
1254 #[default]
1255 Auto,
1256 Concise,
1257 Detailed,
1258}
1259
1260#[derive(Clone, Debug, Deserialize, Serialize)]
1263pub enum Include {
1264 #[serde(rename = "file_search_call.results")]
1265 FileSearchCallResults,
1266 #[serde(rename = "message.input_image.image_url")]
1267 MessageInputImageImageUrl,
1268 #[serde(rename = "computer_call.output.image_url")]
1269 ComputerCallOutputOutputImageUrl,
1270 #[serde(rename = "reasoning.encrypted_content")]
1271 ReasoningEncryptedContent,
1272 #[serde(rename = "code_interpreter_call.outputs")]
1273 CodeInterpreterCallOutputs,
1274}
1275
1276#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
1278#[serde(tag = "type")]
1279#[serde(rename_all = "snake_case")]
1280pub enum Output {
1281 Message(OutputMessage),
1282 #[serde(alias = "function_call")]
1283 FunctionCall(OutputFunctionCall),
1284 Reasoning {
1285 id: String,
1286 summary: Vec<ReasoningSummary>,
1287 #[serde(default)]
1288 encrypted_content: Option<String>,
1289 #[serde(default)]
1290 status: Option<ToolStatus>,
1291 },
1292 #[serde(other)]
1297 Unknown,
1298}
1299
1300impl From<Output> for Vec<completion::AssistantContent> {
1301 fn from(value: Output) -> Self {
1302 let res: Vec<completion::AssistantContent> = match value {
1303 Output::Message(OutputMessage { content, .. }) => content
1304 .into_iter()
1305 .map(completion::AssistantContent::from)
1306 .collect(),
1307 Output::FunctionCall(OutputFunctionCall {
1308 id,
1309 arguments,
1310 call_id,
1311 name,
1312 ..
1313 }) => vec![completion::AssistantContent::tool_call_with_call_id(
1314 id, call_id, name, arguments,
1315 )],
1316 Output::Reasoning {
1317 id,
1318 summary,
1319 encrypted_content,
1320 ..
1321 } => {
1322 let mut content = summary
1323 .into_iter()
1324 .map(|summary| match summary {
1325 ReasoningSummary::SummaryText { text } => {
1326 message::ReasoningContent::Summary(text)
1327 }
1328 })
1329 .collect::<Vec<_>>();
1330 if let Some(encrypted_content) = encrypted_content {
1331 content.push(message::ReasoningContent::Encrypted(encrypted_content));
1332 }
1333 vec![completion::AssistantContent::Reasoning(
1334 message::Reasoning {
1335 id: Some(id),
1336 content,
1337 },
1338 )]
1339 }
1340 Output::Unknown => Vec::new(),
1341 };
1342
1343 res
1344 }
1345}
1346
1347#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
1348pub struct OutputReasoning {
1349 id: String,
1350 summary: Vec<ReasoningSummary>,
1351 status: ToolStatus,
1352}
1353
1354#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
1356pub struct OutputFunctionCall {
1357 pub id: String,
1358 #[serde(with = "json_utils::stringified_json")]
1359 pub arguments: serde_json::Value,
1360 pub call_id: String,
1361 pub name: String,
1362 pub status: ToolStatus,
1363}
1364
1365#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
1367#[serde(rename_all = "snake_case")]
1368pub enum ToolStatus {
1369 InProgress,
1370 Completed,
1371 Incomplete,
1372}
1373
1374#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
1376pub struct OutputMessage {
1377 pub id: String,
1379 pub role: OutputRole,
1381 pub status: ResponseStatus,
1383 pub content: Vec<AssistantContent>,
1385}
1386
1387#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
1389#[serde(rename_all = "snake_case")]
1390pub enum OutputRole {
1391 Assistant,
1392}
1393
1394impl<Ext, H> completion::CompletionModel for GenericResponsesCompletionModel<Ext, H>
1395where
1396 crate::client::Client<Ext, H>:
1397 HttpClientExt + Clone + WasmCompatSend + WasmCompatSync + 'static,
1398 Ext: crate::client::Provider
1399 + crate::client::DebugExt
1400 + Clone
1401 + WasmCompatSend
1402 + WasmCompatSync
1403 + 'static,
1404 H: Clone + Default + std::fmt::Debug + WasmCompatSend + WasmCompatSync + 'static,
1405{
1406 type Response = CompletionResponse;
1407 type StreamingResponse = StreamingCompletionResponse;
1408
1409 type Client = crate::client::Client<Ext, H>;
1410
1411 fn make(client: &Self::Client, model: impl Into<String>) -> Self {
1412 Self::new(client.clone(), model)
1413 }
1414
1415 async fn completion(
1416 &self,
1417 completion_request: crate::completion::CompletionRequest,
1418 ) -> Result<completion::CompletionResponse<Self::Response>, CompletionError> {
1419 let span = if tracing::Span::current().is_disabled() {
1420 info_span!(
1421 target: "rig::completions",
1422 "chat",
1423 gen_ai.operation.name = "chat",
1424 gen_ai.provider.name = tracing::field::Empty,
1425 gen_ai.request.model = tracing::field::Empty,
1426 gen_ai.response.id = tracing::field::Empty,
1427 gen_ai.response.model = tracing::field::Empty,
1428 gen_ai.usage.output_tokens = tracing::field::Empty,
1429 gen_ai.usage.input_tokens = tracing::field::Empty,
1430 gen_ai.usage.cache_read.input_tokens = tracing::field::Empty,
1431 gen_ai.input.messages = tracing::field::Empty,
1432 gen_ai.output.messages = tracing::field::Empty,
1433 )
1434 } else {
1435 tracing::Span::current()
1436 };
1437
1438 span.record("gen_ai.provider.name", "openai");
1439 span.record("gen_ai.request.model", &self.model);
1440 let request = self.create_completion_request(completion_request)?;
1441 let body = serde_json::to_vec(&request)?;
1442
1443 if enabled!(Level::TRACE) {
1444 tracing::trace!(
1445 target: "rig::completions",
1446 "OpenAI Responses completion request: {request}",
1447 request = serde_json::to_string_pretty(&request)?
1448 );
1449 }
1450
1451 let req = self
1452 .client
1453 .post("/responses")?
1454 .body(body)
1455 .map_err(|e| CompletionError::HttpError(e.into()))?;
1456
1457 async move {
1458 let response = self.client.send(req).await?;
1459
1460 if response.status().is_success() {
1461 let t = http_client::text(response).await?;
1462 let response = serde_json::from_str::<Self::Response>(&t)?;
1463 let span = tracing::Span::current();
1464 span.record("gen_ai.response.id", &response.id);
1465 span.record("gen_ai.response.model", &response.model);
1466 if let Some(ref usage) = response.usage {
1467 span.record("gen_ai.usage.output_tokens", usage.output_tokens);
1468 span.record("gen_ai.usage.input_tokens", usage.input_tokens);
1469 let cached_tokens = usage
1470 .input_tokens_details
1471 .as_ref()
1472 .map(|d| d.cached_tokens)
1473 .unwrap_or(0);
1474 span.record("gen_ai.usage.cache_read.input_tokens", cached_tokens);
1475 }
1476 if enabled!(Level::TRACE) {
1477 tracing::trace!(
1478 target: "rig::completions",
1479 "OpenAI Responses completion response: {response}",
1480 response = serde_json::to_string_pretty(&response)?
1481 );
1482 }
1483 response.try_into()
1484 } else {
1485 let text = http_client::text(response).await?;
1486 Err(CompletionError::ProviderError(text))
1487 }
1488 }
1489 .instrument(span)
1490 .await
1491 }
1492
1493 async fn stream(
1494 &self,
1495 request: crate::completion::CompletionRequest,
1496 ) -> Result<
1497 crate::streaming::StreamingCompletionResponse<Self::StreamingResponse>,
1498 CompletionError,
1499 > {
1500 GenericResponsesCompletionModel::stream(self, request).await
1501 }
1502}
1503
1504impl TryFrom<CompletionResponse> for completion::CompletionResponse<CompletionResponse> {
1505 type Error = CompletionError;
1506
1507 fn try_from(response: CompletionResponse) -> Result<Self, Self::Error> {
1508 if response.output.is_empty() {
1509 return Err(CompletionError::ResponseError(
1510 "Response contained no parts".to_owned(),
1511 ));
1512 }
1513
1514 let message_id = response.output.iter().find_map(|item| match item {
1516 Output::Message(msg) => Some(msg.id.clone()),
1517 _ => None,
1518 });
1519
1520 let content: Vec<completion::AssistantContent> = response
1521 .output
1522 .iter()
1523 .cloned()
1524 .flat_map(<Vec<completion::AssistantContent>>::from)
1525 .collect();
1526
1527 let choice = OneOrMany::many(content).map_err(|_| {
1528 CompletionError::ResponseError(
1529 "Response contained no message or tool call (empty)".to_owned(),
1530 )
1531 })?;
1532
1533 let usage = response
1534 .usage
1535 .as_ref()
1536 .and_then(GetTokenUsage::token_usage)
1537 .unwrap_or_default();
1538
1539 Ok(completion::CompletionResponse {
1540 choice,
1541 usage,
1542 raw_response: response,
1543 message_id,
1544 })
1545 }
1546}
1547
1548#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
1550#[serde(tag = "role", rename_all = "lowercase")]
1551pub enum Message {
1552 #[serde(alias = "developer")]
1553 System {
1554 #[serde(deserialize_with = "string_or_one_or_many")]
1555 content: OneOrMany<SystemContent>,
1556 #[serde(skip_serializing_if = "Option::is_none")]
1557 name: Option<String>,
1558 },
1559 User {
1560 #[serde(deserialize_with = "string_or_one_or_many")]
1561 content: OneOrMany<UserContent>,
1562 #[serde(skip_serializing_if = "Option::is_none")]
1563 name: Option<String>,
1564 },
1565 Assistant {
1566 content: OneOrMany<AssistantContentType>,
1567 #[serde(skip_serializing_if = "String::is_empty")]
1568 id: String,
1569 #[serde(skip_serializing_if = "Option::is_none")]
1570 name: Option<String>,
1571 status: ToolStatus,
1572 },
1573 #[serde(rename = "tool")]
1574 ToolResult {
1575 tool_call_id: String,
1576 output: String,
1577 },
1578}
1579
1580#[derive(Default, Debug, Serialize, Deserialize, PartialEq, Clone)]
1582#[serde(rename_all = "lowercase")]
1583pub enum ToolResultContentType {
1584 #[default]
1585 Text,
1586}
1587
1588impl Message {
1589 pub fn system(content: &str) -> Self {
1590 Message::System {
1591 content: OneOrMany::one(content.to_owned().into()),
1592 name: None,
1593 }
1594 }
1595}
1596
1597#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
1600#[serde(tag = "type", rename_all = "snake_case")]
1601pub enum AssistantContent {
1602 OutputText(Text),
1603 Refusal { refusal: String },
1604}
1605
1606impl From<AssistantContent> for completion::AssistantContent {
1607 fn from(value: AssistantContent) -> Self {
1608 match value {
1609 AssistantContent::Refusal { refusal } => {
1610 completion::AssistantContent::Text(Text { text: refusal })
1611 }
1612 AssistantContent::OutputText(Text { text }) => {
1613 completion::AssistantContent::Text(Text { text })
1614 }
1615 }
1616 }
1617}
1618
1619#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
1621#[serde(untagged)]
1622pub enum AssistantContentType {
1623 Text(AssistantContent),
1624 ToolCall(OutputFunctionCall),
1625 Reasoning(OpenAIReasoning),
1626}
1627
1628#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
1631#[serde(tag = "type", rename_all = "snake_case")]
1632pub enum SystemContent {
1633 InputText { text: String },
1634}
1635
1636impl From<String> for SystemContent {
1637 fn from(s: String) -> Self {
1638 SystemContent::InputText { text: s }
1639 }
1640}
1641
1642impl std::str::FromStr for SystemContent {
1643 type Err = std::convert::Infallible;
1644
1645 fn from_str(s: &str) -> Result<Self, Self::Err> {
1646 Ok(SystemContent::InputText {
1647 text: s.to_string(),
1648 })
1649 }
1650}
1651
1652#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
1654#[serde(tag = "type", rename_all = "snake_case")]
1655pub enum UserContent {
1656 InputText {
1657 text: String,
1658 },
1659 InputImage {
1660 image_url: String,
1661 #[serde(default)]
1662 detail: ImageDetail,
1663 },
1664 InputFile {
1665 #[serde(skip_serializing_if = "Option::is_none")]
1666 file_id: Option<String>,
1667 #[serde(skip_serializing_if = "Option::is_none")]
1668 file_url: Option<String>,
1669 #[serde(skip_serializing_if = "Option::is_none")]
1670 file_data: Option<String>,
1671 #[serde(skip_serializing_if = "Option::is_none")]
1672 filename: Option<String>,
1673 },
1674 Audio {
1675 input_audio: InputAudio,
1676 },
1677 #[serde(rename = "tool")]
1678 ToolResult {
1679 tool_call_id: String,
1680 output: String,
1681 },
1682}
1683
1684impl TryFrom<message::Message> for Vec<Message> {
1685 type Error = message::MessageError;
1686
1687 fn try_from(message: message::Message) -> Result<Self, Self::Error> {
1688 match message {
1689 message::Message::System { content } => Ok(vec![Message::System {
1690 content: OneOrMany::one(content.into()),
1691 name: None,
1692 }]),
1693 message::Message::User { content } => {
1694 let (tool_results, other_content): (Vec<_>, Vec<_>) = content
1695 .into_iter()
1696 .partition(|content| matches!(content, message::UserContent::ToolResult(_)));
1697
1698 if !tool_results.is_empty() {
1701 tool_results
1702 .into_iter()
1703 .map(|content| match content {
1704 message::UserContent::ToolResult(message::ToolResult {
1705 call_id,
1706 content,
1707 ..
1708 }) => Ok::<_, message::MessageError>(Message::ToolResult {
1709 tool_call_id: call_id.ok_or_else(|| {
1710 MessageError::ConversionError(
1711 "Tool result `call_id` is required for OpenAI Responses API"
1712 .into(),
1713 )
1714 })?,
1715 output: {
1716 let res = content.first();
1717 match res {
1718 completion::message::ToolResultContent::Text(Text {
1719 text,
1720 }) => text,
1721 _ => return Err(MessageError::ConversionError("This API only currently supports text tool results".into()))
1722 }
1723 },
1724 }),
1725 _ => Err(MessageError::ConversionError(
1726 "expected tool result content while converting Responses API input"
1727 .into(),
1728 )),
1729 })
1730 .collect::<Result<Vec<_>, _>>()
1731 } else {
1732 let other_content = other_content
1733 .into_iter()
1734 .map(|content| match content {
1735 message::UserContent::Text(message::Text { text }) => {
1736 Ok(UserContent::InputText { text })
1737 }
1738 message::UserContent::Image(message::Image {
1739 data,
1740 detail,
1741 media_type,
1742 ..
1743 }) => {
1744 let url = match data {
1745 DocumentSourceKind::Base64(data) => {
1746 let media_type = if let Some(media_type) = media_type {
1747 media_type.to_mime_type().to_string()
1748 } else {
1749 String::new()
1750 };
1751 format!("data:{media_type};base64,{data}")
1752 }
1753 DocumentSourceKind::Url(url) => url,
1754 DocumentSourceKind::Raw(_) => {
1755 return Err(MessageError::ConversionError(
1756 "Raw files not supported, encode as base64 first"
1757 .into(),
1758 ));
1759 }
1760 doc => {
1761 return Err(MessageError::ConversionError(format!(
1762 "Unsupported document type: {doc}"
1763 )));
1764 }
1765 };
1766
1767 Ok(UserContent::InputImage {
1768 image_url: url,
1769 detail: detail.unwrap_or_default(),
1770 })
1771 }
1772 message::UserContent::Document(message::Document {
1773 data: DocumentSourceKind::FileId(file_id),
1774 ..
1775 }) => Ok(UserContent::InputFile {
1776 file_id: Some(file_id),
1777 file_url: None,
1778 file_data: None,
1779 filename: None,
1780 }),
1781 message::UserContent::Document(message::Document {
1782 media_type: Some(DocumentMediaType::PDF),
1783 data,
1784 ..
1785 }) => {
1786 let (file_data, file_url, filename) = match data {
1787 DocumentSourceKind::Base64(data) => (
1788 Some(format!("data:application/pdf;base64,{data}")),
1789 None,
1790 Some("document.pdf".to_string()),
1791 ),
1792 DocumentSourceKind::Url(url) => (None, Some(url), None),
1793 DocumentSourceKind::Raw(_) => {
1794 return Err(MessageError::ConversionError(
1795 "Raw files not supported, encode as base64 first"
1796 .into(),
1797 ));
1798 }
1799 doc => {
1800 return Err(MessageError::ConversionError(format!(
1801 "Unsupported document type: {doc}"
1802 )));
1803 }
1804 };
1805
1806 Ok(UserContent::InputFile {
1807 file_id: None,
1808 file_url,
1809 file_data,
1810 filename,
1811 })
1812 }
1813 message::UserContent::Document(message::Document {
1814 data: DocumentSourceKind::Base64(text),
1815 ..
1816 }) => Ok(UserContent::InputText { text }),
1817 message::UserContent::Audio(message::Audio {
1818 data: DocumentSourceKind::Base64(data),
1819 media_type,
1820 ..
1821 }) => Ok(UserContent::Audio {
1822 input_audio: InputAudio {
1823 data,
1824 format: match media_type {
1825 Some(media_type) => media_type,
1826 None => AudioMediaType::MP3,
1827 },
1828 },
1829 }),
1830 message::UserContent::Audio(_) => Err(MessageError::ConversionError(
1831 "Audio must be base64 encoded data".into(),
1832 )),
1833 _ => Err(MessageError::ConversionError(
1834 "Unsupported user content for OpenAI Responses API".into(),
1835 )),
1836 })
1837 .collect::<Result<Vec<_>, _>>()?;
1838
1839 let other_content = OneOrMany::many(other_content).map_err(|_| {
1840 MessageError::ConversionError(
1841 "User message did not contain OpenAI Responses-compatible content"
1842 .to_string(),
1843 )
1844 })?;
1845
1846 Ok(vec![Message::User {
1847 content: other_content,
1848 name: None,
1849 }])
1850 }
1851 }
1852 message::Message::Assistant { content, id } => {
1853 let assistant_message_id = id.ok_or_else(|| {
1854 MessageError::ConversionError(
1855 "Assistant message ID is required for OpenAI Responses API".into(),
1856 )
1857 })?;
1858
1859 match content.first() {
1860 crate::message::AssistantContent::Text(Text { text }) => {
1861 Ok(vec![Message::Assistant {
1862 id: assistant_message_id.clone(),
1863 status: ToolStatus::Completed,
1864 content: OneOrMany::one(AssistantContentType::Text(
1865 AssistantContent::OutputText(Text { text }),
1866 )),
1867 name: None,
1868 }])
1869 }
1870 crate::message::AssistantContent::ToolCall(crate::message::ToolCall {
1871 id,
1872 call_id,
1873 function,
1874 ..
1875 }) => Ok(vec![Message::Assistant {
1876 content: OneOrMany::one(AssistantContentType::ToolCall(
1877 OutputFunctionCall {
1878 call_id: call_id.ok_or_else(|| {
1879 MessageError::ConversionError(
1880 "Tool call `call_id` is required for OpenAI Responses API"
1881 .into(),
1882 )
1883 })?,
1884 arguments: function.arguments,
1885 id,
1886 name: function.name,
1887 status: ToolStatus::Completed,
1888 },
1889 )),
1890 id: assistant_message_id.clone(),
1891 name: None,
1892 status: ToolStatus::Completed,
1893 }]),
1894 crate::message::AssistantContent::Reasoning(reasoning) => {
1895 let openai_reasoning = openai_reasoning_from_core(&reasoning)?;
1896 Ok(vec![Message::Assistant {
1897 content: OneOrMany::one(AssistantContentType::Reasoning(
1898 openai_reasoning,
1899 )),
1900 id: assistant_message_id,
1901 name: None,
1902 status: ToolStatus::Completed,
1903 }])
1904 }
1905 crate::message::AssistantContent::Image(_) => {
1906 Err(MessageError::ConversionError(
1907 "Assistant image content is not supported in OpenAI Responses API"
1908 .into(),
1909 ))
1910 }
1911 }
1912 }
1913 }
1914 }
1915}
1916
1917impl FromStr for UserContent {
1918 type Err = Infallible;
1919
1920 fn from_str(s: &str) -> Result<Self, Self::Err> {
1921 Ok(UserContent::InputText {
1922 text: s.to_string(),
1923 })
1924 }
1925}
1926
1927#[cfg(test)]
1928mod tests {
1929 use super::*;
1930 use crate::message;
1931 use serde_json::json;
1932
1933 fn response_with_service_tier(service_tier: &str) -> Value {
1934 json!({
1935 "id": "resp_123",
1936 "object": "response",
1937 "created_at": 0,
1938 "status": "completed",
1939 "model": "gpt-5.4",
1940 "output": [],
1941 "service_tier": service_tier,
1942 })
1943 }
1944
1945 #[test]
1946 fn completion_response_deserializes_standard_service_tier() {
1947 let response: CompletionResponse =
1948 serde_json::from_value(response_with_service_tier("standard"))
1949 .expect("response should deserialize");
1950
1951 assert!(matches!(
1952 response.additional_parameters.service_tier,
1953 Some(OpenAIServiceTier::Standard)
1954 ));
1955 }
1956
1957 #[test]
1958 fn completion_response_deserializes_priority_service_tier() {
1959 let response: CompletionResponse =
1960 serde_json::from_value(response_with_service_tier("priority"))
1961 .expect("response should deserialize");
1962
1963 assert!(matches!(
1964 response.additional_parameters.service_tier,
1965 Some(OpenAIServiceTier::Priority)
1966 ));
1967 }
1968
1969 #[test]
1970 fn completion_response_preserves_unknown_service_tier() {
1971 let response: CompletionResponse =
1972 serde_json::from_value(response_with_service_tier("provider_experimental"))
1973 .expect("response should deserialize");
1974
1975 let Some(OpenAIServiceTier::Other(service_tier)) =
1976 response.additional_parameters.service_tier
1977 else {
1978 panic!("expected provider-specific service tier");
1979 };
1980
1981 assert_eq!(service_tier, "provider_experimental");
1982 }
1983
1984 #[test]
1985 fn service_tier_serializes_expected_strings() {
1986 let cases = [
1987 (OpenAIServiceTier::Auto, "auto"),
1988 (OpenAIServiceTier::Default, "default"),
1989 (OpenAIServiceTier::Flex, "flex"),
1990 (OpenAIServiceTier::Priority, "priority"),
1991 (OpenAIServiceTier::Standard, "standard"),
1992 ];
1993
1994 for (service_tier, expected) in cases {
1995 assert_eq!(
1996 serde_json::to_value(service_tier).expect("service tier should serialize"),
1997 json!(expected)
1998 );
1999 }
2000
2001 assert_eq!(
2002 serde_json::to_value(OpenAIServiceTier::Other(
2003 "provider_experimental".to_string()
2004 ))
2005 .expect("provider-specific service tier should serialize"),
2006 json!("provider_experimental")
2007 );
2008 }
2009
2010 #[test]
2011 fn responses_usage_token_usage_preserves_reasoning_tokens() {
2012 let usage = ResponsesUsage {
2013 input_tokens: 100,
2014 input_tokens_details: Some(InputTokensDetails { cached_tokens: 25 }),
2015 output_tokens: 50,
2016 output_tokens_details: OutputTokensDetails {
2017 reasoning_tokens: 15,
2018 },
2019 total_tokens: 150,
2020 };
2021
2022 let token_usage = usage.token_usage().expect("usage should be present");
2023
2024 assert_eq!(token_usage.input_tokens, 100);
2025 assert_eq!(token_usage.cached_input_tokens, 25);
2026 assert_eq!(token_usage.output_tokens, 50);
2027 assert_eq!(token_usage.reasoning_tokens, 15);
2028 assert_eq!(token_usage.total_tokens, 150);
2029 }
2030
2031 #[test]
2032 fn file_id_document_serializes_as_input_file_content() {
2033 let message = message::Message::User {
2034 content: OneOrMany::one(message::UserContent::Document(message::Document {
2035 data: DocumentSourceKind::FileId("file_abc".to_string()),
2036 media_type: None,
2037 additional_params: None,
2038 })),
2039 };
2040
2041 let converted: Vec<Message> = message.try_into().expect("conversion should succeed");
2042 let Message::User { content, .. } = &converted[0] else {
2043 panic!("expected user message");
2044 };
2045
2046 let json = serde_json::to_value(content.first_ref()).expect("serialize content");
2047
2048 assert_eq!(json["type"], "input_file");
2049 assert_eq!(json["file_id"], "file_abc");
2050 assert!(json.get("file_data").is_none());
2051 assert!(json.get("file_url").is_none());
2052 }
2053
2054 #[test]
2055 fn file_id_document_serializes_as_input_item_content() {
2056 let message = completion::Message::User {
2057 content: OneOrMany::one(message::UserContent::Document(message::Document {
2058 data: DocumentSourceKind::FileId("file_abc".to_string()),
2059 media_type: None,
2060 additional_params: None,
2061 })),
2062 };
2063
2064 let converted: Vec<InputItem> = message.try_into().expect("conversion should succeed");
2065 let json = serde_json::to_value(&converted[0]).expect("serialize input item");
2066
2067 assert_eq!(json["type"], "message");
2068 assert_eq!(json["role"], "user");
2069 assert_eq!(json["content"][0]["type"], "input_file");
2070 assert_eq!(json["content"][0]["file_id"], "file_abc");
2071 assert!(json["content"][0].get("file_data").is_none());
2072 assert!(json["content"][0].get("file_url").is_none());
2073 }
2074}