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