Skip to main content

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