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