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, Serialize};
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,
331 media_type: Some(DocumentMediaType::PDF),
332 ..
333 }) => {
334 let (file_data, file_url) = match data {
335 DocumentSourceKind::Base64(data) => {
336 (Some(format!("data:application/pdf;base64,{data}")), None)
337 }
338 DocumentSourceKind::Url(url) => (None, Some(url)),
339 DocumentSourceKind::Raw(_) => {
340 return Err(CompletionError::RequestError(
341 "Raw file data not supported, encode as base64 first"
342 .into(),
343 ));
344 }
345 doc => {
346 return Err(CompletionError::RequestError(
347 format!("Unsupported document type: {doc}").into(),
348 ));
349 }
350 };
351
352 items.push(InputItem {
353 role: Some(Role::User),
354 input: InputContent::Message(Message::User {
355 content: OneOrMany::one(UserContent::InputFile {
356 file_data,
357 file_url,
358 filename: Some("document.pdf".to_string()),
359 }),
360 name: None,
361 }),
362 })
363 }
364 crate::message::UserContent::Document(Document {
365 data:
366 DocumentSourceKind::Base64(text) | DocumentSourceKind::String(text),
367 ..
368 }) => items.push(InputItem {
369 role: Some(Role::User),
370 input: InputContent::Message(Message::User {
371 content: OneOrMany::one(UserContent::InputText { text }),
372 name: None,
373 }),
374 }),
375 crate::message::UserContent::Image(crate::message::Image {
376 data,
377 media_type,
378 detail,
379 ..
380 }) => {
381 let url = match data {
382 DocumentSourceKind::Base64(data) => {
383 let media_type = if let Some(media_type) = media_type {
384 media_type.to_mime_type().to_string()
385 } else {
386 String::new()
387 };
388 format!("data:{media_type};base64,{data}")
389 }
390 DocumentSourceKind::Url(url) => url,
391 DocumentSourceKind::Raw(_) => {
392 return Err(CompletionError::RequestError(
393 "Raw file data not supported, encode as base64 first"
394 .into(),
395 ));
396 }
397 doc => {
398 return Err(CompletionError::RequestError(
399 format!("Unsupported document type: {doc}").into(),
400 ));
401 }
402 };
403 items.push(InputItem {
404 role: Some(Role::User),
405 input: InputContent::Message(Message::User {
406 content: OneOrMany::one(UserContent::InputImage {
407 image_url: url,
408 detail: detail.unwrap_or_default(),
409 }),
410 name: None,
411 }),
412 });
413 }
414 message => {
415 return Err(CompletionError::ProviderError(format!(
416 "Unsupported message: {message:?}"
417 )));
418 }
419 }
420 }
421
422 Ok(items)
423 }
424 crate::completion::Message::Assistant { id, content } => {
425 let mut reasoning_items = Vec::new();
426 let mut other_items = Vec::new();
427
428 for assistant_content in content {
429 match assistant_content {
430 crate::message::AssistantContent::Text(Text { text }) => {
431 let id = id.as_ref().unwrap_or(&String::default()).clone();
432 other_items.push(InputItem {
433 role: Some(Role::Assistant),
434 input: InputContent::Message(Message::Assistant {
435 content: OneOrMany::one(AssistantContentType::Text(
436 AssistantContent::OutputText(Text { text }),
437 )),
438 id,
439 name: None,
440 status: ToolStatus::Completed,
441 }),
442 });
443 }
444 crate::message::AssistantContent::ToolCall(crate::message::ToolCall {
445 id: tool_id,
446 call_id,
447 function,
448 ..
449 }) => {
450 other_items.push(InputItem {
451 role: None,
452 input: InputContent::FunctionCall(OutputFunctionCall {
453 arguments: function.arguments,
454 call_id: require_call_id(call_id, "Assistant tool call")?,
455 id: tool_id,
456 name: function.name,
457 status: ToolStatus::Completed,
458 }),
459 });
460 }
461 crate::message::AssistantContent::Reasoning(reasoning) => {
462 let openai_reasoning = openai_reasoning_from_core(&reasoning)
463 .map_err(|err| CompletionError::ProviderError(err.to_string()))?;
464 reasoning_items.push(InputItem {
465 role: None,
466 input: InputContent::Reasoning(openai_reasoning),
467 });
468 }
469 crate::message::AssistantContent::Image(_) => {
470 return Err(CompletionError::ProviderError(
471 "Assistant image content is not supported in OpenAI Responses API"
472 .to_string(),
473 ));
474 }
475 }
476 }
477
478 let mut items = reasoning_items;
479 items.extend(other_items);
480 Ok(items)
481 }
482 }
483 }
484}
485
486impl From<OneOrMany<String>> for Vec<ReasoningSummary> {
487 fn from(value: OneOrMany<String>) -> Self {
488 value.iter().map(|x| ReasoningSummary::new(x)).collect()
489 }
490}
491
492fn require_call_id(call_id: Option<String>, context: &str) -> Result<String, CompletionError> {
493 call_id.ok_or_else(|| {
494 CompletionError::RequestError(
495 format!("{context} `call_id` is required for OpenAI Responses API").into(),
496 )
497 })
498}
499
500fn openai_reasoning_from_core(
501 reasoning: &crate::message::Reasoning,
502) -> Result<OpenAIReasoning, MessageError> {
503 let id = reasoning.id.clone().ok_or_else(|| {
504 MessageError::ConversionError(
505 "An OpenAI-generated ID is required when using OpenAI reasoning items".to_string(),
506 )
507 })?;
508 let mut summary = Vec::new();
509 let mut encrypted_content = None;
510 for content in &reasoning.content {
511 match content {
512 crate::message::ReasoningContent::Text { text, .. }
513 | crate::message::ReasoningContent::Summary(text) => {
514 summary.push(ReasoningSummary::new(text));
515 }
516 crate::message::ReasoningContent::Encrypted(data)
519 | crate::message::ReasoningContent::Redacted { data } => {
520 encrypted_content.get_or_insert_with(|| data.clone());
521 }
522 }
523 }
524
525 Ok(OpenAIReasoning {
526 id,
527 summary,
528 encrypted_content,
529 status: None,
530 })
531}
532
533#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
535pub struct ResponsesToolDefinition {
536 #[serde(rename = "type")]
538 pub kind: String,
539 #[serde(default, skip_serializing_if = "String::is_empty")]
541 pub name: String,
542 #[serde(default, skip_serializing_if = "is_json_null")]
544 pub parameters: serde_json::Value,
545 #[serde(default, skip_serializing_if = "is_false")]
547 pub strict: bool,
548 #[serde(default, skip_serializing_if = "String::is_empty")]
550 pub description: String,
551 #[serde(flatten, default, skip_serializing_if = "Map::is_empty")]
553 pub config: Map<String, Value>,
554}
555
556fn is_json_null(value: &Value) -> bool {
557 value.is_null()
558}
559
560fn is_false(value: &bool) -> bool {
561 !value
562}
563
564impl ResponsesToolDefinition {
565 pub fn function(
567 name: impl Into<String>,
568 description: impl Into<String>,
569 mut parameters: serde_json::Value,
570 ) -> Self {
571 super::sanitize_schema(&mut parameters);
572
573 Self {
574 kind: "function".to_string(),
575 name: name.into(),
576 parameters,
577 strict: true,
578 description: description.into(),
579 config: Map::new(),
580 }
581 }
582
583 pub fn hosted(kind: impl Into<String>) -> Self {
585 Self {
586 kind: kind.into(),
587 name: String::new(),
588 parameters: Value::Null,
589 strict: false,
590 description: String::new(),
591 config: Map::new(),
592 }
593 }
594
595 pub fn web_search() -> Self {
597 Self::hosted("web_search")
598 }
599
600 pub fn file_search() -> Self {
602 Self::hosted("file_search")
603 }
604
605 pub fn computer_use() -> Self {
607 Self::hosted("computer_use")
608 }
609
610 pub fn with_config(mut self, key: impl Into<String>, value: Value) -> Self {
612 self.config.insert(key.into(), value);
613 self
614 }
615
616 fn normalize(mut self) -> Self {
617 if self.kind == "function" {
618 super::sanitize_schema(&mut self.parameters);
619 self.strict = true;
620 }
621 self
622 }
623}
624
625impl From<completion::ToolDefinition> for ResponsesToolDefinition {
626 fn from(value: completion::ToolDefinition) -> Self {
627 let completion::ToolDefinition {
628 name,
629 parameters,
630 description,
631 } = value;
632
633 Self::function(name, description, parameters)
634 }
635}
636
637#[derive(Clone, Debug, Serialize, Deserialize)]
640pub struct ResponsesUsage {
641 pub input_tokens: u64,
643 #[serde(skip_serializing_if = "Option::is_none")]
645 pub input_tokens_details: Option<InputTokensDetails>,
646 pub output_tokens: u64,
648 pub output_tokens_details: OutputTokensDetails,
650 pub total_tokens: u64,
652}
653
654impl ResponsesUsage {
655 pub(crate) fn new() -> Self {
657 Self {
658 input_tokens: 0,
659 input_tokens_details: Some(InputTokensDetails::new()),
660 output_tokens: 0,
661 output_tokens_details: OutputTokensDetails::new(),
662 total_tokens: 0,
663 }
664 }
665}
666
667impl GetTokenUsage for ResponsesUsage {
668 fn token_usage(&self) -> Option<crate::completion::Usage> {
669 Some(crate::providers::internal::completion_usage(
670 self.input_tokens,
671 self.output_tokens,
672 self.total_tokens,
673 self.input_tokens_details
674 .as_ref()
675 .map(|details| details.cached_tokens)
676 .unwrap_or(0),
677 ))
678 }
679}
680
681impl Add for ResponsesUsage {
682 type Output = Self;
683
684 fn add(self, rhs: Self) -> Self::Output {
685 let input_tokens = self.input_tokens + rhs.input_tokens;
686 let input_tokens_details = self.input_tokens_details.map(|lhs| {
687 if let Some(tokens) = rhs.input_tokens_details {
688 lhs + tokens
689 } else {
690 lhs
691 }
692 });
693 let output_tokens = self.output_tokens + rhs.output_tokens;
694 let output_tokens_details = self.output_tokens_details + rhs.output_tokens_details;
695 let total_tokens = self.total_tokens + rhs.total_tokens;
696 Self {
697 input_tokens,
698 input_tokens_details,
699 output_tokens,
700 output_tokens_details,
701 total_tokens,
702 }
703 }
704}
705
706#[derive(Clone, Debug, Serialize, Deserialize)]
708pub struct InputTokensDetails {
709 pub cached_tokens: u64,
711}
712
713impl InputTokensDetails {
714 pub(crate) fn new() -> Self {
715 Self { cached_tokens: 0 }
716 }
717}
718
719impl Add for InputTokensDetails {
720 type Output = Self;
721 fn add(self, rhs: Self) -> Self::Output {
722 Self {
723 cached_tokens: self.cached_tokens + rhs.cached_tokens,
724 }
725 }
726}
727
728#[derive(Clone, Debug, Serialize, Deserialize)]
730pub struct OutputTokensDetails {
731 pub reasoning_tokens: u64,
733}
734
735impl OutputTokensDetails {
736 pub(crate) fn new() -> Self {
737 Self {
738 reasoning_tokens: 0,
739 }
740 }
741}
742
743impl Add for OutputTokensDetails {
744 type Output = Self;
745 fn add(self, rhs: Self) -> Self::Output {
746 Self {
747 reasoning_tokens: self.reasoning_tokens + rhs.reasoning_tokens,
748 }
749 }
750}
751
752#[derive(Clone, Debug, Default, Serialize, Deserialize)]
754pub struct IncompleteDetailsReason {
755 pub reason: String,
757}
758
759#[derive(Clone, Debug, Default, Serialize, Deserialize)]
761pub struct ResponseError {
762 pub code: String,
764 pub message: String,
766}
767
768#[derive(Clone, Debug, Deserialize, Serialize)]
770#[serde(rename_all = "snake_case")]
771pub enum ResponseObject {
772 Response,
773}
774
775#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
777#[serde(rename_all = "snake_case")]
778pub enum ResponseStatus {
779 InProgress,
780 Completed,
781 Failed,
782 Cancelled,
783 Queued,
784 Incomplete,
785}
786
787impl TryFrom<(String, crate::completion::CompletionRequest)> for CompletionRequest {
789 type Error = CompletionError;
790 fn try_from(
791 (model, mut req): (String, crate::completion::CompletionRequest),
792 ) -> Result<Self, Self::Error> {
793 let model = req.model.clone().unwrap_or(model);
794 let input = {
795 let mut partial_history = vec![];
796 if let Some(docs) = req.normalized_documents() {
797 partial_history.push(docs);
798 }
799 partial_history.extend(req.chat_history);
800
801 let mut full_history: Vec<InputItem> = if let Some(content) = req.preamble {
805 vec![InputItem::system_message(content)]
806 } else {
807 Vec::new()
808 };
809
810 for history_item in partial_history {
811 full_history.extend(<Vec<InputItem>>::try_from(history_item)?);
812 }
813
814 full_history
815 };
816
817 let input = OneOrMany::many(input).map_err(|_| {
818 CompletionError::RequestError(
819 "OpenAI Responses request input must contain at least one item".into(),
820 )
821 })?;
822
823 let mut additional_params_payload = req.additional_params.take().unwrap_or(Value::Null);
824 let stream = match &additional_params_payload {
825 Value::Bool(stream) => Some(*stream),
826 Value::Object(map) => map.get("stream").and_then(Value::as_bool),
827 _ => None,
828 };
829
830 let mut additional_tools = Vec::new();
831 if let Some(additional_params_map) = additional_params_payload.as_object_mut() {
832 if let Some(raw_tools) = additional_params_map.remove("tools") {
833 additional_tools = serde_json::from_value::<Vec<ResponsesToolDefinition>>(
834 raw_tools,
835 )
836 .map_err(|err| {
837 CompletionError::RequestError(
838 format!(
839 "Invalid OpenAI Responses tools payload in additional_params: {err}"
840 )
841 .into(),
842 )
843 })?;
844 }
845 additional_params_map.remove("stream");
846 }
847
848 if additional_params_payload.is_boolean() {
849 additional_params_payload = Value::Null;
850 }
851
852 additional_tools = additional_tools
853 .into_iter()
854 .map(ResponsesToolDefinition::normalize)
855 .collect();
856
857 let mut additional_parameters = if additional_params_payload.is_null() {
858 AdditionalParameters::default()
860 } else {
861 serde_json::from_value::<AdditionalParameters>(additional_params_payload).map_err(
862 |err| {
863 CompletionError::RequestError(
864 format!("Invalid OpenAI Responses additional_params payload: {err}").into(),
865 )
866 },
867 )?
868 };
869 if additional_parameters.reasoning.is_some() {
870 let include = additional_parameters.include.get_or_insert_with(Vec::new);
871 if !include
872 .iter()
873 .any(|item| matches!(item, Include::ReasoningEncryptedContent))
874 {
875 include.push(Include::ReasoningEncryptedContent);
876 }
877 }
878
879 if additional_parameters.text.is_none()
881 && let Some(schema) = req.output_schema
882 {
883 let name = schema
884 .as_object()
885 .and_then(|o| o.get("title"))
886 .and_then(|v| v.as_str())
887 .unwrap_or("response_schema")
888 .to_string();
889 let mut schema_value = schema.to_value();
890 super::sanitize_schema(&mut schema_value);
891 additional_parameters.text = Some(TextConfig::structured_output(name, schema_value));
892 }
893
894 let tool_choice = req.tool_choice.map(ToolChoice::try_from).transpose()?;
895 let mut tools: Vec<ResponsesToolDefinition> = req
896 .tools
897 .into_iter()
898 .map(ResponsesToolDefinition::from)
899 .collect();
900 tools.append(&mut additional_tools);
901
902 Ok(Self {
903 input,
904 model,
905 instructions: None, max_output_tokens: req.max_tokens,
907 stream,
908 tool_choice,
909 tools,
910 temperature: req.temperature,
911 additional_parameters,
912 })
913 }
914}
915
916#[doc(hidden)]
918#[derive(Clone)]
919pub struct GenericResponsesCompletionModel<Ext = super::OpenAIResponsesExt, H = reqwest::Client> {
920 pub(crate) client: crate::client::Client<Ext, H>,
922 pub model: String,
924 pub tools: Vec<ResponsesToolDefinition>,
926}
927
928pub type ResponsesCompletionModel<H = reqwest::Client> =
933 GenericResponsesCompletionModel<super::OpenAIResponsesExt, H>;
934
935impl<Ext, H> GenericResponsesCompletionModel<Ext, H>
936where
937 crate::client::Client<Ext, H>: HttpClientExt + Clone + std::fmt::Debug + 'static,
938 Ext: crate::client::Provider + Clone + 'static,
939 H: Clone + Default + std::fmt::Debug + 'static,
940{
941 pub fn new(client: crate::client::Client<Ext, H>, model: impl Into<String>) -> Self {
943 Self {
944 client,
945 model: model.into(),
946 tools: Vec::new(),
947 }
948 }
949
950 pub fn with_model(client: crate::client::Client<Ext, H>, model: &str) -> Self {
951 Self {
952 client,
953 model: model.to_string(),
954 tools: Vec::new(),
955 }
956 }
957
958 pub fn with_tool(mut self, tool: impl Into<ResponsesToolDefinition>) -> Self {
960 self.tools.push(tool.into());
961 self
962 }
963
964 pub fn with_tools<I, Tool>(mut self, tools: I) -> Self
966 where
967 I: IntoIterator<Item = Tool>,
968 Tool: Into<ResponsesToolDefinition>,
969 {
970 self.tools.extend(tools.into_iter().map(Into::into));
971 self
972 }
973
974 pub(crate) fn create_completion_request(
976 &self,
977 completion_request: crate::completion::CompletionRequest,
978 ) -> Result<CompletionRequest, CompletionError> {
979 let mut req = CompletionRequest::try_from((self.model.clone(), completion_request))?;
980 req.tools.extend(self.tools.clone());
981
982 Ok(req)
983 }
984}
985
986impl<T> GenericResponsesCompletionModel<super::OpenAIResponsesExt, T>
987where
988 T: HttpClientExt + Clone + Default + std::fmt::Debug + 'static,
989{
990 pub fn completions_api(self) -> crate::providers::openai::completion::CompletionModel<T> {
992 super::completion::CompletionModel::with_model(self.client.completions_api(), &self.model)
993 }
994}
995
996#[derive(Clone, Debug, Serialize, Deserialize)]
998pub struct CompletionResponse {
999 pub id: String,
1001 pub object: ResponseObject,
1003 pub created_at: u64,
1005 pub status: ResponseStatus,
1007 pub error: Option<ResponseError>,
1009 pub incomplete_details: Option<IncompleteDetailsReason>,
1011 pub instructions: Option<String>,
1013 pub max_output_tokens: Option<u64>,
1015 pub model: String,
1017 pub usage: Option<ResponsesUsage>,
1019 pub output: Vec<Output>,
1021 #[serde(default)]
1023 pub tools: Vec<ResponsesToolDefinition>,
1024 #[serde(flatten)]
1026 pub additional_parameters: AdditionalParameters,
1027}
1028
1029#[derive(Clone, Debug, Deserialize, Serialize, Default)]
1032pub struct AdditionalParameters {
1033 #[serde(skip_serializing_if = "Option::is_none")]
1035 pub background: Option<bool>,
1036 #[serde(skip_serializing_if = "Option::is_none")]
1038 pub text: Option<TextConfig>,
1039 #[serde(skip_serializing_if = "Option::is_none")]
1041 pub include: Option<Vec<Include>>,
1042 #[serde(skip_serializing_if = "Option::is_none")]
1044 pub top_p: Option<f64>,
1045 #[serde(skip_serializing_if = "Option::is_none")]
1047 pub truncation: Option<TruncationStrategy>,
1048 #[serde(skip_serializing_if = "Option::is_none")]
1050 pub user: Option<String>,
1051 #[serde(skip_serializing_if = "Map::is_empty", default)]
1053 pub metadata: serde_json::Map<String, serde_json::Value>,
1054 #[serde(skip_serializing_if = "Option::is_none")]
1056 pub parallel_tool_calls: Option<bool>,
1057 #[serde(skip_serializing_if = "Option::is_none")]
1059 pub previous_response_id: Option<String>,
1060 #[serde(skip_serializing_if = "Option::is_none")]
1062 pub reasoning: Option<Reasoning>,
1063 #[serde(skip_serializing_if = "Option::is_none")]
1065 pub service_tier: Option<OpenAIServiceTier>,
1066 #[serde(skip_serializing_if = "Option::is_none")]
1068 pub store: Option<bool>,
1069}
1070
1071impl AdditionalParameters {
1072 pub fn to_json(self) -> serde_json::Value {
1073 serde_json::to_value(self).unwrap_or_else(|_| serde_json::Value::Object(Map::new()))
1074 }
1075}
1076
1077#[derive(Clone, Debug, Default, Serialize, Deserialize)]
1081#[serde(rename_all = "snake_case")]
1082pub enum TruncationStrategy {
1083 Auto,
1084 #[default]
1085 Disabled,
1086}
1087
1088#[derive(Clone, Debug, Serialize, Deserialize)]
1091pub struct TextConfig {
1092 pub format: TextFormat,
1093}
1094
1095impl TextConfig {
1096 pub(crate) fn structured_output<S>(name: S, schema: serde_json::Value) -> Self
1097 where
1098 S: Into<String>,
1099 {
1100 Self {
1101 format: TextFormat::JsonSchema(StructuredOutputsInput {
1102 name: name.into(),
1103 schema,
1104 strict: true,
1105 }),
1106 }
1107 }
1108}
1109
1110#[derive(Clone, Debug, Serialize, Deserialize, Default)]
1113#[serde(tag = "type")]
1114#[serde(rename_all = "snake_case")]
1115pub enum TextFormat {
1116 JsonSchema(StructuredOutputsInput),
1117 #[default]
1118 Text,
1119}
1120
1121#[derive(Clone, Debug, Serialize, Deserialize)]
1123pub struct StructuredOutputsInput {
1124 pub name: String,
1126 pub schema: serde_json::Value,
1128 #[serde(default)]
1130 pub strict: bool,
1131}
1132
1133#[derive(Clone, Debug, Default, Serialize, Deserialize)]
1135pub struct Reasoning {
1136 pub effort: Option<ReasoningEffort>,
1138 #[serde(skip_serializing_if = "Option::is_none")]
1140 pub summary: Option<ReasoningSummaryLevel>,
1141}
1142
1143impl Reasoning {
1144 pub fn new() -> Self {
1146 Self {
1147 effort: None,
1148 summary: None,
1149 }
1150 }
1151
1152 pub fn with_effort(mut self, reasoning_effort: ReasoningEffort) -> Self {
1154 self.effort = Some(reasoning_effort);
1155
1156 self
1157 }
1158
1159 pub fn with_summary_level(mut self, reasoning_summary_level: ReasoningSummaryLevel) -> Self {
1161 self.summary = Some(reasoning_summary_level);
1162
1163 self
1164 }
1165}
1166
1167#[derive(Clone, Debug, Default, Serialize, Deserialize)]
1169#[serde(rename_all = "snake_case")]
1170pub enum OpenAIServiceTier {
1171 #[default]
1172 Auto,
1173 Default,
1174 Flex,
1175}
1176
1177#[derive(Clone, Debug, Default, Serialize, Deserialize)]
1179#[serde(rename_all = "snake_case")]
1180pub enum ReasoningEffort {
1181 None,
1182 Minimal,
1183 Low,
1184 #[default]
1185 Medium,
1186 High,
1187 Xhigh,
1188}
1189
1190#[derive(Clone, Debug, Default, Serialize, Deserialize)]
1192#[serde(rename_all = "snake_case")]
1193pub enum ReasoningSummaryLevel {
1194 #[default]
1195 Auto,
1196 Concise,
1197 Detailed,
1198}
1199
1200#[derive(Clone, Debug, Deserialize, Serialize)]
1203pub enum Include {
1204 #[serde(rename = "file_search_call.results")]
1205 FileSearchCallResults,
1206 #[serde(rename = "message.input_image.image_url")]
1207 MessageInputImageImageUrl,
1208 #[serde(rename = "computer_call.output.image_url")]
1209 ComputerCallOutputOutputImageUrl,
1210 #[serde(rename = "reasoning.encrypted_content")]
1211 ReasoningEncryptedContent,
1212 #[serde(rename = "code_interpreter_call.outputs")]
1213 CodeInterpreterCallOutputs,
1214}
1215
1216#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
1218#[serde(tag = "type")]
1219#[serde(rename_all = "snake_case")]
1220pub enum Output {
1221 Message(OutputMessage),
1222 #[serde(alias = "function_call")]
1223 FunctionCall(OutputFunctionCall),
1224 Reasoning {
1225 id: String,
1226 summary: Vec<ReasoningSummary>,
1227 #[serde(default)]
1228 encrypted_content: Option<String>,
1229 #[serde(default)]
1230 status: Option<ToolStatus>,
1231 },
1232 #[serde(other)]
1237 Unknown,
1238}
1239
1240impl From<Output> for Vec<completion::AssistantContent> {
1241 fn from(value: Output) -> Self {
1242 let res: Vec<completion::AssistantContent> = match value {
1243 Output::Message(OutputMessage { content, .. }) => content
1244 .into_iter()
1245 .map(completion::AssistantContent::from)
1246 .collect(),
1247 Output::FunctionCall(OutputFunctionCall {
1248 id,
1249 arguments,
1250 call_id,
1251 name,
1252 ..
1253 }) => vec![completion::AssistantContent::tool_call_with_call_id(
1254 id, call_id, name, arguments,
1255 )],
1256 Output::Reasoning {
1257 id,
1258 summary,
1259 encrypted_content,
1260 ..
1261 } => {
1262 let mut content = summary
1263 .into_iter()
1264 .map(|summary| match summary {
1265 ReasoningSummary::SummaryText { text } => {
1266 message::ReasoningContent::Summary(text)
1267 }
1268 })
1269 .collect::<Vec<_>>();
1270 if let Some(encrypted_content) = encrypted_content {
1271 content.push(message::ReasoningContent::Encrypted(encrypted_content));
1272 }
1273 vec![completion::AssistantContent::Reasoning(
1274 message::Reasoning {
1275 id: Some(id),
1276 content,
1277 },
1278 )]
1279 }
1280 Output::Unknown => Vec::new(),
1281 };
1282
1283 res
1284 }
1285}
1286
1287#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
1288pub struct OutputReasoning {
1289 id: String,
1290 summary: Vec<ReasoningSummary>,
1291 status: ToolStatus,
1292}
1293
1294#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
1296pub struct OutputFunctionCall {
1297 pub id: String,
1298 #[serde(with = "json_utils::stringified_json")]
1299 pub arguments: serde_json::Value,
1300 pub call_id: String,
1301 pub name: String,
1302 pub status: ToolStatus,
1303}
1304
1305#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
1307#[serde(rename_all = "snake_case")]
1308pub enum ToolStatus {
1309 InProgress,
1310 Completed,
1311 Incomplete,
1312}
1313
1314#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
1316pub struct OutputMessage {
1317 pub id: String,
1319 pub role: OutputRole,
1321 pub status: ResponseStatus,
1323 pub content: Vec<AssistantContent>,
1325}
1326
1327#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
1329#[serde(rename_all = "snake_case")]
1330pub enum OutputRole {
1331 Assistant,
1332}
1333
1334impl<Ext, H> completion::CompletionModel for GenericResponsesCompletionModel<Ext, H>
1335where
1336 crate::client::Client<Ext, H>:
1337 HttpClientExt + Clone + WasmCompatSend + WasmCompatSync + 'static,
1338 Ext: crate::client::Provider
1339 + crate::client::DebugExt
1340 + Clone
1341 + WasmCompatSend
1342 + WasmCompatSync
1343 + 'static,
1344 H: Clone + Default + std::fmt::Debug + WasmCompatSend + WasmCompatSync + 'static,
1345{
1346 type Response = CompletionResponse;
1347 type StreamingResponse = StreamingCompletionResponse;
1348
1349 type Client = crate::client::Client<Ext, H>;
1350
1351 fn make(client: &Self::Client, model: impl Into<String>) -> Self {
1352 Self::new(client.clone(), model)
1353 }
1354
1355 async fn completion(
1356 &self,
1357 completion_request: crate::completion::CompletionRequest,
1358 ) -> Result<completion::CompletionResponse<Self::Response>, CompletionError> {
1359 let span = if tracing::Span::current().is_disabled() {
1360 info_span!(
1361 target: "rig::completions",
1362 "chat",
1363 gen_ai.operation.name = "chat",
1364 gen_ai.provider.name = tracing::field::Empty,
1365 gen_ai.request.model = tracing::field::Empty,
1366 gen_ai.response.id = tracing::field::Empty,
1367 gen_ai.response.model = tracing::field::Empty,
1368 gen_ai.usage.output_tokens = tracing::field::Empty,
1369 gen_ai.usage.input_tokens = tracing::field::Empty,
1370 gen_ai.usage.cache_read.input_tokens = tracing::field::Empty,
1371 gen_ai.input.messages = tracing::field::Empty,
1372 gen_ai.output.messages = tracing::field::Empty,
1373 )
1374 } else {
1375 tracing::Span::current()
1376 };
1377
1378 span.record("gen_ai.provider.name", "openai");
1379 span.record("gen_ai.request.model", &self.model);
1380 let request = self.create_completion_request(completion_request)?;
1381 let body = serde_json::to_vec(&request)?;
1382
1383 if enabled!(Level::TRACE) {
1384 tracing::trace!(
1385 target: "rig::completions",
1386 "OpenAI Responses completion request: {request}",
1387 request = serde_json::to_string_pretty(&request)?
1388 );
1389 }
1390
1391 let req = self
1392 .client
1393 .post("/responses")?
1394 .body(body)
1395 .map_err(|e| CompletionError::HttpError(e.into()))?;
1396
1397 async move {
1398 let response = self.client.send(req).await?;
1399
1400 if response.status().is_success() {
1401 let t = http_client::text(response).await?;
1402 let response = serde_json::from_str::<Self::Response>(&t)?;
1403 let span = tracing::Span::current();
1404 span.record("gen_ai.response.id", &response.id);
1405 span.record("gen_ai.response.model", &response.model);
1406 if let Some(ref usage) = response.usage {
1407 span.record("gen_ai.usage.output_tokens", usage.output_tokens);
1408 span.record("gen_ai.usage.input_tokens", usage.input_tokens);
1409 let cached_tokens = usage
1410 .input_tokens_details
1411 .as_ref()
1412 .map(|d| d.cached_tokens)
1413 .unwrap_or(0);
1414 span.record("gen_ai.usage.cache_read.input_tokens", cached_tokens);
1415 }
1416 if enabled!(Level::TRACE) {
1417 tracing::trace!(
1418 target: "rig::completions",
1419 "OpenAI Responses completion response: {response}",
1420 response = serde_json::to_string_pretty(&response)?
1421 );
1422 }
1423 response.try_into()
1424 } else {
1425 let text = http_client::text(response).await?;
1426 Err(CompletionError::ProviderError(text))
1427 }
1428 }
1429 .instrument(span)
1430 .await
1431 }
1432
1433 async fn stream(
1434 &self,
1435 request: crate::completion::CompletionRequest,
1436 ) -> Result<
1437 crate::streaming::StreamingCompletionResponse<Self::StreamingResponse>,
1438 CompletionError,
1439 > {
1440 GenericResponsesCompletionModel::stream(self, request).await
1441 }
1442}
1443
1444impl TryFrom<CompletionResponse> for completion::CompletionResponse<CompletionResponse> {
1445 type Error = CompletionError;
1446
1447 fn try_from(response: CompletionResponse) -> Result<Self, Self::Error> {
1448 if response.output.is_empty() {
1449 return Err(CompletionError::ResponseError(
1450 "Response contained no parts".to_owned(),
1451 ));
1452 }
1453
1454 let message_id = response.output.iter().find_map(|item| match item {
1456 Output::Message(msg) => Some(msg.id.clone()),
1457 _ => None,
1458 });
1459
1460 let content: Vec<completion::AssistantContent> = response
1461 .output
1462 .iter()
1463 .cloned()
1464 .flat_map(<Vec<completion::AssistantContent>>::from)
1465 .collect();
1466
1467 let choice = OneOrMany::many(content).map_err(|_| {
1468 CompletionError::ResponseError(
1469 "Response contained no message or tool call (empty)".to_owned(),
1470 )
1471 })?;
1472
1473 let usage = response
1474 .usage
1475 .as_ref()
1476 .map(|usage| completion::Usage {
1477 input_tokens: usage.input_tokens,
1478 output_tokens: usage.output_tokens,
1479 total_tokens: usage.total_tokens,
1480 cached_input_tokens: usage
1481 .input_tokens_details
1482 .as_ref()
1483 .map(|d| d.cached_tokens)
1484 .unwrap_or(0),
1485 cache_creation_input_tokens: 0,
1486 })
1487 .unwrap_or_default();
1488
1489 Ok(completion::CompletionResponse {
1490 choice,
1491 usage,
1492 raw_response: response,
1493 message_id,
1494 })
1495 }
1496}
1497
1498#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
1500#[serde(tag = "role", rename_all = "lowercase")]
1501pub enum Message {
1502 #[serde(alias = "developer")]
1503 System {
1504 #[serde(deserialize_with = "string_or_one_or_many")]
1505 content: OneOrMany<SystemContent>,
1506 #[serde(skip_serializing_if = "Option::is_none")]
1507 name: Option<String>,
1508 },
1509 User {
1510 #[serde(deserialize_with = "string_or_one_or_many")]
1511 content: OneOrMany<UserContent>,
1512 #[serde(skip_serializing_if = "Option::is_none")]
1513 name: Option<String>,
1514 },
1515 Assistant {
1516 content: OneOrMany<AssistantContentType>,
1517 #[serde(skip_serializing_if = "String::is_empty")]
1518 id: String,
1519 #[serde(skip_serializing_if = "Option::is_none")]
1520 name: Option<String>,
1521 status: ToolStatus,
1522 },
1523 #[serde(rename = "tool")]
1524 ToolResult {
1525 tool_call_id: String,
1526 output: String,
1527 },
1528}
1529
1530#[derive(Default, Debug, Serialize, Deserialize, PartialEq, Clone)]
1532#[serde(rename_all = "lowercase")]
1533pub enum ToolResultContentType {
1534 #[default]
1535 Text,
1536}
1537
1538impl Message {
1539 pub fn system(content: &str) -> Self {
1540 Message::System {
1541 content: OneOrMany::one(content.to_owned().into()),
1542 name: None,
1543 }
1544 }
1545}
1546
1547#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
1550#[serde(tag = "type", rename_all = "snake_case")]
1551pub enum AssistantContent {
1552 OutputText(Text),
1553 Refusal { refusal: String },
1554}
1555
1556impl From<AssistantContent> for completion::AssistantContent {
1557 fn from(value: AssistantContent) -> Self {
1558 match value {
1559 AssistantContent::Refusal { refusal } => {
1560 completion::AssistantContent::Text(Text { text: refusal })
1561 }
1562 AssistantContent::OutputText(Text { text }) => {
1563 completion::AssistantContent::Text(Text { text })
1564 }
1565 }
1566 }
1567}
1568
1569#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
1571#[serde(untagged)]
1572pub enum AssistantContentType {
1573 Text(AssistantContent),
1574 ToolCall(OutputFunctionCall),
1575 Reasoning(OpenAIReasoning),
1576}
1577
1578#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
1581#[serde(tag = "type", rename_all = "snake_case")]
1582pub enum SystemContent {
1583 InputText { text: String },
1584}
1585
1586impl From<String> for SystemContent {
1587 fn from(s: String) -> Self {
1588 SystemContent::InputText { text: s }
1589 }
1590}
1591
1592impl std::str::FromStr for SystemContent {
1593 type Err = std::convert::Infallible;
1594
1595 fn from_str(s: &str) -> Result<Self, Self::Err> {
1596 Ok(SystemContent::InputText {
1597 text: s.to_string(),
1598 })
1599 }
1600}
1601
1602#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
1604#[serde(tag = "type", rename_all = "snake_case")]
1605pub enum UserContent {
1606 InputText {
1607 text: String,
1608 },
1609 InputImage {
1610 image_url: String,
1611 #[serde(default)]
1612 detail: ImageDetail,
1613 },
1614 InputFile {
1615 #[serde(skip_serializing_if = "Option::is_none")]
1616 file_url: Option<String>,
1617 #[serde(skip_serializing_if = "Option::is_none")]
1618 file_data: Option<String>,
1619 #[serde(skip_serializing_if = "Option::is_none")]
1620 filename: Option<String>,
1621 },
1622 Audio {
1623 input_audio: InputAudio,
1624 },
1625 #[serde(rename = "tool")]
1626 ToolResult {
1627 tool_call_id: String,
1628 output: String,
1629 },
1630}
1631
1632impl TryFrom<message::Message> for Vec<Message> {
1633 type Error = message::MessageError;
1634
1635 fn try_from(message: message::Message) -> Result<Self, Self::Error> {
1636 match message {
1637 message::Message::System { content } => Ok(vec![Message::System {
1638 content: OneOrMany::one(content.into()),
1639 name: None,
1640 }]),
1641 message::Message::User { content } => {
1642 let (tool_results, other_content): (Vec<_>, Vec<_>) = content
1643 .into_iter()
1644 .partition(|content| matches!(content, message::UserContent::ToolResult(_)));
1645
1646 if !tool_results.is_empty() {
1649 tool_results
1650 .into_iter()
1651 .map(|content| match content {
1652 message::UserContent::ToolResult(message::ToolResult {
1653 call_id,
1654 content,
1655 ..
1656 }) => Ok::<_, message::MessageError>(Message::ToolResult {
1657 tool_call_id: call_id.ok_or_else(|| {
1658 MessageError::ConversionError(
1659 "Tool result `call_id` is required for OpenAI Responses API"
1660 .into(),
1661 )
1662 })?,
1663 output: {
1664 let res = content.first();
1665 match res {
1666 completion::message::ToolResultContent::Text(Text {
1667 text,
1668 }) => text,
1669 _ => return Err(MessageError::ConversionError("This API only currently supports text tool results".into()))
1670 }
1671 },
1672 }),
1673 _ => Err(MessageError::ConversionError(
1674 "expected tool result content while converting Responses API input"
1675 .into(),
1676 )),
1677 })
1678 .collect::<Result<Vec<_>, _>>()
1679 } else {
1680 let other_content = other_content
1681 .into_iter()
1682 .map(|content| match content {
1683 message::UserContent::Text(message::Text { text }) => {
1684 Ok(UserContent::InputText { text })
1685 }
1686 message::UserContent::Image(message::Image {
1687 data,
1688 detail,
1689 media_type,
1690 ..
1691 }) => {
1692 let url = match data {
1693 DocumentSourceKind::Base64(data) => {
1694 let media_type = if let Some(media_type) = media_type {
1695 media_type.to_mime_type().to_string()
1696 } else {
1697 String::new()
1698 };
1699 format!("data:{media_type};base64,{data}")
1700 }
1701 DocumentSourceKind::Url(url) => url,
1702 DocumentSourceKind::Raw(_) => {
1703 return Err(MessageError::ConversionError(
1704 "Raw files not supported, encode as base64 first"
1705 .into(),
1706 ));
1707 }
1708 doc => {
1709 return Err(MessageError::ConversionError(format!(
1710 "Unsupported document type: {doc}"
1711 )));
1712 }
1713 };
1714
1715 Ok(UserContent::InputImage {
1716 image_url: url,
1717 detail: detail.unwrap_or_default(),
1718 })
1719 }
1720 message::UserContent::Document(message::Document {
1721 media_type: Some(DocumentMediaType::PDF),
1722 data,
1723 ..
1724 }) => {
1725 let (file_data, file_url, filename) = match data {
1726 DocumentSourceKind::Base64(data) => (
1727 Some(format!("data:application/pdf;base64,{data}")),
1728 None,
1729 Some("document.pdf".to_string()),
1730 ),
1731 DocumentSourceKind::Url(url) => (None, Some(url), None),
1732 DocumentSourceKind::Raw(_) => {
1733 return Err(MessageError::ConversionError(
1734 "Raw files not supported, encode as base64 first"
1735 .into(),
1736 ));
1737 }
1738 doc => {
1739 return Err(MessageError::ConversionError(format!(
1740 "Unsupported document type: {doc}"
1741 )));
1742 }
1743 };
1744
1745 Ok(UserContent::InputFile {
1746 file_url,
1747 file_data,
1748 filename,
1749 })
1750 }
1751 message::UserContent::Document(message::Document {
1752 data: DocumentSourceKind::Base64(text),
1753 ..
1754 }) => Ok(UserContent::InputText { text }),
1755 message::UserContent::Audio(message::Audio {
1756 data: DocumentSourceKind::Base64(data),
1757 media_type,
1758 ..
1759 }) => Ok(UserContent::Audio {
1760 input_audio: InputAudio {
1761 data,
1762 format: match media_type {
1763 Some(media_type) => media_type,
1764 None => AudioMediaType::MP3,
1765 },
1766 },
1767 }),
1768 message::UserContent::Audio(_) => Err(MessageError::ConversionError(
1769 "Audio must be base64 encoded data".into(),
1770 )),
1771 _ => Err(MessageError::ConversionError(
1772 "Unsupported user content for OpenAI Responses API".into(),
1773 )),
1774 })
1775 .collect::<Result<Vec<_>, _>>()?;
1776
1777 let other_content = OneOrMany::many(other_content).map_err(|_| {
1778 MessageError::ConversionError(
1779 "User message did not contain OpenAI Responses-compatible content"
1780 .to_string(),
1781 )
1782 })?;
1783
1784 Ok(vec![Message::User {
1785 content: other_content,
1786 name: None,
1787 }])
1788 }
1789 }
1790 message::Message::Assistant { content, id } => {
1791 let assistant_message_id = id.ok_or_else(|| {
1792 MessageError::ConversionError(
1793 "Assistant message ID is required for OpenAI Responses API".into(),
1794 )
1795 })?;
1796
1797 match content.first() {
1798 crate::message::AssistantContent::Text(Text { text }) => {
1799 Ok(vec![Message::Assistant {
1800 id: assistant_message_id.clone(),
1801 status: ToolStatus::Completed,
1802 content: OneOrMany::one(AssistantContentType::Text(
1803 AssistantContent::OutputText(Text { text }),
1804 )),
1805 name: None,
1806 }])
1807 }
1808 crate::message::AssistantContent::ToolCall(crate::message::ToolCall {
1809 id,
1810 call_id,
1811 function,
1812 ..
1813 }) => Ok(vec![Message::Assistant {
1814 content: OneOrMany::one(AssistantContentType::ToolCall(
1815 OutputFunctionCall {
1816 call_id: call_id.ok_or_else(|| {
1817 MessageError::ConversionError(
1818 "Tool call `call_id` is required for OpenAI Responses API"
1819 .into(),
1820 )
1821 })?,
1822 arguments: function.arguments,
1823 id,
1824 name: function.name,
1825 status: ToolStatus::Completed,
1826 },
1827 )),
1828 id: assistant_message_id.clone(),
1829 name: None,
1830 status: ToolStatus::Completed,
1831 }]),
1832 crate::message::AssistantContent::Reasoning(reasoning) => {
1833 let openai_reasoning = openai_reasoning_from_core(&reasoning)?;
1834 Ok(vec![Message::Assistant {
1835 content: OneOrMany::one(AssistantContentType::Reasoning(
1836 openai_reasoning,
1837 )),
1838 id: assistant_message_id,
1839 name: None,
1840 status: ToolStatus::Completed,
1841 }])
1842 }
1843 crate::message::AssistantContent::Image(_) => {
1844 Err(MessageError::ConversionError(
1845 "Assistant image content is not supported in OpenAI Responses API"
1846 .into(),
1847 ))
1848 }
1849 }
1850 }
1851 }
1852 }
1853}
1854
1855impl FromStr for UserContent {
1856 type Err = Infallible;
1857
1858 fn from_str(s: &str) -> Result<Self, Self::Err> {
1859 Ok(UserContent::InputText {
1860 text: s.to_string(),
1861 })
1862 }
1863}