1use super::InputAudio;
11use super::completion::ToolChoice;
12use super::{Client, responses_api::streaming::StreamingCompletionResponse};
13use crate::completion::CompletionError;
14use crate::http_client;
15use crate::http_client::HttpClientExt;
16use crate::json_utils;
17use crate::message::{
18 AudioMediaType, Document, DocumentMediaType, DocumentSourceKind, ImageDetail, MessageError,
19 MimeType, Text,
20};
21use crate::one_or_many::string_or_one_or_many;
22
23use crate::wasm_compat::{WasmCompatSend, WasmCompatSync};
24use crate::{OneOrMany, completion, message};
25use serde::{Deserialize, Serialize};
26use serde_json::{Map, Value};
27use tracing::{Instrument, Level, enabled, info_span};
28
29use std::convert::Infallible;
30use std::ops::Add;
31use std::str::FromStr;
32
33pub mod streaming;
34#[cfg(all(not(target_family = "wasm"), feature = "websocket"))]
35pub mod websocket;
36
37#[derive(Debug, Deserialize, Serialize, Clone)]
40pub struct CompletionRequest {
41 pub input: OneOrMany<InputItem>,
43 pub model: String,
45 #[serde(skip_serializing_if = "Option::is_none")]
47 pub instructions: Option<String>,
48 #[serde(skip_serializing_if = "Option::is_none")]
50 pub max_output_tokens: Option<u64>,
51 #[serde(skip_serializing_if = "Option::is_none")]
53 pub stream: Option<bool>,
54 #[serde(skip_serializing_if = "Option::is_none")]
56 pub temperature: Option<f64>,
57 #[serde(skip_serializing_if = "Option::is_none")]
60 tool_choice: Option<ToolChoice>,
61 #[serde(skip_serializing_if = "Vec::is_empty")]
64 pub tools: Vec<ResponsesToolDefinition>,
65 #[serde(flatten)]
67 pub additional_parameters: AdditionalParameters,
68}
69
70impl CompletionRequest {
71 pub fn with_structured_outputs<S>(mut self, schema_name: S, schema: serde_json::Value) -> Self
72 where
73 S: Into<String>,
74 {
75 self.additional_parameters.text = Some(TextConfig::structured_output(schema_name, schema));
76
77 self
78 }
79
80 pub fn with_reasoning(mut self, reasoning: Reasoning) -> Self {
81 self.additional_parameters.reasoning = Some(reasoning);
82
83 self
84 }
85
86 pub fn with_tool(mut self, tool: impl Into<ResponsesToolDefinition>) -> Self {
90 self.tools.push(tool.into());
91 self
92 }
93
94 pub fn with_tools<I, Tool>(mut self, tools: I) -> Self
97 where
98 I: IntoIterator<Item = Tool>,
99 Tool: Into<ResponsesToolDefinition>,
100 {
101 self.tools.extend(tools.into_iter().map(Into::into));
102 self
103 }
104}
105
106#[derive(Debug, Deserialize, Clone)]
108pub struct InputItem {
109 #[serde(skip_serializing_if = "Option::is_none")]
113 role: Option<Role>,
114 #[serde(flatten)]
116 input: InputContent,
117}
118
119impl Serialize for InputItem {
120 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
121 where
122 S: serde::Serializer,
123 {
124 let mut value = serde_json::to_value(&self.input).map_err(serde::ser::Error::custom)?;
125 let map = value.as_object_mut().ok_or_else(|| {
126 serde::ser::Error::custom("Input content must serialize to an object")
127 })?;
128
129 if let Some(role) = &self.role
130 && !map.contains_key("role")
131 {
132 map.insert(
133 "role".to_string(),
134 serde_json::to_value(role).map_err(serde::ser::Error::custom)?,
135 );
136 }
137
138 value.serialize(serializer)
139 }
140}
141
142impl InputItem {
143 pub fn system_message(content: impl Into<String>) -> Self {
144 Self {
145 role: Some(Role::System),
146 input: InputContent::Message(Message::System {
147 content: OneOrMany::one(SystemContent::InputText {
148 text: content.into(),
149 }),
150 name: None,
151 }),
152 }
153 }
154}
155
156#[derive(Debug, Deserialize, Serialize, Clone)]
158#[serde(rename_all = "lowercase")]
159pub enum Role {
160 User,
161 Assistant,
162 System,
163}
164
165#[derive(Debug, Deserialize, Serialize, Clone)]
167#[serde(tag = "type", rename_all = "snake_case")]
168pub enum InputContent {
169 Message(Message),
170 Reasoning(OpenAIReasoning),
171 FunctionCall(OutputFunctionCall),
172 FunctionCallOutput(ToolResult),
173}
174
175#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
176pub struct OpenAIReasoning {
177 id: String,
178 pub summary: Vec<ReasoningSummary>,
179 #[serde(skip_serializing_if = "Option::is_none")]
180 pub encrypted_content: Option<String>,
181 #[serde(skip_serializing_if = "Option::is_none")]
182 pub status: Option<ToolStatus>,
183}
184
185#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
186#[serde(tag = "type", rename_all = "snake_case")]
187pub enum ReasoningSummary {
188 SummaryText { text: String },
189}
190
191impl ReasoningSummary {
192 fn new(input: &str) -> Self {
193 Self::SummaryText {
194 text: input.to_string(),
195 }
196 }
197
198 pub fn text(&self) -> String {
199 let ReasoningSummary::SummaryText { text } = self;
200 text.clone()
201 }
202}
203
204#[derive(Debug, Deserialize, Serialize, Clone)]
206pub struct ToolResult {
207 call_id: String,
209 output: String,
211 status: ToolStatus,
213}
214
215impl From<Message> for InputItem {
216 fn from(value: Message) -> Self {
217 match value {
218 Message::User { .. } => Self {
219 role: Some(Role::User),
220 input: InputContent::Message(value),
221 },
222 Message::Assistant { ref content, .. } => {
223 let role = if content
224 .iter()
225 .any(|x| matches!(x, AssistantContentType::Reasoning(_)))
226 {
227 None
228 } else {
229 Some(Role::Assistant)
230 };
231 Self {
232 role,
233 input: InputContent::Message(value),
234 }
235 }
236 Message::System { .. } => Self {
237 role: Some(Role::System),
238 input: InputContent::Message(value),
239 },
240 Message::ToolResult {
241 tool_call_id,
242 output,
243 } => Self {
244 role: None,
245 input: InputContent::FunctionCallOutput(ToolResult {
246 call_id: tool_call_id,
247 output,
248 status: ToolStatus::Completed,
249 }),
250 },
251 }
252 }
253}
254
255impl TryFrom<crate::completion::Message> for Vec<InputItem> {
256 type Error = CompletionError;
257
258 fn try_from(value: crate::completion::Message) -> Result<Self, Self::Error> {
259 match value {
260 crate::completion::Message::System { content } => Ok(vec![InputItem {
261 role: Some(Role::System),
262 input: InputContent::Message(Message::System {
263 content: OneOrMany::one(content.into()),
264 name: None,
265 }),
266 }]),
267 crate::completion::Message::User { content } => {
268 let mut items = Vec::new();
269
270 for user_content in content {
271 match user_content {
272 crate::message::UserContent::Text(Text { text }) => {
273 items.push(InputItem {
274 role: Some(Role::User),
275 input: InputContent::Message(Message::User {
276 content: OneOrMany::one(UserContent::InputText { text }),
277 name: None,
278 }),
279 });
280 }
281 crate::message::UserContent::ToolResult(
282 crate::completion::message::ToolResult {
283 call_id,
284 content: tool_content,
285 ..
286 },
287 ) => {
288 for tool_result_content in tool_content {
289 let crate::completion::message::ToolResultContent::Text(Text {
290 text,
291 }) = tool_result_content
292 else {
293 return Err(CompletionError::ProviderError(
294 "This thing only supports text!".to_string(),
295 ));
296 };
297 items.push(InputItem {
299 role: None,
300 input: InputContent::FunctionCallOutput(ToolResult {
301 call_id: require_call_id(call_id.clone(), "Tool result")?,
302 output: text,
303 status: ToolStatus::Completed,
304 }),
305 });
306 }
307 }
308 crate::message::UserContent::Document(Document {
309 data,
310 media_type: Some(DocumentMediaType::PDF),
311 ..
312 }) => {
313 let (file_data, file_url) = match data {
314 DocumentSourceKind::Base64(data) => {
315 (Some(format!("data:application/pdf;base64,{data}")), None)
316 }
317 DocumentSourceKind::Url(url) => (None, Some(url)),
318 DocumentSourceKind::Raw(_) => {
319 return Err(CompletionError::RequestError(
320 "Raw file data not supported, encode as base64 first"
321 .into(),
322 ));
323 }
324 doc => {
325 return Err(CompletionError::RequestError(
326 format!("Unsupported document type: {doc}").into(),
327 ));
328 }
329 };
330
331 items.push(InputItem {
332 role: Some(Role::User),
333 input: InputContent::Message(Message::User {
334 content: OneOrMany::one(UserContent::InputFile {
335 file_data,
336 file_url,
337 filename: Some("document.pdf".to_string()),
338 }),
339 name: None,
340 }),
341 })
342 }
343 crate::message::UserContent::Document(Document {
344 data:
345 DocumentSourceKind::Base64(text) | DocumentSourceKind::String(text),
346 ..
347 }) => items.push(InputItem {
348 role: Some(Role::User),
349 input: InputContent::Message(Message::User {
350 content: OneOrMany::one(UserContent::InputText { text }),
351 name: None,
352 }),
353 }),
354 crate::message::UserContent::Image(crate::message::Image {
355 data,
356 media_type,
357 detail,
358 ..
359 }) => {
360 let url = match data {
361 DocumentSourceKind::Base64(data) => {
362 let media_type = if let Some(media_type) = media_type {
363 media_type.to_mime_type().to_string()
364 } else {
365 String::new()
366 };
367 format!("data:{media_type};base64,{data}")
368 }
369 DocumentSourceKind::Url(url) => url,
370 DocumentSourceKind::Raw(_) => {
371 return Err(CompletionError::RequestError(
372 "Raw file data not supported, encode as base64 first"
373 .into(),
374 ));
375 }
376 doc => {
377 return Err(CompletionError::RequestError(
378 format!("Unsupported document type: {doc}").into(),
379 ));
380 }
381 };
382 items.push(InputItem {
383 role: Some(Role::User),
384 input: InputContent::Message(Message::User {
385 content: OneOrMany::one(UserContent::InputImage {
386 image_url: url,
387 detail: detail.unwrap_or_default(),
388 }),
389 name: None,
390 }),
391 });
392 }
393 message => {
394 return Err(CompletionError::ProviderError(format!(
395 "Unsupported message: {message:?}"
396 )));
397 }
398 }
399 }
400
401 Ok(items)
402 }
403 crate::completion::Message::Assistant { id, content } => {
404 let mut reasoning_items = Vec::new();
405 let mut other_items = Vec::new();
406
407 for assistant_content in content {
408 match assistant_content {
409 crate::message::AssistantContent::Text(Text { text }) => {
410 let id = id.as_ref().unwrap_or(&String::default()).clone();
411 other_items.push(InputItem {
412 role: Some(Role::Assistant),
413 input: InputContent::Message(Message::Assistant {
414 content: OneOrMany::one(AssistantContentType::Text(
415 AssistantContent::OutputText(Text { text }),
416 )),
417 id,
418 name: None,
419 status: ToolStatus::Completed,
420 }),
421 });
422 }
423 crate::message::AssistantContent::ToolCall(crate::message::ToolCall {
424 id: tool_id,
425 call_id,
426 function,
427 ..
428 }) => {
429 other_items.push(InputItem {
430 role: None,
431 input: InputContent::FunctionCall(OutputFunctionCall {
432 arguments: function.arguments,
433 call_id: require_call_id(call_id, "Assistant tool call")?,
434 id: tool_id,
435 name: function.name,
436 status: ToolStatus::Completed,
437 }),
438 });
439 }
440 crate::message::AssistantContent::Reasoning(reasoning) => {
441 let openai_reasoning = openai_reasoning_from_core(&reasoning)
442 .map_err(|err| CompletionError::ProviderError(err.to_string()))?;
443 reasoning_items.push(InputItem {
444 role: None,
445 input: InputContent::Reasoning(openai_reasoning),
446 });
447 }
448 crate::message::AssistantContent::Image(_) => {
449 return Err(CompletionError::ProviderError(
450 "Assistant image content is not supported in OpenAI Responses API"
451 .to_string(),
452 ));
453 }
454 }
455 }
456
457 let mut items = reasoning_items;
458 items.extend(other_items);
459 Ok(items)
460 }
461 }
462 }
463}
464
465impl From<OneOrMany<String>> for Vec<ReasoningSummary> {
466 fn from(value: OneOrMany<String>) -> Self {
467 value.iter().map(|x| ReasoningSummary::new(x)).collect()
468 }
469}
470
471fn require_call_id(call_id: Option<String>, context: &str) -> Result<String, CompletionError> {
472 call_id.ok_or_else(|| {
473 CompletionError::RequestError(
474 format!("{context} `call_id` is required for OpenAI Responses API").into(),
475 )
476 })
477}
478
479fn openai_reasoning_from_core(
480 reasoning: &crate::message::Reasoning,
481) -> Result<OpenAIReasoning, MessageError> {
482 let id = reasoning.id.clone().ok_or_else(|| {
483 MessageError::ConversionError(
484 "An OpenAI-generated ID is required when using OpenAI reasoning items".to_string(),
485 )
486 })?;
487 let mut summary = Vec::new();
488 let mut encrypted_content = None;
489 for content in &reasoning.content {
490 match content {
491 crate::message::ReasoningContent::Text { text, .. }
492 | crate::message::ReasoningContent::Summary(text) => {
493 summary.push(ReasoningSummary::new(text));
494 }
495 crate::message::ReasoningContent::Encrypted(data)
498 | crate::message::ReasoningContent::Redacted { data } => {
499 encrypted_content.get_or_insert_with(|| data.clone());
500 }
501 }
502 }
503
504 Ok(OpenAIReasoning {
505 id,
506 summary,
507 encrypted_content,
508 status: None,
509 })
510}
511
512#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
514pub struct ResponsesToolDefinition {
515 #[serde(rename = "type")]
517 pub kind: String,
518 #[serde(default, skip_serializing_if = "String::is_empty")]
520 pub name: String,
521 #[serde(default, skip_serializing_if = "is_json_null")]
523 pub parameters: serde_json::Value,
524 #[serde(default, skip_serializing_if = "is_false")]
526 pub strict: bool,
527 #[serde(default, skip_serializing_if = "String::is_empty")]
529 pub description: String,
530 #[serde(flatten, default, skip_serializing_if = "Map::is_empty")]
532 pub config: Map<String, Value>,
533}
534
535fn is_json_null(value: &Value) -> bool {
536 value.is_null()
537}
538
539fn is_false(value: &bool) -> bool {
540 !value
541}
542
543impl ResponsesToolDefinition {
544 pub fn function(
546 name: impl Into<String>,
547 description: impl Into<String>,
548 mut parameters: serde_json::Value,
549 ) -> Self {
550 super::sanitize_schema(&mut parameters);
551
552 Self {
553 kind: "function".to_string(),
554 name: name.into(),
555 parameters,
556 strict: true,
557 description: description.into(),
558 config: Map::new(),
559 }
560 }
561
562 pub fn hosted(kind: impl Into<String>) -> Self {
564 Self {
565 kind: kind.into(),
566 name: String::new(),
567 parameters: Value::Null,
568 strict: false,
569 description: String::new(),
570 config: Map::new(),
571 }
572 }
573
574 pub fn web_search() -> Self {
576 Self::hosted("web_search")
577 }
578
579 pub fn file_search() -> Self {
581 Self::hosted("file_search")
582 }
583
584 pub fn computer_use() -> Self {
586 Self::hosted("computer_use")
587 }
588
589 pub fn with_config(mut self, key: impl Into<String>, value: Value) -> Self {
591 self.config.insert(key.into(), value);
592 self
593 }
594
595 fn normalize(mut self) -> Self {
596 if self.kind == "function" {
597 super::sanitize_schema(&mut self.parameters);
598 self.strict = true;
599 }
600 self
601 }
602}
603
604impl From<completion::ToolDefinition> for ResponsesToolDefinition {
605 fn from(value: completion::ToolDefinition) -> Self {
606 let completion::ToolDefinition {
607 name,
608 parameters,
609 description,
610 } = value;
611
612 Self::function(name, description, parameters)
613 }
614}
615
616#[derive(Clone, Debug, Serialize, Deserialize)]
619pub struct ResponsesUsage {
620 pub input_tokens: u64,
622 #[serde(skip_serializing_if = "Option::is_none")]
624 pub input_tokens_details: Option<InputTokensDetails>,
625 pub output_tokens: u64,
627 pub output_tokens_details: OutputTokensDetails,
629 pub total_tokens: u64,
631}
632
633impl ResponsesUsage {
634 pub(crate) fn new() -> Self {
636 Self {
637 input_tokens: 0,
638 input_tokens_details: Some(InputTokensDetails::new()),
639 output_tokens: 0,
640 output_tokens_details: OutputTokensDetails::new(),
641 total_tokens: 0,
642 }
643 }
644}
645
646impl Add for ResponsesUsage {
647 type Output = Self;
648
649 fn add(self, rhs: Self) -> Self::Output {
650 let input_tokens = self.input_tokens + rhs.input_tokens;
651 let input_tokens_details = self.input_tokens_details.map(|lhs| {
652 if let Some(tokens) = rhs.input_tokens_details {
653 lhs + tokens
654 } else {
655 lhs
656 }
657 });
658 let output_tokens = self.output_tokens + rhs.output_tokens;
659 let output_tokens_details = self.output_tokens_details + rhs.output_tokens_details;
660 let total_tokens = self.total_tokens + rhs.total_tokens;
661 Self {
662 input_tokens,
663 input_tokens_details,
664 output_tokens,
665 output_tokens_details,
666 total_tokens,
667 }
668 }
669}
670
671#[derive(Clone, Debug, Serialize, Deserialize)]
673pub struct InputTokensDetails {
674 pub cached_tokens: u64,
676}
677
678impl InputTokensDetails {
679 pub(crate) fn new() -> Self {
680 Self { cached_tokens: 0 }
681 }
682}
683
684impl Add for InputTokensDetails {
685 type Output = Self;
686 fn add(self, rhs: Self) -> Self::Output {
687 Self {
688 cached_tokens: self.cached_tokens + rhs.cached_tokens,
689 }
690 }
691}
692
693#[derive(Clone, Debug, Serialize, Deserialize)]
695pub struct OutputTokensDetails {
696 pub reasoning_tokens: u64,
698}
699
700impl OutputTokensDetails {
701 pub(crate) fn new() -> Self {
702 Self {
703 reasoning_tokens: 0,
704 }
705 }
706}
707
708impl Add for OutputTokensDetails {
709 type Output = Self;
710 fn add(self, rhs: Self) -> Self::Output {
711 Self {
712 reasoning_tokens: self.reasoning_tokens + rhs.reasoning_tokens,
713 }
714 }
715}
716
717#[derive(Clone, Debug, Default, Serialize, Deserialize)]
719pub struct IncompleteDetailsReason {
720 pub reason: String,
722}
723
724#[derive(Clone, Debug, Default, Serialize, Deserialize)]
726pub struct ResponseError {
727 pub code: String,
729 pub message: String,
731}
732
733#[derive(Clone, Debug, Deserialize, Serialize)]
735#[serde(rename_all = "snake_case")]
736pub enum ResponseObject {
737 Response,
738}
739
740#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
742#[serde(rename_all = "snake_case")]
743pub enum ResponseStatus {
744 InProgress,
745 Completed,
746 Failed,
747 Cancelled,
748 Queued,
749 Incomplete,
750}
751
752impl TryFrom<(String, crate::completion::CompletionRequest)> for CompletionRequest {
754 type Error = CompletionError;
755 fn try_from(
756 (model, mut req): (String, crate::completion::CompletionRequest),
757 ) -> Result<Self, Self::Error> {
758 let model = req.model.clone().unwrap_or(model);
759 let input = {
760 let mut partial_history = vec![];
761 if let Some(docs) = req.normalized_documents() {
762 partial_history.push(docs);
763 }
764 partial_history.extend(req.chat_history);
765
766 let mut full_history: Vec<InputItem> = if let Some(content) = req.preamble {
770 vec![InputItem::system_message(content)]
771 } else {
772 Vec::new()
773 };
774
775 for history_item in partial_history {
776 full_history.extend(<Vec<InputItem>>::try_from(history_item)?);
777 }
778
779 full_history
780 };
781
782 let input = OneOrMany::many(input).map_err(|_| {
783 CompletionError::RequestError(
784 "OpenAI Responses request input must contain at least one item".into(),
785 )
786 })?;
787
788 let mut additional_params_payload = req.additional_params.take().unwrap_or(Value::Null);
789 let stream = match &additional_params_payload {
790 Value::Bool(stream) => Some(*stream),
791 Value::Object(map) => map.get("stream").and_then(Value::as_bool),
792 _ => None,
793 };
794
795 let mut additional_tools = Vec::new();
796 if let Some(additional_params_map) = additional_params_payload.as_object_mut() {
797 if let Some(raw_tools) = additional_params_map.remove("tools") {
798 additional_tools = serde_json::from_value::<Vec<ResponsesToolDefinition>>(
799 raw_tools,
800 )
801 .map_err(|err| {
802 CompletionError::RequestError(
803 format!(
804 "Invalid OpenAI Responses tools payload in additional_params: {err}"
805 )
806 .into(),
807 )
808 })?;
809 }
810 additional_params_map.remove("stream");
811 }
812
813 if additional_params_payload.is_boolean() {
814 additional_params_payload = Value::Null;
815 }
816
817 additional_tools = additional_tools
818 .into_iter()
819 .map(ResponsesToolDefinition::normalize)
820 .collect();
821
822 let mut additional_parameters = if additional_params_payload.is_null() {
823 AdditionalParameters::default()
825 } else {
826 serde_json::from_value::<AdditionalParameters>(additional_params_payload).map_err(
827 |err| {
828 CompletionError::RequestError(
829 format!("Invalid OpenAI Responses additional_params payload: {err}").into(),
830 )
831 },
832 )?
833 };
834 if additional_parameters.reasoning.is_some() {
835 let include = additional_parameters.include.get_or_insert_with(Vec::new);
836 if !include
837 .iter()
838 .any(|item| matches!(item, Include::ReasoningEncryptedContent))
839 {
840 include.push(Include::ReasoningEncryptedContent);
841 }
842 }
843
844 if additional_parameters.text.is_none()
846 && let Some(schema) = req.output_schema
847 {
848 let name = schema
849 .as_object()
850 .and_then(|o| o.get("title"))
851 .and_then(|v| v.as_str())
852 .unwrap_or("response_schema")
853 .to_string();
854 let mut schema_value = schema.to_value();
855 super::sanitize_schema(&mut schema_value);
856 additional_parameters.text = Some(TextConfig::structured_output(name, schema_value));
857 }
858
859 let tool_choice = req.tool_choice.map(ToolChoice::try_from).transpose()?;
860 let mut tools: Vec<ResponsesToolDefinition> = req
861 .tools
862 .into_iter()
863 .map(ResponsesToolDefinition::from)
864 .collect();
865 tools.append(&mut additional_tools);
866
867 Ok(Self {
868 input,
869 model,
870 instructions: None, max_output_tokens: req.max_tokens,
872 stream,
873 tool_choice,
874 tools,
875 temperature: req.temperature,
876 additional_parameters,
877 })
878 }
879}
880
881#[derive(Clone)]
883pub struct ResponsesCompletionModel<T = reqwest::Client> {
884 pub(crate) client: Client<T>,
886 pub model: String,
888 pub tools: Vec<ResponsesToolDefinition>,
890}
891
892impl<T> ResponsesCompletionModel<T>
893where
894 T: HttpClientExt + Clone + Default + std::fmt::Debug + 'static,
895{
896 pub fn new(client: Client<T>, model: impl Into<String>) -> Self {
898 Self {
899 client,
900 model: model.into(),
901 tools: Vec::new(),
902 }
903 }
904
905 pub fn with_model(client: Client<T>, model: &str) -> Self {
906 Self {
907 client,
908 model: model.to_string(),
909 tools: Vec::new(),
910 }
911 }
912
913 pub fn with_tool(mut self, tool: impl Into<ResponsesToolDefinition>) -> Self {
915 self.tools.push(tool.into());
916 self
917 }
918
919 pub fn with_tools<I, Tool>(mut self, tools: I) -> Self
921 where
922 I: IntoIterator<Item = Tool>,
923 Tool: Into<ResponsesToolDefinition>,
924 {
925 self.tools.extend(tools.into_iter().map(Into::into));
926 self
927 }
928
929 pub fn completions_api(self) -> crate::providers::openai::completion::CompletionModel<T> {
931 super::completion::CompletionModel::with_model(self.client.completions_api(), &self.model)
932 }
933
934 pub(crate) fn create_completion_request(
936 &self,
937 completion_request: crate::completion::CompletionRequest,
938 ) -> Result<CompletionRequest, CompletionError> {
939 let mut req = CompletionRequest::try_from((self.model.clone(), completion_request))?;
940 req.tools.extend(self.tools.clone());
941
942 Ok(req)
943 }
944}
945
946#[derive(Clone, Debug, Serialize, Deserialize)]
948pub struct CompletionResponse {
949 pub id: String,
951 pub object: ResponseObject,
953 pub created_at: u64,
955 pub status: ResponseStatus,
957 pub error: Option<ResponseError>,
959 pub incomplete_details: Option<IncompleteDetailsReason>,
961 pub instructions: Option<String>,
963 pub max_output_tokens: Option<u64>,
965 pub model: String,
967 pub usage: Option<ResponsesUsage>,
969 pub output: Vec<Output>,
971 #[serde(default)]
973 pub tools: Vec<ResponsesToolDefinition>,
974 #[serde(flatten)]
976 pub additional_parameters: AdditionalParameters,
977}
978
979#[derive(Clone, Debug, Deserialize, Serialize, Default)]
982pub struct AdditionalParameters {
983 #[serde(skip_serializing_if = "Option::is_none")]
985 pub background: Option<bool>,
986 #[serde(skip_serializing_if = "Option::is_none")]
988 pub text: Option<TextConfig>,
989 #[serde(skip_serializing_if = "Option::is_none")]
991 pub include: Option<Vec<Include>>,
992 #[serde(skip_serializing_if = "Option::is_none")]
994 pub top_p: Option<f64>,
995 #[serde(skip_serializing_if = "Option::is_none")]
997 pub truncation: Option<TruncationStrategy>,
998 #[serde(skip_serializing_if = "Option::is_none")]
1000 pub user: Option<String>,
1001 #[serde(skip_serializing_if = "Map::is_empty", default)]
1003 pub metadata: serde_json::Map<String, serde_json::Value>,
1004 #[serde(skip_serializing_if = "Option::is_none")]
1006 pub parallel_tool_calls: Option<bool>,
1007 #[serde(skip_serializing_if = "Option::is_none")]
1009 pub previous_response_id: Option<String>,
1010 #[serde(skip_serializing_if = "Option::is_none")]
1012 pub reasoning: Option<Reasoning>,
1013 #[serde(skip_serializing_if = "Option::is_none")]
1015 pub service_tier: Option<OpenAIServiceTier>,
1016 #[serde(skip_serializing_if = "Option::is_none")]
1018 pub store: Option<bool>,
1019}
1020
1021impl AdditionalParameters {
1022 pub fn to_json(self) -> serde_json::Value {
1023 serde_json::to_value(self).unwrap_or_else(|_| serde_json::Value::Object(Map::new()))
1024 }
1025}
1026
1027#[derive(Clone, Debug, Default, Serialize, Deserialize)]
1031#[serde(rename_all = "snake_case")]
1032pub enum TruncationStrategy {
1033 Auto,
1034 #[default]
1035 Disabled,
1036}
1037
1038#[derive(Clone, Debug, Serialize, Deserialize)]
1041pub struct TextConfig {
1042 pub format: TextFormat,
1043}
1044
1045impl TextConfig {
1046 pub(crate) fn structured_output<S>(name: S, schema: serde_json::Value) -> Self
1047 where
1048 S: Into<String>,
1049 {
1050 Self {
1051 format: TextFormat::JsonSchema(StructuredOutputsInput {
1052 name: name.into(),
1053 schema,
1054 strict: true,
1055 }),
1056 }
1057 }
1058}
1059
1060#[derive(Clone, Debug, Serialize, Deserialize, Default)]
1063#[serde(tag = "type")]
1064#[serde(rename_all = "snake_case")]
1065pub enum TextFormat {
1066 JsonSchema(StructuredOutputsInput),
1067 #[default]
1068 Text,
1069}
1070
1071#[derive(Clone, Debug, Serialize, Deserialize)]
1073pub struct StructuredOutputsInput {
1074 pub name: String,
1076 pub schema: serde_json::Value,
1078 #[serde(default)]
1080 pub strict: bool,
1081}
1082
1083#[derive(Clone, Debug, Default, Serialize, Deserialize)]
1085pub struct Reasoning {
1086 pub effort: Option<ReasoningEffort>,
1088 #[serde(skip_serializing_if = "Option::is_none")]
1090 pub summary: Option<ReasoningSummaryLevel>,
1091}
1092
1093impl Reasoning {
1094 pub fn new() -> Self {
1096 Self {
1097 effort: None,
1098 summary: None,
1099 }
1100 }
1101
1102 pub fn with_effort(mut self, reasoning_effort: ReasoningEffort) -> Self {
1104 self.effort = Some(reasoning_effort);
1105
1106 self
1107 }
1108
1109 pub fn with_summary_level(mut self, reasoning_summary_level: ReasoningSummaryLevel) -> Self {
1111 self.summary = Some(reasoning_summary_level);
1112
1113 self
1114 }
1115}
1116
1117#[derive(Clone, Debug, Default, Serialize, Deserialize)]
1119#[serde(rename_all = "snake_case")]
1120pub enum OpenAIServiceTier {
1121 #[default]
1122 Auto,
1123 Default,
1124 Flex,
1125}
1126
1127#[derive(Clone, Debug, Default, Serialize, Deserialize)]
1129#[serde(rename_all = "snake_case")]
1130pub enum ReasoningEffort {
1131 None,
1132 Minimal,
1133 Low,
1134 #[default]
1135 Medium,
1136 High,
1137 Xhigh,
1138}
1139
1140#[derive(Clone, Debug, Default, Serialize, Deserialize)]
1142#[serde(rename_all = "snake_case")]
1143pub enum ReasoningSummaryLevel {
1144 #[default]
1145 Auto,
1146 Concise,
1147 Detailed,
1148}
1149
1150#[derive(Clone, Debug, Deserialize, Serialize)]
1153pub enum Include {
1154 #[serde(rename = "file_search_call.results")]
1155 FileSearchCallResults,
1156 #[serde(rename = "message.input_image.image_url")]
1157 MessageInputImageImageUrl,
1158 #[serde(rename = "computer_call.output.image_url")]
1159 ComputerCallOutputOutputImageUrl,
1160 #[serde(rename = "reasoning.encrypted_content")]
1161 ReasoningEncryptedContent,
1162 #[serde(rename = "code_interpreter_call.outputs")]
1163 CodeInterpreterCallOutputs,
1164}
1165
1166#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
1168#[serde(tag = "type")]
1169#[serde(rename_all = "snake_case")]
1170pub enum Output {
1171 Message(OutputMessage),
1172 #[serde(alias = "function_call")]
1173 FunctionCall(OutputFunctionCall),
1174 Reasoning {
1175 id: String,
1176 summary: Vec<ReasoningSummary>,
1177 #[serde(default)]
1178 encrypted_content: Option<String>,
1179 #[serde(default)]
1180 status: Option<ToolStatus>,
1181 },
1182}
1183
1184impl From<Output> for Vec<completion::AssistantContent> {
1185 fn from(value: Output) -> Self {
1186 let res: Vec<completion::AssistantContent> = match value {
1187 Output::Message(OutputMessage { content, .. }) => content
1188 .into_iter()
1189 .map(completion::AssistantContent::from)
1190 .collect(),
1191 Output::FunctionCall(OutputFunctionCall {
1192 id,
1193 arguments,
1194 call_id,
1195 name,
1196 ..
1197 }) => vec![completion::AssistantContent::tool_call_with_call_id(
1198 id, call_id, name, arguments,
1199 )],
1200 Output::Reasoning {
1201 id,
1202 summary,
1203 encrypted_content,
1204 ..
1205 } => {
1206 let mut content = summary
1207 .into_iter()
1208 .map(|summary| match summary {
1209 ReasoningSummary::SummaryText { text } => {
1210 message::ReasoningContent::Summary(text)
1211 }
1212 })
1213 .collect::<Vec<_>>();
1214 if let Some(encrypted_content) = encrypted_content {
1215 content.push(message::ReasoningContent::Encrypted(encrypted_content));
1216 }
1217 vec![completion::AssistantContent::Reasoning(
1218 message::Reasoning {
1219 id: Some(id),
1220 content,
1221 },
1222 )]
1223 }
1224 };
1225
1226 res
1227 }
1228}
1229
1230#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
1231pub struct OutputReasoning {
1232 id: String,
1233 summary: Vec<ReasoningSummary>,
1234 status: ToolStatus,
1235}
1236
1237#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
1239pub struct OutputFunctionCall {
1240 pub id: String,
1241 #[serde(with = "json_utils::stringified_json")]
1242 pub arguments: serde_json::Value,
1243 pub call_id: String,
1244 pub name: String,
1245 pub status: ToolStatus,
1246}
1247
1248#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
1250#[serde(rename_all = "snake_case")]
1251pub enum ToolStatus {
1252 InProgress,
1253 Completed,
1254 Incomplete,
1255}
1256
1257#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
1259pub struct OutputMessage {
1260 pub id: String,
1262 pub role: OutputRole,
1264 pub status: ResponseStatus,
1266 pub content: Vec<AssistantContent>,
1268}
1269
1270#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
1272#[serde(rename_all = "snake_case")]
1273pub enum OutputRole {
1274 Assistant,
1275}
1276
1277impl<T> completion::CompletionModel for ResponsesCompletionModel<T>
1278where
1279 T: HttpClientExt
1280 + Clone
1281 + std::fmt::Debug
1282 + Default
1283 + WasmCompatSend
1284 + WasmCompatSync
1285 + 'static,
1286{
1287 type Response = CompletionResponse;
1288 type StreamingResponse = StreamingCompletionResponse;
1289
1290 type Client = super::Client<T>;
1291
1292 fn make(client: &Self::Client, model: impl Into<String>) -> Self {
1293 Self::new(client.clone(), model)
1294 }
1295
1296 async fn completion(
1297 &self,
1298 completion_request: crate::completion::CompletionRequest,
1299 ) -> Result<completion::CompletionResponse<Self::Response>, CompletionError> {
1300 let span = if tracing::Span::current().is_disabled() {
1301 info_span!(
1302 target: "rig::completions",
1303 "chat",
1304 gen_ai.operation.name = "chat",
1305 gen_ai.provider.name = tracing::field::Empty,
1306 gen_ai.request.model = tracing::field::Empty,
1307 gen_ai.response.id = tracing::field::Empty,
1308 gen_ai.response.model = tracing::field::Empty,
1309 gen_ai.usage.output_tokens = tracing::field::Empty,
1310 gen_ai.usage.input_tokens = tracing::field::Empty,
1311 gen_ai.usage.cached_tokens = tracing::field::Empty,
1312 gen_ai.input.messages = tracing::field::Empty,
1313 gen_ai.output.messages = tracing::field::Empty,
1314 )
1315 } else {
1316 tracing::Span::current()
1317 };
1318
1319 span.record("gen_ai.provider.name", "openai");
1320 span.record("gen_ai.request.model", &self.model);
1321 let request = self.create_completion_request(completion_request)?;
1322 let body = serde_json::to_vec(&request)?;
1323
1324 if enabled!(Level::TRACE) {
1325 tracing::trace!(
1326 target: "rig::completions",
1327 "OpenAI Responses completion request: {request}",
1328 request = serde_json::to_string_pretty(&request)?
1329 );
1330 }
1331
1332 let req = self
1333 .client
1334 .post("/responses")?
1335 .body(body)
1336 .map_err(|e| CompletionError::HttpError(e.into()))?;
1337
1338 async move {
1339 let response = self.client.send(req).await?;
1340
1341 if response.status().is_success() {
1342 let t = http_client::text(response).await?;
1343 let response = serde_json::from_str::<Self::Response>(&t)?;
1344 let span = tracing::Span::current();
1345 span.record("gen_ai.response.id", &response.id);
1346 span.record("gen_ai.response.model", &response.model);
1347 if let Some(ref usage) = response.usage {
1348 span.record("gen_ai.usage.output_tokens", usage.output_tokens);
1349 span.record("gen_ai.usage.input_tokens", usage.input_tokens);
1350 span.record(
1351 "gen_ai.usage.cached_tokens",
1352 usage
1353 .input_tokens_details
1354 .as_ref()
1355 .map(|d| d.cached_tokens)
1356 .unwrap_or(0),
1357 );
1358 }
1359 if enabled!(Level::TRACE) {
1360 tracing::trace!(
1361 target: "rig::completions",
1362 "OpenAI Responses completion response: {response}",
1363 response = serde_json::to_string_pretty(&response)?
1364 );
1365 }
1366 response.try_into()
1367 } else {
1368 let text = http_client::text(response).await?;
1369 Err(CompletionError::ProviderError(text))
1370 }
1371 }
1372 .instrument(span)
1373 .await
1374 }
1375
1376 async fn stream(
1377 &self,
1378 request: crate::completion::CompletionRequest,
1379 ) -> Result<
1380 crate::streaming::StreamingCompletionResponse<Self::StreamingResponse>,
1381 CompletionError,
1382 > {
1383 ResponsesCompletionModel::stream(self, request).await
1384 }
1385}
1386
1387impl TryFrom<CompletionResponse> for completion::CompletionResponse<CompletionResponse> {
1388 type Error = CompletionError;
1389
1390 fn try_from(response: CompletionResponse) -> Result<Self, Self::Error> {
1391 if response.output.is_empty() {
1392 return Err(CompletionError::ResponseError(
1393 "Response contained no parts".to_owned(),
1394 ));
1395 }
1396
1397 let message_id = response.output.iter().find_map(|item| match item {
1399 Output::Message(msg) => Some(msg.id.clone()),
1400 _ => None,
1401 });
1402
1403 let content: Vec<completion::AssistantContent> = response
1404 .output
1405 .iter()
1406 .cloned()
1407 .flat_map(<Vec<completion::AssistantContent>>::from)
1408 .collect();
1409
1410 let choice = OneOrMany::many(content).map_err(|_| {
1411 CompletionError::ResponseError(
1412 "Response contained no message or tool call (empty)".to_owned(),
1413 )
1414 })?;
1415
1416 let usage = response
1417 .usage
1418 .as_ref()
1419 .map(|usage| completion::Usage {
1420 input_tokens: usage.input_tokens,
1421 output_tokens: usage.output_tokens,
1422 total_tokens: usage.total_tokens,
1423 cached_input_tokens: usage
1424 .input_tokens_details
1425 .as_ref()
1426 .map(|d| d.cached_tokens)
1427 .unwrap_or(0),
1428 cache_creation_input_tokens: 0,
1429 })
1430 .unwrap_or_default();
1431
1432 Ok(completion::CompletionResponse {
1433 choice,
1434 usage,
1435 raw_response: response,
1436 message_id,
1437 })
1438 }
1439}
1440
1441#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
1443#[serde(tag = "role", rename_all = "lowercase")]
1444pub enum Message {
1445 #[serde(alias = "developer")]
1446 System {
1447 #[serde(deserialize_with = "string_or_one_or_many")]
1448 content: OneOrMany<SystemContent>,
1449 #[serde(skip_serializing_if = "Option::is_none")]
1450 name: Option<String>,
1451 },
1452 User {
1453 #[serde(deserialize_with = "string_or_one_or_many")]
1454 content: OneOrMany<UserContent>,
1455 #[serde(skip_serializing_if = "Option::is_none")]
1456 name: Option<String>,
1457 },
1458 Assistant {
1459 content: OneOrMany<AssistantContentType>,
1460 #[serde(skip_serializing_if = "String::is_empty")]
1461 id: String,
1462 #[serde(skip_serializing_if = "Option::is_none")]
1463 name: Option<String>,
1464 status: ToolStatus,
1465 },
1466 #[serde(rename = "tool")]
1467 ToolResult {
1468 tool_call_id: String,
1469 output: String,
1470 },
1471}
1472
1473#[derive(Default, Debug, Serialize, Deserialize, PartialEq, Clone)]
1475#[serde(rename_all = "lowercase")]
1476pub enum ToolResultContentType {
1477 #[default]
1478 Text,
1479}
1480
1481impl Message {
1482 pub fn system(content: &str) -> Self {
1483 Message::System {
1484 content: OneOrMany::one(content.to_owned().into()),
1485 name: None,
1486 }
1487 }
1488}
1489
1490#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
1493#[serde(tag = "type", rename_all = "snake_case")]
1494pub enum AssistantContent {
1495 OutputText(Text),
1496 Refusal { refusal: String },
1497}
1498
1499impl From<AssistantContent> for completion::AssistantContent {
1500 fn from(value: AssistantContent) -> Self {
1501 match value {
1502 AssistantContent::Refusal { refusal } => {
1503 completion::AssistantContent::Text(Text { text: refusal })
1504 }
1505 AssistantContent::OutputText(Text { text }) => {
1506 completion::AssistantContent::Text(Text { text })
1507 }
1508 }
1509 }
1510}
1511
1512#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
1514#[serde(untagged)]
1515pub enum AssistantContentType {
1516 Text(AssistantContent),
1517 ToolCall(OutputFunctionCall),
1518 Reasoning(OpenAIReasoning),
1519}
1520
1521#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
1524#[serde(tag = "type", rename_all = "snake_case")]
1525pub enum SystemContent {
1526 InputText { text: String },
1527}
1528
1529impl From<String> for SystemContent {
1530 fn from(s: String) -> Self {
1531 SystemContent::InputText { text: s }
1532 }
1533}
1534
1535impl std::str::FromStr for SystemContent {
1536 type Err = std::convert::Infallible;
1537
1538 fn from_str(s: &str) -> Result<Self, Self::Err> {
1539 Ok(SystemContent::InputText {
1540 text: s.to_string(),
1541 })
1542 }
1543}
1544
1545#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
1547#[serde(tag = "type", rename_all = "snake_case")]
1548pub enum UserContent {
1549 InputText {
1550 text: String,
1551 },
1552 InputImage {
1553 image_url: String,
1554 #[serde(default)]
1555 detail: ImageDetail,
1556 },
1557 InputFile {
1558 #[serde(skip_serializing_if = "Option::is_none")]
1559 file_url: Option<String>,
1560 #[serde(skip_serializing_if = "Option::is_none")]
1561 file_data: Option<String>,
1562 #[serde(skip_serializing_if = "Option::is_none")]
1563 filename: Option<String>,
1564 },
1565 Audio {
1566 input_audio: InputAudio,
1567 },
1568 #[serde(rename = "tool")]
1569 ToolResult {
1570 tool_call_id: String,
1571 output: String,
1572 },
1573}
1574
1575impl TryFrom<message::Message> for Vec<Message> {
1576 type Error = message::MessageError;
1577
1578 fn try_from(message: message::Message) -> Result<Self, Self::Error> {
1579 match message {
1580 message::Message::System { content } => Ok(vec![Message::System {
1581 content: OneOrMany::one(content.into()),
1582 name: None,
1583 }]),
1584 message::Message::User { content } => {
1585 let (tool_results, other_content): (Vec<_>, Vec<_>) = content
1586 .into_iter()
1587 .partition(|content| matches!(content, message::UserContent::ToolResult(_)));
1588
1589 if !tool_results.is_empty() {
1592 tool_results
1593 .into_iter()
1594 .map(|content| match content {
1595 message::UserContent::ToolResult(message::ToolResult {
1596 call_id,
1597 content,
1598 ..
1599 }) => Ok::<_, message::MessageError>(Message::ToolResult {
1600 tool_call_id: call_id.ok_or_else(|| {
1601 MessageError::ConversionError(
1602 "Tool result `call_id` is required for OpenAI Responses API"
1603 .into(),
1604 )
1605 })?,
1606 output: {
1607 let res = content.first();
1608 match res {
1609 completion::message::ToolResultContent::Text(Text {
1610 text,
1611 }) => text,
1612 _ => return Err(MessageError::ConversionError("This API only currently supports text tool results".into()))
1613 }
1614 },
1615 }),
1616 _ => unreachable!(),
1617 })
1618 .collect::<Result<Vec<_>, _>>()
1619 } else {
1620 let other_content = other_content
1621 .into_iter()
1622 .map(|content| match content {
1623 message::UserContent::Text(message::Text { text }) => {
1624 Ok(UserContent::InputText { text })
1625 }
1626 message::UserContent::Image(message::Image {
1627 data,
1628 detail,
1629 media_type,
1630 ..
1631 }) => {
1632 let url = match data {
1633 DocumentSourceKind::Base64(data) => {
1634 let media_type = if let Some(media_type) = media_type {
1635 media_type.to_mime_type().to_string()
1636 } else {
1637 String::new()
1638 };
1639 format!("data:{media_type};base64,{data}")
1640 }
1641 DocumentSourceKind::Url(url) => url,
1642 DocumentSourceKind::Raw(_) => {
1643 return Err(MessageError::ConversionError(
1644 "Raw files not supported, encode as base64 first"
1645 .into(),
1646 ));
1647 }
1648 doc => {
1649 return Err(MessageError::ConversionError(format!(
1650 "Unsupported document type: {doc}"
1651 )));
1652 }
1653 };
1654
1655 Ok(UserContent::InputImage {
1656 image_url: url,
1657 detail: detail.unwrap_or_default(),
1658 })
1659 }
1660 message::UserContent::Document(message::Document {
1661 media_type: Some(DocumentMediaType::PDF),
1662 data,
1663 ..
1664 }) => {
1665 let (file_data, file_url, filename) = match data {
1666 DocumentSourceKind::Base64(data) => (
1667 Some(format!("data:application/pdf;base64,{data}")),
1668 None,
1669 Some("document.pdf".to_string()),
1670 ),
1671 DocumentSourceKind::Url(url) => (None, Some(url), None),
1672 DocumentSourceKind::Raw(_) => {
1673 return Err(MessageError::ConversionError(
1674 "Raw files not supported, encode as base64 first"
1675 .into(),
1676 ));
1677 }
1678 doc => {
1679 return Err(MessageError::ConversionError(format!(
1680 "Unsupported document type: {doc}"
1681 )));
1682 }
1683 };
1684
1685 Ok(UserContent::InputFile {
1686 file_url,
1687 file_data,
1688 filename,
1689 })
1690 }
1691 message::UserContent::Document(message::Document {
1692 data: DocumentSourceKind::Base64(text),
1693 ..
1694 }) => Ok(UserContent::InputText { text }),
1695 message::UserContent::Audio(message::Audio {
1696 data: DocumentSourceKind::Base64(data),
1697 media_type,
1698 ..
1699 }) => Ok(UserContent::Audio {
1700 input_audio: InputAudio {
1701 data,
1702 format: match media_type {
1703 Some(media_type) => media_type,
1704 None => AudioMediaType::MP3,
1705 },
1706 },
1707 }),
1708 message::UserContent::Audio(_) => Err(MessageError::ConversionError(
1709 "Audio must be base64 encoded data".into(),
1710 )),
1711 _ => unreachable!(),
1712 })
1713 .collect::<Result<Vec<_>, _>>()?;
1714
1715 let other_content = OneOrMany::many(other_content).map_err(|_| {
1716 MessageError::ConversionError(
1717 "User message did not contain OpenAI Responses-compatible content"
1718 .to_string(),
1719 )
1720 })?;
1721
1722 Ok(vec![Message::User {
1723 content: other_content,
1724 name: None,
1725 }])
1726 }
1727 }
1728 message::Message::Assistant { content, id } => {
1729 let assistant_message_id = id.ok_or_else(|| {
1730 MessageError::ConversionError(
1731 "Assistant message ID is required for OpenAI Responses API".into(),
1732 )
1733 })?;
1734
1735 match content.first() {
1736 crate::message::AssistantContent::Text(Text { text }) => {
1737 Ok(vec![Message::Assistant {
1738 id: assistant_message_id.clone(),
1739 status: ToolStatus::Completed,
1740 content: OneOrMany::one(AssistantContentType::Text(
1741 AssistantContent::OutputText(Text { text }),
1742 )),
1743 name: None,
1744 }])
1745 }
1746 crate::message::AssistantContent::ToolCall(crate::message::ToolCall {
1747 id,
1748 call_id,
1749 function,
1750 ..
1751 }) => Ok(vec![Message::Assistant {
1752 content: OneOrMany::one(AssistantContentType::ToolCall(
1753 OutputFunctionCall {
1754 call_id: call_id.ok_or_else(|| {
1755 MessageError::ConversionError(
1756 "Tool call `call_id` is required for OpenAI Responses API"
1757 .into(),
1758 )
1759 })?,
1760 arguments: function.arguments,
1761 id,
1762 name: function.name,
1763 status: ToolStatus::Completed,
1764 },
1765 )),
1766 id: assistant_message_id.clone(),
1767 name: None,
1768 status: ToolStatus::Completed,
1769 }]),
1770 crate::message::AssistantContent::Reasoning(reasoning) => {
1771 let openai_reasoning = openai_reasoning_from_core(&reasoning)?;
1772 Ok(vec![Message::Assistant {
1773 content: OneOrMany::one(AssistantContentType::Reasoning(
1774 openai_reasoning,
1775 )),
1776 id: assistant_message_id,
1777 name: None,
1778 status: ToolStatus::Completed,
1779 }])
1780 }
1781 crate::message::AssistantContent::Image(_) => {
1782 Err(MessageError::ConversionError(
1783 "Assistant image content is not supported in OpenAI Responses API"
1784 .into(),
1785 ))
1786 }
1787 }
1788 }
1789 }
1790 }
1791}
1792
1793impl FromStr for UserContent {
1794 type Err = Infallible;
1795
1796 fn from_str(s: &str) -> Result<Self, Self::Err> {
1797 Ok(UserContent::InputText {
1798 text: s.to_string(),
1799 })
1800 }
1801}