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::AssistantInput { .. } => Self {
258                role: Some(Role::Assistant),
259                input: InputContent::Message(value),
260            },
261            Message::System { .. } => Self {
262                role: Some(Role::System),
263                input: InputContent::Message(value),
264            },
265            Message::ToolResult {
266                tool_call_id,
267                output,
268            } => Self {
269                role: None,
270                input: InputContent::FunctionCallOutput(ToolResult {
271                    call_id: tool_call_id,
272                    output,
273                    status: ToolStatus::Completed,
274                }),
275            },
276        }
277    }
278}
279
280impl TryFrom<crate::completion::Message> for Vec<InputItem> {
281    type Error = CompletionError;
282
283    fn try_from(value: crate::completion::Message) -> Result<Self, Self::Error> {
284        match value {
285            crate::completion::Message::System { content } => Ok(vec![InputItem {
286                role: Some(Role::System),
287                input: InputContent::Message(Message::System {
288                    content: OneOrMany::one(content.into()),
289                    name: None,
290                }),
291            }]),
292            crate::completion::Message::User { content } => {
293                let mut items = Vec::new();
294
295                for user_content in content {
296                    match user_content {
297                        crate::message::UserContent::Text(Text { text, .. }) => {
298                            items.push(InputItem {
299                                role: Some(Role::User),
300                                input: InputContent::Message(Message::User {
301                                    content: OneOrMany::one(UserContent::InputText { text }),
302                                    name: None,
303                                }),
304                            });
305                        }
306                        crate::message::UserContent::ToolResult(
307                            crate::completion::message::ToolResult {
308                                call_id,
309                                content: tool_content,
310                                ..
311                            },
312                        ) => {
313                            for tool_result_content in tool_content {
314                                let crate::completion::message::ToolResultContent::Text(Text {
315                                    text,
316                                    ..
317                                }) = tool_result_content
318                                else {
319                                    return Err(CompletionError::ProviderError(
320                                        "This thing only supports text!".to_string(),
321                                    ));
322                                };
323                                // let output = serde_json::from_str(&text)?;
324                                items.push(InputItem {
325                                    role: None,
326                                    input: InputContent::FunctionCallOutput(ToolResult {
327                                        call_id: require_call_id(call_id.clone(), "Tool result")?,
328                                        output: text,
329                                        status: ToolStatus::Completed,
330                                    }),
331                                });
332                            }
333                        }
334                        crate::message::UserContent::Document(Document {
335                            data: DocumentSourceKind::FileId(file_id),
336                            ..
337                        }) => items.push(InputItem {
338                            role: Some(Role::User),
339                            input: InputContent::Message(Message::User {
340                                content: OneOrMany::one(UserContent::InputFile {
341                                    file_id: Some(file_id),
342                                    file_data: None,
343                                    file_url: None,
344                                    filename: None,
345                                }),
346                                name: None,
347                            }),
348                        }),
349                        crate::message::UserContent::Document(Document {
350                            data,
351                            media_type: Some(DocumentMediaType::PDF),
352                            ..
353                        }) => {
354                            let (file_data, file_url) = match data {
355                                DocumentSourceKind::Base64(data) => {
356                                    (Some(format!("data:application/pdf;base64,{data}")), None)
357                                }
358                                DocumentSourceKind::Url(url) => (None, Some(url)),
359                                DocumentSourceKind::Raw(_) => {
360                                    return Err(CompletionError::RequestError(
361                                        "Raw file data not supported, encode as base64 first"
362                                            .into(),
363                                    ));
364                                }
365                                doc => {
366                                    return Err(CompletionError::RequestError(
367                                        format!("Unsupported document type: {doc}").into(),
368                                    ));
369                                }
370                            };
371
372                            items.push(InputItem {
373                                role: Some(Role::User),
374                                input: InputContent::Message(Message::User {
375                                    content: OneOrMany::one(UserContent::InputFile {
376                                        file_id: None,
377                                        file_data,
378                                        file_url,
379                                        filename: Some("document.pdf".to_string()),
380                                    }),
381                                    name: None,
382                                }),
383                            })
384                        }
385                        crate::message::UserContent::Document(Document {
386                            data:
387                                DocumentSourceKind::Base64(text) | DocumentSourceKind::String(text),
388                            ..
389                        }) => items.push(InputItem {
390                            role: Some(Role::User),
391                            input: InputContent::Message(Message::User {
392                                content: OneOrMany::one(UserContent::InputText { text }),
393                                name: None,
394                            }),
395                        }),
396                        crate::message::UserContent::Image(crate::message::Image {
397                            data,
398                            media_type,
399                            detail,
400                            ..
401                        }) => {
402                            let url = match data {
403                                DocumentSourceKind::Base64(data) => {
404                                    let media_type = if let Some(media_type) = media_type {
405                                        media_type.to_mime_type().to_string()
406                                    } else {
407                                        String::new()
408                                    };
409                                    format!("data:{media_type};base64,{data}")
410                                }
411                                DocumentSourceKind::Url(url) => url,
412                                DocumentSourceKind::Raw(_) => {
413                                    return Err(CompletionError::RequestError(
414                                        "Raw file data not supported, encode as base64 first"
415                                            .into(),
416                                    ));
417                                }
418                                doc => {
419                                    return Err(CompletionError::RequestError(
420                                        format!("Unsupported document type: {doc}").into(),
421                                    ));
422                                }
423                            };
424                            items.push(InputItem {
425                                role: Some(Role::User),
426                                input: InputContent::Message(Message::User {
427                                    content: OneOrMany::one(UserContent::InputImage {
428                                        image_url: url,
429                                        detail: detail.unwrap_or_default(),
430                                    }),
431                                    name: None,
432                                }),
433                            });
434                        }
435                        message => {
436                            return Err(CompletionError::ProviderError(format!(
437                                "Unsupported message: {message:?}"
438                            )));
439                        }
440                    }
441                }
442
443                Ok(items)
444            }
445            crate::completion::Message::Assistant { id, content } => {
446                let mut reasoning_items = Vec::new();
447                let mut other_items = Vec::new();
448
449                for assistant_content in content {
450                    match assistant_content {
451                        crate::message::AssistantContent::Text(Text { text, .. }) => {
452                            if text.is_empty() {
453                                continue;
454                            }
455                            let message = if let Some(id) = id.clone() {
456                                Message::Assistant {
457                                    content: OneOrMany::one(AssistantContentType::Text(
458                                        AssistantContent::OutputText(Text::new(text)),
459                                    )),
460                                    id,
461                                    name: None,
462                                    status: ToolStatus::Completed,
463                                }
464                            } else {
465                                Message::AssistantInput {
466                                    content: text,
467                                    name: None,
468                                }
469                            };
470
471                            other_items.push(InputItem {
472                                role: Some(Role::Assistant),
473                                input: InputContent::Message(message),
474                            });
475                        }
476                        crate::message::AssistantContent::ToolCall(crate::message::ToolCall {
477                            id: tool_id,
478                            call_id,
479                            function,
480                            ..
481                        }) => {
482                            other_items.push(InputItem {
483                                role: None,
484                                input: InputContent::FunctionCall(OutputFunctionCall {
485                                    arguments: function.arguments,
486                                    call_id: require_call_id(call_id, "Assistant tool call")?,
487                                    id: tool_id,
488                                    name: function.name,
489                                    status: ToolStatus::Completed,
490                                }),
491                            });
492                        }
493                        crate::message::AssistantContent::Reasoning(reasoning) => {
494                            let openai_reasoning = openai_reasoning_from_core(&reasoning)
495                                .map_err(|err| CompletionError::ProviderError(err.to_string()))?;
496                            if let Some(openai_reasoning) = openai_reasoning {
497                                reasoning_items.push(InputItem {
498                                    role: None,
499                                    input: InputContent::Reasoning(openai_reasoning),
500                                });
501                            }
502                        }
503                        crate::message::AssistantContent::Image(_) => {
504                            return Err(CompletionError::ProviderError(
505                                "Assistant image content is not supported in OpenAI Responses API"
506                                    .to_string(),
507                            ));
508                        }
509                    }
510                }
511
512                let mut items = reasoning_items;
513                items.extend(other_items);
514                Ok(items)
515            }
516        }
517    }
518}
519
520impl From<OneOrMany<String>> for Vec<ReasoningSummary> {
521    fn from(value: OneOrMany<String>) -> Self {
522        value.iter().map(|x| ReasoningSummary::new(x)).collect()
523    }
524}
525
526fn require_call_id(call_id: Option<String>, context: &str) -> Result<String, CompletionError> {
527    call_id.ok_or_else(|| {
528        CompletionError::RequestError(
529            format!("{context} `call_id` is required for OpenAI Responses API").into(),
530        )
531    })
532}
533
534fn openai_reasoning_from_core(
535    reasoning: &crate::message::Reasoning,
536) -> Result<Option<OpenAIReasoning>, MessageError> {
537    let Some(id) = reasoning.id.clone() else {
538        return Ok(None);
539    };
540
541    let mut summary = Vec::new();
542    let mut encrypted_content = None;
543    for content in &reasoning.content {
544        match content {
545            crate::message::ReasoningContent::Text { text, .. }
546            | crate::message::ReasoningContent::Summary(text) => {
547                summary.push(ReasoningSummary::new(text));
548            }
549            // OpenAI reasoning input has one opaque payload field; preserve either
550            // encrypted or redacted blocks there, preferring the first one seen.
551            crate::message::ReasoningContent::Encrypted(data)
552            | crate::message::ReasoningContent::Redacted { data } => {
553                encrypted_content.get_or_insert_with(|| data.clone());
554            }
555        }
556    }
557
558    Ok(Some(OpenAIReasoning {
559        id,
560        summary,
561        encrypted_content,
562        status: None,
563    }))
564}
565
566fn optional_reasoning_string<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
567where
568    D: Deserializer<'de>,
569{
570    Ok(
571        match Option::<serde_json::Value>::deserialize(deserializer)? {
572            Some(serde_json::Value::String(reasoning)) => Some(reasoning),
573            _ => None,
574        },
575    )
576}
577
578/// The definition of a tool response, repurposed for OpenAI's Responses API.
579#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
580pub struct ResponsesToolDefinition {
581    /// The type of tool.
582    #[serde(rename = "type")]
583    pub kind: String,
584    /// Tool name
585    #[serde(default, skip_serializing_if = "String::is_empty")]
586    pub name: String,
587    /// 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).
588    #[serde(default, skip_serializing_if = "is_json_null")]
589    pub parameters: serde_json::Value,
590    /// Whether to use strict mode. Enabled by default as it allows for improved efficiency.
591    #[serde(default, skip_serializing_if = "is_false")]
592    pub strict: bool,
593    /// Tool description.
594    #[serde(default, skip_serializing_if = "String::is_empty")]
595    pub description: String,
596    /// Additional provider-specific configuration for hosted tools.
597    #[serde(flatten, default, skip_serializing_if = "Map::is_empty")]
598    pub config: Map<String, Value>,
599}
600
601fn is_json_null(value: &Value) -> bool {
602    value.is_null()
603}
604
605fn is_false(value: &bool) -> bool {
606    !value
607}
608
609impl ResponsesToolDefinition {
610    /// Creates a function tool definition.
611    pub fn function(
612        name: impl Into<String>,
613        description: impl Into<String>,
614        mut parameters: serde_json::Value,
615    ) -> Self {
616        super::sanitize_schema(&mut parameters);
617
618        Self {
619            kind: "function".to_string(),
620            name: name.into(),
621            parameters,
622            strict: true,
623            description: description.into(),
624            config: Map::new(),
625        }
626    }
627
628    /// Creates a hosted tool definition for an arbitrary hosted tool type.
629    pub fn hosted(kind: impl Into<String>) -> Self {
630        Self {
631            kind: kind.into(),
632            name: String::new(),
633            parameters: Value::Null,
634            strict: false,
635            description: String::new(),
636            config: Map::new(),
637        }
638    }
639
640    /// Creates a hosted `web_search` tool definition.
641    pub fn web_search() -> Self {
642        Self::hosted("web_search")
643    }
644
645    /// Creates a hosted `file_search` tool definition.
646    pub fn file_search() -> Self {
647        Self::hosted("file_search")
648    }
649
650    /// Creates a hosted `computer_use` tool definition.
651    pub fn computer_use() -> Self {
652        Self::hosted("computer_use")
653    }
654
655    /// Adds hosted-tool configuration fields.
656    pub fn with_config(mut self, key: impl Into<String>, value: Value) -> Self {
657        self.config.insert(key.into(), value);
658        self
659    }
660
661    fn normalize(mut self) -> Self {
662        if self.kind == "function" {
663            super::sanitize_schema(&mut self.parameters);
664            self.strict = true;
665        }
666        self
667    }
668}
669
670impl From<completion::ToolDefinition> for ResponsesToolDefinition {
671    fn from(value: completion::ToolDefinition) -> Self {
672        let completion::ToolDefinition {
673            name,
674            parameters,
675            description,
676        } = value;
677
678        Self::function(name, description, parameters)
679    }
680}
681
682/// Token usage.
683/// 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.
684#[derive(Clone, Debug, Serialize, Deserialize)]
685pub struct ResponsesUsage {
686    /// Input tokens
687    pub input_tokens: u64,
688    /// In-depth detail on input tokens (cached tokens)
689    #[serde(skip_serializing_if = "Option::is_none")]
690    pub input_tokens_details: Option<InputTokensDetails>,
691    /// Output tokens
692    pub output_tokens: u64,
693    /// In-depth detail on output tokens (reasoning tokens)
694    #[serde(skip_serializing_if = "Option::is_none")]
695    pub output_tokens_details: Option<OutputTokensDetails>,
696    /// Total tokens used (for a given prompt)
697    pub total_tokens: u64,
698}
699
700impl ResponsesUsage {
701    /// Create a new ResponsesUsage instance
702    pub(crate) fn new() -> Self {
703        Self {
704            input_tokens: 0,
705            input_tokens_details: Some(InputTokensDetails::new()),
706            output_tokens: 0,
707            output_tokens_details: Some(OutputTokensDetails::new()),
708            total_tokens: 0,
709        }
710    }
711}
712
713impl GetTokenUsage for ResponsesUsage {
714    fn token_usage(&self) -> crate::completion::Usage {
715        crate::completion::Usage {
716            input_tokens: self.input_tokens,
717            output_tokens: self.output_tokens,
718            total_tokens: self.total_tokens,
719            cached_input_tokens: self
720                .input_tokens_details
721                .as_ref()
722                .map(|details| details.cached_tokens)
723                .unwrap_or(0),
724            cache_creation_input_tokens: 0,
725            tool_use_prompt_tokens: 0,
726            reasoning_tokens: self
727                .output_tokens_details
728                .as_ref()
729                .map(|details| details.reasoning_tokens)
730                .unwrap_or(0),
731        }
732    }
733}
734
735impl Add for ResponsesUsage {
736    type Output = Self;
737
738    fn add(self, rhs: Self) -> Self::Output {
739        let input_tokens = self.input_tokens + rhs.input_tokens;
740        let input_tokens_details = match (self.input_tokens_details, rhs.input_tokens_details) {
741            (Some(lhs), Some(rhs)) => Some(lhs + rhs),
742            (Some(lhs), None) => Some(lhs),
743            (None, Some(rhs)) => Some(rhs),
744            (None, None) => None,
745        };
746        let output_tokens = self.output_tokens + rhs.output_tokens;
747        let output_tokens_details = match (self.output_tokens_details, rhs.output_tokens_details) {
748            (Some(lhs), Some(rhs)) => Some(lhs + rhs),
749            (Some(lhs), None) => Some(lhs),
750            (None, Some(rhs)) => Some(rhs),
751            (None, None) => None,
752        };
753        let total_tokens = self.total_tokens + rhs.total_tokens;
754        Self {
755            input_tokens,
756            input_tokens_details,
757            output_tokens,
758            output_tokens_details,
759            total_tokens,
760        }
761    }
762}
763
764/// In-depth details on input tokens.
765#[derive(Clone, Debug, Serialize, Deserialize)]
766pub struct InputTokensDetails {
767    /// Cached tokens from OpenAI
768    pub cached_tokens: u64,
769}
770
771impl InputTokensDetails {
772    pub(crate) fn new() -> Self {
773        Self { cached_tokens: 0 }
774    }
775}
776
777impl Add for InputTokensDetails {
778    type Output = Self;
779    fn add(self, rhs: Self) -> Self::Output {
780        Self {
781            cached_tokens: self.cached_tokens + rhs.cached_tokens,
782        }
783    }
784}
785
786/// In-depth details on output tokens.
787#[derive(Clone, Debug, Serialize, Deserialize)]
788pub struct OutputTokensDetails {
789    /// Reasoning tokens
790    pub reasoning_tokens: u64,
791}
792
793impl OutputTokensDetails {
794    pub(crate) fn new() -> Self {
795        Self {
796            reasoning_tokens: 0,
797        }
798    }
799}
800
801impl Add for OutputTokensDetails {
802    type Output = Self;
803    fn add(self, rhs: Self) -> Self::Output {
804        Self {
805            reasoning_tokens: self.reasoning_tokens + rhs.reasoning_tokens,
806        }
807    }
808}
809
810/// Occasionally, when using OpenAI's Responses API you may get an incomplete response. This struct holds the reason as to why it happened.
811#[derive(Clone, Debug, Default, Serialize, Deserialize)]
812pub struct IncompleteDetailsReason {
813    /// The reason for an incomplete [`CompletionResponse`].
814    pub reason: String,
815}
816
817/// A response error from OpenAI's Response API.
818#[derive(Clone, Debug, Default, Serialize, Deserialize)]
819pub struct ResponseError {
820    /// Error code
821    pub code: String,
822    /// Error message
823    pub message: String,
824}
825
826/// A response object as an enum (ensures type validation)
827#[derive(Clone, Debug, Deserialize, Serialize)]
828#[serde(rename_all = "snake_case")]
829pub enum ResponseObject {
830    Response,
831}
832
833/// The response status as an enum (ensures type validation)
834#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
835#[serde(rename_all = "snake_case")]
836pub enum ResponseStatus {
837    InProgress,
838    Completed,
839    Failed,
840    Cancelled,
841    Queued,
842    Incomplete,
843}
844
845/// Attempt to try and create a `NewCompletionRequest` from a model name and [`crate::completion::CompletionRequest`]
846impl TryFrom<(String, crate::completion::CompletionRequest)> for CompletionRequest {
847    type Error = CompletionError;
848    fn try_from(
849        (model, mut req): (String, crate::completion::CompletionRequest),
850    ) -> Result<Self, Self::Error> {
851        let chat_history = req.chat_history_with_documents();
852        let model = req.model.clone().unwrap_or(model);
853        let input = {
854            let mut partial_history = vec![];
855            partial_history.extend(chat_history);
856
857            // Initialize full history with preamble (or empty if non-existent)
858            // Some "Responses API compatible" providers don't support `instructions` field
859            // so we need to add a system message until further notice
860            let mut full_history: Vec<InputItem> = if let Some(content) = req.preamble {
861                vec![InputItem::system_message(content)]
862            } else {
863                Vec::new()
864            };
865
866            for history_item in partial_history {
867                full_history.extend(<Vec<InputItem>>::try_from(history_item)?);
868            }
869
870            full_history
871        };
872
873        let input = OneOrMany::many(input).map_err(|_| {
874            CompletionError::RequestError(
875                "OpenAI Responses request input must contain at least one item".into(),
876            )
877        })?;
878
879        let mut additional_params_payload = req.additional_params.take().unwrap_or(Value::Null);
880        let stream = match &additional_params_payload {
881            Value::Bool(stream) => Some(*stream),
882            Value::Object(map) => map.get("stream").and_then(Value::as_bool),
883            _ => None,
884        };
885
886        let mut additional_tools = Vec::new();
887        if let Some(additional_params_map) = additional_params_payload.as_object_mut() {
888            if let Some(raw_tools) = additional_params_map.remove("tools") {
889                additional_tools = serde_json::from_value::<Vec<ResponsesToolDefinition>>(
890                    raw_tools,
891                )
892                .map_err(|err| {
893                    CompletionError::RequestError(
894                        format!(
895                            "Invalid OpenAI Responses tools payload in additional_params: {err}"
896                        )
897                        .into(),
898                    )
899                })?;
900            }
901            additional_params_map.remove("stream");
902        }
903
904        if additional_params_payload.is_boolean() {
905            additional_params_payload = Value::Null;
906        }
907
908        additional_tools = additional_tools
909            .into_iter()
910            .map(ResponsesToolDefinition::normalize)
911            .collect();
912
913        let mut additional_parameters = if additional_params_payload.is_null() {
914            // If there's no additional parameters, initialise an empty object
915            AdditionalParameters::default()
916        } else {
917            serde_json::from_value::<AdditionalParameters>(additional_params_payload).map_err(
918                |err| {
919                    CompletionError::RequestError(
920                        format!("Invalid OpenAI Responses additional_params payload: {err}").into(),
921                    )
922                },
923            )?
924        };
925        if additional_parameters.reasoning.is_some() {
926            let include = additional_parameters.include.get_or_insert_with(Vec::new);
927            if !include
928                .iter()
929                .any(|item| matches!(item, Include::ReasoningEncryptedContent))
930            {
931                include.push(Include::ReasoningEncryptedContent);
932            }
933        }
934
935        // Apply output_schema as structured output if not already configured via additional_params
936        if additional_parameters.text.is_none()
937            && let Some(schema) = req.output_schema
938        {
939            let name = schema
940                .as_object()
941                .and_then(|o| o.get("title"))
942                .and_then(|v| v.as_str())
943                .unwrap_or("response_schema")
944                .to_string();
945            let mut schema_value = schema.to_value();
946            super::sanitize_schema(&mut schema_value);
947            additional_parameters.text = Some(TextConfig::structured_output(name, schema_value));
948        }
949
950        let tool_choice = req.tool_choice.map(ToolChoice::try_from).transpose()?;
951        let mut tools: Vec<ResponsesToolDefinition> = req
952            .tools
953            .into_iter()
954            .map(ResponsesToolDefinition::from)
955            .collect();
956        tools.append(&mut additional_tools);
957
958        Ok(Self {
959            input,
960            model,
961            instructions: None, // is currently None due to lack of support in compliant providers
962            max_output_tokens: req.max_tokens,
963            stream,
964            tool_choice,
965            tools,
966            temperature: req.temperature,
967            additional_parameters,
968        })
969    }
970}
971
972/// The completion model struct for OpenAI's response API.
973#[doc(hidden)]
974#[derive(Clone)]
975pub struct GenericResponsesCompletionModel<Ext = super::OpenAIResponsesExt, H = reqwest::Client> {
976    /// The OpenAI client
977    pub(crate) client: crate::client::Client<Ext, H>,
978    /// Name of the model (e.g.: gpt-3.5-turbo-1106)
979    pub model: String,
980    /// Model-level default tools that are always added to outgoing requests.
981    pub tools: Vec<ResponsesToolDefinition>,
982}
983
984/// The completion model struct for OpenAI's Responses API.
985///
986/// This preserves the historical public generic shape where the first generic
987/// parameter is the HTTP client type.
988pub type ResponsesCompletionModel<H = reqwest::Client> =
989    GenericResponsesCompletionModel<super::OpenAIResponsesExt, H>;
990
991impl<Ext, H> GenericResponsesCompletionModel<Ext, H>
992where
993    crate::client::Client<Ext, H>: HttpClientExt + Clone + std::fmt::Debug + 'static,
994    Ext: crate::client::Provider + Clone + 'static,
995    H: Clone + Default + std::fmt::Debug + 'static,
996{
997    /// Creates a new [`ResponsesCompletionModel`].
998    pub fn new(client: crate::client::Client<Ext, H>, model: impl Into<String>) -> Self {
999        Self {
1000            client,
1001            model: model.into(),
1002            tools: Vec::new(),
1003        }
1004    }
1005
1006    pub fn with_model(client: crate::client::Client<Ext, H>, model: &str) -> Self {
1007        Self {
1008            client,
1009            model: model.to_string(),
1010            tools: Vec::new(),
1011        }
1012    }
1013
1014    /// Adds a default tool to all requests from this model.
1015    pub fn with_tool(mut self, tool: impl Into<ResponsesToolDefinition>) -> Self {
1016        self.tools.push(tool.into());
1017        self
1018    }
1019
1020    /// Adds default tools to all requests from this model.
1021    pub fn with_tools<I, Tool>(mut self, tools: I) -> Self
1022    where
1023        I: IntoIterator<Item = Tool>,
1024        Tool: Into<ResponsesToolDefinition>,
1025    {
1026        self.tools.extend(tools.into_iter().map(Into::into));
1027        self
1028    }
1029
1030    /// Attempt to create a completion request from [`crate::completion::CompletionRequest`].
1031    pub(crate) fn create_completion_request(
1032        &self,
1033        completion_request: crate::completion::CompletionRequest,
1034    ) -> Result<CompletionRequest, CompletionError> {
1035        let mut req = CompletionRequest::try_from((self.model.clone(), completion_request))?;
1036        req.tools.extend(self.tools.clone());
1037
1038        Ok(req)
1039    }
1040}
1041
1042impl<T> GenericResponsesCompletionModel<super::OpenAIResponsesExt, T>
1043where
1044    T: HttpClientExt + Clone + Default + std::fmt::Debug + 'static,
1045{
1046    /// Use the Completions API instead of Responses.
1047    pub fn completions_api(self) -> crate::providers::openai::completion::CompletionModel<T> {
1048        super::completion::CompletionModel::with_model(self.client.completions_api(), &self.model)
1049    }
1050}
1051
1052/// The standard response format from OpenAI's Responses API.
1053#[derive(Clone, Debug, Serialize, Deserialize)]
1054pub struct CompletionResponse {
1055    /// The ID of a completion response.
1056    pub id: String,
1057    /// The type of the object.
1058    pub object: ResponseObject,
1059    /// The time at which a given response has been created, in seconds from the UNIX epoch (01/01/1970 00:00:00).
1060    pub created_at: u64,
1061    /// The status of the response.
1062    pub status: ResponseStatus,
1063    /// Response error (optional)
1064    pub error: Option<ResponseError>,
1065    /// Incomplete response details (optional)
1066    pub incomplete_details: Option<IncompleteDetailsReason>,
1067    /// System prompt/preamble
1068    pub instructions: Option<String>,
1069    /// The maximum number of tokens the model should output
1070    pub max_output_tokens: Option<u64>,
1071    /// The model name
1072    pub model: String,
1073    /// Provider-specific top-level reasoning content returned by some
1074    /// OpenAI-compatible Responses implementations.
1075    #[serde(
1076        default,
1077        rename = "reasoning",
1078        deserialize_with = "optional_reasoning_string",
1079        skip_serializing_if = "Option::is_none"
1080    )]
1081    pub provider_reasoning: Option<String>,
1082    /// Token usage
1083    pub usage: Option<ResponsesUsage>,
1084    /// The model output (messages, etc will go here)
1085    #[serde(default)]
1086    pub output: Vec<Output>,
1087    /// Tools
1088    #[serde(default)]
1089    pub tools: Vec<ResponsesToolDefinition>,
1090    /// Additional parameters
1091    #[serde(flatten)]
1092    pub additional_parameters: AdditionalParameters,
1093}
1094
1095/// Additional parameters for the completion request type for OpenAI's Response API: <https://platform.openai.com/docs/api-reference/responses/create>
1096/// Intended to be derived from [`crate::completion::request::CompletionRequest`].
1097#[derive(Clone, Debug, Deserialize, Serialize, Default)]
1098pub struct AdditionalParameters {
1099    /// Whether or not a given model task should run in the background (ie a detached process).
1100    #[serde(skip_serializing_if = "Option::is_none")]
1101    pub background: Option<bool>,
1102    /// The text response format. This is where you would add structured outputs (if you want them).
1103    #[serde(skip_serializing_if = "Option::is_none")]
1104    pub text: Option<TextConfig>,
1105    /// 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!
1106    #[serde(skip_serializing_if = "Option::is_none")]
1107    pub include: Option<Vec<Include>>,
1108    /// `top_p`. Mutually exclusive with the `temperature` argument.
1109    #[serde(skip_serializing_if = "Option::is_none")]
1110    pub top_p: Option<f64>,
1111    /// Whether or not the response should be truncated.
1112    #[serde(skip_serializing_if = "Option::is_none")]
1113    pub truncation: Option<TruncationStrategy>,
1114    /// The username of the user (that you want to use).
1115    #[serde(skip_serializing_if = "Option::is_none")]
1116    pub user: Option<String>,
1117    /// Any additional metadata you'd like to add. This will additionally be returned by the response.
1118    #[serde(skip_serializing_if = "Map::is_empty", default)]
1119    pub metadata: serde_json::Map<String, serde_json::Value>,
1120    /// Whether or not you want tool calls to run in parallel.
1121    #[serde(skip_serializing_if = "Option::is_none")]
1122    pub parallel_tool_calls: Option<bool>,
1123    /// Previous response ID. If you are not sending a full conversation, this can help to track the message flow.
1124    #[serde(skip_serializing_if = "Option::is_none")]
1125    pub previous_response_id: Option<String>,
1126    /// Add thinking/reasoning to your response. The response will be emitted as a list member of the `output` field.
1127    #[serde(skip_serializing_if = "Option::is_none")]
1128    pub reasoning: Option<Reasoning>,
1129    /// The service tier you're using.
1130    #[serde(skip_serializing_if = "Option::is_none")]
1131    pub service_tier: Option<OpenAIServiceTier>,
1132    /// Whether or not to store the response for later retrieval by API.
1133    #[serde(skip_serializing_if = "Option::is_none")]
1134    pub store: Option<bool>,
1135}
1136
1137impl AdditionalParameters {
1138    pub fn to_json(self) -> serde_json::Value {
1139        serde_json::to_value(self).unwrap_or_else(|_| serde_json::Value::Object(Map::new()))
1140    }
1141}
1142
1143/// The truncation strategy.
1144/// 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.
1145/// Otherwise, does nothing (and is disabled by default).
1146#[derive(Clone, Debug, Default, Serialize, Deserialize)]
1147#[serde(rename_all = "snake_case")]
1148pub enum TruncationStrategy {
1149    Auto,
1150    #[default]
1151    Disabled,
1152}
1153
1154/// The model output format configuration.
1155/// You can either have plain text by default, or attach a JSON schema for the purposes of structured outputs.
1156#[derive(Clone, Debug, Serialize, Deserialize)]
1157pub struct TextConfig {
1158    pub format: TextFormat,
1159}
1160
1161impl TextConfig {
1162    pub(crate) fn structured_output<S>(name: S, schema: serde_json::Value) -> Self
1163    where
1164        S: Into<String>,
1165    {
1166        Self {
1167            format: TextFormat::JsonSchema(StructuredOutputsInput {
1168                name: name.into(),
1169                schema,
1170                strict: true,
1171            }),
1172        }
1173    }
1174}
1175
1176/// The text format (contained by [`TextConfig`]).
1177/// You can either have plain text by default, or attach a JSON schema for the purposes of structured outputs.
1178#[derive(Clone, Debug, Serialize, Deserialize, Default)]
1179#[serde(tag = "type")]
1180#[serde(rename_all = "snake_case")]
1181pub enum TextFormat {
1182    JsonSchema(StructuredOutputsInput),
1183    #[default]
1184    Text,
1185}
1186
1187/// The inputs required for adding structured outputs.
1188#[derive(Clone, Debug, Serialize, Deserialize)]
1189pub struct StructuredOutputsInput {
1190    /// The name of your schema.
1191    pub name: String,
1192    /// 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>.
1193    pub schema: serde_json::Value,
1194    /// 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.
1195    #[serde(default)]
1196    pub strict: bool,
1197}
1198
1199/// Add reasoning to a [`CompletionRequest`].
1200#[derive(Clone, Debug, Default, Serialize, Deserialize)]
1201pub struct Reasoning {
1202    /// How much effort you want the model to put into thinking/reasoning.
1203    pub effort: Option<ReasoningEffort>,
1204    /// How much effort you want the model to put into writing the reasoning summary.
1205    #[serde(skip_serializing_if = "Option::is_none")]
1206    pub summary: Option<ReasoningSummaryLevel>,
1207}
1208
1209impl Reasoning {
1210    /// Creates a new Reasoning instantiation (with empty values).
1211    pub fn new() -> Self {
1212        Self {
1213            effort: None,
1214            summary: None,
1215        }
1216    }
1217
1218    /// Adds reasoning effort.
1219    pub fn with_effort(mut self, reasoning_effort: ReasoningEffort) -> Self {
1220        self.effort = Some(reasoning_effort);
1221
1222        self
1223    }
1224
1225    /// Adds summary level (how detailed the reasoning summary will be).
1226    pub fn with_summary_level(mut self, reasoning_summary_level: ReasoningSummaryLevel) -> Self {
1227        self.summary = Some(reasoning_summary_level);
1228
1229        self
1230    }
1231}
1232
1233/// The billing service tier that will be used. On auto by default.
1234#[derive(Clone, Debug, Default)]
1235pub enum OpenAIServiceTier {
1236    /// Let OpenAI choose the service tier.
1237    #[default]
1238    Auto,
1239    /// Use the default service tier.
1240    Default,
1241    /// Use the flex service tier.
1242    Flex,
1243    /// Use the priority service tier.
1244    Priority,
1245    /// Use the standard service tier returned by OpenAI-compatible providers.
1246    Standard,
1247    /// Preserve an unknown provider-specific service tier.
1248    Other(String),
1249}
1250
1251impl Serialize for OpenAIServiceTier {
1252    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1253    where
1254        S: Serializer,
1255    {
1256        serializer.serialize_str(match self {
1257            Self::Auto => "auto",
1258            Self::Default => "default",
1259            Self::Flex => "flex",
1260            Self::Priority => "priority",
1261            Self::Standard => "standard",
1262            Self::Other(value) => value,
1263        })
1264    }
1265}
1266
1267impl<'de> Deserialize<'de> for OpenAIServiceTier {
1268    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1269    where
1270        D: Deserializer<'de>,
1271    {
1272        let value = String::deserialize(deserializer)?;
1273        Ok(match value.as_str() {
1274            "auto" => Self::Auto,
1275            "default" => Self::Default,
1276            "flex" => Self::Flex,
1277            "priority" => Self::Priority,
1278            "standard" => Self::Standard,
1279            _ => Self::Other(value),
1280        })
1281    }
1282}
1283
1284/// The amount of reasoning effort that will be used by a given model.
1285#[derive(Clone, Debug, Default, Serialize, Deserialize)]
1286#[serde(rename_all = "snake_case")]
1287pub enum ReasoningEffort {
1288    None,
1289    Minimal,
1290    Low,
1291    #[default]
1292    Medium,
1293    High,
1294    Xhigh,
1295}
1296
1297/// The amount of effort that will go into a reasoning summary by a given model.
1298#[derive(Clone, Debug, Default, Serialize, Deserialize)]
1299#[serde(rename_all = "snake_case")]
1300pub enum ReasoningSummaryLevel {
1301    #[default]
1302    Auto,
1303    Concise,
1304    Detailed,
1305}
1306
1307/// Results to additionally include in the OpenAI Responses API.
1308/// Note that most of these are currently unsupported, but have been added for completeness.
1309#[derive(Clone, Debug, Deserialize, Serialize)]
1310pub enum Include {
1311    #[serde(rename = "file_search_call.results")]
1312    FileSearchCallResults,
1313    #[serde(rename = "message.input_image.image_url")]
1314    MessageInputImageImageUrl,
1315    #[serde(rename = "computer_call.output.image_url")]
1316    ComputerCallOutputOutputImageUrl,
1317    #[serde(rename = "reasoning.encrypted_content")]
1318    ReasoningEncryptedContent,
1319    #[serde(rename = "code_interpreter_call.outputs")]
1320    CodeInterpreterCallOutputs,
1321}
1322
1323/// A currently non-exhaustive list of output types.
1324#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
1325#[serde(tag = "type")]
1326#[serde(rename_all = "snake_case")]
1327pub enum Output {
1328    Message(OutputMessage),
1329    #[serde(alias = "function_call")]
1330    FunctionCall(OutputFunctionCall),
1331    Reasoning {
1332        id: String,
1333        summary: Vec<ReasoningSummary>,
1334        #[serde(default)]
1335        encrypted_content: Option<String>,
1336        #[serde(default)]
1337        status: Option<ToolStatus>,
1338    },
1339    /// Catch-all variant for unknown output types (e.g., `web_search_call`,
1340    /// `file_search_call`, `computer_use_call`). This prevents unknown types
1341    /// from breaking deserialization of the entire `CompletionResponse`,
1342    /// which previously caused streaming token usage to be silently dropped.
1343    #[serde(other)]
1344    Unknown,
1345}
1346
1347impl From<Output> for Vec<completion::AssistantContent> {
1348    fn from(value: Output) -> Self {
1349        let res: Vec<completion::AssistantContent> = match value {
1350            Output::Message(OutputMessage { content, .. }) => content
1351                .into_iter()
1352                .map(completion::AssistantContent::from)
1353                .collect(),
1354            Output::FunctionCall(OutputFunctionCall {
1355                id,
1356                arguments,
1357                call_id,
1358                name,
1359                ..
1360            }) => vec![completion::AssistantContent::tool_call_with_call_id(
1361                id, call_id, name, arguments,
1362            )],
1363            Output::Reasoning {
1364                id,
1365                summary,
1366                encrypted_content,
1367                ..
1368            } => {
1369                let mut content = summary
1370                    .into_iter()
1371                    .map(|summary| match summary {
1372                        ReasoningSummary::SummaryText { text } => {
1373                            message::ReasoningContent::Summary(text)
1374                        }
1375                    })
1376                    .collect::<Vec<_>>();
1377                if let Some(encrypted_content) = encrypted_content {
1378                    content.push(message::ReasoningContent::Encrypted(encrypted_content));
1379                }
1380                vec![completion::AssistantContent::Reasoning(
1381                    message::Reasoning {
1382                        id: Some(id),
1383                        content,
1384                    },
1385                )]
1386            }
1387            Output::Unknown => Vec::new(),
1388        };
1389
1390        res
1391    }
1392}
1393
1394#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
1395pub struct OutputReasoning {
1396    id: String,
1397    summary: Vec<ReasoningSummary>,
1398    status: ToolStatus,
1399}
1400
1401/// 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.
1402#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
1403pub struct OutputFunctionCall {
1404    pub id: String,
1405    #[serde(with = "json_utils::stringified_json")]
1406    pub arguments: serde_json::Value,
1407    pub call_id: String,
1408    pub name: String,
1409    pub status: ToolStatus,
1410}
1411
1412/// The status of a given tool.
1413#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
1414#[serde(rename_all = "snake_case")]
1415pub enum ToolStatus {
1416    InProgress,
1417    Completed,
1418    Incomplete,
1419}
1420
1421/// An output message from OpenAI's Responses API.
1422#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
1423pub struct OutputMessage {
1424    /// The message ID. Must be included when sending the message back to OpenAI
1425    pub id: String,
1426    /// The role (currently only Assistant is available as this struct is only created when receiving an LLM message as a response)
1427    pub role: OutputRole,
1428    /// The status of the response
1429    pub status: ResponseStatus,
1430    /// The actual message content
1431    pub content: Vec<AssistantContent>,
1432}
1433
1434/// The role of an output message.
1435#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
1436#[serde(rename_all = "snake_case")]
1437pub enum OutputRole {
1438    Assistant,
1439}
1440
1441impl<Ext, H> completion::CompletionModel for GenericResponsesCompletionModel<Ext, H>
1442where
1443    crate::client::Client<Ext, H>:
1444        HttpClientExt + Clone + WasmCompatSend + WasmCompatSync + 'static,
1445    Ext: crate::client::Provider
1446        + crate::client::DebugExt
1447        + Clone
1448        + WasmCompatSend
1449        + WasmCompatSync
1450        + 'static,
1451    H: Clone + Default + std::fmt::Debug + WasmCompatSend + WasmCompatSync + 'static,
1452{
1453    type Response = CompletionResponse;
1454    type StreamingResponse = StreamingCompletionResponse;
1455
1456    type Client = crate::client::Client<Ext, H>;
1457
1458    fn make(client: &Self::Client, model: impl Into<String>) -> Self {
1459        Self::new(client.clone(), model)
1460    }
1461
1462    async fn completion(
1463        &self,
1464        completion_request: crate::completion::CompletionRequest,
1465    ) -> Result<completion::CompletionResponse<Self::Response>, CompletionError> {
1466        let span = if tracing::Span::current().is_disabled() {
1467            info_span!(
1468                target: "rig::completions",
1469                "chat",
1470                gen_ai.operation.name = "chat",
1471                gen_ai.provider.name = tracing::field::Empty,
1472                gen_ai.request.model = tracing::field::Empty,
1473                gen_ai.response.id = tracing::field::Empty,
1474                gen_ai.response.model = tracing::field::Empty,
1475                gen_ai.usage.output_tokens = tracing::field::Empty,
1476                gen_ai.usage.input_tokens = tracing::field::Empty,
1477                gen_ai.usage.cache_read.input_tokens = tracing::field::Empty,
1478                gen_ai.input.messages = tracing::field::Empty,
1479                gen_ai.output.messages = tracing::field::Empty,
1480            )
1481        } else {
1482            tracing::Span::current()
1483        };
1484
1485        span.record("gen_ai.provider.name", "openai");
1486        span.record("gen_ai.request.model", &self.model);
1487        let request = self.create_completion_request(completion_request)?;
1488        let body = serde_json::to_vec(&request)?;
1489
1490        if enabled!(Level::TRACE) {
1491            tracing::trace!(
1492                target: "rig::completions",
1493                "OpenAI Responses completion request: {request}",
1494                request = serde_json::to_string_pretty(&request)?
1495            );
1496        }
1497
1498        let req = self
1499            .client
1500            .post("/responses")?
1501            .body(body)
1502            .map_err(|e| CompletionError::HttpError(e.into()))?;
1503
1504        async move {
1505            let response = self.client.send(req).await?;
1506
1507            if response.status().is_success() {
1508                let t = http_client::text(response).await?;
1509                let response = serde_json::from_str::<Self::Response>(&t)?;
1510                let span = tracing::Span::current();
1511                span.record("gen_ai.response.id", &response.id);
1512                span.record("gen_ai.response.model", &response.model);
1513                if let Some(ref usage) = response.usage {
1514                    span.record("gen_ai.usage.output_tokens", usage.output_tokens);
1515                    span.record("gen_ai.usage.input_tokens", usage.input_tokens);
1516                    let cached_tokens = usage
1517                        .input_tokens_details
1518                        .as_ref()
1519                        .map(|d| d.cached_tokens)
1520                        .unwrap_or(0);
1521                    span.record("gen_ai.usage.cache_read.input_tokens", cached_tokens);
1522                }
1523                if enabled!(Level::TRACE) {
1524                    tracing::trace!(
1525                        target: "rig::completions",
1526                        "OpenAI Responses completion response: {response}",
1527                        response = serde_json::to_string_pretty(&response)?
1528                    );
1529                }
1530                response.try_into()
1531            } else {
1532                let text = http_client::text(response).await?;
1533                Err(CompletionError::ProviderError(text))
1534            }
1535        }
1536        .instrument(span)
1537        .await
1538    }
1539
1540    async fn stream(
1541        &self,
1542        request: crate::completion::CompletionRequest,
1543    ) -> Result<
1544        crate::streaming::StreamingCompletionResponse<Self::StreamingResponse>,
1545        CompletionError,
1546    > {
1547        GenericResponsesCompletionModel::stream(self, request).await
1548    }
1549}
1550
1551impl TryFrom<CompletionResponse> for completion::CompletionResponse<CompletionResponse> {
1552    type Error = CompletionError;
1553
1554    fn try_from(response: CompletionResponse) -> Result<Self, Self::Error> {
1555        // Extract the msg_ ID from the first Output::Message item
1556        let message_id = response.output.iter().find_map(|item| match item {
1557            Output::Message(msg) => Some(msg.id.clone()),
1558            _ => None,
1559        });
1560
1561        let output_content: Vec<completion::AssistantContent> = response
1562            .output
1563            .iter()
1564            .cloned()
1565            .flat_map(<Vec<completion::AssistantContent>>::from)
1566            .collect();
1567        let has_structured_reasoning = response
1568            .output
1569            .iter()
1570            .any(|item| matches!(item, Output::Reasoning { .. }));
1571        let content = response
1572            .provider_reasoning
1573            .as_ref()
1574            .filter(|reasoning| !has_structured_reasoning && !reasoning.is_empty())
1575            .map(|reasoning| {
1576                let mut content = Vec::with_capacity(output_content.len() + 1);
1577                content.push(completion::AssistantContent::Reasoning(
1578                    message::Reasoning::new(reasoning),
1579                ));
1580                content.extend(output_content.clone());
1581                content
1582            })
1583            .unwrap_or(output_content);
1584
1585        let choice = OneOrMany::many(content).map_err(|_| {
1586            CompletionError::ResponseError(
1587                "Response contained no message or tool call (empty)".to_owned(),
1588            )
1589        })?;
1590
1591        let usage = response
1592            .usage
1593            .as_ref()
1594            .map(GetTokenUsage::token_usage)
1595            .unwrap_or_default();
1596
1597        Ok(completion::CompletionResponse {
1598            choice,
1599            usage,
1600            raw_response: response,
1601            message_id,
1602        })
1603    }
1604}
1605
1606/// An OpenAI Responses API message.
1607#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
1608#[serde(tag = "role", rename_all = "lowercase")]
1609pub enum Message {
1610    #[serde(alias = "developer")]
1611    System {
1612        #[serde(deserialize_with = "string_or_one_or_many")]
1613        content: OneOrMany<SystemContent>,
1614        #[serde(skip_serializing_if = "Option::is_none")]
1615        name: Option<String>,
1616    },
1617    User {
1618        #[serde(deserialize_with = "string_or_one_or_many")]
1619        content: OneOrMany<UserContent>,
1620        #[serde(skip_serializing_if = "Option::is_none")]
1621        name: Option<String>,
1622    },
1623    Assistant {
1624        content: OneOrMany<AssistantContentType>,
1625        #[serde(skip_serializing_if = "String::is_empty")]
1626        id: String,
1627        #[serde(skip_serializing_if = "Option::is_none")]
1628        name: Option<String>,
1629        status: ToolStatus,
1630    },
1631    #[serde(rename = "assistant", skip_deserializing)]
1632    AssistantInput {
1633        content: String,
1634        #[serde(skip_serializing_if = "Option::is_none")]
1635        name: Option<String>,
1636    },
1637    #[serde(rename = "tool")]
1638    ToolResult {
1639        tool_call_id: String,
1640        output: String,
1641    },
1642}
1643
1644/// The type of a tool result content item.
1645#[derive(Default, Debug, Serialize, Deserialize, PartialEq, Clone)]
1646#[serde(rename_all = "lowercase")]
1647pub enum ToolResultContentType {
1648    #[default]
1649    Text,
1650}
1651
1652impl Message {
1653    pub fn system(content: &str) -> Self {
1654        Message::System {
1655            content: OneOrMany::one(content.to_owned().into()),
1656            name: None,
1657        }
1658    }
1659}
1660
1661/// Text assistant content.
1662/// Note that the text type in comparison to the Completions API is actually `output_text` rather than `text`.
1663#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
1664#[serde(tag = "type", rename_all = "snake_case")]
1665pub enum AssistantContent {
1666    OutputText(Text),
1667    Refusal { refusal: String },
1668}
1669
1670impl From<AssistantContent> for completion::AssistantContent {
1671    fn from(value: AssistantContent) -> Self {
1672        match value {
1673            AssistantContent::Refusal { refusal } => {
1674                completion::AssistantContent::Text(Text::new(refusal))
1675            }
1676            AssistantContent::OutputText(Text { text, .. }) => {
1677                completion::AssistantContent::Text(Text::new(text))
1678            }
1679        }
1680    }
1681}
1682
1683/// The type of assistant content.
1684#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
1685#[serde(untagged)]
1686pub enum AssistantContentType {
1687    Text(AssistantContent),
1688    ToolCall(OutputFunctionCall),
1689    Reasoning(OpenAIReasoning),
1690}
1691
1692/// System content for the OpenAI Responses API.
1693/// Uses `input_text` type to match the Responses API format.
1694#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
1695#[serde(tag = "type", rename_all = "snake_case")]
1696pub enum SystemContent {
1697    InputText { text: String },
1698}
1699
1700impl From<String> for SystemContent {
1701    fn from(s: String) -> Self {
1702        SystemContent::InputText { text: s }
1703    }
1704}
1705
1706impl std::str::FromStr for SystemContent {
1707    type Err = std::convert::Infallible;
1708
1709    fn from_str(s: &str) -> Result<Self, Self::Err> {
1710        Ok(SystemContent::InputText {
1711            text: s.to_string(),
1712        })
1713    }
1714}
1715
1716/// Different types of user content.
1717#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
1718#[serde(tag = "type", rename_all = "snake_case")]
1719pub enum UserContent {
1720    InputText {
1721        text: String,
1722    },
1723    InputImage {
1724        image_url: String,
1725        #[serde(default)]
1726        detail: ImageDetail,
1727    },
1728    InputFile {
1729        #[serde(skip_serializing_if = "Option::is_none")]
1730        file_id: Option<String>,
1731        #[serde(skip_serializing_if = "Option::is_none")]
1732        file_url: Option<String>,
1733        #[serde(skip_serializing_if = "Option::is_none")]
1734        file_data: Option<String>,
1735        #[serde(skip_serializing_if = "Option::is_none")]
1736        filename: Option<String>,
1737    },
1738    Audio {
1739        input_audio: InputAudio,
1740    },
1741    #[serde(rename = "tool")]
1742    ToolResult {
1743        tool_call_id: String,
1744        output: String,
1745    },
1746}
1747
1748impl TryFrom<message::Message> for Vec<Message> {
1749    type Error = message::MessageError;
1750
1751    fn try_from(message: message::Message) -> Result<Self, Self::Error> {
1752        match message {
1753            message::Message::System { content } => Ok(vec![Message::System {
1754                content: OneOrMany::one(content.into()),
1755                name: None,
1756            }]),
1757            message::Message::User { content } => {
1758                let (tool_results, other_content): (Vec<_>, Vec<_>) = content
1759                    .into_iter()
1760                    .partition(|content| matches!(content, message::UserContent::ToolResult(_)));
1761
1762                // If there are messages with both tool results and user content, openai will only
1763                //  handle tool results. It's unlikely that there will be both.
1764                if !tool_results.is_empty() {
1765                    tool_results
1766                        .into_iter()
1767                        .map(|content| match content {
1768                            message::UserContent::ToolResult(message::ToolResult {
1769                                call_id,
1770                                content,
1771                                ..
1772                            }) => Ok::<_, message::MessageError>(Message::ToolResult {
1773                                tool_call_id: call_id.ok_or_else(|| {
1774                                    MessageError::ConversionError(
1775                                        "Tool result `call_id` is required for OpenAI Responses API"
1776                                            .into(),
1777                                    )
1778                                })?,
1779                                output: {
1780                                    let res = content.first();
1781                                    match res {
1782                                        completion::message::ToolResultContent::Text(Text {
1783                                            text,
1784                                            ..
1785                                        }) => text,
1786                                        _ => return  Err(MessageError::ConversionError("This API only currently supports text tool results".into()))
1787                                    }
1788                                },
1789                            }),
1790                            _ => Err(MessageError::ConversionError(
1791                                "expected tool result content while converting Responses API input"
1792                                    .into(),
1793                            )),
1794                        })
1795                        .collect::<Result<Vec<_>, _>>()
1796                } else {
1797                    let other_content = other_content
1798                        .into_iter()
1799                        .map(|content| match content {
1800                            message::UserContent::Text(message::Text { text, .. }) => {
1801                                Ok(UserContent::InputText { text })
1802                            }
1803                            message::UserContent::Image(message::Image {
1804                                data,
1805                                detail,
1806                                media_type,
1807                                ..
1808                            }) => {
1809                                let url = match data {
1810                                    DocumentSourceKind::Base64(data) => {
1811                                        let media_type = if let Some(media_type) = media_type {
1812                                            media_type.to_mime_type().to_string()
1813                                        } else {
1814                                            String::new()
1815                                        };
1816                                        format!("data:{media_type};base64,{data}")
1817                                    }
1818                                    DocumentSourceKind::Url(url) => url,
1819                                    DocumentSourceKind::Raw(_) => {
1820                                        return Err(MessageError::ConversionError(
1821                                            "Raw files not supported, encode as base64 first"
1822                                                .into(),
1823                                        ));
1824                                    }
1825                                    doc => {
1826                                        return Err(MessageError::ConversionError(format!(
1827                                            "Unsupported document type: {doc}"
1828                                        )));
1829                                    }
1830                                };
1831
1832                                Ok(UserContent::InputImage {
1833                                    image_url: url,
1834                                    detail: detail.unwrap_or_default(),
1835                                })
1836                            }
1837                            message::UserContent::Document(message::Document {
1838                                data: DocumentSourceKind::FileId(file_id),
1839                                ..
1840                            }) => Ok(UserContent::InputFile {
1841                                file_id: Some(file_id),
1842                                file_url: None,
1843                                file_data: None,
1844                                filename: None,
1845                            }),
1846                            message::UserContent::Document(message::Document {
1847                                media_type: Some(DocumentMediaType::PDF),
1848                                data,
1849                                ..
1850                            }) => {
1851                                let (file_data, file_url, filename) = match data {
1852                                    DocumentSourceKind::Base64(data) => (
1853                                        Some(format!("data:application/pdf;base64,{data}")),
1854                                        None,
1855                                        Some("document.pdf".to_string()),
1856                                    ),
1857                                    DocumentSourceKind::Url(url) => (None, Some(url), None),
1858                                    DocumentSourceKind::Raw(_) => {
1859                                        return Err(MessageError::ConversionError(
1860                                            "Raw files not supported, encode as base64 first"
1861                                                .into(),
1862                                        ));
1863                                    }
1864                                    doc => {
1865                                        return Err(MessageError::ConversionError(format!(
1866                                            "Unsupported document type: {doc}"
1867                                        )));
1868                                    }
1869                                };
1870
1871                                Ok(UserContent::InputFile {
1872                                    file_id: None,
1873                                    file_url,
1874                                    file_data,
1875                                    filename,
1876                                })
1877                            }
1878                            message::UserContent::Document(message::Document {
1879                                data: DocumentSourceKind::Base64(text),
1880                                ..
1881                            }) => Ok(UserContent::InputText { text }),
1882                            message::UserContent::Audio(message::Audio {
1883                                data: DocumentSourceKind::Base64(data),
1884                                media_type,
1885                                ..
1886                            }) => Ok(UserContent::Audio {
1887                                input_audio: InputAudio {
1888                                    data,
1889                                    format: match media_type {
1890                                        Some(media_type) => media_type,
1891                                        None => AudioMediaType::MP3,
1892                                    },
1893                                },
1894                            }),
1895                            message::UserContent::Audio(_) => Err(MessageError::ConversionError(
1896                                "Audio must be base64 encoded data".into(),
1897                            )),
1898                            _ => Err(MessageError::ConversionError(
1899                                "Unsupported user content for OpenAI Responses API".into(),
1900                            )),
1901                        })
1902                        .collect::<Result<Vec<_>, _>>()?;
1903
1904                    let other_content = OneOrMany::many(other_content).map_err(|_| {
1905                        MessageError::ConversionError(
1906                            "User message did not contain OpenAI Responses-compatible content"
1907                                .to_string(),
1908                        )
1909                    })?;
1910
1911                    Ok(vec![Message::User {
1912                        content: other_content,
1913                        name: None,
1914                    }])
1915                }
1916            }
1917            message::Message::Assistant {
1918                content,
1919                id: assistant_message_id,
1920            } => {
1921                let mut messages = Vec::new();
1922
1923                for assistant_content in content {
1924                    match assistant_content {
1925                        crate::message::AssistantContent::Text(Text { text, .. }) => {
1926                            if text.is_empty() {
1927                                continue;
1928                            }
1929                            if let Some(id) = assistant_message_id.clone() {
1930                                messages.push(Message::Assistant {
1931                                    id,
1932                                    status: ToolStatus::Completed,
1933                                    content: OneOrMany::one(AssistantContentType::Text(
1934                                        AssistantContent::OutputText(Text::new(text)),
1935                                    )),
1936                                    name: None,
1937                                });
1938                            } else {
1939                                messages.push(Message::AssistantInput {
1940                                    content: text,
1941                                    name: None,
1942                                });
1943                            }
1944                        }
1945                        crate::message::AssistantContent::ToolCall(crate::message::ToolCall {
1946                            id: tool_id,
1947                            call_id,
1948                            function,
1949                            ..
1950                        }) => {
1951                            messages.push(Message::Assistant {
1952                                content: OneOrMany::one(AssistantContentType::ToolCall(
1953                                    OutputFunctionCall {
1954                                        call_id: call_id.ok_or_else(|| {
1955                                            MessageError::ConversionError(
1956                                                "Tool call `call_id` is required for OpenAI Responses API"
1957                                                    .into(),
1958                                            )
1959                                        })?,
1960                                        arguments: function.arguments,
1961                                        id: tool_id,
1962                                        name: function.name,
1963                                        status: ToolStatus::Completed,
1964                                    },
1965                                )),
1966                                id: assistant_message_id.clone().unwrap_or_default(),
1967                                name: None,
1968                                status: ToolStatus::Completed,
1969                            });
1970                        }
1971                        crate::message::AssistantContent::Reasoning(reasoning) => {
1972                            if let Some(openai_reasoning) = openai_reasoning_from_core(&reasoning)?
1973                            {
1974                                messages.push(Message::Assistant {
1975                                    content: OneOrMany::one(AssistantContentType::Reasoning(
1976                                        openai_reasoning,
1977                                    )),
1978                                    id: assistant_message_id.clone().unwrap_or_default(),
1979                                    name: None,
1980                                    status: ToolStatus::Completed,
1981                                });
1982                            }
1983                        }
1984                        crate::message::AssistantContent::Image(_) => {
1985                            return Err(MessageError::ConversionError(
1986                                "Assistant image content is not supported in OpenAI Responses API"
1987                                    .into(),
1988                            ));
1989                        }
1990                    }
1991                }
1992
1993                Ok(messages)
1994            }
1995        }
1996    }
1997}
1998
1999impl FromStr for UserContent {
2000    type Err = Infallible;
2001
2002    fn from_str(s: &str) -> Result<Self, Self::Err> {
2003        Ok(UserContent::InputText {
2004            text: s.to_string(),
2005        })
2006    }
2007}
2008
2009#[cfg(test)]
2010mod tests {
2011    use super::*;
2012    use crate::completion::CompletionRequestBuilder;
2013    use crate::message;
2014    use crate::test_utils::MockCompletionModel;
2015    use serde_json::json;
2016    use std::collections::HashMap;
2017
2018    fn test_document(id: &str, text: &str) -> crate::completion::Document {
2019        crate::completion::Document {
2020            id: id.to_string(),
2021            text: text.to_string(),
2022            additional_props: HashMap::new(),
2023        }
2024    }
2025
2026    fn response_with_service_tier(service_tier: &str) -> Value {
2027        json!({
2028            "id": "resp_123",
2029            "object": "response",
2030            "created_at": 0,
2031            "status": "completed",
2032            "model": "gpt-5.4",
2033            "output": [],
2034            "service_tier": service_tier,
2035        })
2036    }
2037
2038    #[test]
2039    fn completion_response_deserializes_standard_service_tier() {
2040        let response: CompletionResponse =
2041            serde_json::from_value(response_with_service_tier("standard"))
2042                .expect("response should deserialize");
2043
2044        assert!(matches!(
2045            response.additional_parameters.service_tier,
2046            Some(OpenAIServiceTier::Standard)
2047        ));
2048    }
2049
2050    #[test]
2051    fn completion_response_deserializes_priority_service_tier() {
2052        let response: CompletionResponse =
2053            serde_json::from_value(response_with_service_tier("priority"))
2054                .expect("response should deserialize");
2055
2056        assert!(matches!(
2057            response.additional_parameters.service_tier,
2058            Some(OpenAIServiceTier::Priority)
2059        ));
2060    }
2061
2062    #[test]
2063    fn completion_response_preserves_unknown_service_tier() {
2064        let response: CompletionResponse =
2065            serde_json::from_value(response_with_service_tier("provider_experimental"))
2066                .expect("response should deserialize");
2067
2068        let Some(OpenAIServiceTier::Other(service_tier)) =
2069            response.additional_parameters.service_tier
2070        else {
2071            panic!("expected provider-specific service tier");
2072        };
2073
2074        assert_eq!(service_tier, "provider_experimental");
2075    }
2076
2077    #[test]
2078    fn responses_request_keeps_documents_after_system_messages() {
2079        let request = CompletionRequestBuilder::new(MockCompletionModel::default(), "Prompt")
2080            .message(completion::Message::system("System prompt"))
2081            .message(completion::Message::user("Earlier user turn"))
2082            .message(completion::Message::assistant("Earlier assistant turn"))
2083            .document(test_document("doc1", "Document text."))
2084            .build();
2085
2086        let responses_request = CompletionRequest::try_from(("gpt-4o-mini".to_string(), request))
2087            .expect("request conversion should succeed");
2088
2089        let serialized =
2090            serde_json::to_value(&responses_request.input).expect("input should serialize");
2091        let input = serialized.as_array().expect("input should be an array");
2092
2093        assert_eq!(input.len(), 5);
2094        assert_eq!(input[0]["role"], "system");
2095        assert_eq!(input[1]["role"], "user");
2096        assert!(
2097            input[1].to_string().contains("<file id: doc1>"),
2098            "document input should follow system input: {input:?}"
2099        );
2100        assert_eq!(input[2]["role"], "user");
2101        assert!(
2102            input[2].to_string().contains("Earlier user turn"),
2103            "prior user history should follow document input: {input:?}"
2104        );
2105        assert_eq!(input[3]["role"], "assistant");
2106        assert!(
2107            input[3].to_string().contains("Earlier assistant turn"),
2108            "prior assistant history should follow prior user history: {input:?}"
2109        );
2110        assert_eq!(input[4]["role"], "user");
2111        assert!(
2112            input[4].to_string().contains("Prompt"),
2113            "prompt should remain last: {input:?}"
2114        );
2115    }
2116
2117    #[test]
2118    fn responses_direct_request_keeps_documents_after_system_messages() {
2119        let request = crate::completion::CompletionRequest {
2120            model: None,
2121            preamble: None,
2122            chat_history: crate::OneOrMany::many(vec![
2123                completion::Message::system("System prompt"),
2124                completion::Message::assistant("Earlier assistant turn"),
2125                completion::Message::system("Mid-conversation instruction"),
2126                completion::Message::user("Prompt"),
2127            ])
2128            .unwrap(),
2129            documents: vec![test_document("doc1", "Document text.")],
2130            tools: vec![],
2131            temperature: None,
2132            max_tokens: None,
2133            tool_choice: None,
2134            additional_params: None,
2135            output_schema: None,
2136        };
2137
2138        let responses_request = CompletionRequest::try_from(("gpt-4o-mini".to_string(), request))
2139            .expect("request conversion should succeed");
2140
2141        let serialized =
2142            serde_json::to_value(&responses_request.input).expect("input should serialize");
2143        let input = serialized.as_array().expect("input should be an array");
2144
2145        assert_eq!(input.len(), 5);
2146        assert_eq!(input[0]["role"], "system");
2147        assert_eq!(input[1]["role"], "user");
2148        assert!(
2149            input[1].to_string().contains("<file id: doc1>"),
2150            "document input should follow leading system input: {input:?}"
2151        );
2152        assert_eq!(input[2]["role"], "assistant");
2153        assert_eq!(input[3]["role"], "system");
2154        assert_eq!(input[4]["role"], "user");
2155        assert_eq!(
2156            input
2157                .iter()
2158                .filter(|message| message.to_string().contains("<file id: doc1>"))
2159                .count(),
2160            1,
2161            "document input should appear exactly once: {input:?}"
2162        );
2163    }
2164
2165    #[test]
2166    fn service_tier_serializes_expected_strings() {
2167        let cases = [
2168            (OpenAIServiceTier::Auto, "auto"),
2169            (OpenAIServiceTier::Default, "default"),
2170            (OpenAIServiceTier::Flex, "flex"),
2171            (OpenAIServiceTier::Priority, "priority"),
2172            (OpenAIServiceTier::Standard, "standard"),
2173        ];
2174
2175        for (service_tier, expected) in cases {
2176            assert_eq!(
2177                serde_json::to_value(service_tier).expect("service tier should serialize"),
2178                json!(expected)
2179            );
2180        }
2181
2182        assert_eq!(
2183            serde_json::to_value(OpenAIServiceTier::Other(
2184                "provider_experimental".to_string()
2185            ))
2186            .expect("provider-specific service tier should serialize"),
2187            json!("provider_experimental")
2188        );
2189    }
2190
2191    #[test]
2192    fn responses_usage_token_usage_preserves_reasoning_tokens() {
2193        let usage = ResponsesUsage {
2194            input_tokens: 100,
2195            input_tokens_details: Some(InputTokensDetails { cached_tokens: 25 }),
2196            output_tokens: 50,
2197            output_tokens_details: Some(OutputTokensDetails {
2198                reasoning_tokens: 15,
2199            }),
2200            total_tokens: 150,
2201        };
2202
2203        let token_usage = usage.token_usage();
2204
2205        assert_eq!(token_usage.input_tokens, 100);
2206        assert_eq!(token_usage.cached_input_tokens, 25);
2207        assert_eq!(token_usage.output_tokens, 50);
2208        assert_eq!(token_usage.reasoning_tokens, 15);
2209        assert_eq!(token_usage.total_tokens, 150);
2210    }
2211
2212    #[test]
2213    fn responses_usage_deserializes_without_output_token_details() {
2214        let usage: ResponsesUsage = serde_json::from_value(json!({
2215            "input_tokens": 100,
2216            "input_tokens_details": {
2217                "cached_tokens": 25
2218            },
2219            "output_tokens": 50,
2220            "total_tokens": 150
2221        }))
2222        .expect("usage should deserialize when output token details are omitted");
2223
2224        assert!(usage.output_tokens_details.is_none());
2225
2226        let token_usage = usage.token_usage();
2227
2228        assert_eq!(token_usage.input_tokens, 100);
2229        assert_eq!(token_usage.cached_input_tokens, 25);
2230        assert_eq!(token_usage.output_tokens, 50);
2231        assert_eq!(token_usage.reasoning_tokens, 0);
2232        assert_eq!(token_usage.total_tokens, 150);
2233    }
2234
2235    #[test]
2236    fn completion_response_accepts_top_level_reasoning_string() {
2237        let response: CompletionResponse = serde_json::from_value(json!({
2238            "id": "resp_123",
2239            "object": "response",
2240            "created_at": 0,
2241            "status": "completed",
2242            "model": "Qwen/Qwen3-4B",
2243            "reasoning": "thinking through the answer",
2244            "usage": {
2245                "input_tokens": 1,
2246                "output_tokens": 2,
2247                "total_tokens": 3
2248            },
2249            "output": [{
2250                "type": "message",
2251                "id": "msg_123",
2252                "status": "completed",
2253                "role": "assistant",
2254                "content": [{
2255                    "type": "output_text",
2256                    "annotations": [],
2257                    "text": "done"
2258                }]
2259            }],
2260            "tools": []
2261        }))
2262        .expect("mistral.rs-style reasoning string should deserialize");
2263
2264        assert_eq!(
2265            response.provider_reasoning.as_deref(),
2266            Some("thinking through the answer")
2267        );
2268
2269        let completion: completion::CompletionResponse<CompletionResponse> =
2270            response.try_into().expect("response should convert");
2271        let items = completion.choice.iter().collect::<Vec<_>>();
2272        assert!(matches!(
2273            items[0],
2274            completion::AssistantContent::Reasoning(_)
2275        ));
2276        assert!(matches!(items[1], completion::AssistantContent::Text(_)));
2277    }
2278
2279    #[test]
2280    fn completion_response_accepts_reasoning_only_response() {
2281        let response: CompletionResponse = serde_json::from_value(json!({
2282            "id": "resp_123",
2283            "object": "response",
2284            "created_at": 0,
2285            "status": "completed",
2286            "model": "Qwen/Qwen3-4B",
2287            "reasoning": "thinking only",
2288            "usage": {
2289                "input_tokens": 1,
2290                "output_tokens": 2,
2291                "total_tokens": 3
2292            },
2293            "output": [],
2294            "tools": []
2295        }))
2296        .expect("reasoning-only response should deserialize");
2297
2298        let completion: completion::CompletionResponse<CompletionResponse> = response
2299            .try_into()
2300            .expect("reasoning-only response should convert");
2301        let items = completion.choice.iter().collect::<Vec<_>>();
2302
2303        assert_eq!(items.len(), 1);
2304        assert!(matches!(
2305            items[0],
2306            completion::AssistantContent::Reasoning(_)
2307        ));
2308    }
2309
2310    #[test]
2311    fn completion_response_rejects_empty_response_without_reasoning() {
2312        let response: CompletionResponse = serde_json::from_value(json!({
2313            "id": "resp_123",
2314            "object": "response",
2315            "created_at": 0,
2316            "status": "completed",
2317            "model": "Qwen/Qwen3-4B",
2318            "output": [],
2319            "tools": []
2320        }))
2321        .expect("empty response shape should deserialize");
2322
2323        let err = completion::CompletionResponse::<CompletionResponse>::try_from(response)
2324            .expect_err("empty response without reasoning should be rejected");
2325
2326        assert!(
2327            err.to_string()
2328                .contains("Response contained no message or tool call")
2329        );
2330    }
2331
2332    #[test]
2333    fn completion_response_ignores_top_level_reasoning_object_as_text() {
2334        let response: CompletionResponse = serde_json::from_value(json!({
2335            "id": "resp_123",
2336            "object": "response",
2337            "created_at": 0,
2338            "status": "completed",
2339            "model": "Qwen/Qwen3-4B",
2340            "reasoning": {
2341                "effort": "high"
2342            },
2343            "output": [{
2344                "type": "message",
2345                "id": "msg_123",
2346                "status": "completed",
2347                "role": "assistant",
2348                "content": [{
2349                    "type": "output_text",
2350                    "annotations": [],
2351                    "text": "done"
2352                }]
2353            }],
2354            "tools": []
2355        }))
2356        .expect("object-shaped reasoning should be tolerated");
2357
2358        assert!(response.provider_reasoning.is_none());
2359
2360        let completion: completion::CompletionResponse<CompletionResponse> =
2361            response.try_into().expect("response should convert");
2362        let items = completion.choice.iter().collect::<Vec<_>>();
2363        assert_eq!(items.len(), 1);
2364        assert!(matches!(items[0], completion::AssistantContent::Text(_)));
2365    }
2366
2367    #[test]
2368    fn completion_response_does_not_duplicate_structured_reasoning() {
2369        let response: CompletionResponse = serde_json::from_value(json!({
2370            "id": "resp_123",
2371            "object": "response",
2372            "created_at": 0,
2373            "status": "completed",
2374            "model": "gpt-5.4",
2375            "reasoning": "provider top-level text",
2376            "output": [{
2377                "type": "reasoning",
2378                "id": "rs_123",
2379                "summary": [{
2380                    "type": "summary_text",
2381                    "text": "structured summary"
2382                }]
2383            }, {
2384                "type": "message",
2385                "id": "msg_123",
2386                "status": "completed",
2387                "role": "assistant",
2388                "content": [{
2389                    "type": "output_text",
2390                    "annotations": [],
2391                    "text": "done"
2392                }]
2393            }],
2394            "tools": []
2395        }))
2396        .expect("response should deserialize");
2397
2398        let completion: completion::CompletionResponse<CompletionResponse> =
2399            response.try_into().expect("response should convert");
2400        let reasoning_count = completion
2401            .choice
2402            .iter()
2403            .filter(|item| matches!(item, completion::AssistantContent::Reasoning(_)))
2404            .count();
2405
2406        assert_eq!(reasoning_count, 1);
2407    }
2408
2409    #[test]
2410    fn idless_reasoning_is_skipped_when_converting_responses_history() {
2411        let assistant = message::Message::Assistant {
2412            id: Some("msg_123".to_string()),
2413            content: OneOrMany::one(message::AssistantContent::Reasoning(
2414                message::Reasoning::new("provider reasoning"),
2415            )),
2416        };
2417
2418        let converted = Vec::<Message>::try_from(assistant)
2419            .expect("idless reasoning should degrade gracefully");
2420
2421        assert!(converted.is_empty());
2422    }
2423
2424    #[test]
2425    fn idless_reasoning_only_is_skipped_without_empty_input_item() {
2426        let assistant = completion::Message::Assistant {
2427            id: None,
2428            content: OneOrMany::one(message::AssistantContent::Reasoning(
2429                message::Reasoning::new("provider reasoning"),
2430            )),
2431        };
2432
2433        let converted = Vec::<InputItem>::try_from(assistant)
2434            .expect("idless reasoning should degrade gracefully");
2435
2436        assert!(converted.is_empty());
2437    }
2438
2439    #[test]
2440    fn idless_reasoning_plus_text_preserves_text_for_responses_history() {
2441        let assistant = message::Message::Assistant {
2442            id: Some("msg_123".to_string()),
2443            content: OneOrMany::many(vec![
2444                message::AssistantContent::Reasoning(message::Reasoning::new("provider reasoning")),
2445                message::AssistantContent::Text(Text::new("final answer")),
2446            ])
2447            .expect("assistant content should be non-empty"),
2448        };
2449
2450        let converted =
2451            Vec::<Message>::try_from(assistant).expect("assistant history should convert");
2452
2453        assert_eq!(converted.len(), 1);
2454        let Message::Assistant { content, .. } = &converted[0] else {
2455            panic!("expected assistant message");
2456        };
2457        assert!(matches!(
2458            content.first_ref(),
2459            AssistantContentType::Text(AssistantContent::OutputText(Text { text, .. })) if text == "final answer"
2460        ));
2461    }
2462
2463    #[test]
2464    fn completion_history_idless_reasoning_plus_text_preserves_text_input_item() {
2465        let assistant = completion::Message::Assistant {
2466            id: Some("msg_123".to_string()),
2467            content: OneOrMany::many(vec![
2468                message::AssistantContent::Reasoning(message::Reasoning::new("provider reasoning")),
2469                message::AssistantContent::Text(Text::new("final answer")),
2470            ])
2471            .expect("assistant content should be non-empty"),
2472        };
2473
2474        let converted =
2475            Vec::<InputItem>::try_from(assistant).expect("assistant history should convert");
2476
2477        assert_eq!(converted.len(), 1);
2478        assert!(matches!(converted[0].role, Some(Role::Assistant)));
2479        let InputContent::Message(Message::Assistant { content, .. }) = &converted[0].input else {
2480            panic!("expected assistant message input item");
2481        };
2482        assert!(matches!(
2483            content.first_ref(),
2484            AssistantContentType::Text(AssistantContent::OutputText(Text { text, .. })) if text == "final answer"
2485        ));
2486    }
2487
2488    #[test]
2489    fn assistant_text_without_idless_reasoning_replays_as_output_text() {
2490        let assistant = completion::Message::Assistant {
2491            id: Some("msg_123".to_string()),
2492            content: OneOrMany::one(message::AssistantContent::Text(Text::new("final answer"))),
2493        };
2494
2495        let converted =
2496            Vec::<InputItem>::try_from(assistant).expect("assistant history should convert");
2497
2498        assert_eq!(converted.len(), 1);
2499        let InputContent::Message(Message::Assistant { content, .. }) = &converted[0].input else {
2500            panic!("expected assistant message input item");
2501        };
2502        assert!(matches!(
2503            content.first_ref(),
2504            AssistantContentType::Text(AssistantContent::OutputText(Text { text, .. })) if text == "final answer"
2505        ));
2506    }
2507
2508    #[test]
2509    fn idless_completion_assistant_text_replays_as_easy_input_message() {
2510        let assistant = completion::Message::Assistant {
2511            id: None,
2512            content: OneOrMany::one(message::AssistantContent::Text(Text::new("final answer"))),
2513        };
2514
2515        let converted =
2516            Vec::<InputItem>::try_from(assistant).expect("assistant history should convert");
2517
2518        assert_eq!(converted.len(), 1);
2519        assert!(matches!(converted[0].role, Some(Role::Assistant)));
2520        let InputContent::Message(Message::AssistantInput { content, .. }) = &converted[0].input
2521        else {
2522            panic!("expected assistant input message item");
2523        };
2524        assert_eq!(content, "final answer");
2525
2526        let serialized =
2527            serde_json::to_value(&converted[0]).expect("input item should serialize to JSON");
2528        assert_eq!(serialized["type"], json!("message"));
2529        assert_eq!(serialized["role"], json!("assistant"));
2530        assert_eq!(serialized["content"], json!("final answer"));
2531        assert!(serialized.get("id").is_none());
2532        assert!(serialized.get("status").is_none());
2533    }
2534
2535    #[test]
2536    fn idless_message_assistant_text_replays_as_easy_input_message() {
2537        let assistant = message::Message::Assistant {
2538            id: None,
2539            content: OneOrMany::one(message::AssistantContent::Text(Text::new("final answer"))),
2540        };
2541
2542        let converted =
2543            Vec::<Message>::try_from(assistant).expect("assistant history should convert");
2544
2545        assert_eq!(converted.len(), 1);
2546        let Message::AssistantInput { content, .. } = &converted[0] else {
2547            panic!("expected assistant input message");
2548        };
2549        assert_eq!(content, "final answer");
2550
2551        let serialized = serde_json::to_value(&converted[0])
2552            .expect("assistant message should serialize to JSON");
2553        assert_eq!(serialized["role"], json!("assistant"));
2554        assert_eq!(serialized["content"], json!("final answer"));
2555        assert!(serialized.get("id").is_none());
2556        assert!(serialized.get("status").is_none());
2557    }
2558
2559    #[test]
2560    fn structured_reasoning_with_id_still_converts_for_responses_history() {
2561        let assistant = message::Message::Assistant {
2562            id: Some("msg_123".to_string()),
2563            content: OneOrMany::one(message::AssistantContent::Reasoning(message::Reasoning {
2564                id: Some("rs_123".to_string()),
2565                content: vec![message::ReasoningContent::Summary(
2566                    "structured summary".to_string(),
2567                )],
2568            })),
2569        };
2570
2571        let converted =
2572            Vec::<Message>::try_from(assistant).expect("structured reasoning should still convert");
2573
2574        assert_eq!(converted.len(), 1);
2575        let Message::Assistant { content, .. } = &converted[0] else {
2576            panic!("expected assistant message");
2577        };
2578        assert!(matches!(
2579            content.first_ref(),
2580            AssistantContentType::Reasoning(OpenAIReasoning { id, .. }) if id == "rs_123"
2581        ));
2582    }
2583
2584    #[test]
2585    fn structured_reasoning_with_id_still_converts_to_input_item() {
2586        let assistant = completion::Message::Assistant {
2587            id: Some("msg_123".to_string()),
2588            content: OneOrMany::one(message::AssistantContent::Reasoning(message::Reasoning {
2589                id: Some("rs_123".to_string()),
2590                content: vec![message::ReasoningContent::Summary(
2591                    "structured summary".to_string(),
2592                )],
2593            })),
2594        };
2595
2596        let converted =
2597            Vec::<InputItem>::try_from(assistant).expect("structured reasoning should convert");
2598
2599        assert_eq!(converted.len(), 1);
2600        assert!(converted[0].role.is_none());
2601        assert!(matches!(
2602            &converted[0].input,
2603            InputContent::Reasoning(OpenAIReasoning { id, .. }) if id == "rs_123"
2604        ));
2605    }
2606
2607    #[test]
2608    fn assistant_reasoning_text_tool_call_convert_in_responses_replay_order() {
2609        let assistant = completion::Message::Assistant {
2610            id: Some("msg_123".to_string()),
2611            content: OneOrMany::many(vec![
2612                message::AssistantContent::Reasoning(message::Reasoning {
2613                    id: Some("rs_123".to_string()),
2614                    content: vec![message::ReasoningContent::Summary(
2615                        "structured summary".to_string(),
2616                    )],
2617                }),
2618                message::AssistantContent::Text(Text::new("final answer")),
2619                message::AssistantContent::tool_call_with_call_id(
2620                    "fc_123",
2621                    "call_123".to_string(),
2622                    "lookup",
2623                    json!({"query": "rig"}),
2624                ),
2625            ])
2626            .expect("assistant content should be non-empty"),
2627        };
2628
2629        let converted =
2630            Vec::<InputItem>::try_from(assistant).expect("assistant history should convert");
2631
2632        assert_eq!(converted.len(), 3);
2633        assert!(converted[0].role.is_none());
2634        assert!(matches!(
2635            &converted[0].input,
2636            InputContent::Reasoning(OpenAIReasoning { id, .. }) if id == "rs_123"
2637        ));
2638
2639        assert!(matches!(converted[1].role, Some(Role::Assistant)));
2640        let InputContent::Message(Message::Assistant { content, id, .. }) = &converted[1].input
2641        else {
2642            panic!("expected assistant output message");
2643        };
2644        assert_eq!(id, "msg_123");
2645        assert!(matches!(
2646            content.first_ref(),
2647            AssistantContentType::Text(AssistantContent::OutputText(Text { text, .. }))
2648                if text == "final answer"
2649        ));
2650
2651        assert!(converted[2].role.is_none());
2652        let InputContent::FunctionCall(OutputFunctionCall {
2653            id, call_id, name, ..
2654        }) = &converted[2].input
2655        else {
2656            panic!("expected function call input item");
2657        };
2658        assert_eq!(id, "fc_123");
2659        assert_eq!(call_id, "call_123");
2660        assert_eq!(name, "lookup");
2661    }
2662
2663    #[test]
2664    fn mocked_second_turn_request_omits_unreplayable_reasoning() {
2665        let request = crate::completion::CompletionRequest {
2666            model: None,
2667            preamble: Some("You are concise.".to_string()),
2668            chat_history: OneOrMany::many(vec![
2669                completion::Message::User {
2670                    content: OneOrMany::one(message::UserContent::Text(Text::new(
2671                        "Think briefly, then answer.",
2672                    ))),
2673                },
2674                completion::Message::Assistant {
2675                    id: Some("msg_123".to_string()),
2676                    content: OneOrMany::many(vec![
2677                        message::AssistantContent::Reasoning(message::Reasoning::new(
2678                            "provider reasoning",
2679                        )),
2680                        message::AssistantContent::Text(Text::new("final answer")),
2681                    ])
2682                    .expect("assistant content should be non-empty"),
2683                },
2684                completion::Message::Assistant {
2685                    id: None,
2686                    content: OneOrMany::many(vec![
2687                        message::AssistantContent::Reasoning(message::Reasoning::new(
2688                            "provider reasoning only",
2689                        )),
2690                        message::AssistantContent::Text(Text::new("")),
2691                    ])
2692                    .expect("assistant content should be non-empty"),
2693                },
2694                completion::Message::User {
2695                    content: OneOrMany::one(message::UserContent::Text(Text::new(
2696                        "/no_think Reply with exactly: OK",
2697                    ))),
2698                },
2699            ])
2700            .expect("history should be non-empty"),
2701            documents: Vec::new(),
2702            tools: Vec::new(),
2703            temperature: None,
2704            max_tokens: Some(64),
2705            tool_choice: None,
2706            additional_params: None,
2707            output_schema: None,
2708        };
2709
2710        let request = CompletionRequest::try_from(("Qwen/Qwen3-4B".to_string(), request))
2711            .expect("request should convert");
2712        let value = serde_json::to_value(&request).expect("request should serialize");
2713        let input = value["input"]
2714            .as_array()
2715            .expect("mocked multi-turn request should serialize input as an array");
2716
2717        assert!(!input.iter().any(|item| {
2718            item.get("type") == Some(&json!("reasoning")) && item.get("id").is_none()
2719        }));
2720        assert!(!input.iter().any(|item| {
2721            item.get("role") == Some(&json!("assistant"))
2722                && item
2723                    .get("content")
2724                    .and_then(Value::as_array)
2725                    .is_some_and(Vec::is_empty)
2726        }));
2727
2728        let assistant_items = input
2729            .iter()
2730            .filter(|item| item.get("role") == Some(&json!("assistant")))
2731            .collect::<Vec<_>>();
2732
2733        assert_eq!(assistant_items.len(), 1);
2734        assert_eq!(assistant_items[0]["content"][0]["type"], "output_text");
2735        assert_eq!(assistant_items[0]["content"][0]["text"], "final answer");
2736    }
2737
2738    #[test]
2739    fn responses_usage_add_preserves_rhs_details_when_lhs_details_are_absent() {
2740        let lhs = ResponsesUsage {
2741            input_tokens: 10,
2742            input_tokens_details: None,
2743            output_tokens: 20,
2744            output_tokens_details: None,
2745            total_tokens: 30,
2746        };
2747        let rhs = ResponsesUsage {
2748            input_tokens: 3,
2749            input_tokens_details: Some(InputTokensDetails { cached_tokens: 2 }),
2750            output_tokens: 5,
2751            output_tokens_details: Some(OutputTokensDetails {
2752                reasoning_tokens: 4,
2753            }),
2754            total_tokens: 8,
2755        };
2756
2757        let usage = lhs + rhs;
2758        let token_usage = usage.token_usage();
2759
2760        assert_eq!(token_usage.input_tokens, 13);
2761        assert_eq!(token_usage.cached_input_tokens, 2);
2762        assert_eq!(token_usage.output_tokens, 25);
2763        assert_eq!(token_usage.reasoning_tokens, 4);
2764        assert_eq!(token_usage.total_tokens, 38);
2765    }
2766
2767    #[test]
2768    fn file_id_document_serializes_as_input_file_content() {
2769        let message = message::Message::User {
2770            content: OneOrMany::one(message::UserContent::Document(message::Document {
2771                data: DocumentSourceKind::FileId("file_abc".to_string()),
2772                media_type: None,
2773                additional_params: None,
2774            })),
2775        };
2776
2777        let converted: Vec<Message> = message.try_into().expect("conversion should succeed");
2778        let Message::User { content, .. } = &converted[0] else {
2779            panic!("expected user message");
2780        };
2781
2782        let json = serde_json::to_value(content.first_ref()).expect("serialize content");
2783
2784        assert_eq!(json["type"], "input_file");
2785        assert_eq!(json["file_id"], "file_abc");
2786        assert!(json.get("file_data").is_none());
2787        assert!(json.get("file_url").is_none());
2788    }
2789
2790    #[test]
2791    fn file_id_document_serializes_as_input_item_content() {
2792        let message = completion::Message::User {
2793            content: OneOrMany::one(message::UserContent::Document(message::Document {
2794                data: DocumentSourceKind::FileId("file_abc".to_string()),
2795                media_type: None,
2796                additional_params: None,
2797            })),
2798        };
2799
2800        let converted: Vec<InputItem> = message.try_into().expect("conversion should succeed");
2801        let json = serde_json::to_value(&converted[0]).expect("serialize input item");
2802
2803        assert_eq!(json["type"], "message");
2804        assert_eq!(json["role"], "user");
2805        assert_eq!(json["content"][0]["type"], "input_file");
2806        assert_eq!(json["content"][0]["file_id"], "file_abc");
2807        assert!(json["content"][0].get("file_data").is_none());
2808        assert!(json["content"][0].get("file_url").is_none());
2809    }
2810}