Skip to main content

rig/providers/openai/responses_api/
mod.rs

1//! The OpenAI Responses API.
2//!
3//! By default when creating a completion client, this is the API that gets used.
4//!
5//! If you'd like to switch back to the regular Completions API, you can do so by using the `.completions_api()` function - see below for an example:
6//! ```rust
7//! use rig::client::{CompletionClient, ProviderClient};
8//!
9//! # fn example() -> Result<(), Box<dyn std::error::Error>> {
10//! let openai_client = rig::providers::openai::Client::from_env()?;
11//! let model = openai_client.completion_model("gpt-4o").completions_api();
12//! # let _ = model;
13//! # Ok(())
14//! # }
15//! ```
16use super::InputAudio;
17use super::completion::ToolChoice;
18use super::responses_api::streaming::StreamingCompletionResponse;
19use crate::completion::{CompletionError, GetTokenUsage};
20use crate::http_client;
21use crate::http_client::HttpClientExt;
22use crate::json_utils;
23use crate::message::{
24    AudioMediaType, Document, DocumentMediaType, DocumentSourceKind, ImageDetail, MessageError,
25    MimeType, Text,
26};
27use crate::one_or_many::string_or_one_or_many;
28
29use crate::wasm_compat::{WasmCompatSend, WasmCompatSync};
30use crate::{OneOrMany, completion, message};
31use serde::{Deserialize, Serialize};
32use serde_json::{Map, Value};
33use tracing::{Instrument, Level, enabled, info_span};
34
35use std::convert::Infallible;
36use std::ops::Add;
37use std::str::FromStr;
38
39pub mod streaming;
40#[cfg(all(not(target_family = "wasm"), feature = "websocket"))]
41pub mod websocket;
42
43/// The completion request type for OpenAI's Response API: <https://platform.openai.com/docs/api-reference/responses/create>
44/// Intended to be derived from [`crate::completion::request::CompletionRequest`].
45#[derive(Debug, Deserialize, Serialize, Clone)]
46pub struct CompletionRequest {
47    /// Message inputs
48    pub input: OneOrMany<InputItem>,
49    /// The model name
50    pub model: String,
51    /// Instructions (also referred to as preamble, although in other APIs this would be the "system prompt")
52    #[serde(skip_serializing_if = "Option::is_none")]
53    pub instructions: Option<String>,
54    /// The maximum number of output tokens.
55    #[serde(skip_serializing_if = "Option::is_none")]
56    pub max_output_tokens: Option<u64>,
57    /// Toggle to true for streaming responses.
58    #[serde(skip_serializing_if = "Option::is_none")]
59    pub stream: Option<bool>,
60    /// The temperature. Set higher (up to a max of 1.0) for more creative responses.
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub temperature: Option<f64>,
63    /// Whether the LLM should be forced to use a tool before returning a response.
64    /// If none provided, the default option is "auto".
65    #[serde(skip_serializing_if = "Option::is_none")]
66    tool_choice: Option<ToolChoice>,
67    /// The tools you want to use. This supports both function tools and hosted tools
68    /// such as `web_search`, `file_search`, and `computer_use`.
69    #[serde(skip_serializing_if = "Vec::is_empty")]
70    pub tools: Vec<ResponsesToolDefinition>,
71    /// Additional parameters
72    #[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    /// Adds a provider-native hosted tool (e.g. `web_search`, `file_search`, `computer_use`)
93    /// to the request. These tools are executed by OpenAI's infrastructure, not by Rig's
94    /// agent loop.
95    pub fn with_tool(mut self, tool: impl Into<ResponsesToolDefinition>) -> Self {
96        self.tools.push(tool.into());
97        self
98    }
99
100    /// Adds multiple provider-native hosted tools to the request. These tools are executed
101    /// by OpenAI's infrastructure, not by Rig's agent loop.
102    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/// An input item for [`CompletionRequest`].
113#[derive(Debug, Deserialize, Clone)]
114pub struct InputItem {
115    /// The role of an input item/message.
116    /// Input messages should be Some(Role::User), and output messages should be Some(Role::Assistant).
117    /// Everything else should be None.
118    #[serde(skip_serializing_if = "Option::is_none")]
119    role: Option<Role>,
120    /// The input content itself.
121    #[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/// Message roles. Used by OpenAI Responses API to determine who created a given message.
178#[derive(Debug, Deserialize, Serialize, Clone)]
179#[serde(rename_all = "lowercase")]
180pub enum Role {
181    User,
182    Assistant,
183    System,
184}
185
186/// The type of content used in an [`InputItem`]. Additionally holds data for each type of input content.
187#[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/// A tool result.
226#[derive(Debug, Deserialize, Serialize, Clone)]
227pub struct ToolResult {
228    /// The call ID of a tool (this should be linked to the call ID for a tool call, otherwise an error will be received)
229    call_id: String,
230    /// The result of a tool call.
231    output: String,
232    /// The status of a tool call (if used in a completion request, this should always be Completed)
233    status: ToolStatus,
234}
235
236impl From<Message> for InputItem {
237    fn from(value: Message) -> Self {
238        match value {
239            Message::User { .. } => Self {
240                role: Some(Role::User),
241                input: InputContent::Message(value),
242            },
243            Message::Assistant { ref content, .. } => {
244                let role = if content
245                    .iter()
246                    .any(|x| matches!(x, AssistantContentType::Reasoning(_)))
247                {
248                    None
249                } else {
250                    Some(Role::Assistant)
251                };
252                Self {
253                    role,
254                    input: InputContent::Message(value),
255                }
256            }
257            Message::System { .. } => Self {
258                role: Some(Role::System),
259                input: InputContent::Message(value),
260            },
261            Message::ToolResult {
262                tool_call_id,
263                output,
264            } => Self {
265                role: None,
266                input: InputContent::FunctionCallOutput(ToolResult {
267                    call_id: tool_call_id,
268                    output,
269                    status: ToolStatus::Completed,
270                }),
271            },
272        }
273    }
274}
275
276impl TryFrom<crate::completion::Message> for Vec<InputItem> {
277    type Error = CompletionError;
278
279    fn try_from(value: crate::completion::Message) -> Result<Self, Self::Error> {
280        match value {
281            crate::completion::Message::System { content } => Ok(vec![InputItem {
282                role: Some(Role::System),
283                input: InputContent::Message(Message::System {
284                    content: OneOrMany::one(content.into()),
285                    name: None,
286                }),
287            }]),
288            crate::completion::Message::User { content } => {
289                let mut items = Vec::new();
290
291                for user_content in content {
292                    match user_content {
293                        crate::message::UserContent::Text(Text { text }) => {
294                            items.push(InputItem {
295                                role: Some(Role::User),
296                                input: InputContent::Message(Message::User {
297                                    content: OneOrMany::one(UserContent::InputText { text }),
298                                    name: None,
299                                }),
300                            });
301                        }
302                        crate::message::UserContent::ToolResult(
303                            crate::completion::message::ToolResult {
304                                call_id,
305                                content: tool_content,
306                                ..
307                            },
308                        ) => {
309                            for tool_result_content in tool_content {
310                                let crate::completion::message::ToolResultContent::Text(Text {
311                                    text,
312                                }) = tool_result_content
313                                else {
314                                    return Err(CompletionError::ProviderError(
315                                        "This thing only supports text!".to_string(),
316                                    ));
317                                };
318                                // let output = serde_json::from_str(&text)?;
319                                items.push(InputItem {
320                                    role: None,
321                                    input: InputContent::FunctionCallOutput(ToolResult {
322                                        call_id: require_call_id(call_id.clone(), "Tool result")?,
323                                        output: text,
324                                        status: ToolStatus::Completed,
325                                    }),
326                                });
327                            }
328                        }
329                        crate::message::UserContent::Document(Document {
330                            data,
331                            media_type: Some(DocumentMediaType::PDF),
332                            ..
333                        }) => {
334                            let (file_data, file_url) = match data {
335                                DocumentSourceKind::Base64(data) => {
336                                    (Some(format!("data:application/pdf;base64,{data}")), None)
337                                }
338                                DocumentSourceKind::Url(url) => (None, Some(url)),
339                                DocumentSourceKind::Raw(_) => {
340                                    return Err(CompletionError::RequestError(
341                                        "Raw file data not supported, encode as base64 first"
342                                            .into(),
343                                    ));
344                                }
345                                doc => {
346                                    return Err(CompletionError::RequestError(
347                                        format!("Unsupported document type: {doc}").into(),
348                                    ));
349                                }
350                            };
351
352                            items.push(InputItem {
353                                role: Some(Role::User),
354                                input: InputContent::Message(Message::User {
355                                    content: OneOrMany::one(UserContent::InputFile {
356                                        file_data,
357                                        file_url,
358                                        filename: Some("document.pdf".to_string()),
359                                    }),
360                                    name: None,
361                                }),
362                            })
363                        }
364                        crate::message::UserContent::Document(Document {
365                            data:
366                                DocumentSourceKind::Base64(text) | DocumentSourceKind::String(text),
367                            ..
368                        }) => items.push(InputItem {
369                            role: Some(Role::User),
370                            input: InputContent::Message(Message::User {
371                                content: OneOrMany::one(UserContent::InputText { text }),
372                                name: None,
373                            }),
374                        }),
375                        crate::message::UserContent::Image(crate::message::Image {
376                            data,
377                            media_type,
378                            detail,
379                            ..
380                        }) => {
381                            let url = match data {
382                                DocumentSourceKind::Base64(data) => {
383                                    let media_type = if let Some(media_type) = media_type {
384                                        media_type.to_mime_type().to_string()
385                                    } else {
386                                        String::new()
387                                    };
388                                    format!("data:{media_type};base64,{data}")
389                                }
390                                DocumentSourceKind::Url(url) => url,
391                                DocumentSourceKind::Raw(_) => {
392                                    return Err(CompletionError::RequestError(
393                                        "Raw file data not supported, encode as base64 first"
394                                            .into(),
395                                    ));
396                                }
397                                doc => {
398                                    return Err(CompletionError::RequestError(
399                                        format!("Unsupported document type: {doc}").into(),
400                                    ));
401                                }
402                            };
403                            items.push(InputItem {
404                                role: Some(Role::User),
405                                input: InputContent::Message(Message::User {
406                                    content: OneOrMany::one(UserContent::InputImage {
407                                        image_url: url,
408                                        detail: detail.unwrap_or_default(),
409                                    }),
410                                    name: None,
411                                }),
412                            });
413                        }
414                        message => {
415                            return Err(CompletionError::ProviderError(format!(
416                                "Unsupported message: {message:?}"
417                            )));
418                        }
419                    }
420                }
421
422                Ok(items)
423            }
424            crate::completion::Message::Assistant { id, content } => {
425                let mut reasoning_items = Vec::new();
426                let mut other_items = Vec::new();
427
428                for assistant_content in content {
429                    match assistant_content {
430                        crate::message::AssistantContent::Text(Text { text }) => {
431                            let id = id.as_ref().unwrap_or(&String::default()).clone();
432                            other_items.push(InputItem {
433                                role: Some(Role::Assistant),
434                                input: InputContent::Message(Message::Assistant {
435                                    content: OneOrMany::one(AssistantContentType::Text(
436                                        AssistantContent::OutputText(Text { text }),
437                                    )),
438                                    id,
439                                    name: None,
440                                    status: ToolStatus::Completed,
441                                }),
442                            });
443                        }
444                        crate::message::AssistantContent::ToolCall(crate::message::ToolCall {
445                            id: tool_id,
446                            call_id,
447                            function,
448                            ..
449                        }) => {
450                            other_items.push(InputItem {
451                                role: None,
452                                input: InputContent::FunctionCall(OutputFunctionCall {
453                                    arguments: function.arguments,
454                                    call_id: require_call_id(call_id, "Assistant tool call")?,
455                                    id: tool_id,
456                                    name: function.name,
457                                    status: ToolStatus::Completed,
458                                }),
459                            });
460                        }
461                        crate::message::AssistantContent::Reasoning(reasoning) => {
462                            let openai_reasoning = openai_reasoning_from_core(&reasoning)
463                                .map_err(|err| CompletionError::ProviderError(err.to_string()))?;
464                            reasoning_items.push(InputItem {
465                                role: None,
466                                input: InputContent::Reasoning(openai_reasoning),
467                            });
468                        }
469                        crate::message::AssistantContent::Image(_) => {
470                            return Err(CompletionError::ProviderError(
471                                "Assistant image content is not supported in OpenAI Responses API"
472                                    .to_string(),
473                            ));
474                        }
475                    }
476                }
477
478                let mut items = reasoning_items;
479                items.extend(other_items);
480                Ok(items)
481            }
482        }
483    }
484}
485
486impl From<OneOrMany<String>> for Vec<ReasoningSummary> {
487    fn from(value: OneOrMany<String>) -> Self {
488        value.iter().map(|x| ReasoningSummary::new(x)).collect()
489    }
490}
491
492fn require_call_id(call_id: Option<String>, context: &str) -> Result<String, CompletionError> {
493    call_id.ok_or_else(|| {
494        CompletionError::RequestError(
495            format!("{context} `call_id` is required for OpenAI Responses API").into(),
496        )
497    })
498}
499
500fn openai_reasoning_from_core(
501    reasoning: &crate::message::Reasoning,
502) -> Result<OpenAIReasoning, MessageError> {
503    let id = reasoning.id.clone().ok_or_else(|| {
504        MessageError::ConversionError(
505            "An OpenAI-generated ID is required when using OpenAI reasoning items".to_string(),
506        )
507    })?;
508    let mut summary = Vec::new();
509    let mut encrypted_content = None;
510    for content in &reasoning.content {
511        match content {
512            crate::message::ReasoningContent::Text { text, .. }
513            | crate::message::ReasoningContent::Summary(text) => {
514                summary.push(ReasoningSummary::new(text));
515            }
516            // OpenAI reasoning input has one opaque payload field; preserve either
517            // encrypted or redacted blocks there, preferring the first one seen.
518            crate::message::ReasoningContent::Encrypted(data)
519            | crate::message::ReasoningContent::Redacted { data } => {
520                encrypted_content.get_or_insert_with(|| data.clone());
521            }
522        }
523    }
524
525    Ok(OpenAIReasoning {
526        id,
527        summary,
528        encrypted_content,
529        status: None,
530    })
531}
532
533/// The definition of a tool response, repurposed for OpenAI's Responses API.
534#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
535pub struct ResponsesToolDefinition {
536    /// The type of tool.
537    #[serde(rename = "type")]
538    pub kind: String,
539    /// Tool name
540    #[serde(default, skip_serializing_if = "String::is_empty")]
541    pub name: String,
542    /// Parameters - this should be a JSON schema. Tools should additionally ensure an "additionalParameters" field has been added with the value set to false, as this is required if using OpenAI's strict mode (enabled by default).
543    #[serde(default, skip_serializing_if = "is_json_null")]
544    pub parameters: serde_json::Value,
545    /// Whether to use strict mode. Enabled by default as it allows for improved efficiency.
546    #[serde(default, skip_serializing_if = "is_false")]
547    pub strict: bool,
548    /// Tool description.
549    #[serde(default, skip_serializing_if = "String::is_empty")]
550    pub description: String,
551    /// Additional provider-specific configuration for hosted tools.
552    #[serde(flatten, default, skip_serializing_if = "Map::is_empty")]
553    pub config: Map<String, Value>,
554}
555
556fn is_json_null(value: &Value) -> bool {
557    value.is_null()
558}
559
560fn is_false(value: &bool) -> bool {
561    !value
562}
563
564impl ResponsesToolDefinition {
565    /// Creates a function tool definition.
566    pub fn function(
567        name: impl Into<String>,
568        description: impl Into<String>,
569        mut parameters: serde_json::Value,
570    ) -> Self {
571        super::sanitize_schema(&mut parameters);
572
573        Self {
574            kind: "function".to_string(),
575            name: name.into(),
576            parameters,
577            strict: true,
578            description: description.into(),
579            config: Map::new(),
580        }
581    }
582
583    /// Creates a hosted tool definition for an arbitrary hosted tool type.
584    pub fn hosted(kind: impl Into<String>) -> Self {
585        Self {
586            kind: kind.into(),
587            name: String::new(),
588            parameters: Value::Null,
589            strict: false,
590            description: String::new(),
591            config: Map::new(),
592        }
593    }
594
595    /// Creates a hosted `web_search` tool definition.
596    pub fn web_search() -> Self {
597        Self::hosted("web_search")
598    }
599
600    /// Creates a hosted `file_search` tool definition.
601    pub fn file_search() -> Self {
602        Self::hosted("file_search")
603    }
604
605    /// Creates a hosted `computer_use` tool definition.
606    pub fn computer_use() -> Self {
607        Self::hosted("computer_use")
608    }
609
610    /// Adds hosted-tool configuration fields.
611    pub fn with_config(mut self, key: impl Into<String>, value: Value) -> Self {
612        self.config.insert(key.into(), value);
613        self
614    }
615
616    fn normalize(mut self) -> Self {
617        if self.kind == "function" {
618            super::sanitize_schema(&mut self.parameters);
619            self.strict = true;
620        }
621        self
622    }
623}
624
625impl From<completion::ToolDefinition> for ResponsesToolDefinition {
626    fn from(value: completion::ToolDefinition) -> Self {
627        let completion::ToolDefinition {
628            name,
629            parameters,
630            description,
631        } = value;
632
633        Self::function(name, description, parameters)
634    }
635}
636
637/// Token usage.
638/// Token usage from the OpenAI Responses API generally shows the input tokens and output tokens (both with more in-depth details) as well as a total tokens field.
639#[derive(Clone, Debug, Serialize, Deserialize)]
640pub struct ResponsesUsage {
641    /// Input tokens
642    pub input_tokens: u64,
643    /// In-depth detail on input tokens (cached tokens)
644    #[serde(skip_serializing_if = "Option::is_none")]
645    pub input_tokens_details: Option<InputTokensDetails>,
646    /// Output tokens
647    pub output_tokens: u64,
648    /// In-depth detail on output tokens (reasoning tokens)
649    pub output_tokens_details: OutputTokensDetails,
650    /// Total tokens used (for a given prompt)
651    pub total_tokens: u64,
652}
653
654impl ResponsesUsage {
655    /// Create a new ResponsesUsage instance
656    pub(crate) fn new() -> Self {
657        Self {
658            input_tokens: 0,
659            input_tokens_details: Some(InputTokensDetails::new()),
660            output_tokens: 0,
661            output_tokens_details: OutputTokensDetails::new(),
662            total_tokens: 0,
663        }
664    }
665}
666
667impl GetTokenUsage for ResponsesUsage {
668    fn token_usage(&self) -> Option<crate::completion::Usage> {
669        Some(crate::providers::internal::completion_usage(
670            self.input_tokens,
671            self.output_tokens,
672            self.total_tokens,
673            self.input_tokens_details
674                .as_ref()
675                .map(|details| details.cached_tokens)
676                .unwrap_or(0),
677        ))
678    }
679}
680
681impl Add for ResponsesUsage {
682    type Output = Self;
683
684    fn add(self, rhs: Self) -> Self::Output {
685        let input_tokens = self.input_tokens + rhs.input_tokens;
686        let input_tokens_details = self.input_tokens_details.map(|lhs| {
687            if let Some(tokens) = rhs.input_tokens_details {
688                lhs + tokens
689            } else {
690                lhs
691            }
692        });
693        let output_tokens = self.output_tokens + rhs.output_tokens;
694        let output_tokens_details = self.output_tokens_details + rhs.output_tokens_details;
695        let total_tokens = self.total_tokens + rhs.total_tokens;
696        Self {
697            input_tokens,
698            input_tokens_details,
699            output_tokens,
700            output_tokens_details,
701            total_tokens,
702        }
703    }
704}
705
706/// In-depth details on input tokens.
707#[derive(Clone, Debug, Serialize, Deserialize)]
708pub struct InputTokensDetails {
709    /// Cached tokens from OpenAI
710    pub cached_tokens: u64,
711}
712
713impl InputTokensDetails {
714    pub(crate) fn new() -> Self {
715        Self { cached_tokens: 0 }
716    }
717}
718
719impl Add for InputTokensDetails {
720    type Output = Self;
721    fn add(self, rhs: Self) -> Self::Output {
722        Self {
723            cached_tokens: self.cached_tokens + rhs.cached_tokens,
724        }
725    }
726}
727
728/// In-depth details on output tokens.
729#[derive(Clone, Debug, Serialize, Deserialize)]
730pub struct OutputTokensDetails {
731    /// Reasoning tokens
732    pub reasoning_tokens: u64,
733}
734
735impl OutputTokensDetails {
736    pub(crate) fn new() -> Self {
737        Self {
738            reasoning_tokens: 0,
739        }
740    }
741}
742
743impl Add for OutputTokensDetails {
744    type Output = Self;
745    fn add(self, rhs: Self) -> Self::Output {
746        Self {
747            reasoning_tokens: self.reasoning_tokens + rhs.reasoning_tokens,
748        }
749    }
750}
751
752/// Occasionally, when using OpenAI's Responses API you may get an incomplete response. This struct holds the reason as to why it happened.
753#[derive(Clone, Debug, Default, Serialize, Deserialize)]
754pub struct IncompleteDetailsReason {
755    /// The reason for an incomplete [`CompletionResponse`].
756    pub reason: String,
757}
758
759/// A response error from OpenAI's Response API.
760#[derive(Clone, Debug, Default, Serialize, Deserialize)]
761pub struct ResponseError {
762    /// Error code
763    pub code: String,
764    /// Error message
765    pub message: String,
766}
767
768/// A response object as an enum (ensures type validation)
769#[derive(Clone, Debug, Deserialize, Serialize)]
770#[serde(rename_all = "snake_case")]
771pub enum ResponseObject {
772    Response,
773}
774
775/// The response status as an enum (ensures type validation)
776#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
777#[serde(rename_all = "snake_case")]
778pub enum ResponseStatus {
779    InProgress,
780    Completed,
781    Failed,
782    Cancelled,
783    Queued,
784    Incomplete,
785}
786
787/// Attempt to try and create a `NewCompletionRequest` from a model name and [`crate::completion::CompletionRequest`]
788impl TryFrom<(String, crate::completion::CompletionRequest)> for CompletionRequest {
789    type Error = CompletionError;
790    fn try_from(
791        (model, mut req): (String, crate::completion::CompletionRequest),
792    ) -> Result<Self, Self::Error> {
793        let model = req.model.clone().unwrap_or(model);
794        let input = {
795            let mut partial_history = vec![];
796            if let Some(docs) = req.normalized_documents() {
797                partial_history.push(docs);
798            }
799            partial_history.extend(req.chat_history);
800
801            // Initialize full history with preamble (or empty if non-existent)
802            // Some "Responses API compatible" providers don't support `instructions` field
803            // so we need to add a system message until further notice
804            let mut full_history: Vec<InputItem> = if let Some(content) = req.preamble {
805                vec![InputItem::system_message(content)]
806            } else {
807                Vec::new()
808            };
809
810            for history_item in partial_history {
811                full_history.extend(<Vec<InputItem>>::try_from(history_item)?);
812            }
813
814            full_history
815        };
816
817        let input = OneOrMany::many(input).map_err(|_| {
818            CompletionError::RequestError(
819                "OpenAI Responses request input must contain at least one item".into(),
820            )
821        })?;
822
823        let mut additional_params_payload = req.additional_params.take().unwrap_or(Value::Null);
824        let stream = match &additional_params_payload {
825            Value::Bool(stream) => Some(*stream),
826            Value::Object(map) => map.get("stream").and_then(Value::as_bool),
827            _ => None,
828        };
829
830        let mut additional_tools = Vec::new();
831        if let Some(additional_params_map) = additional_params_payload.as_object_mut() {
832            if let Some(raw_tools) = additional_params_map.remove("tools") {
833                additional_tools = serde_json::from_value::<Vec<ResponsesToolDefinition>>(
834                    raw_tools,
835                )
836                .map_err(|err| {
837                    CompletionError::RequestError(
838                        format!(
839                            "Invalid OpenAI Responses tools payload in additional_params: {err}"
840                        )
841                        .into(),
842                    )
843                })?;
844            }
845            additional_params_map.remove("stream");
846        }
847
848        if additional_params_payload.is_boolean() {
849            additional_params_payload = Value::Null;
850        }
851
852        additional_tools = additional_tools
853            .into_iter()
854            .map(ResponsesToolDefinition::normalize)
855            .collect();
856
857        let mut additional_parameters = if additional_params_payload.is_null() {
858            // If there's no additional parameters, initialise an empty object
859            AdditionalParameters::default()
860        } else {
861            serde_json::from_value::<AdditionalParameters>(additional_params_payload).map_err(
862                |err| {
863                    CompletionError::RequestError(
864                        format!("Invalid OpenAI Responses additional_params payload: {err}").into(),
865                    )
866                },
867            )?
868        };
869        if additional_parameters.reasoning.is_some() {
870            let include = additional_parameters.include.get_or_insert_with(Vec::new);
871            if !include
872                .iter()
873                .any(|item| matches!(item, Include::ReasoningEncryptedContent))
874            {
875                include.push(Include::ReasoningEncryptedContent);
876            }
877        }
878
879        // Apply output_schema as structured output if not already configured via additional_params
880        if additional_parameters.text.is_none()
881            && let Some(schema) = req.output_schema
882        {
883            let name = schema
884                .as_object()
885                .and_then(|o| o.get("title"))
886                .and_then(|v| v.as_str())
887                .unwrap_or("response_schema")
888                .to_string();
889            let mut schema_value = schema.to_value();
890            super::sanitize_schema(&mut schema_value);
891            additional_parameters.text = Some(TextConfig::structured_output(name, schema_value));
892        }
893
894        let tool_choice = req.tool_choice.map(ToolChoice::try_from).transpose()?;
895        let mut tools: Vec<ResponsesToolDefinition> = req
896            .tools
897            .into_iter()
898            .map(ResponsesToolDefinition::from)
899            .collect();
900        tools.append(&mut additional_tools);
901
902        Ok(Self {
903            input,
904            model,
905            instructions: None, // is currently None due to lack of support in compliant providers
906            max_output_tokens: req.max_tokens,
907            stream,
908            tool_choice,
909            tools,
910            temperature: req.temperature,
911            additional_parameters,
912        })
913    }
914}
915
916/// The completion model struct for OpenAI's response API.
917#[doc(hidden)]
918#[derive(Clone)]
919pub struct GenericResponsesCompletionModel<Ext = super::OpenAIResponsesExt, H = reqwest::Client> {
920    /// The OpenAI client
921    pub(crate) client: crate::client::Client<Ext, H>,
922    /// Name of the model (e.g.: gpt-3.5-turbo-1106)
923    pub model: String,
924    /// Model-level default tools that are always added to outgoing requests.
925    pub tools: Vec<ResponsesToolDefinition>,
926}
927
928/// The completion model struct for OpenAI's Responses API.
929///
930/// This preserves the historical public generic shape where the first generic
931/// parameter is the HTTP client type.
932pub type ResponsesCompletionModel<H = reqwest::Client> =
933    GenericResponsesCompletionModel<super::OpenAIResponsesExt, H>;
934
935impl<Ext, H> GenericResponsesCompletionModel<Ext, H>
936where
937    crate::client::Client<Ext, H>: HttpClientExt + Clone + std::fmt::Debug + 'static,
938    Ext: crate::client::Provider + Clone + 'static,
939    H: Clone + Default + std::fmt::Debug + 'static,
940{
941    /// Creates a new [`ResponsesCompletionModel`].
942    pub fn new(client: crate::client::Client<Ext, H>, model: impl Into<String>) -> Self {
943        Self {
944            client,
945            model: model.into(),
946            tools: Vec::new(),
947        }
948    }
949
950    pub fn with_model(client: crate::client::Client<Ext, H>, model: &str) -> Self {
951        Self {
952            client,
953            model: model.to_string(),
954            tools: Vec::new(),
955        }
956    }
957
958    /// Adds a default tool to all requests from this model.
959    pub fn with_tool(mut self, tool: impl Into<ResponsesToolDefinition>) -> Self {
960        self.tools.push(tool.into());
961        self
962    }
963
964    /// Adds default tools to all requests from this model.
965    pub fn with_tools<I, Tool>(mut self, tools: I) -> Self
966    where
967        I: IntoIterator<Item = Tool>,
968        Tool: Into<ResponsesToolDefinition>,
969    {
970        self.tools.extend(tools.into_iter().map(Into::into));
971        self
972    }
973
974    /// Attempt to create a completion request from [`crate::completion::CompletionRequest`].
975    pub(crate) fn create_completion_request(
976        &self,
977        completion_request: crate::completion::CompletionRequest,
978    ) -> Result<CompletionRequest, CompletionError> {
979        let mut req = CompletionRequest::try_from((self.model.clone(), completion_request))?;
980        req.tools.extend(self.tools.clone());
981
982        Ok(req)
983    }
984}
985
986impl<T> GenericResponsesCompletionModel<super::OpenAIResponsesExt, T>
987where
988    T: HttpClientExt + Clone + Default + std::fmt::Debug + 'static,
989{
990    /// Use the Completions API instead of Responses.
991    pub fn completions_api(self) -> crate::providers::openai::completion::CompletionModel<T> {
992        super::completion::CompletionModel::with_model(self.client.completions_api(), &self.model)
993    }
994}
995
996/// The standard response format from OpenAI's Responses API.
997#[derive(Clone, Debug, Serialize, Deserialize)]
998pub struct CompletionResponse {
999    /// The ID of a completion response.
1000    pub id: String,
1001    /// The type of the object.
1002    pub object: ResponseObject,
1003    /// The time at which a given response has been created, in seconds from the UNIX epoch (01/01/1970 00:00:00).
1004    pub created_at: u64,
1005    /// The status of the response.
1006    pub status: ResponseStatus,
1007    /// Response error (optional)
1008    pub error: Option<ResponseError>,
1009    /// Incomplete response details (optional)
1010    pub incomplete_details: Option<IncompleteDetailsReason>,
1011    /// System prompt/preamble
1012    pub instructions: Option<String>,
1013    /// The maximum number of tokens the model should output
1014    pub max_output_tokens: Option<u64>,
1015    /// The model name
1016    pub model: String,
1017    /// Token usage
1018    pub usage: Option<ResponsesUsage>,
1019    /// The model output (messages, etc will go here)
1020    pub output: Vec<Output>,
1021    /// Tools
1022    #[serde(default)]
1023    pub tools: Vec<ResponsesToolDefinition>,
1024    /// Additional parameters
1025    #[serde(flatten)]
1026    pub additional_parameters: AdditionalParameters,
1027}
1028
1029/// Additional parameters for the completion request type for OpenAI's Response API: <https://platform.openai.com/docs/api-reference/responses/create>
1030/// Intended to be derived from [`crate::completion::request::CompletionRequest`].
1031#[derive(Clone, Debug, Deserialize, Serialize, Default)]
1032pub struct AdditionalParameters {
1033    /// Whether or not a given model task should run in the background (ie a detached process).
1034    #[serde(skip_serializing_if = "Option::is_none")]
1035    pub background: Option<bool>,
1036    /// The text response format. This is where you would add structured outputs (if you want them).
1037    #[serde(skip_serializing_if = "Option::is_none")]
1038    pub text: Option<TextConfig>,
1039    /// What types of extra data you would like to include. This is mostly useless at the moment since the types of extra data to add is currently unsupported, but this will be coming soon!
1040    #[serde(skip_serializing_if = "Option::is_none")]
1041    pub include: Option<Vec<Include>>,
1042    /// `top_p`. Mutually exclusive with the `temperature` argument.
1043    #[serde(skip_serializing_if = "Option::is_none")]
1044    pub top_p: Option<f64>,
1045    /// Whether or not the response should be truncated.
1046    #[serde(skip_serializing_if = "Option::is_none")]
1047    pub truncation: Option<TruncationStrategy>,
1048    /// The username of the user (that you want to use).
1049    #[serde(skip_serializing_if = "Option::is_none")]
1050    pub user: Option<String>,
1051    /// Any additional metadata you'd like to add. This will additionally be returned by the response.
1052    #[serde(skip_serializing_if = "Map::is_empty", default)]
1053    pub metadata: serde_json::Map<String, serde_json::Value>,
1054    /// Whether or not you want tool calls to run in parallel.
1055    #[serde(skip_serializing_if = "Option::is_none")]
1056    pub parallel_tool_calls: Option<bool>,
1057    /// Previous response ID. If you are not sending a full conversation, this can help to track the message flow.
1058    #[serde(skip_serializing_if = "Option::is_none")]
1059    pub previous_response_id: Option<String>,
1060    /// Add thinking/reasoning to your response. The response will be emitted as a list member of the `output` field.
1061    #[serde(skip_serializing_if = "Option::is_none")]
1062    pub reasoning: Option<Reasoning>,
1063    /// The service tier you're using.
1064    #[serde(skip_serializing_if = "Option::is_none")]
1065    pub service_tier: Option<OpenAIServiceTier>,
1066    /// Whether or not to store the response for later retrieval by API.
1067    #[serde(skip_serializing_if = "Option::is_none")]
1068    pub store: Option<bool>,
1069}
1070
1071impl AdditionalParameters {
1072    pub fn to_json(self) -> serde_json::Value {
1073        serde_json::to_value(self).unwrap_or_else(|_| serde_json::Value::Object(Map::new()))
1074    }
1075}
1076
1077/// The truncation strategy.
1078/// When using auto, if the context of this response and previous ones exceeds the model's context window size, the model will truncate the response to fit the context window by dropping input items in the middle of the conversation.
1079/// Otherwise, does nothing (and is disabled by default).
1080#[derive(Clone, Debug, Default, Serialize, Deserialize)]
1081#[serde(rename_all = "snake_case")]
1082pub enum TruncationStrategy {
1083    Auto,
1084    #[default]
1085    Disabled,
1086}
1087
1088/// The model output format configuration.
1089/// You can either have plain text by default, or attach a JSON schema for the purposes of structured outputs.
1090#[derive(Clone, Debug, Serialize, Deserialize)]
1091pub struct TextConfig {
1092    pub format: TextFormat,
1093}
1094
1095impl TextConfig {
1096    pub(crate) fn structured_output<S>(name: S, schema: serde_json::Value) -> Self
1097    where
1098        S: Into<String>,
1099    {
1100        Self {
1101            format: TextFormat::JsonSchema(StructuredOutputsInput {
1102                name: name.into(),
1103                schema,
1104                strict: true,
1105            }),
1106        }
1107    }
1108}
1109
1110/// The text format (contained by [`TextConfig`]).
1111/// You can either have plain text by default, or attach a JSON schema for the purposes of structured outputs.
1112#[derive(Clone, Debug, Serialize, Deserialize, Default)]
1113#[serde(tag = "type")]
1114#[serde(rename_all = "snake_case")]
1115pub enum TextFormat {
1116    JsonSchema(StructuredOutputsInput),
1117    #[default]
1118    Text,
1119}
1120
1121/// The inputs required for adding structured outputs.
1122#[derive(Clone, Debug, Serialize, Deserialize)]
1123pub struct StructuredOutputsInput {
1124    /// The name of your schema.
1125    pub name: String,
1126    /// Your required output schema. It is recommended that you use the JsonSchema macro, which you can check out at <https://docs.rs/schemars/latest/schemars/trait.JsonSchema.html>.
1127    pub schema: serde_json::Value,
1128    /// Enable strict output. If you are using your AI agent in a data pipeline or another scenario that requires the data to be absolutely fixed to a given schema, it is recommended to set this to true.
1129    #[serde(default)]
1130    pub strict: bool,
1131}
1132
1133/// Add reasoning to a [`CompletionRequest`].
1134#[derive(Clone, Debug, Default, Serialize, Deserialize)]
1135pub struct Reasoning {
1136    /// How much effort you want the model to put into thinking/reasoning.
1137    pub effort: Option<ReasoningEffort>,
1138    /// How much effort you want the model to put into writing the reasoning summary.
1139    #[serde(skip_serializing_if = "Option::is_none")]
1140    pub summary: Option<ReasoningSummaryLevel>,
1141}
1142
1143impl Reasoning {
1144    /// Creates a new Reasoning instantiation (with empty values).
1145    pub fn new() -> Self {
1146        Self {
1147            effort: None,
1148            summary: None,
1149        }
1150    }
1151
1152    /// Adds reasoning effort.
1153    pub fn with_effort(mut self, reasoning_effort: ReasoningEffort) -> Self {
1154        self.effort = Some(reasoning_effort);
1155
1156        self
1157    }
1158
1159    /// Adds summary level (how detailed the reasoning summary will be).
1160    pub fn with_summary_level(mut self, reasoning_summary_level: ReasoningSummaryLevel) -> Self {
1161        self.summary = Some(reasoning_summary_level);
1162
1163        self
1164    }
1165}
1166
1167/// The billing service tier that will be used. On auto by default.
1168#[derive(Clone, Debug, Default, Serialize, Deserialize)]
1169#[serde(rename_all = "snake_case")]
1170pub enum OpenAIServiceTier {
1171    #[default]
1172    Auto,
1173    Default,
1174    Flex,
1175}
1176
1177/// The amount of reasoning effort that will be used by a given model.
1178#[derive(Clone, Debug, Default, Serialize, Deserialize)]
1179#[serde(rename_all = "snake_case")]
1180pub enum ReasoningEffort {
1181    None,
1182    Minimal,
1183    Low,
1184    #[default]
1185    Medium,
1186    High,
1187    Xhigh,
1188}
1189
1190/// The amount of effort that will go into a reasoning summary by a given model.
1191#[derive(Clone, Debug, Default, Serialize, Deserialize)]
1192#[serde(rename_all = "snake_case")]
1193pub enum ReasoningSummaryLevel {
1194    #[default]
1195    Auto,
1196    Concise,
1197    Detailed,
1198}
1199
1200/// Results to additionally include in the OpenAI Responses API.
1201/// Note that most of these are currently unsupported, but have been added for completeness.
1202#[derive(Clone, Debug, Deserialize, Serialize)]
1203pub enum Include {
1204    #[serde(rename = "file_search_call.results")]
1205    FileSearchCallResults,
1206    #[serde(rename = "message.input_image.image_url")]
1207    MessageInputImageImageUrl,
1208    #[serde(rename = "computer_call.output.image_url")]
1209    ComputerCallOutputOutputImageUrl,
1210    #[serde(rename = "reasoning.encrypted_content")]
1211    ReasoningEncryptedContent,
1212    #[serde(rename = "code_interpreter_call.outputs")]
1213    CodeInterpreterCallOutputs,
1214}
1215
1216/// A currently non-exhaustive list of output types.
1217#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
1218#[serde(tag = "type")]
1219#[serde(rename_all = "snake_case")]
1220pub enum Output {
1221    Message(OutputMessage),
1222    #[serde(alias = "function_call")]
1223    FunctionCall(OutputFunctionCall),
1224    Reasoning {
1225        id: String,
1226        summary: Vec<ReasoningSummary>,
1227        #[serde(default)]
1228        encrypted_content: Option<String>,
1229        #[serde(default)]
1230        status: Option<ToolStatus>,
1231    },
1232    /// Catch-all variant for unknown output types (e.g., `web_search_call`,
1233    /// `file_search_call`, `computer_use_call`). This prevents unknown types
1234    /// from breaking deserialization of the entire `CompletionResponse`,
1235    /// which previously caused streaming token usage to be silently dropped.
1236    #[serde(other)]
1237    Unknown,
1238}
1239
1240impl From<Output> for Vec<completion::AssistantContent> {
1241    fn from(value: Output) -> Self {
1242        let res: Vec<completion::AssistantContent> = match value {
1243            Output::Message(OutputMessage { content, .. }) => content
1244                .into_iter()
1245                .map(completion::AssistantContent::from)
1246                .collect(),
1247            Output::FunctionCall(OutputFunctionCall {
1248                id,
1249                arguments,
1250                call_id,
1251                name,
1252                ..
1253            }) => vec![completion::AssistantContent::tool_call_with_call_id(
1254                id, call_id, name, arguments,
1255            )],
1256            Output::Reasoning {
1257                id,
1258                summary,
1259                encrypted_content,
1260                ..
1261            } => {
1262                let mut content = summary
1263                    .into_iter()
1264                    .map(|summary| match summary {
1265                        ReasoningSummary::SummaryText { text } => {
1266                            message::ReasoningContent::Summary(text)
1267                        }
1268                    })
1269                    .collect::<Vec<_>>();
1270                if let Some(encrypted_content) = encrypted_content {
1271                    content.push(message::ReasoningContent::Encrypted(encrypted_content));
1272                }
1273                vec![completion::AssistantContent::Reasoning(
1274                    message::Reasoning {
1275                        id: Some(id),
1276                        content,
1277                    },
1278                )]
1279            }
1280            Output::Unknown => Vec::new(),
1281        };
1282
1283        res
1284    }
1285}
1286
1287#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
1288pub struct OutputReasoning {
1289    id: String,
1290    summary: Vec<ReasoningSummary>,
1291    status: ToolStatus,
1292}
1293
1294/// An OpenAI Responses API tool call. A call ID will be returned that must be used when creating a tool result to send back to OpenAI as a message input, otherwise an error will be received.
1295#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
1296pub struct OutputFunctionCall {
1297    pub id: String,
1298    #[serde(with = "json_utils::stringified_json")]
1299    pub arguments: serde_json::Value,
1300    pub call_id: String,
1301    pub name: String,
1302    pub status: ToolStatus,
1303}
1304
1305/// The status of a given tool.
1306#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
1307#[serde(rename_all = "snake_case")]
1308pub enum ToolStatus {
1309    InProgress,
1310    Completed,
1311    Incomplete,
1312}
1313
1314/// An output message from OpenAI's Responses API.
1315#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
1316pub struct OutputMessage {
1317    /// The message ID. Must be included when sending the message back to OpenAI
1318    pub id: String,
1319    /// The role (currently only Assistant is available as this struct is only created when receiving an LLM message as a response)
1320    pub role: OutputRole,
1321    /// The status of the response
1322    pub status: ResponseStatus,
1323    /// The actual message content
1324    pub content: Vec<AssistantContent>,
1325}
1326
1327/// The role of an output message.
1328#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
1329#[serde(rename_all = "snake_case")]
1330pub enum OutputRole {
1331    Assistant,
1332}
1333
1334impl<Ext, H> completion::CompletionModel for GenericResponsesCompletionModel<Ext, H>
1335where
1336    crate::client::Client<Ext, H>:
1337        HttpClientExt + Clone + WasmCompatSend + WasmCompatSync + 'static,
1338    Ext: crate::client::Provider
1339        + crate::client::DebugExt
1340        + Clone
1341        + WasmCompatSend
1342        + WasmCompatSync
1343        + 'static,
1344    H: Clone + Default + std::fmt::Debug + WasmCompatSend + WasmCompatSync + 'static,
1345{
1346    type Response = CompletionResponse;
1347    type StreamingResponse = StreamingCompletionResponse;
1348
1349    type Client = crate::client::Client<Ext, H>;
1350
1351    fn make(client: &Self::Client, model: impl Into<String>) -> Self {
1352        Self::new(client.clone(), model)
1353    }
1354
1355    async fn completion(
1356        &self,
1357        completion_request: crate::completion::CompletionRequest,
1358    ) -> Result<completion::CompletionResponse<Self::Response>, CompletionError> {
1359        let span = if tracing::Span::current().is_disabled() {
1360            info_span!(
1361                target: "rig::completions",
1362                "chat",
1363                gen_ai.operation.name = "chat",
1364                gen_ai.provider.name = tracing::field::Empty,
1365                gen_ai.request.model = tracing::field::Empty,
1366                gen_ai.response.id = tracing::field::Empty,
1367                gen_ai.response.model = tracing::field::Empty,
1368                gen_ai.usage.output_tokens = tracing::field::Empty,
1369                gen_ai.usage.input_tokens = tracing::field::Empty,
1370                gen_ai.usage.cache_read.input_tokens = tracing::field::Empty,
1371                gen_ai.input.messages = tracing::field::Empty,
1372                gen_ai.output.messages = tracing::field::Empty,
1373            )
1374        } else {
1375            tracing::Span::current()
1376        };
1377
1378        span.record("gen_ai.provider.name", "openai");
1379        span.record("gen_ai.request.model", &self.model);
1380        let request = self.create_completion_request(completion_request)?;
1381        let body = serde_json::to_vec(&request)?;
1382
1383        if enabled!(Level::TRACE) {
1384            tracing::trace!(
1385                target: "rig::completions",
1386                "OpenAI Responses completion request: {request}",
1387                request = serde_json::to_string_pretty(&request)?
1388            );
1389        }
1390
1391        let req = self
1392            .client
1393            .post("/responses")?
1394            .body(body)
1395            .map_err(|e| CompletionError::HttpError(e.into()))?;
1396
1397        async move {
1398            let response = self.client.send(req).await?;
1399
1400            if response.status().is_success() {
1401                let t = http_client::text(response).await?;
1402                let response = serde_json::from_str::<Self::Response>(&t)?;
1403                let span = tracing::Span::current();
1404                span.record("gen_ai.response.id", &response.id);
1405                span.record("gen_ai.response.model", &response.model);
1406                if let Some(ref usage) = response.usage {
1407                    span.record("gen_ai.usage.output_tokens", usage.output_tokens);
1408                    span.record("gen_ai.usage.input_tokens", usage.input_tokens);
1409                    let cached_tokens = usage
1410                        .input_tokens_details
1411                        .as_ref()
1412                        .map(|d| d.cached_tokens)
1413                        .unwrap_or(0);
1414                    span.record("gen_ai.usage.cache_read.input_tokens", cached_tokens);
1415                }
1416                if enabled!(Level::TRACE) {
1417                    tracing::trace!(
1418                        target: "rig::completions",
1419                        "OpenAI Responses completion response: {response}",
1420                        response = serde_json::to_string_pretty(&response)?
1421                    );
1422                }
1423                response.try_into()
1424            } else {
1425                let text = http_client::text(response).await?;
1426                Err(CompletionError::ProviderError(text))
1427            }
1428        }
1429        .instrument(span)
1430        .await
1431    }
1432
1433    async fn stream(
1434        &self,
1435        request: crate::completion::CompletionRequest,
1436    ) -> Result<
1437        crate::streaming::StreamingCompletionResponse<Self::StreamingResponse>,
1438        CompletionError,
1439    > {
1440        GenericResponsesCompletionModel::stream(self, request).await
1441    }
1442}
1443
1444impl TryFrom<CompletionResponse> for completion::CompletionResponse<CompletionResponse> {
1445    type Error = CompletionError;
1446
1447    fn try_from(response: CompletionResponse) -> Result<Self, Self::Error> {
1448        if response.output.is_empty() {
1449            return Err(CompletionError::ResponseError(
1450                "Response contained no parts".to_owned(),
1451            ));
1452        }
1453
1454        // Extract the msg_ ID from the first Output::Message item
1455        let message_id = response.output.iter().find_map(|item| match item {
1456            Output::Message(msg) => Some(msg.id.clone()),
1457            _ => None,
1458        });
1459
1460        let content: Vec<completion::AssistantContent> = response
1461            .output
1462            .iter()
1463            .cloned()
1464            .flat_map(<Vec<completion::AssistantContent>>::from)
1465            .collect();
1466
1467        let choice = OneOrMany::many(content).map_err(|_| {
1468            CompletionError::ResponseError(
1469                "Response contained no message or tool call (empty)".to_owned(),
1470            )
1471        })?;
1472
1473        let usage = response
1474            .usage
1475            .as_ref()
1476            .map(|usage| completion::Usage {
1477                input_tokens: usage.input_tokens,
1478                output_tokens: usage.output_tokens,
1479                total_tokens: usage.total_tokens,
1480                cached_input_tokens: usage
1481                    .input_tokens_details
1482                    .as_ref()
1483                    .map(|d| d.cached_tokens)
1484                    .unwrap_or(0),
1485                cache_creation_input_tokens: 0,
1486            })
1487            .unwrap_or_default();
1488
1489        Ok(completion::CompletionResponse {
1490            choice,
1491            usage,
1492            raw_response: response,
1493            message_id,
1494        })
1495    }
1496}
1497
1498/// An OpenAI Responses API message.
1499#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
1500#[serde(tag = "role", rename_all = "lowercase")]
1501pub enum Message {
1502    #[serde(alias = "developer")]
1503    System {
1504        #[serde(deserialize_with = "string_or_one_or_many")]
1505        content: OneOrMany<SystemContent>,
1506        #[serde(skip_serializing_if = "Option::is_none")]
1507        name: Option<String>,
1508    },
1509    User {
1510        #[serde(deserialize_with = "string_or_one_or_many")]
1511        content: OneOrMany<UserContent>,
1512        #[serde(skip_serializing_if = "Option::is_none")]
1513        name: Option<String>,
1514    },
1515    Assistant {
1516        content: OneOrMany<AssistantContentType>,
1517        #[serde(skip_serializing_if = "String::is_empty")]
1518        id: String,
1519        #[serde(skip_serializing_if = "Option::is_none")]
1520        name: Option<String>,
1521        status: ToolStatus,
1522    },
1523    #[serde(rename = "tool")]
1524    ToolResult {
1525        tool_call_id: String,
1526        output: String,
1527    },
1528}
1529
1530/// The type of a tool result content item.
1531#[derive(Default, Debug, Serialize, Deserialize, PartialEq, Clone)]
1532#[serde(rename_all = "lowercase")]
1533pub enum ToolResultContentType {
1534    #[default]
1535    Text,
1536}
1537
1538impl Message {
1539    pub fn system(content: &str) -> Self {
1540        Message::System {
1541            content: OneOrMany::one(content.to_owned().into()),
1542            name: None,
1543        }
1544    }
1545}
1546
1547/// Text assistant content.
1548/// Note that the text type in comparison to the Completions API is actually `output_text` rather than `text`.
1549#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
1550#[serde(tag = "type", rename_all = "snake_case")]
1551pub enum AssistantContent {
1552    OutputText(Text),
1553    Refusal { refusal: String },
1554}
1555
1556impl From<AssistantContent> for completion::AssistantContent {
1557    fn from(value: AssistantContent) -> Self {
1558        match value {
1559            AssistantContent::Refusal { refusal } => {
1560                completion::AssistantContent::Text(Text { text: refusal })
1561            }
1562            AssistantContent::OutputText(Text { text }) => {
1563                completion::AssistantContent::Text(Text { text })
1564            }
1565        }
1566    }
1567}
1568
1569/// The type of assistant content.
1570#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
1571#[serde(untagged)]
1572pub enum AssistantContentType {
1573    Text(AssistantContent),
1574    ToolCall(OutputFunctionCall),
1575    Reasoning(OpenAIReasoning),
1576}
1577
1578/// System content for the OpenAI Responses API.
1579/// Uses `input_text` type to match the Responses API format.
1580#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
1581#[serde(tag = "type", rename_all = "snake_case")]
1582pub enum SystemContent {
1583    InputText { text: String },
1584}
1585
1586impl From<String> for SystemContent {
1587    fn from(s: String) -> Self {
1588        SystemContent::InputText { text: s }
1589    }
1590}
1591
1592impl std::str::FromStr for SystemContent {
1593    type Err = std::convert::Infallible;
1594
1595    fn from_str(s: &str) -> Result<Self, Self::Err> {
1596        Ok(SystemContent::InputText {
1597            text: s.to_string(),
1598        })
1599    }
1600}
1601
1602/// Different types of user content.
1603#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
1604#[serde(tag = "type", rename_all = "snake_case")]
1605pub enum UserContent {
1606    InputText {
1607        text: String,
1608    },
1609    InputImage {
1610        image_url: String,
1611        #[serde(default)]
1612        detail: ImageDetail,
1613    },
1614    InputFile {
1615        #[serde(skip_serializing_if = "Option::is_none")]
1616        file_url: Option<String>,
1617        #[serde(skip_serializing_if = "Option::is_none")]
1618        file_data: Option<String>,
1619        #[serde(skip_serializing_if = "Option::is_none")]
1620        filename: Option<String>,
1621    },
1622    Audio {
1623        input_audio: InputAudio,
1624    },
1625    #[serde(rename = "tool")]
1626    ToolResult {
1627        tool_call_id: String,
1628        output: String,
1629    },
1630}
1631
1632impl TryFrom<message::Message> for Vec<Message> {
1633    type Error = message::MessageError;
1634
1635    fn try_from(message: message::Message) -> Result<Self, Self::Error> {
1636        match message {
1637            message::Message::System { content } => Ok(vec![Message::System {
1638                content: OneOrMany::one(content.into()),
1639                name: None,
1640            }]),
1641            message::Message::User { content } => {
1642                let (tool_results, other_content): (Vec<_>, Vec<_>) = content
1643                    .into_iter()
1644                    .partition(|content| matches!(content, message::UserContent::ToolResult(_)));
1645
1646                // If there are messages with both tool results and user content, openai will only
1647                //  handle tool results. It's unlikely that there will be both.
1648                if !tool_results.is_empty() {
1649                    tool_results
1650                        .into_iter()
1651                        .map(|content| match content {
1652                            message::UserContent::ToolResult(message::ToolResult {
1653                                call_id,
1654                                content,
1655                                ..
1656                            }) => Ok::<_, message::MessageError>(Message::ToolResult {
1657                                tool_call_id: call_id.ok_or_else(|| {
1658                                    MessageError::ConversionError(
1659                                        "Tool result `call_id` is required for OpenAI Responses API"
1660                                            .into(),
1661                                    )
1662                                })?,
1663                                output: {
1664                                    let res = content.first();
1665                                    match res {
1666                                        completion::message::ToolResultContent::Text(Text {
1667                                            text,
1668                                        }) => text,
1669                                        _ => return  Err(MessageError::ConversionError("This API only currently supports text tool results".into()))
1670                                    }
1671                                },
1672                            }),
1673                            _ => Err(MessageError::ConversionError(
1674                                "expected tool result content while converting Responses API input"
1675                                    .into(),
1676                            )),
1677                        })
1678                        .collect::<Result<Vec<_>, _>>()
1679                } else {
1680                    let other_content = other_content
1681                        .into_iter()
1682                        .map(|content| match content {
1683                            message::UserContent::Text(message::Text { text }) => {
1684                                Ok(UserContent::InputText { text })
1685                            }
1686                            message::UserContent::Image(message::Image {
1687                                data,
1688                                detail,
1689                                media_type,
1690                                ..
1691                            }) => {
1692                                let url = match data {
1693                                    DocumentSourceKind::Base64(data) => {
1694                                        let media_type = if let Some(media_type) = media_type {
1695                                            media_type.to_mime_type().to_string()
1696                                        } else {
1697                                            String::new()
1698                                        };
1699                                        format!("data:{media_type};base64,{data}")
1700                                    }
1701                                    DocumentSourceKind::Url(url) => url,
1702                                    DocumentSourceKind::Raw(_) => {
1703                                        return Err(MessageError::ConversionError(
1704                                            "Raw files not supported, encode as base64 first"
1705                                                .into(),
1706                                        ));
1707                                    }
1708                                    doc => {
1709                                        return Err(MessageError::ConversionError(format!(
1710                                            "Unsupported document type: {doc}"
1711                                        )));
1712                                    }
1713                                };
1714
1715                                Ok(UserContent::InputImage {
1716                                    image_url: url,
1717                                    detail: detail.unwrap_or_default(),
1718                                })
1719                            }
1720                            message::UserContent::Document(message::Document {
1721                                media_type: Some(DocumentMediaType::PDF),
1722                                data,
1723                                ..
1724                            }) => {
1725                                let (file_data, file_url, filename) = match data {
1726                                    DocumentSourceKind::Base64(data) => (
1727                                        Some(format!("data:application/pdf;base64,{data}")),
1728                                        None,
1729                                        Some("document.pdf".to_string()),
1730                                    ),
1731                                    DocumentSourceKind::Url(url) => (None, Some(url), None),
1732                                    DocumentSourceKind::Raw(_) => {
1733                                        return Err(MessageError::ConversionError(
1734                                            "Raw files not supported, encode as base64 first"
1735                                                .into(),
1736                                        ));
1737                                    }
1738                                    doc => {
1739                                        return Err(MessageError::ConversionError(format!(
1740                                            "Unsupported document type: {doc}"
1741                                        )));
1742                                    }
1743                                };
1744
1745                                Ok(UserContent::InputFile {
1746                                    file_url,
1747                                    file_data,
1748                                    filename,
1749                                })
1750                            }
1751                            message::UserContent::Document(message::Document {
1752                                data: DocumentSourceKind::Base64(text),
1753                                ..
1754                            }) => Ok(UserContent::InputText { text }),
1755                            message::UserContent::Audio(message::Audio {
1756                                data: DocumentSourceKind::Base64(data),
1757                                media_type,
1758                                ..
1759                            }) => Ok(UserContent::Audio {
1760                                input_audio: InputAudio {
1761                                    data,
1762                                    format: match media_type {
1763                                        Some(media_type) => media_type,
1764                                        None => AudioMediaType::MP3,
1765                                    },
1766                                },
1767                            }),
1768                            message::UserContent::Audio(_) => Err(MessageError::ConversionError(
1769                                "Audio must be base64 encoded data".into(),
1770                            )),
1771                            _ => Err(MessageError::ConversionError(
1772                                "Unsupported user content for OpenAI Responses API".into(),
1773                            )),
1774                        })
1775                        .collect::<Result<Vec<_>, _>>()?;
1776
1777                    let other_content = OneOrMany::many(other_content).map_err(|_| {
1778                        MessageError::ConversionError(
1779                            "User message did not contain OpenAI Responses-compatible content"
1780                                .to_string(),
1781                        )
1782                    })?;
1783
1784                    Ok(vec![Message::User {
1785                        content: other_content,
1786                        name: None,
1787                    }])
1788                }
1789            }
1790            message::Message::Assistant { content, id } => {
1791                let assistant_message_id = id.ok_or_else(|| {
1792                    MessageError::ConversionError(
1793                        "Assistant message ID is required for OpenAI Responses API".into(),
1794                    )
1795                })?;
1796
1797                match content.first() {
1798                    crate::message::AssistantContent::Text(Text { text }) => {
1799                        Ok(vec![Message::Assistant {
1800                            id: assistant_message_id.clone(),
1801                            status: ToolStatus::Completed,
1802                            content: OneOrMany::one(AssistantContentType::Text(
1803                                AssistantContent::OutputText(Text { text }),
1804                            )),
1805                            name: None,
1806                        }])
1807                    }
1808                    crate::message::AssistantContent::ToolCall(crate::message::ToolCall {
1809                        id,
1810                        call_id,
1811                        function,
1812                        ..
1813                    }) => Ok(vec![Message::Assistant {
1814                        content: OneOrMany::one(AssistantContentType::ToolCall(
1815                            OutputFunctionCall {
1816                                call_id: call_id.ok_or_else(|| {
1817                                    MessageError::ConversionError(
1818                                        "Tool call `call_id` is required for OpenAI Responses API"
1819                                            .into(),
1820                                    )
1821                                })?,
1822                                arguments: function.arguments,
1823                                id,
1824                                name: function.name,
1825                                status: ToolStatus::Completed,
1826                            },
1827                        )),
1828                        id: assistant_message_id.clone(),
1829                        name: None,
1830                        status: ToolStatus::Completed,
1831                    }]),
1832                    crate::message::AssistantContent::Reasoning(reasoning) => {
1833                        let openai_reasoning = openai_reasoning_from_core(&reasoning)?;
1834                        Ok(vec![Message::Assistant {
1835                            content: OneOrMany::one(AssistantContentType::Reasoning(
1836                                openai_reasoning,
1837                            )),
1838                            id: assistant_message_id,
1839                            name: None,
1840                            status: ToolStatus::Completed,
1841                        }])
1842                    }
1843                    crate::message::AssistantContent::Image(_) => {
1844                        Err(MessageError::ConversionError(
1845                            "Assistant image content is not supported in OpenAI Responses API"
1846                                .into(),
1847                        ))
1848                    }
1849                }
1850            }
1851        }
1852    }
1853}
1854
1855impl FromStr for UserContent {
1856    type Err = Infallible;
1857
1858    fn from_str(s: &str) -> Result<Self, Self::Err> {
1859        Ok(UserContent::InputText {
1860            text: s.to_string(),
1861        })
1862    }
1863}