Skip to main content

rig_core/providers/openai/responses_api/
mod.rs

1//! Request and response types for the OpenAI Responses API.
2//! Endpoint and dialect configuration lives in [`wire`].
3//!
4//! ```no_run
5//! use rig_core::providers::openai::{self, OpenAI};
6//!
7//! # fn example() -> Result<(), Box<dyn std::error::Error>> {
8//! let model = OpenAI::from_env()?.responses(openai::GPT_5_2);
9//! # let _ = model;
10//! # Ok(())
11//! # }
12//! ```
13
14use crate::error::EncodeError;
15use crate::json_utils;
16use crate::json_utils::string_or_vec;
17use crate::message::{
18    Document, DocumentMediaType, DocumentSourceKind, ImageDetail, MessageError, MimeType, Text,
19};
20use crate::{completion, message};
21use serde::{Deserialize, Deserializer, Serialize, Serializer};
22use serde_json::{Map, Value};
23
24use std::convert::Infallible;
25use std::ops::Add;
26use std::str::FromStr;
27
28pub mod streaming;
29#[cfg(feature = "websocket")]
30#[cfg_attr(docsrs, doc(cfg(feature = "websocket")))]
31pub mod websocket;
32pub mod wire;
33
34/// The completion request type for OpenAI's Response API: <https://platform.openai.com/docs/api-reference/responses/create>
35/// Intended to be derived from [`crate::completion::request::CompletionRequest`].
36#[derive(Debug, Deserialize, Serialize, Clone)]
37pub struct CompletionRequest {
38    /// Message inputs
39    pub input: Vec<InputItem>,
40    /// The model name
41    pub model: String,
42    /// Top-level system instructions.
43    #[serde(skip_serializing_if = "Option::is_none")]
44    pub instructions: Option<String>,
45    /// The maximum number of output tokens.
46    #[serde(skip_serializing_if = "Option::is_none")]
47    pub max_output_tokens: Option<u64>,
48    /// Toggle to true for streaming responses.
49    #[serde(skip_serializing_if = "Option::is_none")]
50    pub stream: Option<bool>,
51    /// Sampling temperature. Supported values depend on the model.
52    #[serde(skip_serializing_if = "Option::is_none")]
53    pub temperature: Option<f64>,
54    /// Whether the LLM should be forced to use a tool before returning a response.
55    /// If none provided, the default option is "auto".
56    #[serde(skip_serializing_if = "Option::is_none")]
57    tool_choice: Option<ToolChoice>,
58    /// The tools you want to use. This supports both function tools and hosted tools
59    /// such as `web_search`, `file_search`, and `computer_use`.
60    #[serde(skip_serializing_if = "Vec::is_empty")]
61    pub tools: Vec<ResponsesToolDefinition>,
62    /// Additional parameters
63    #[serde(flatten)]
64    pub additional_parameters: AdditionalParameters,
65}
66
67impl CompletionRequest {
68    /// Appends a function or provider-hosted tool to the request.
69    pub fn with_tool(mut self, tool: impl Into<ResponsesToolDefinition>) -> Self {
70        self.tools.push(tool.into());
71        self
72    }
73
74    /// Appends function or provider-hosted tools in iteration order.
75    pub fn with_tools<I, Tool>(mut self, tools: I) -> Self
76    where
77        I: IntoIterator<Item = Tool>,
78        Tool: Into<ResponsesToolDefinition>,
79    {
80        self.tools.extend(tools.into_iter().map(Into::into));
81        self
82    }
83}
84
85/// An input item for [`CompletionRequest`].
86#[derive(Debug, Deserialize, Clone)]
87pub struct InputItem {
88    /// The role of an input item/message.
89    /// Input messages should be Some(Role::User), and output messages should be Some(Role::Assistant).
90    /// Everything else should be None.
91    #[serde(skip_serializing_if = "Option::is_none")]
92    role: Option<Role>,
93    /// The input content itself.
94    #[serde(flatten)]
95    input: InputContent,
96}
97
98impl Serialize for InputItem {
99    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
100    where
101        S: serde::Serializer,
102    {
103        let mut value = serde_json::to_value(&self.input).map_err(serde::ser::Error::custom)?;
104        let map = value.as_object_mut().ok_or_else(|| {
105            serde::ser::Error::custom("Input content must serialize to an object")
106        })?;
107
108        if let Some(role) = &self.role
109            && !map.contains_key("role")
110        {
111            map.insert(
112                "role".to_string(),
113                serde_json::to_value(role).map_err(serde::ser::Error::custom)?,
114            );
115        }
116
117        value.serialize(serializer)
118    }
119}
120
121impl InputItem {
122    pub fn system_message(content: impl Into<String>) -> Self {
123        Self {
124            role: Some(Role::System),
125            input: InputContent::Message(Message::System {
126                content: vec![SystemContent::InputText {
127                    text: content.into(),
128                }],
129                name: None,
130            }),
131        }
132    }
133
134    /// A user-role input item carrying one content part.
135    fn user_content(content: UserContent) -> Self {
136        Self {
137            role: Some(Role::User),
138            input: InputContent::Message(Message::User {
139                content: vec![content],
140                name: None,
141            }),
142        }
143    }
144
145    pub(crate) fn system_text(&self) -> Option<String> {
146        match &self.input {
147            InputContent::Message(Message::System { content, .. }) => Some(
148                content
149                    .iter()
150                    .map(|item| match item {
151                        SystemContent::InputText { text } => text.as_str(),
152                    })
153                    .collect::<Vec<_>>()
154                    .join("\n"),
155            ),
156            _ => None,
157        }
158    }
159}
160
161/// Message roles. Used by OpenAI Responses API to determine who created a given message.
162#[derive(Debug, Deserialize, Serialize, Clone)]
163#[serde(rename_all = "lowercase")]
164pub enum Role {
165    User,
166    Assistant,
167    System,
168}
169
170/// Content carried by an [`InputItem`].
171#[derive(Debug, Deserialize, Serialize, Clone)]
172#[serde(tag = "type", rename_all = "snake_case")]
173pub enum InputContent {
174    Message(Message),
175    Reasoning(OpenAIReasoning),
176    FunctionCall(OutputFunctionCall),
177    FunctionCallOutput(ToolResult),
178    /// Opaque compaction data for replaying a compacted context. All fields
179    /// other than the separately serialized `type` tag are preserved.
180    Compaction(Map<String, Value>),
181}
182
183#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
184pub struct OpenAIReasoning {
185    id: String,
186    pub summary: Vec<ReasoningSummary>,
187    #[serde(
188        default,
189        deserialize_with = "deserialize_reasoning_text_content",
190        serialize_with = "serialize_reasoning_text_content",
191        skip_serializing_if = "Vec::is_empty"
192    )]
193    pub content: Vec<String>,
194    #[serde(skip_serializing_if = "Option::is_none")]
195    pub encrypted_content: Option<String>,
196    /// The upstream's signature over the reasoning text, which a gateway
197    /// (OpenRouter relaying Claude) returns beside it and needs back.
198    #[serde(default, skip_serializing_if = "Option::is_none")]
199    pub signature: Option<String>,
200    #[serde(skip_serializing_if = "Option::is_none")]
201    pub status: Option<ToolStatus>,
202}
203
204#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
205#[serde(tag = "type", rename_all = "snake_case")]
206pub enum ReasoningSummary {
207    SummaryText { text: String },
208}
209
210impl ReasoningSummary {
211    fn new(input: &str) -> Self {
212        Self::SummaryText {
213            text: input.to_string(),
214        }
215    }
216
217    pub fn text(&self) -> &str {
218        let ReasoningSummary::SummaryText { text } = self;
219        text
220    }
221}
222
223fn reasoning_text_content_json(content: &[String]) -> Value {
224    Value::Array(
225        content
226            .iter()
227            .map(|text| {
228                serde_json::json!({
229                    "type": "reasoning_text",
230                    "text": text,
231                })
232            })
233            .collect(),
234    )
235}
236
237fn serialize_reasoning_text_content<S>(content: &[String], serializer: S) -> Result<S::Ok, S::Error>
238where
239    S: Serializer,
240{
241    reasoning_text_content_json(content).serialize(serializer)
242}
243
244fn deserialize_reasoning_text_content<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
245where
246    D: Deserializer<'de>,
247{
248    let value = Value::deserialize(deserializer)?;
249    Ok(match value {
250        Value::Array(items) => items
251            .into_iter()
252            .filter_map(|item| match item {
253                Value::Object(mut item) => item
254                    .remove("text")
255                    .and_then(|text| text.as_str().map(ToOwned::to_owned)),
256                Value::String(text) => Some(text),
257                _ => None,
258            })
259            .collect(),
260        Value::String(text) => vec![text],
261        _ => Vec::new(),
262    })
263}
264
265/// A tool result.
266#[derive(Debug, Deserialize, Serialize, Clone)]
267pub struct ToolResult {
268    /// The call ID of a tool (this should be linked to the call ID for a tool call, otherwise an error will be received)
269    call_id: String,
270    /// The result of a tool call.
271    output: ToolResultOutput,
272    /// The status of a tool call (if used in a completion request, this should always be Completed)
273    status: ToolStatus,
274}
275
276/// Responses API function-call output, which accepts either plain text or an
277/// ordered list of rich input blocks.
278#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
279#[serde(untagged)]
280pub enum ToolResultOutput {
281    /// A plain textual function result.
282    Text(String),
283    /// Ordered rich input blocks for a multimodal function result.
284    Content(Vec<ToolResultOutputContent>),
285}
286
287/// Rich content supported by a Responses API function-call output.
288#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
289#[serde(tag = "type", rename_all = "snake_case")]
290pub enum ToolResultOutputContent {
291    /// Textual function-output content.
292    InputText {
293        /// The text presented to the model.
294        text: String,
295    },
296    /// Image function-output content.
297    InputImage {
298        /// A public URL or base64 data URL, mutually exclusive with `file_id`.
299        #[serde(skip_serializing_if = "Option::is_none")]
300        image_url: Option<String>,
301        /// An uploaded OpenAI file identifier, mutually exclusive with
302        /// `image_url`.
303        #[serde(skip_serializing_if = "Option::is_none")]
304        file_id: Option<String>,
305        /// Provider image-detail preference.
306        #[serde(default)]
307        detail: ImageDetail,
308    },
309}
310
311/// Returns a request error for an unsupported source.
312/// Callers must base64-encode raw bytes before request conversion.
313fn unsupported_document_source(source: DocumentSourceKind) -> EncodeError {
314    match source {
315        DocumentSourceKind::Raw(_) => {
316            EncodeError::request("Raw file data not supported, encode as base64 first")
317        }
318        source => EncodeError::request(format!("Unsupported document type: {source}")),
319    }
320}
321
322fn responses_tool_result_output(
323    content: Vec<message::ToolResultContent>,
324) -> Result<ToolResultOutput, MessageError> {
325    let mut rich_output = Vec::new();
326
327    for content in content {
328        match content {
329            message::ToolResultContent::Text(Text { text, .. }) => {
330                rich_output.push(ToolResultOutputContent::InputText { text });
331            }
332            message::ToolResultContent::Json { value } => {
333                rich_output.push(ToolResultOutputContent::InputText {
334                    text: value.to_string(),
335                });
336            }
337            message::ToolResultContent::Image(message::Image {
338                data,
339                media_type,
340                detail,
341                ..
342            }) => {
343                let (image_url, file_id) = match data {
344                    DocumentSourceKind::Base64(data) => {
345                        let media_type = media_type.ok_or_else(|| {
346                            MessageError::ConversionError(
347                                "A media type is required for base64 tool-result images".into(),
348                            )
349                        })?;
350                        (
351                            Some(format!(
352                                "data:{media_type};base64,{data}",
353                                media_type = media_type.to_mime_type()
354                            )),
355                            None,
356                        )
357                    }
358                    DocumentSourceKind::Url(url) => (Some(url), None),
359                    DocumentSourceKind::FileId(file_id) => (None, Some(file_id)),
360                    unsupported => {
361                        return Err(MessageError::ConversionError(format!(
362                            "Unsupported tool-result image source: {unsupported}"
363                        )));
364                    }
365                };
366                rich_output.push(ToolResultOutputContent::InputImage {
367                    image_url,
368                    file_id,
369                    detail: detail.unwrap_or_default(),
370                });
371            }
372        }
373    }
374
375    match rich_output.as_slice() {
376        [ToolResultOutputContent::InputText { text }] => Ok(ToolResultOutput::Text(text.clone())),
377
378        _ => Ok(ToolResultOutput::Content(rich_output)),
379    }
380}
381
382/// The issuer-free conversion: no reasoning opens, so none is replayed. A
383/// request's own conversion ([`ResponsesRequestParams`]) replays the
384/// reasoning its issuers open.
385impl TryFrom<crate::completion::Message> for Vec<InputItem> {
386    type Error = EncodeError;
387
388    fn try_from(value: crate::completion::Message) -> Result<Self, Self::Error> {
389        input_items(value, &[])
390    }
391}
392
393/// `value` as input items, replaying the reasoning `issuers` open.
394fn input_items(
395    value: crate::completion::Message,
396    issuers: &[crate::message::Issuer],
397) -> Result<Vec<InputItem>, EncodeError> {
398    {
399        match value {
400            crate::completion::Message::System { content } => Ok(vec![InputItem {
401                role: Some(Role::System),
402                input: InputContent::Message(Message::System {
403                    content: vec![content.into()],
404                    name: None,
405                }),
406            }]),
407            crate::completion::Message::User { content } => {
408                let mut items = Vec::new();
409
410                for user_content in content {
411                    match user_content {
412                        crate::message::UserContent::Text(Text { text, .. }) => {
413                            items.push(InputItem::user_content(UserContent::InputText { text }));
414                        }
415                        crate::message::UserContent::ToolResult(tool_result) => {
416                            // Prefer provider identity so results match replayed calls.
417                            let call_id = tool_result.call.wire().into_owned();
418                            let output = responses_tool_result_output(tool_result.content)?;
419                            items.push(InputItem {
420                                role: None,
421                                input: InputContent::FunctionCallOutput(ToolResult {
422                                    call_id,
423                                    output,
424                                    status: ToolStatus::Completed,
425                                }),
426                            });
427                        }
428                        crate::message::UserContent::Document(Document {
429                            data: DocumentSourceKind::FileId(file_id),
430                            ..
431                        }) => items.push(InputItem::user_content(UserContent::InputFile {
432                            file_id: Some(file_id),
433                            file_data: None,
434                            file_url: None,
435                            filename: None,
436                        })),
437                        crate::message::UserContent::Document(Document {
438                            data,
439                            media_type: Some(DocumentMediaType::PDF),
440                            ..
441                        }) => {
442                            let (file_data, file_url, filename) = match data {
443                                DocumentSourceKind::Base64(data) => (
444                                    Some(format!("data:application/pdf;base64,{data}")),
445                                    None,
446                                    Some("document.pdf".to_string()),
447                                ),
448                                DocumentSourceKind::Url(url) => (None, Some(url), None),
449                                source => return Err(unsupported_document_source(source)),
450                            };
451
452                            items.push(InputItem::user_content(UserContent::InputFile {
453                                file_id: None,
454                                file_data,
455                                file_url,
456                                filename,
457                            }));
458                        }
459                        // A URL whose type the caller did not name: `input_file`
460                        // fetches it and reads the type itself.
461                        crate::message::UserContent::Document(Document {
462                            data: DocumentSourceKind::Url(url),
463                            media_type: None,
464                            ..
465                        }) => items.push(InputItem::user_content(UserContent::InputFile {
466                            file_id: None,
467                            file_data: None,
468                            file_url: Some(url),
469                            filename: None,
470                        })),
471                        crate::message::UserContent::Document(Document {
472                            data:
473                                DocumentSourceKind::Base64(text) | DocumentSourceKind::String(text),
474                            ..
475                        }) => items.push(InputItem::user_content(UserContent::InputText { text })),
476                        crate::message::UserContent::Image(crate::message::Image {
477                            data,
478                            media_type,
479                            detail,
480                            ..
481                        }) => {
482                            let url = match data {
483                                DocumentSourceKind::Base64(data) => {
484                                    let media_type = media_type
485                                        .map(|media_type| media_type.to_mime_type().to_string())
486                                        .unwrap_or_default();
487                                    format!("data:{media_type};base64,{data}")
488                                }
489                                DocumentSourceKind::Url(url) => url,
490                                source => return Err(unsupported_document_source(source)),
491                            };
492                            items.push(InputItem::user_content(UserContent::InputImage {
493                                image_url: url,
494                                detail: detail.unwrap_or_default(),
495                            }));
496                        }
497                        message => {
498                            return Err(EncodeError::request(format!(
499                                "Unsupported message: {message:?}"
500                            )));
501                        }
502                    }
503                }
504
505                Ok(items)
506            }
507            crate::completion::Message::Assistant { id, content } => {
508                let mut reasoning_items = Vec::new();
509                let mut other_items: Vec<InputItem> = Vec::new();
510                // Each message item's position, by id. Text blocks of one
511                // item join it as more content parts, because duplicate
512                // input item ids are rejected; blocks of distinct items keep
513                // their own item, id and `phase`.
514                let mut message_items: Vec<(String, usize)> = Vec::new();
515
516                for assistant_content in content {
517                    match assistant_content {
518                        crate::message::AssistantContent::Text(Text {
519                            text,
520                            additional_params,
521                        }) => {
522                            let Some(message) = assistant_text_replay_message(
523                                id.as_deref(),
524                                text,
525                                additional_params,
526                            ) else {
527                                continue;
528                            };
529                            let joined = match &message {
530                                Message::Assistant { id: item_id, .. } if !item_id.is_empty() => {
531                                    message_items
532                                        .iter()
533                                        .find(|(seen, _)| seen == item_id)
534                                        .map(|(_, at)| *at)
535                                }
536                                _ => None,
537                            };
538                            match (message, joined) {
539                                (Message::Assistant { content: more, .. }, Some(at)) => {
540                                    if let Some(InputItem {
541                                        input:
542                                            InputContent::Message(Message::Assistant {
543                                                content, ..
544                                            }),
545                                        ..
546                                    }) = other_items.get_mut(at)
547                                    {
548                                        content.extend(more);
549                                    }
550                                }
551                                (message, _) => {
552                                    if let Message::Assistant { id: item_id, .. } = &message
553                                        && !item_id.is_empty()
554                                    {
555                                        message_items.push((item_id.clone(), other_items.len()));
556                                    }
557                                    other_items.push(InputItem {
558                                        role: Some(Role::Assistant),
559                                        input: InputContent::Message(message),
560                                    });
561                                }
562                            }
563                        }
564                        crate::message::AssistantContent::ToolCall(crate::message::ToolCall {
565                            id,
566                            function,
567                            ..
568                        }) => {
569                            let (call_id, item_id) = match id {
570                                crate::message::CallId::Provider(provider) => {
571                                    (provider.call_id, provider.item_id.unwrap_or_default())
572                                }
573                                local => (local.wire().into_owned(), String::new()),
574                            };
575                            other_items.push(InputItem {
576                                role: None,
577                                input: InputContent::FunctionCall(OutputFunctionCall {
578                                    arguments: function.arguments.into(),
579                                    call_id,
580                                    id: item_id,
581                                    name: function.name.into(),
582                                    status: ToolStatus::Completed,
583                                }),
584                            });
585                        }
586                        crate::message::AssistantContent::Reasoning(reasoning) => {
587                            // Reasoning another service issued is not replayed.
588                            if let Some(openai_reasoning) = reasoning
589                                .open_for(issuers)
590                                .and_then(openai_reasoning_from_core)
591                            {
592                                reasoning_items.push(InputItem {
593                                    role: None,
594                                    input: InputContent::Reasoning(openai_reasoning),
595                                });
596                            }
597                        }
598                        crate::message::AssistantContent::Image(_) => {
599                            return Err(EncodeError::request(
600                                "Assistant image content is not supported in OpenAI Responses API"
601                                    .to_string(),
602                            ));
603                        }
604                    }
605                }
606
607                let mut items = reasoning_items;
608                items.extend(other_items);
609                Ok(items)
610            }
611        }
612    }
613}
614
615/// Builds reasoning blocks in summary, text, encrypted-content order.
616/// Empty encrypted content contributes no block. A signature signs the
617/// reasoning text, so it rides on the last text block, or on an empty one
618/// when the item carried no text.
619pub(crate) fn reasoning_content_blocks(
620    summary: Vec<ReasoningSummary>,
621    content: Vec<String>,
622    encrypted_content: Option<String>,
623    signature: Option<String>,
624) -> Vec<message::ReasoningContent> {
625    let mut blocks = summary
626        .into_iter()
627        .map(|summary| match summary {
628            ReasoningSummary::SummaryText { text } => message::ReasoningContent::Summary(text),
629        })
630        .collect::<Vec<_>>();
631
632    blocks.extend(
633        content
634            .into_iter()
635            .map(|text| message::ReasoningContent::Text {
636                text,
637                signature: None,
638            }),
639    );
640    if let Some(signature) = signature {
641        match blocks.iter_mut().rev().find_map(|block| match block {
642            message::ReasoningContent::Text { signature, .. } => Some(signature),
643            _ => None,
644        }) {
645            Some(slot) => *slot = Some(signature),
646            None => blocks.push(message::ReasoningContent::Text {
647                text: String::new(),
648                signature: Some(signature),
649            }),
650        }
651    }
652
653    if let Some(encrypted_content) = encrypted_content.filter(|content| !content.is_empty()) {
654        blocks.push(message::ReasoningContent::Encrypted(encrypted_content));
655    }
656
657    blocks
658}
659
660fn openai_reasoning_from_core(reasoning: &crate::message::Reasoning) -> Option<OpenAIReasoning> {
661    // Reasoning without a provider item ID cannot be replayed.
662    let id = reasoning.id.clone()?;
663
664    let mut summary = Vec::new();
665    let mut reasoning_content = Vec::new();
666    let mut encrypted_content = None;
667    let mut text_signature = None;
668    for content in &reasoning.content {
669        match content {
670            crate::message::ReasoningContent::Text { text, signature } => {
671                // An empty block that only carries the signature adds no text.
672                if !(text.is_empty() && signature.is_some()) {
673                    reasoning_content.push(text.clone());
674                }
675                // A signature covers the reasoning before it, so the last
676                // one is the item's, matching where decoding puts it.
677                if let Some(signature) = signature {
678                    text_signature = Some(signature.clone());
679                }
680            }
681            crate::message::ReasoningContent::Summary(text) => {
682                summary.push(ReasoningSummary::new(text));
683            }
684            // OpenAI reasoning input has one opaque payload field; preserve either
685            // encrypted or redacted blocks there, preferring the first one seen.
686            crate::message::ReasoningContent::Encrypted(data)
687            | crate::message::ReasoningContent::Redacted { data } => {
688                encrypted_content.get_or_insert_with(|| data.clone());
689            }
690        }
691    }
692
693    Some(OpenAIReasoning {
694        id,
695        summary,
696        content: reasoning_content,
697        encrypted_content,
698        signature: text_signature,
699        status: None,
700    })
701}
702
703/// A function or hosted tool available to a Responses request.
704#[derive(Debug, Deserialize, Clone, PartialEq)]
705pub struct ResponsesToolDefinition {
706    /// The type of tool.
707    #[serde(rename = "type")]
708    pub kind: String,
709    /// Tool name
710    #[serde(default)]
711    pub name: String,
712    /// Parameters - this should be a JSON schema. Strict function tools must use OpenAI's supported strict schema subset.
713    #[serde(default)]
714    pub parameters: serde_json::Value,
715    /// Whether to use strict mode. Disabled by default; opt in with [`Self::with_strict`]
716    /// or [`wire::Responses::with_strict_tools`].
717    ///
718    /// Always serialized on a function tool: the Responses API treats an omitted `strict`
719    /// as "attempt strict mode", so `false` must reach the wire for non-strict tools to
720    /// actually be non-strict. Never serialized on a hosted tool, which answers the field
721    /// with a 400 (`Unknown parameter: 'tools[0].strict'`).
722    #[serde(default, deserialize_with = "json_utils::null_or_default")]
723    pub strict: bool,
724    /// Tool description.
725    #[serde(default)]
726    pub description: String,
727    /// Additional provider-specific configuration for hosted tools.
728    #[serde(flatten, default)]
729    pub config: Map<String, Value>,
730}
731
732impl Serialize for ResponsesToolDefinition {
733    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
734    where
735        S: Serializer,
736    {
737        use serde::ser::SerializeMap;
738
739        let mut map = serializer.serialize_map(None)?;
740        map.serialize_entry("type", &self.kind)?;
741        if !self.name.is_empty() {
742            map.serialize_entry("name", &self.name)?;
743        }
744        if !self.parameters.is_null() {
745            map.serialize_entry("parameters", &self.parameters)?;
746        }
747        if self.kind == "function" {
748            map.serialize_entry("strict", &self.strict)?;
749        }
750        if !self.description.is_empty() {
751            map.serialize_entry("description", &self.description)?;
752        }
753        for (key, value) in &self.config {
754            map.serialize_entry(key, value)?;
755        }
756        map.end()
757    }
758}
759
760impl ResponsesToolDefinition {
761    /// Creates a function tool definition with strict mode disabled.
762    pub fn function(
763        name: impl Into<String>,
764        description: impl Into<String>,
765        parameters: serde_json::Value,
766    ) -> Self {
767        Self {
768            kind: "function".to_string(),
769            name: name.into(),
770            parameters,
771            strict: false,
772            description: description.into(),
773            config: Map::new(),
774        }
775    }
776
777    /// Creates a strict function tool definition.
778    ///
779    /// The schema is sanitized to OpenAI's strict subset (`additionalProperties: false`
780    /// added and every property forced into `required`).
781    pub fn strict_function(
782        name: impl Into<String>,
783        description: impl Into<String>,
784        parameters: serde_json::Value,
785    ) -> Self {
786        Self::function(name, description, parameters).with_strict()
787    }
788
789    /// Enables strict mode for this function tool.
790    ///
791    /// Function schemas are sanitized to OpenAI's strict subset. Hosted tools are
792    /// returned unchanged because strict mode only applies to function tools.
793    pub fn with_strict(mut self) -> Self {
794        if self.kind == "function" {
795            super::sanitize_schema(&mut self.parameters);
796            self.strict = true;
797        }
798        self
799    }
800
801    /// Creates a hosted tool definition for an arbitrary hosted tool type.
802    pub fn hosted(kind: impl Into<String>) -> Self {
803        Self {
804            kind: kind.into(),
805            name: String::new(),
806            parameters: Value::Null,
807            strict: false,
808            description: String::new(),
809            config: Map::new(),
810        }
811    }
812
813    /// Creates a hosted `web_search` tool definition.
814    pub fn web_search() -> Self {
815        Self::hosted("web_search")
816    }
817
818    /// Creates a hosted `file_search` tool definition.
819    pub fn file_search() -> Self {
820        Self::hosted("file_search")
821    }
822
823    /// Creates a hosted `computer_use` tool definition.
824    pub fn computer_use() -> Self {
825        Self::hosted("computer_use")
826    }
827
828    /// Adds hosted-tool configuration fields.
829    pub fn with_config(mut self, key: impl Into<String>, value: Value) -> Self {
830        self.config.insert(key.into(), value);
831        self
832    }
833
834    fn normalize(self) -> Self {
835        self.with_strict()
836    }
837}
838
839impl From<completion::ToolDefinition> for ResponsesToolDefinition {
840    fn from(value: completion::ToolDefinition) -> Self {
841        let completion::ToolDefinition {
842            name,
843            parameters,
844            description,
845        } = value;
846
847        Self::function(name, description, parameters)
848    }
849}
850
851/// Automatic, disabled, required, named-function, or restricted tool selection.
852#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
853#[serde(untagged)]
854pub enum ToolChoice {
855    /// `"auto"`, `"none"`, or `"required"`. Do not use the wrapped enum's
856    /// `Function` variant; use [`ToolChoiceDefinition::Function`] instead.
857    Mode(super::completion::ToolChoice),
858    /// A typed tool-choice object (`function` or `allowed_tools`).
859    Definition(ToolChoiceDefinition),
860}
861
862/// A typed Responses API tool-choice object.
863#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
864#[serde(tag = "type", rename_all = "snake_case")]
865pub enum ToolChoiceDefinition {
866    /// Force the model to call the named function tool.
867    Function {
868        /// Name of the function tool the model must call.
869        name: String,
870    },
871    /// Restrict the model to a subset of the request's tools.
872    AllowedTools {
873        /// Whether the model may still answer without a tool call (`auto`)
874        /// or must call one of the allowed tools (`required`).
875        mode: AllowedToolsMode,
876        /// The tools the model is allowed to call.
877        tools: Vec<AllowedTool>,
878    },
879}
880
881/// Constrains how the model may use the tools listed in
882/// [`ToolChoiceDefinition::AllowedTools`].
883#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq)]
884#[serde(rename_all = "snake_case")]
885pub enum AllowedToolsMode {
886    /// The model may call one of the allowed tools or answer directly.
887    Auto,
888    /// The model must call one of the allowed tools.
889    Required,
890}
891
892/// One entry of a [`ToolChoiceDefinition::AllowedTools`] tool list.
893#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
894#[serde(tag = "type", rename_all = "snake_case")]
895pub enum AllowedTool {
896    /// A function tool referenced by name.
897    Function {
898        /// Name of the allowed function tool.
899        name: String,
900    },
901}
902
903impl TryFrom<message::ToolChoice> for ToolChoice {
904    type Error = EncodeError;
905
906    fn try_from(value: message::ToolChoice) -> Result<Self, Self::Error> {
907        let choice = match value {
908            message::ToolChoice::Auto => Self::Mode(super::completion::ToolChoice::Auto),
909            message::ToolChoice::None => Self::Mode(super::completion::ToolChoice::None),
910            message::ToolChoice::Required => Self::Mode(super::completion::ToolChoice::Required),
911            message::ToolChoice::Specific { function_names } => {
912                let mut names = function_names.into_iter();
913                let Some(first) = names.next() else {
914                    return Err(EncodeError::request(
915                        "ToolChoice::Specific requires at least one function name",
916                    ));
917                };
918
919                match names.next() {
920                    None => Self::Definition(ToolChoiceDefinition::Function { name: first }),
921                    Some(second) => {
922                        let tools = std::iter::once(first)
923                            .chain(std::iter::once(second))
924                            .chain(names)
925                            .map(|name| AllowedTool::Function { name })
926                            .collect();
927                        Self::Definition(ToolChoiceDefinition::AllowedTools {
928                            mode: AllowedToolsMode::Required,
929                            tools,
930                        })
931                    }
932                }
933            }
934        };
935
936        Ok(choice)
937    }
938}
939
940/// Response token counts and optional cached-input and reasoning breakdowns.
941#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
942pub struct ResponsesUsage {
943    /// Input tokens
944    pub input_tokens: u64,
945    /// In-depth detail on input tokens (cached tokens)
946    #[serde(skip_serializing_if = "Option::is_none")]
947    pub input_tokens_details: Option<InputTokensDetails>,
948    /// Output tokens
949    pub output_tokens: u64,
950    /// In-depth detail on output tokens (reasoning tokens)
951    #[serde(skip_serializing_if = "Option::is_none")]
952    pub output_tokens_details: Option<OutputTokensDetails>,
953    /// Total tokens used (for a given prompt)
954    pub total_tokens: u64,
955}
956
957impl From<&ResponsesUsage> for crate::completion::Usage {
958    fn from(usage: &ResponsesUsage) -> Self {
959        crate::completion::Usage {
960            input_tokens: Some(usage.input_tokens),
961            output_tokens: Some(usage.output_tokens),
962            total_tokens: Some(usage.total_tokens),
963            cached_input_tokens: usage
964                .input_tokens_details
965                .as_ref()
966                .map(|details| details.cached_tokens),
967            cache_creation_input_tokens: usage
968                .input_tokens_details
969                .as_ref()
970                .and_then(|details| details.cache_write_tokens),
971            reasoning_tokens: usage
972                .output_tokens_details
973                .as_ref()
974                .map(|details| details.reasoning_tokens),
975            ..Default::default()
976        }
977    }
978}
979
980impl From<ResponsesUsage> for crate::completion::Usage {
981    fn from(usage: ResponsesUsage) -> Self {
982        Self::from(&usage)
983    }
984}
985
986/// Adds present breakdowns, preserving a lone value or joint absence.
987fn add_optional_details<T: Add<Output = T>>(lhs: Option<T>, rhs: Option<T>) -> Option<T> {
988    match (lhs, rhs) {
989        (Some(lhs), Some(rhs)) => Some(lhs + rhs),
990        (lhs, rhs) => lhs.or(rhs),
991    }
992}
993
994impl Add for ResponsesUsage {
995    type Output = Self;
996
997    fn add(self, rhs: Self) -> Self::Output {
998        Self {
999            input_tokens: self.input_tokens + rhs.input_tokens,
1000            input_tokens_details: add_optional_details(
1001                self.input_tokens_details,
1002                rhs.input_tokens_details,
1003            ),
1004            output_tokens: self.output_tokens + rhs.output_tokens,
1005            output_tokens_details: add_optional_details(
1006                self.output_tokens_details,
1007                rhs.output_tokens_details,
1008            ),
1009            total_tokens: self.total_tokens + rhs.total_tokens,
1010        }
1011    }
1012}
1013
1014/// In-depth details on input tokens.
1015#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
1016pub struct InputTokensDetails {
1017    /// Cached tokens from OpenAI
1018    pub cached_tokens: u64,
1019    /// Input tokens written to the prompt cache, part of `input_tokens`.
1020    /// Absent when the model does not report cache writes.
1021    #[serde(default, skip_serializing_if = "Option::is_none")]
1022    pub cache_write_tokens: Option<u64>,
1023}
1024
1025impl Add for InputTokensDetails {
1026    type Output = Self;
1027    fn add(self, rhs: Self) -> Self::Output {
1028        Self {
1029            cached_tokens: self.cached_tokens + rhs.cached_tokens,
1030            cache_write_tokens: add_optional_details(
1031                self.cache_write_tokens,
1032                rhs.cache_write_tokens,
1033            ),
1034        }
1035    }
1036}
1037
1038/// In-depth details on output tokens.
1039#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
1040pub struct OutputTokensDetails {
1041    /// Reasoning tokens
1042    pub reasoning_tokens: u64,
1043}
1044
1045impl Add for OutputTokensDetails {
1046    type Output = Self;
1047    fn add(self, rhs: Self) -> Self::Output {
1048        Self {
1049            reasoning_tokens: self.reasoning_tokens + rhs.reasoning_tokens,
1050        }
1051    }
1052}
1053
1054/// Provider-reported reason for an incomplete response.
1055#[derive(Clone, Debug, Default, Serialize, Deserialize)]
1056pub struct IncompleteDetailsReason {
1057    /// The reason for an incomplete [`CompletionResponse`].
1058    pub reason: String,
1059}
1060
1061/// A response error from OpenAI's Response API.
1062#[derive(Clone, Debug, Default, Serialize, Deserialize)]
1063pub struct ResponseError {
1064    /// Error code
1065    pub code: String,
1066    /// Error message
1067    pub message: String,
1068}
1069
1070/// A response object as an enum (ensures type validation)
1071#[derive(Clone, Debug, Deserialize, Serialize)]
1072#[serde(rename_all = "snake_case")]
1073pub enum ResponseObject {
1074    Response,
1075}
1076
1077/// The response status as an enum (ensures type validation)
1078#[derive(Clone, Debug, PartialEq)]
1079pub enum ResponseStatus {
1080    InProgress,
1081    Completed,
1082    Failed,
1083    Cancelled,
1084    Queued,
1085    Incomplete,
1086    /// A provider-specific status added after this client was released.
1087    Other(String),
1088}
1089
1090/// The wire spelling of a [`ResponseStatus`].
1091///
1092/// Statuses outside the normalized finish-reason vocabulary are carried through
1093/// as [`completion::FinishReason::Other`], so they must keep OpenAI's own
1094/// spelling rather than a Rust `Debug` name.
1095fn response_status_wire_name(status: &ResponseStatus) -> &str {
1096    match status {
1097        ResponseStatus::InProgress => "in_progress",
1098        ResponseStatus::Completed => "completed",
1099        ResponseStatus::Failed => "failed",
1100        ResponseStatus::Cancelled => "cancelled",
1101        ResponseStatus::Queued => "queued",
1102        ResponseStatus::Incomplete => "incomplete",
1103        ResponseStatus::Other(status) => status,
1104    }
1105}
1106
1107impl Serialize for ResponseStatus {
1108    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1109    where
1110        S: Serializer,
1111    {
1112        serializer.serialize_str(response_status_wire_name(self))
1113    }
1114}
1115
1116impl<'de> Deserialize<'de> for ResponseStatus {
1117    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1118    where
1119        D: Deserializer<'de>,
1120    {
1121        Ok(match String::deserialize(deserializer)?.as_str() {
1122            "in_progress" => Self::InProgress,
1123            "completed" => Self::Completed,
1124            "failed" => Self::Failed,
1125            "cancelled" => Self::Cancelled,
1126            "queued" => Self::Queued,
1127            "incomplete" => Self::Incomplete,
1128            other => Self::Other(other.to_owned()),
1129        })
1130    }
1131}
1132
1133/// Maps status and incomplete details to a finish reason, preserving unknown
1134/// terminal values. In-flight statuses and empty unknown statuses return `None`.
1135/// Completed responses map to `Stop`; response assembly upgrades tool-call turns.
1136pub(crate) fn map_finish_reason(
1137    status: &ResponseStatus,
1138    incomplete_details: Option<&IncompleteDetailsReason>,
1139) -> Option<completion::FinishReason> {
1140    match status {
1141        ResponseStatus::Completed => Some(completion::FinishReason::Stop),
1142        ResponseStatus::Incomplete => Some(
1143            match incomplete_details
1144                .map(|details| details.reason.as_str())
1145                .filter(|reason| !reason.is_empty())
1146            {
1147                Some("max_output_tokens") => completion::FinishReason::Length,
1148                Some("content_filter") => completion::FinishReason::ContentFilter,
1149                Some(other) => completion::FinishReason::Other(other.to_owned()),
1150                // Incomplete without a stated reason: the status itself is all
1151                // the provider told us.
1152                None => {
1153                    completion::FinishReason::Other(response_status_wire_name(status).to_owned())
1154                }
1155            },
1156        ),
1157        ResponseStatus::Other(status) if status.is_empty() => None,
1158        ResponseStatus::Failed | ResponseStatus::Cancelled | ResponseStatus::Other(_) => Some(
1159            completion::FinishReason::Other(response_status_wire_name(status).to_owned()),
1160        ),
1161        // The turn has not terminated, so there is genuinely no reason yet.
1162        ResponseStatus::InProgress | ResponseStatus::Queued => None,
1163    }
1164}
1165
1166/// Controls where Rig system instructions are placed in an OpenAI Responses request.
1167///
1168/// Serialized because it is a field of the [`wire::Responses`] wire, which is
1169/// data a host may store.
1170#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1171#[serde(rename_all = "snake_case")]
1172pub enum SystemInstructionsPlacement {
1173    /// Send the leading run of system instructions (the preamble and any system
1174    /// messages that open the conversation) through the official top-level
1175    /// `instructions` field. Mid-conversation system messages keep their
1176    /// position in `input`.
1177    #[default]
1178    Instructions,
1179    /// Send every system message through the top-level `instructions` field,
1180    /// including mid-conversation ones.
1181    ///
1182    /// Use this for backends that reject the `system` role in `input` entirely.
1183    AllInstructions,
1184    /// Send system instructions as `system` messages in `input`.
1185    ///
1186    /// Use this only for OpenAI-compatible providers that do not support top-level
1187    /// `instructions`.
1188    InputSystemMessages,
1189}
1190
1191/// Converts a Rig request using the default system-instruction placement.
1192impl TryFrom<(String, crate::completion::CompletionRequest)> for CompletionRequest {
1193    type Error = EncodeError;
1194    fn try_from(
1195        (model, request): (String, crate::completion::CompletionRequest),
1196    ) -> Result<Self, Self::Error> {
1197        Self::try_from(ResponsesRequestParams {
1198            model,
1199            request,
1200            system_instructions_placement: SystemInstructionsPlacement::default(),
1201            issuers: Vec::new(),
1202        })
1203    }
1204}
1205
1206/// Parameters for converting a [`crate::completion::CompletionRequest`] into a
1207/// Responses API [`CompletionRequest`] with a non-default configuration.
1208pub struct ResponsesRequestParams {
1209    pub model: String,
1210    pub request: crate::completion::CompletionRequest,
1211    pub system_instructions_placement: SystemInstructionsPlacement,
1212    /// The issuers whose reasoning the request replays.
1213    pub issuers: Vec<crate::message::Issuer>,
1214}
1215
1216impl TryFrom<ResponsesRequestParams> for CompletionRequest {
1217    type Error = EncodeError;
1218
1219    fn try_from(params: ResponsesRequestParams) -> Result<Self, Self::Error> {
1220        let ResponsesRequestParams {
1221            model,
1222            request: mut req,
1223            system_instructions_placement,
1224            issuers,
1225        } = params;
1226        let chat_history = req.chat_history_with_documents();
1227        let model = req.model.clone().unwrap_or(model);
1228        let mut instruction_parts = Vec::new();
1229        let mut input = {
1230            let mut full_history: Vec<InputItem> = Vec::new();
1231            let tool_ids = crate::providers::internal::wire_ids::WireIds::new(&chat_history);
1232            for (position, history_item) in chat_history.into_iter().enumerate() {
1233                let mut items = input_items(history_item, &issuers)?;
1234                tool_ids
1235                    .apply(
1236                        position,
1237                        items.iter_mut().filter_map(|item| match &mut item.input {
1238                            InputContent::FunctionCall(call) => Some(&mut call.call_id),
1239                            InputContent::FunctionCallOutput(result) => Some(&mut result.call_id),
1240                            _ => None,
1241                        }),
1242                    )
1243                    .map_err(EncodeError::request)?;
1244                full_history.extend(items);
1245            }
1246            full_history
1247        };
1248
1249        let mut lift_system_text = |text: String| {
1250            let text = text.trim();
1251            if !text.is_empty() {
1252                instruction_parts.push(text.to_string());
1253            }
1254        };
1255        let items_before_lift = input.len();
1256        match system_instructions_placement {
1257            SystemInstructionsPlacement::Instructions => {
1258                // Lift only the leading run of system items (the preamble and any
1259                // system messages that open the conversation) into the top-level
1260                // `instructions` field. Mid-conversation system messages keep
1261                // their position in `input`, and a request made up solely of
1262                // system messages keeps them in `input` so it stays non-empty.
1263                let leading_system_texts: Vec<String> =
1264                    input.iter().map_while(InputItem::system_text).collect();
1265                if leading_system_texts.len() < input.len() {
1266                    input.drain(..leading_system_texts.len());
1267                    leading_system_texts
1268                        .into_iter()
1269                        .for_each(&mut lift_system_text);
1270                }
1271            }
1272            SystemInstructionsPlacement::AllInstructions => {
1273                // Lift every system item, wherever it appears, for backends
1274                // that reject the `system` role in `input` entirely.
1275                let mut remaining = Vec::with_capacity(input.len());
1276                for item in input {
1277                    match item.system_text() {
1278                        Some(text) => lift_system_text(text),
1279                        None => remaining.push(item),
1280                    }
1281                }
1282                input = remaining;
1283            }
1284            SystemInstructionsPlacement::InputSystemMessages => {}
1285        }
1286        let instructions = (!instruction_parts.is_empty()).then(|| instruction_parts.join("\n\n"));
1287        let lifted_system_items = input.len() < items_before_lift;
1288
1289        let input = crate::message::require_non_empty(input, || {
1290            EncodeError::request(if lifted_system_items {
1291                "OpenAI Responses request input must contain at least one non-system item \
1292                 (system messages were lifted into the top-level `instructions` field)"
1293            } else {
1294                "OpenAI Responses request input must contain at least one item"
1295            })
1296        })?;
1297
1298        let mut additional_params_payload = req.additional_params.take().unwrap_or(Value::Null);
1299        let stream = match &additional_params_payload {
1300            Value::Bool(stream) => Some(*stream),
1301            Value::Object(map) => map.get("stream").and_then(Value::as_bool),
1302            _ => None,
1303        };
1304
1305        let mut additional_tools = Vec::new();
1306        if let Some(additional_params_map) = additional_params_payload.as_object_mut() {
1307            if let Some(raw_tools) = additional_params_map.remove("tools") {
1308                additional_tools = serde_json::from_value::<Vec<ResponsesToolDefinition>>(
1309                    raw_tools,
1310                )
1311                .map_err(|err| {
1312                    EncodeError::request(format!(
1313                        "Invalid OpenAI Responses tools payload in additional_params: {err}"
1314                    ))
1315                })?;
1316            }
1317            additional_params_map.remove("stream");
1318        }
1319
1320        if additional_params_payload.is_boolean() {
1321            additional_params_payload = Value::Null;
1322        }
1323
1324        let mut additional_parameters = if additional_params_payload.is_null() {
1325            // If there's no additional parameters, initialise an empty object
1326            AdditionalParameters::default()
1327        } else {
1328            serde_json::from_value::<AdditionalParameters>(additional_params_payload).map_err(
1329                |err| {
1330                    EncodeError::request(format!(
1331                        "Invalid OpenAI Responses additional_params payload: {err}"
1332                    ))
1333                },
1334            )?
1335        };
1336        if additional_parameters.reasoning.is_some() {
1337            let include = additional_parameters.include.get_or_insert_with(Vec::new);
1338            if !include
1339                .iter()
1340                .any(|item| matches!(item, Include::ReasoningEncryptedContent))
1341            {
1342                include.push(Include::ReasoningEncryptedContent);
1343            }
1344        }
1345
1346        // Apply output_schema as structured output if not already configured via additional_params
1347        if additional_parameters.text.is_none()
1348            && let Some(schema) = req.output_schema
1349        {
1350            let (name, schema_value) = super::structured_output_schema(schema);
1351            additional_parameters.text = Some(TextConfig::structured_output(name, schema_value));
1352        }
1353
1354        let tool_choice = req.tool_choice.map(ToolChoice::try_from).transpose()?;
1355        let mut tools: Vec<ResponsesToolDefinition> = req
1356            .tools
1357            .into_iter()
1358            .map(ResponsesToolDefinition::from)
1359            .collect();
1360        tools.append(&mut additional_tools);
1361
1362        Ok(Self {
1363            input,
1364            model,
1365            instructions,
1366            max_output_tokens: req.max_tokens,
1367            stream,
1368            tool_choice,
1369            tools,
1370            temperature: req.temperature,
1371            additional_parameters,
1372        })
1373    }
1374}
1375
1376/// The standard response format from OpenAI's Responses API.
1377#[derive(Clone, Debug)]
1378pub struct CompletionResponse {
1379    /// The ID of a completion response.
1380    pub id: String,
1381    /// The type of the object.
1382    pub object: ResponseObject,
1383    /// The time at which a given response has been created, in seconds from the UNIX epoch (01/01/1970 00:00:00).
1384    pub created_at: u64,
1385    /// The status of the response.
1386    pub status: ResponseStatus,
1387    /// Response error (optional)
1388    pub error: Option<ResponseError>,
1389    /// Incomplete response details (optional)
1390    pub incomplete_details: Option<IncompleteDetailsReason>,
1391    /// System prompt/preamble
1392    pub instructions: Option<String>,
1393    /// The maximum number of tokens the model should output
1394    pub max_output_tokens: Option<u64>,
1395    /// The model name
1396    pub model: String,
1397    /// Provider-specific top-level reasoning content returned by some
1398    /// OpenAI-compatible Responses implementations.
1399    pub provider_reasoning: Option<String>,
1400    /// The complete object-shaped top-level reasoning metadata returned by the provider.
1401    ///
1402    /// Unknown fields, unknown values, and null-valued members inside the object
1403    /// are preserved value-equivalently. A top-level null, missing field, or
1404    /// unsupported non-object shape is normalized to no reasoning metadata.
1405    /// When serializing manually constructed responses, [`Self::provider_reasoning`]
1406    /// takes precedence over this field, and this field takes precedence over
1407    /// [`Self::reasoning_context`].
1408    pub reasoning_metadata: Option<Map<String, Value>>,
1409    /// The effective reasoning context returned by OpenAI.
1410    ///
1411    /// This is populated as a convenience projection of
1412    /// [`Self::reasoning_metadata`]. String-shaped reasoning returned by compatible
1413    /// providers remains available through [`Self::provider_reasoning`].
1414    pub reasoning_context: Option<String>,
1415    /// Token usage
1416    pub usage: Option<ResponsesUsage>,
1417    /// The model output (messages, etc will go here)
1418    pub output: Vec<Output>,
1419    /// Tools
1420    pub tools: Vec<ResponsesToolDefinition>,
1421    /// Additional parameters
1422    pub additional_parameters: AdditionalParameters,
1423}
1424
1425#[derive(Serialize)]
1426#[serde(untagged)]
1427enum CompletionResponseReasoningRef<'a> {
1428    Text(&'a str),
1429    Metadata(&'a Map<String, Value>),
1430    Context { context: &'a str },
1431}
1432
1433#[derive(Serialize)]
1434struct CompletionResponseWireRef<'a> {
1435    id: &'a str,
1436    object: &'a ResponseObject,
1437    created_at: u64,
1438    status: &'a ResponseStatus,
1439    error: &'a Option<ResponseError>,
1440    incomplete_details: &'a Option<IncompleteDetailsReason>,
1441    instructions: &'a Option<String>,
1442    max_output_tokens: &'a Option<u64>,
1443    model: &'a str,
1444    #[serde(skip_serializing_if = "Option::is_none")]
1445    reasoning: Option<CompletionResponseReasoningRef<'a>>,
1446    usage: &'a Option<ResponsesUsage>,
1447    output: &'a Vec<Output>,
1448    tools: &'a Vec<ResponsesToolDefinition>,
1449    #[serde(flatten)]
1450    additional_parameters: &'a AdditionalParameters,
1451}
1452
1453/// Response body with untyped echoed metadata. Metadata is decoded separately
1454/// so an incompatible optional field does not reject the response.
1455#[derive(Deserialize)]
1456struct CompletionResponseWire {
1457    id: String,
1458    object: ResponseObject,
1459    created_at: u64,
1460    status: ResponseStatus,
1461    error: Option<ResponseError>,
1462    incomplete_details: Option<IncompleteDetailsReason>,
1463    instructions: Option<String>,
1464    max_output_tokens: Option<u64>,
1465    model: String,
1466    #[serde(default)]
1467    reasoning: Option<Value>,
1468    usage: Option<ResponsesUsage>,
1469    #[serde(default)]
1470    output: Vec<Output>,
1471    #[serde(default)]
1472    tools: Vec<ResponsesToolDefinition>,
1473    #[serde(flatten)]
1474    metadata: Map<String, Value>,
1475}
1476
1477impl Serialize for CompletionResponse {
1478    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1479    where
1480        S: Serializer,
1481    {
1482        // Omit request reasoning configuration to avoid duplicate response keys.
1483        let mut additional_parameters = self.additional_parameters.clone();
1484        additional_parameters.reasoning = None;
1485
1486        let reasoning = self
1487            .provider_reasoning
1488            .as_deref()
1489            .map(CompletionResponseReasoningRef::Text)
1490            .or_else(|| {
1491                self.reasoning_metadata
1492                    .as_ref()
1493                    .map(CompletionResponseReasoningRef::Metadata)
1494            })
1495            .or_else(|| {
1496                self.reasoning_context
1497                    .as_deref()
1498                    .map(|context| CompletionResponseReasoningRef::Context { context })
1499            });
1500
1501        CompletionResponseWireRef {
1502            id: &self.id,
1503            object: &self.object,
1504            created_at: self.created_at,
1505            status: &self.status,
1506            error: &self.error,
1507            incomplete_details: &self.incomplete_details,
1508            instructions: &self.instructions,
1509            max_output_tokens: &self.max_output_tokens,
1510            model: &self.model,
1511            reasoning,
1512            usage: &self.usage,
1513            output: &self.output,
1514            tools: &self.tools,
1515            additional_parameters: &additional_parameters,
1516        }
1517        .serialize(serializer)
1518    }
1519}
1520
1521impl<'de> Deserialize<'de> for CompletionResponse {
1522    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1523    where
1524        D: Deserializer<'de>,
1525    {
1526        let response = CompletionResponseWire::deserialize(deserializer)?;
1527        let (provider_reasoning, reasoning_metadata) = match response.reasoning {
1528            Some(Value::String(reasoning)) => (Some(reasoning), None),
1529            Some(Value::Object(metadata)) => (None, Some(metadata)),
1530            // Unsupported reasoning shapes must not reject the response.
1531            _ => (None, None),
1532        };
1533        let reasoning_context = reasoning_metadata
1534            .as_ref()
1535            .and_then(|reasoning| reasoning.get("context"))
1536            .and_then(Value::as_str)
1537            .map(ToOwned::to_owned);
1538
1539        Ok(Self {
1540            id: response.id,
1541            object: response.object,
1542            created_at: response.created_at,
1543            status: response.status,
1544            error: response.error,
1545            incomplete_details: response.incomplete_details,
1546            instructions: response.instructions,
1547            max_output_tokens: response.max_output_tokens,
1548            model: response.model,
1549            provider_reasoning,
1550            reasoning_metadata,
1551            reasoning_context,
1552            usage: response.usage,
1553            output: response.output,
1554            tools: response.tools,
1555            additional_parameters: AdditionalParameters::from_response_metadata(response.metadata),
1556        })
1557    }
1558}
1559
1560/// Additional parameters for the completion request type for OpenAI's Response API: <https://platform.openai.com/docs/api-reference/responses/create>
1561/// Intended to be derived from [`crate::completion::request::CompletionRequest`].
1562#[derive(Clone, Debug, Deserialize, Serialize, Default)]
1563pub struct AdditionalParameters {
1564    /// Whether or not a given model task should run in the background (ie a detached process).
1565    #[serde(skip_serializing_if = "Option::is_none")]
1566    pub background: Option<bool>,
1567    /// The text response format. This is where you would add structured outputs (if you want them).
1568    #[serde(skip_serializing_if = "Option::is_none")]
1569    pub text: Option<TextConfig>,
1570    /// Additional response fields to request from the provider.
1571    #[serde(skip_serializing_if = "Option::is_none")]
1572    pub include: Option<Vec<Include>>,
1573    /// `top_p`. Mutually exclusive with the `temperature` argument.
1574    #[serde(skip_serializing_if = "Option::is_none")]
1575    pub top_p: Option<f64>,
1576    /// Whether or not the response should be truncated.
1577    #[serde(skip_serializing_if = "Option::is_none")]
1578    pub truncation: Option<TruncationStrategy>,
1579    /// The username of the user (that you want to use).
1580    #[serde(skip_serializing_if = "Option::is_none")]
1581    pub user: Option<String>,
1582    /// A stable cache routing key for prompt caching.
1583    #[serde(skip_serializing_if = "Option::is_none")]
1584    pub prompt_cache_key: Option<String>,
1585    /// Prompt cache retention policy.
1586    #[serde(skip_serializing_if = "Option::is_none")]
1587    pub prompt_cache_retention: Option<String>,
1588    /// Any additional metadata you'd like to add. This will additionally be returned by the response.
1589    #[serde(
1590        skip_serializing_if = "Map::is_empty",
1591        default,
1592        deserialize_with = "deserialize_metadata"
1593    )]
1594    pub metadata: serde_json::Map<String, serde_json::Value>,
1595    /// Whether or not you want tool calls to run in parallel.
1596    #[serde(skip_serializing_if = "Option::is_none")]
1597    pub parallel_tool_calls: Option<bool>,
1598    /// Previous response ID. If you are not sending a full conversation, this can help to track the message flow.
1599    #[serde(skip_serializing_if = "Option::is_none")]
1600    pub previous_response_id: Option<String>,
1601    /// Add thinking/reasoning to your response. The response will be emitted as a list member of the `output` field.
1602    #[serde(skip_serializing_if = "Option::is_none")]
1603    pub reasoning: Option<Reasoning>,
1604    /// The service tier you're using.
1605    #[serde(skip_serializing_if = "Option::is_none")]
1606    pub service_tier: Option<OpenAIServiceTier>,
1607    /// Whether or not to store the response for later retrieval by API.
1608    #[serde(skip_serializing_if = "Option::is_none")]
1609    pub store: Option<bool>,
1610}
1611
1612fn deserialize_metadata<'de, D>(
1613    deserializer: D,
1614) -> Result<serde_json::Map<String, serde_json::Value>, D::Error>
1615where
1616    D: Deserializer<'de>,
1617{
1618    Ok(
1619        Option::<serde_json::Map<String, serde_json::Value>>::deserialize(deserializer)?
1620            .unwrap_or_default(),
1621    )
1622}
1623
1624impl AdditionalParameters {
1625    /// Project echoed response metadata into the request-shaped parameters.
1626    ///
1627    /// Each key is decoded on its own; a key whose value does not fit its
1628    /// field is dropped rather than failing the response. A non-numeric
1629    /// `top_p` from a compatible endpoint therefore reads back as `None`.
1630    fn from_response_metadata(metadata: Map<String, Value>) -> Self {
1631        let mut accepted = Map::with_capacity(metadata.len());
1632        for (key, value) in metadata {
1633            let probe = Value::Object(Map::from_iter([(key.clone(), value.clone())]));
1634            if serde_json::from_value::<Self>(probe).is_ok() {
1635                accepted.insert(key, value);
1636            } else {
1637                tracing::debug!(
1638                    target: "rig::providers::openai",
1639                    field = %key,
1640                    "ignoring response metadata field that does not match its expected type"
1641                );
1642            }
1643        }
1644        // Every remaining key was individually accepted, so this cannot fail;
1645        // `unwrap_or_default` keeps the projection total without a panic path.
1646        serde_json::from_value(Value::Object(accepted)).unwrap_or_default()
1647    }
1648
1649    pub fn to_json(self) -> serde_json::Value {
1650        serde_json::to_value(self).unwrap_or_else(|_| serde_json::Value::Object(Map::new()))
1651    }
1652}
1653
1654/// The truncation strategy.
1655/// 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.
1656/// Otherwise, does nothing (and is disabled by default).
1657#[derive(Clone, Debug, Default, Serialize, Deserialize)]
1658#[serde(rename_all = "snake_case")]
1659pub enum TruncationStrategy {
1660    Auto,
1661    #[default]
1662    Disabled,
1663}
1664
1665/// The model output format configuration.
1666/// You can either have plain text by default, or attach a JSON schema for the purposes of structured outputs.
1667#[derive(Clone, Debug, Serialize, Deserialize)]
1668pub struct TextConfig {
1669    pub format: TextFormat,
1670}
1671
1672impl TextConfig {
1673    pub(crate) fn structured_output<S>(name: S, schema: serde_json::Value) -> Self
1674    where
1675        S: Into<String>,
1676    {
1677        Self {
1678            format: TextFormat::JsonSchema(StructuredOutputsInput {
1679                name: name.into(),
1680                schema,
1681                strict: true,
1682            }),
1683        }
1684    }
1685}
1686
1687/// The text format (contained by [`TextConfig`]).
1688/// You can either have plain text by default, or attach a JSON schema for the purposes of structured outputs.
1689#[derive(Clone, Debug, Serialize, Deserialize, Default)]
1690#[serde(tag = "type")]
1691#[serde(rename_all = "snake_case")]
1692pub enum TextFormat {
1693    JsonSchema(StructuredOutputsInput),
1694    #[default]
1695    Text,
1696}
1697
1698/// The inputs required for adding structured outputs.
1699#[derive(Clone, Debug, Serialize, Deserialize)]
1700pub struct StructuredOutputsInput {
1701    /// The name of your schema.
1702    ///
1703    /// Compatible providers may omit it when echoing a response configuration.
1704    #[serde(default)]
1705    pub name: String,
1706    /// 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>.
1707    pub schema: serde_json::Value,
1708    /// 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.
1709    #[serde(default)]
1710    pub strict: bool,
1711}
1712
1713/// Add reasoning to a [`CompletionRequest`].
1714///
1715/// # Example
1716/// ```
1717/// use rig_core::providers::openai::responses_api::{
1718///     Reasoning, ReasoningContext, ReasoningEffort, ReasoningMode,
1719/// };
1720///
1721/// // GPT-5.6 reasoning controls: effort, pro mode, and persisted-reasoning context.
1722/// let reasoning = Reasoning::new()
1723///     .with_effort(ReasoningEffort::Max)
1724///     .with_mode(ReasoningMode::Pro)
1725///     .with_context(ReasoningContext::AllTurns);
1726/// ```
1727#[derive(Clone, Debug, Default, Serialize, Deserialize)]
1728pub struct Reasoning {
1729    /// How much effort you want the model to put into thinking/reasoning.
1730    #[serde(skip_serializing_if = "Option::is_none")]
1731    pub effort: Option<ReasoningEffort>,
1732    /// How much effort you want the model to put into writing the reasoning summary.
1733    #[serde(skip_serializing_if = "Option::is_none")]
1734    pub summary: Option<ReasoningSummaryLevel>,
1735    /// The reasoning mode. Independent from `effort`; the standard mode is
1736    /// represented by omitting the field. Supported by the GPT-5.6 model family.
1737    #[serde(skip_serializing_if = "Option::is_none")]
1738    pub mode: Option<ReasoningMode>,
1739    /// How persisted reasoning is carried across turns. Supported by the
1740    /// GPT-5.6 model family.
1741    #[serde(skip_serializing_if = "Option::is_none")]
1742    pub context: Option<ReasoningContext>,
1743}
1744
1745impl Reasoning {
1746    /// Creates a new Reasoning instantiation (with empty values).
1747    pub fn new() -> Self {
1748        Self::default()
1749    }
1750
1751    /// Adds reasoning effort.
1752    pub fn with_effort(mut self, reasoning_effort: ReasoningEffort) -> Self {
1753        self.effort = Some(reasoning_effort);
1754
1755        self
1756    }
1757
1758    /// Adds summary level (how detailed the reasoning summary will be).
1759    pub fn with_summary_level(mut self, reasoning_summary_level: ReasoningSummaryLevel) -> Self {
1760        self.summary = Some(reasoning_summary_level);
1761
1762        self
1763    }
1764
1765    /// Sets the reasoning mode (e.g. pro mode on GPT-5.6 models).
1766    pub fn with_mode(mut self, reasoning_mode: ReasoningMode) -> Self {
1767        self.mode = Some(reasoning_mode);
1768
1769        self
1770    }
1771
1772    /// Sets how persisted reasoning is carried across turns (GPT-5.6 models).
1773    pub fn with_context(mut self, reasoning_context: ReasoningContext) -> Self {
1774        self.context = Some(reasoning_context);
1775
1776        self
1777    }
1778}
1779
1780/// The billing service tier that will be used. On auto by default.
1781#[derive(Clone, Debug, Default)]
1782pub enum OpenAIServiceTier {
1783    /// Let OpenAI choose the service tier.
1784    #[default]
1785    Auto,
1786    /// Use the default service tier.
1787    Default,
1788    /// Use the flex service tier.
1789    Flex,
1790    /// Use the priority service tier.
1791    Priority,
1792    /// Use the standard service tier returned by OpenAI-compatible providers.
1793    Standard,
1794    /// Preserve an unknown provider-specific service tier.
1795    Other(String),
1796}
1797
1798impl Serialize for OpenAIServiceTier {
1799    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1800    where
1801        S: Serializer,
1802    {
1803        serializer.serialize_str(match self {
1804            Self::Auto => "auto",
1805            Self::Default => "default",
1806            Self::Flex => "flex",
1807            Self::Priority => "priority",
1808            Self::Standard => "standard",
1809            Self::Other(value) => value,
1810        })
1811    }
1812}
1813
1814impl<'de> Deserialize<'de> for OpenAIServiceTier {
1815    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1816    where
1817        D: Deserializer<'de>,
1818    {
1819        let value = String::deserialize(deserializer)?;
1820        Ok(match value.as_str() {
1821            "auto" => Self::Auto,
1822            "default" => Self::Default,
1823            "flex" => Self::Flex,
1824            "priority" => Self::Priority,
1825            "standard" => Self::Standard,
1826            _ => Self::Other(value),
1827        })
1828    }
1829}
1830
1831/// The amount of reasoning effort that will be used by a given model.
1832#[derive(Clone, Debug, Default, Serialize, Deserialize)]
1833#[serde(rename_all = "snake_case")]
1834pub enum ReasoningEffort {
1835    None,
1836    Minimal,
1837    Low,
1838    #[default]
1839    Medium,
1840    High,
1841    Xhigh,
1842    /// The highest reasoning effort. Supported by the GPT-5.6 model family.
1843    Max,
1844}
1845
1846/// The reasoning mode used by a given model. Independent from
1847/// [`ReasoningEffort`]; the standard mode is represented by omitting the field
1848/// (`None` on [`Reasoning::mode`]), so this enum only carries the documented
1849/// non-default modes.
1850#[derive(Clone, Debug, Serialize, Deserialize)]
1851#[serde(rename_all = "snake_case")]
1852pub enum ReasoningMode {
1853    /// Pro mode. Supported by the GPT-5.6 model family.
1854    Pro,
1855}
1856
1857/// How persisted reasoning is carried across turns. Supported by the GPT-5.6
1858/// model family.
1859#[derive(Clone, Debug, Default, Serialize, Deserialize)]
1860#[serde(rename_all = "snake_case")]
1861pub enum ReasoningContext {
1862    /// Let the model decide how much persisted reasoning to reuse.
1863    #[default]
1864    Auto,
1865    /// Reuse persisted reasoning from all previous turns.
1866    AllTurns,
1867    /// Only use reasoning from the current turn.
1868    CurrentTurn,
1869}
1870
1871/// The amount of effort that will go into a reasoning summary by a given model.
1872#[derive(Clone, Debug, Default, Serialize, Deserialize)]
1873#[serde(rename_all = "snake_case")]
1874pub enum ReasoningSummaryLevel {
1875    #[default]
1876    Auto,
1877    Concise,
1878    Detailed,
1879}
1880
1881/// Additional response fields requested through [`AdditionalParameters::include`].
1882#[derive(Clone, Debug, Deserialize, Serialize)]
1883pub enum Include {
1884    #[serde(rename = "file_search_call.results")]
1885    FileSearchCallResults,
1886    #[serde(rename = "message.input_image.image_url")]
1887    MessageInputImageImageUrl,
1888    #[serde(rename = "computer_call.output.image_url")]
1889    ComputerCallOutputOutputImageUrl,
1890    #[serde(rename = "reasoning.encrypted_content")]
1891    ReasoningEncryptedContent,
1892    #[serde(rename = "code_interpreter_call.outputs")]
1893    CodeInterpreterCallOutputs,
1894}
1895
1896/// A Responses output item. Unrecognized types, including hosted tools, decode
1897/// to [`Output::Unknown`] with their JSON value preserved. Malformed known
1898/// types fail deserialization.
1899#[derive(Clone, Debug, PartialEq)]
1900pub enum Output {
1901    Message(OutputMessage),
1902    FunctionCall(OutputFunctionCall),
1903    Reasoning {
1904        id: String,
1905        summary: Vec<ReasoningSummary>,
1906        content: Vec<String>,
1907        encrypted_content: Option<String>,
1908        /// The upstream's signature over the reasoning text, when a gateway
1909        /// relays one (OpenRouter for Claude).
1910        signature: Option<String>,
1911        status: Option<ToolStatus>,
1912    },
1913    /// An opaque compaction item (`"type": "compaction"`), preserved verbatim
1914    /// so it can be sent back as an input item on the next request. Kept
1915    /// distinct from [`Output::Unknown`] because OpenAI documents it as a
1916    /// must-replay item, and [`InputContent::Compaction`] is its input twin.
1917    Compaction(Map<String, Value>),
1918    /// Catch-all for output item types this version does not model. Holds the
1919    /// raw item object exactly as it appeared in the provider's `output[]`
1920    /// array, so hosted-tool payloads survive the typed decode.
1921    Unknown(Value),
1922}
1923
1924/// Deserialization fields for [`Output::Reasoning`].
1925#[derive(Deserialize)]
1926struct ReasoningFields {
1927    id: String,
1928    #[serde(default)]
1929    summary: Vec<ReasoningSummary>,
1930    #[serde(default, deserialize_with = "deserialize_reasoning_text_content")]
1931    content: Vec<String>,
1932    #[serde(default)]
1933    encrypted_content: Option<String>,
1934    #[serde(default)]
1935    signature: Option<String>,
1936    #[serde(default)]
1937    status: Option<ToolStatus>,
1938}
1939
1940impl From<ReasoningFields> for Output {
1941    fn from(fields: ReasoningFields) -> Self {
1942        Output::Reasoning {
1943            id: fields.id,
1944            summary: fields.summary,
1945            content: fields.content,
1946            encrypted_content: fields.encrypted_content,
1947            signature: fields.signature,
1948            status: fields.status,
1949        }
1950    }
1951}
1952
1953/// Serializes an object payload with a `type` tag. Non-object payloads fail.
1954/// Object key order is not preserved.
1955fn tagged_output_object<T>(tag: &str, payload: &T) -> Result<Value, serde_json::Error>
1956where
1957    T: Serialize,
1958{
1959    let mut value = serde_json::to_value(payload)?;
1960    let map = value.as_object_mut().ok_or_else(|| {
1961        <serde_json::Error as serde::ser::Error>::custom(
1962            "output payload must serialize to a JSON object",
1963        )
1964    })?;
1965    map.insert("type".to_string(), Value::String(tag.to_string()));
1966    Ok(value)
1967}
1968
1969impl Serialize for Output {
1970    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1971    where
1972        S: Serializer,
1973    {
1974        let value = match self {
1975            Output::Message(message) => tagged_output_object("message", message),
1976            Output::FunctionCall(call) => tagged_output_object("function_call", call),
1977            Output::Reasoning {
1978                id,
1979                summary,
1980                content,
1981                encrypted_content,
1982                signature,
1983                status,
1984            } => {
1985                let mut value = serde_json::json!({
1986                    "type": "reasoning",
1987                    "id": id,
1988                    "summary": summary,
1989                    "encrypted_content": encrypted_content,
1990                    "status": status,
1991                });
1992                let map = value.as_object_mut().ok_or_else(|| {
1993                    serde::ser::Error::custom("reasoning output must serialize to an object")
1994                })?;
1995                if !content.is_empty() {
1996                    map.insert("content".to_string(), reasoning_text_content_json(content));
1997                }
1998                if let Some(signature) = signature {
1999                    map.insert("signature".to_string(), Value::String(signature.clone()));
2000                }
2001                Ok(value)
2002            }
2003            Output::Compaction(fields) => {
2004                let mut map = fields.clone();
2005                map.insert("type".to_string(), Value::String("compaction".to_string()));
2006                return Value::Object(map).serialize(serializer);
2007            }
2008            Output::Unknown(value) => return value.serialize(serializer),
2009        };
2010        value
2011            .map_err(serde::ser::Error::custom)?
2012            .serialize(serializer)
2013    }
2014}
2015
2016impl<'de> Deserialize<'de> for Output {
2017    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2018    where
2019        D: Deserializer<'de>,
2020    {
2021        // Preserve unmodeled items, including absent or non-string tags.
2022        // Malformed bodies with known tags must still fail.
2023        let value = Value::deserialize(deserializer)?;
2024        let Some(tag) = value.get("type").and_then(Value::as_str) else {
2025            return Ok(Output::Unknown(value));
2026        };
2027        match tag {
2028            "message" => serde_json::from_value(value)
2029                .map(Output::Message)
2030                .map_err(serde::de::Error::custom),
2031            "function_call" => serde_json::from_value(value)
2032                .map(Output::FunctionCall)
2033                .map_err(serde::de::Error::custom),
2034            "reasoning" => serde_json::from_value::<ReasoningFields>(value)
2035                .map(Output::from)
2036                .map_err(serde::de::Error::custom),
2037            "compaction" => {
2038                let Value::Object(mut map) = value else {
2039                    return Ok(Output::Unknown(value));
2040                };
2041                map.remove("type");
2042                Ok(Output::Compaction(map))
2043            }
2044            _ => Ok(Output::Unknown(value)),
2045        }
2046    }
2047}
2048
2049/// 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.
2050#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
2051pub struct OutputFunctionCall {
2052    /// Provider-assigned `fc_...` item ID. The Responses API rejects
2053    /// `function_call` input IDs that are not native `fc` item IDs, so IDs
2054    /// minted outside the Responses API (by Rig's agent loop or another
2055    /// provider) are omitted on serialization and the call is paired with its
2056    /// output by `call_id` alone.
2057    #[serde(default, skip_serializing_if = "is_not_function_call_item_id")]
2058    pub id: String,
2059    pub arguments: FunctionCallArguments,
2060    pub call_id: String,
2061    pub name: String,
2062    pub status: ToolStatus,
2063}
2064
2065/// Raw Responses function-call arguments, parsed as JSON at consumption time.
2066/// Truncated arguments remain valid wire data; parsing them can fail without
2067/// discarding the enclosing response or its terminal status.
2068#[derive(Clone, Debug, PartialEq)]
2069pub struct FunctionCallArguments(String);
2070
2071impl FunctionCallArguments {
2072    /// Parse the raw wire string into JSON arguments. An empty string is a
2073    /// parameterless invocation (`{}`); anything else must parse as JSON.
2074    pub fn parse(&self) -> serde_json::Result<serde_json::Value> {
2075        json_utils::parse_tool_arguments(&self.0)
2076    }
2077
2078    /// The raw wire string, exactly as the provider sent it.
2079    pub fn as_str(&self) -> &str {
2080        &self.0
2081    }
2082}
2083
2084impl From<serde_json::Value> for FunctionCallArguments {
2085    /// Encode already-parsed arguments (Rig's canonical tool-call form) in
2086    /// the wire's stringified-JSON spelling.
2087    fn from(value: serde_json::Value) -> Self {
2088        Self(value.to_string())
2089    }
2090}
2091
2092impl Serialize for FunctionCallArguments {
2093    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
2094    where
2095        S: Serializer,
2096    {
2097        serializer.serialize_str(&self.0)
2098    }
2099}
2100
2101impl<'de> Deserialize<'de> for FunctionCallArguments {
2102    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2103    where
2104        D: Deserializer<'de>,
2105    {
2106        // The wire spells arguments as a string; a non-string payload is
2107        // still a schema defect of the known `function_call` shape.
2108        String::deserialize(deserializer).map(Self)
2109    }
2110}
2111
2112/// See [`OutputFunctionCall::id`]: only provider-native `fc` item IDs may be
2113/// sent back to the Responses API.
2114fn is_not_function_call_item_id(id: &str) -> bool {
2115    !id.starts_with("fc_")
2116}
2117
2118/// The status of a given tool.
2119#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
2120#[serde(rename_all = "snake_case")]
2121pub enum ToolStatus {
2122    InProgress,
2123    Completed,
2124    Incomplete,
2125}
2126
2127/// An output message from OpenAI's Responses API.
2128#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
2129pub struct OutputMessage {
2130    /// The message ID. Must be included when sending the message back to OpenAI
2131    pub id: String,
2132    /// The role (currently only Assistant is available as this struct is only created when receiving an LLM message as a response)
2133    pub role: OutputRole,
2134    /// The status of the response
2135    pub status: ResponseStatus,
2136    /// The actual message content
2137    pub content: Vec<AssistantContent>,
2138    /// Generation phase, such as `"final_answer"`, preserved for follow-up requests.
2139    #[serde(default, skip_serializing_if = "Option::is_none")]
2140    pub phase: Option<String>,
2141}
2142
2143/// The role of an output message.
2144#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
2145#[serde(rename_all = "snake_case")]
2146pub enum OutputRole {
2147    Assistant,
2148}
2149
2150/// An OpenAI Responses API message.
2151#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
2152#[serde(tag = "role", rename_all = "lowercase")]
2153pub enum Message {
2154    #[serde(alias = "developer")]
2155    System {
2156        #[serde(deserialize_with = "string_or_vec")]
2157        content: Vec<SystemContent>,
2158        #[serde(skip_serializing_if = "Option::is_none")]
2159        name: Option<String>,
2160    },
2161    User {
2162        #[serde(deserialize_with = "string_or_vec")]
2163        content: Vec<UserContent>,
2164        #[serde(skip_serializing_if = "Option::is_none")]
2165        name: Option<String>,
2166    },
2167    Assistant {
2168        content: Vec<AssistantContentType>,
2169        #[serde(skip_serializing_if = "String::is_empty")]
2170        id: String,
2171        #[serde(skip_serializing_if = "Option::is_none")]
2172        name: Option<String>,
2173        status: ToolStatus,
2174        /// Generation phase preserved from the output message.
2175        #[serde(default, skip_serializing_if = "Option::is_none")]
2176        phase: Option<String>,
2177    },
2178    #[serde(rename = "assistant", skip_deserializing)]
2179    AssistantInput {
2180        content: String,
2181        #[serde(skip_serializing_if = "Option::is_none")]
2182        name: Option<String>,
2183    },
2184}
2185
2186impl Message {
2187    pub fn system(content: &str) -> Self {
2188        Message::System {
2189            content: vec![content.to_owned().into()],
2190            name: None,
2191        }
2192    }
2193}
2194
2195/// Text assistant content.
2196/// Note that the text type in comparison to the Completions API is actually `output_text` rather than `text`.
2197#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
2198#[serde(tag = "type", rename_all = "snake_case")]
2199pub enum AssistantContent {
2200    OutputText(OutputText),
2201    Refusal { refusal: String },
2202}
2203
2204/// Responses `output_text` block with unmodeled sibling fields preserved as JSON.
2205#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
2206pub struct OutputText {
2207    pub text: String,
2208    /// OpenAI's sibling keys, preserved verbatim for value-equal replay.
2209    /// The `Map` form (not `Option<Value>`) makes absence and the empty map
2210    /// one value, so a decoded bare block equals a request-assembled one.
2211    #[serde(flatten, default, skip_serializing_if = "Map::is_empty")]
2212    pub extras: Map<String, Value>,
2213}
2214
2215impl OutputText {
2216    /// A bare text block, as request assembly emits (no wire extras).
2217    pub fn new(text: impl Into<String>) -> Self {
2218        Self {
2219            text: text.into(),
2220            extras: Map::new(),
2221        }
2222    }
2223
2224    /// Rebuild a wire block from a rig text block, re-attaching only the
2225    /// extras this wire recognizes as its own: the sibling keys captured off
2226    /// an `output_text` block at ingest (see
2227    /// [`From<AssistantContent> for completion::AssistantContent`]).
2228    fn from_message_text(
2229        text: impl Into<String>,
2230        additional_params: Option<crate::message::AdditionalParams>,
2231    ) -> Self {
2232        let Some(params) = additional_params else {
2233            return Self::new(text);
2234        };
2235        // The caller diagnoses malformed extras before this conversion drops them.
2236        let extras = params
2237            .into_wire_extras(OPENAI_RESPONSES_EXTRAS_KEY)
2238            .map(|map| {
2239                map.into_iter()
2240                    // Reserved keys would duplicate the block's text or tag.
2241                    // Phase and the item id belong on the message, not the
2242                    // content block.
2243                    .filter(|(key, _)| {
2244                        key != "text"
2245                            && key != "type"
2246                            && key != OPENAI_RESPONSES_PHASE_KEY
2247                            && key != OPENAI_RESPONSES_MESSAGE_ID_KEY
2248                    })
2249                    .collect()
2250            })
2251            .unwrap_or_default();
2252        Self {
2253            text: text.into(),
2254            extras,
2255        }
2256    }
2257}
2258
2259/// Builds the assistant input one text block replays as, using only
2260/// Responses-owned extras. The item is the block's own message item when its
2261/// extras name one, and `id` otherwise; its `phase` comes from the extras.
2262/// Empty text is skipped unless the item has an id and owned extras. Without
2263/// an id the text replays id-less: `phase` rides it, content-part extras do
2264/// not. Malformed or dropped extras warn.
2265fn assistant_text_replay_message(
2266    id: Option<&str>,
2267    text: String,
2268    additional_params: Option<crate::message::AdditionalParams>,
2269) -> Option<Message> {
2270    // Diagnose malformed extras before normalization makes them indistinguishable
2271    // from absent extras, including when empty text is skipped.
2272    if let Some(non_object) = additional_params
2273        .as_ref()
2274        .and_then(|params| params.get(OPENAI_RESPONSES_EXTRAS_KEY))
2275        .filter(|value| !value.is_object())
2276    {
2277        tracing::warn!(
2278            %non_object,
2279            "`additional_params[\"{OPENAI_RESPONSES_EXTRAS_KEY}\"]` must be a JSON \
2280             object — replaying without these extras"
2281        );
2282    }
2283    let own_extras = additional_params
2284        .as_ref()
2285        .and_then(|params| params.wire_extras(OPENAI_RESPONSES_EXTRAS_KEY));
2286    // `phase` and the item id ride the text block's own-wire extras on
2287    // ingest; they belong to the message, so they are lifted here and
2288    // filtered from the block.
2289    let message_field = |key: &str| {
2290        own_extras
2291            .and_then(|extras| extras.get(key))
2292            .and_then(Value::as_str)
2293            .filter(|value| !value.is_empty())
2294            .map(str::to_owned)
2295    };
2296    let phase = message_field(OPENAI_RESPONSES_PHASE_KEY);
2297    let id = message_field(OPENAI_RESPONSES_MESSAGE_ID_KEY).or_else(|| id.map(str::to_owned));
2298    let content_extras = own_extras.is_some_and(|extras| {
2299        extras
2300            .keys()
2301            .any(|key| key != OPENAI_RESPONSES_PHASE_KEY && key != OPENAI_RESPONSES_MESSAGE_ID_KEY)
2302    });
2303    if text.is_empty() && !(own_extras.is_some() && id.is_some()) {
2304        return None;
2305    }
2306    if id.is_none() && content_extras {
2307        tracing::warn!(
2308            "own-wire extras cannot ride the id-less assistant form — \
2309             replaying the text without them"
2310        );
2311    }
2312    match (id, phase) {
2313        (Some(id), phase) => Some(Message::Assistant {
2314            content: vec![AssistantContentType::Text(AssistantContent::OutputText(
2315                OutputText::from_message_text(text, additional_params),
2316            ))],
2317            id,
2318            name: None,
2319            status: ToolStatus::Completed,
2320            phase,
2321        }),
2322        // The id-less input message form has no `phase`; an output message
2323        // without its id carries one.
2324        (None, Some(phase)) => Some(Message::Assistant {
2325            content: vec![AssistantContentType::Text(AssistantContent::OutputText(
2326                OutputText::new(text),
2327            ))],
2328            id: String::new(),
2329            name: None,
2330            status: ToolStatus::Completed,
2331            phase: Some(phase),
2332        }),
2333        (None, None) => Some(Message::AssistantInput {
2334            content: text,
2335            name: None,
2336        }),
2337    }
2338}
2339
2340/// Responses-owned extras in [`Text::additional_params`](crate::message::Text).
2341/// Both paths capture these fields for replay: streamed text takes them from
2342/// its message item's snapshot, not from `output_text.annotation.added`.
2343pub(crate) const OPENAI_RESPONSES_EXTRAS_KEY: &str = "openai_responses";
2344
2345/// Key inside the [`OPENAI_RESPONSES_EXTRAS_KEY`] object that carries the
2346/// output message's `phase`. It is message-level on the wire but rides the
2347/// text block's extras in rig history (the only own-wire seat), and is
2348/// lifted back onto the assistant input item at replay.
2349pub(crate) const OPENAI_RESPONSES_PHASE_KEY: &str = "phase";
2350
2351/// Key inside the [`OPENAI_RESPONSES_EXTRAS_KEY`] object that carries the
2352/// id of the message item a text block came from. Recorded only when a
2353/// reply carries several message items, which one rig assistant message
2354/// id cannot name; lifted back onto the assistant input item at replay.
2355pub(crate) const OPENAI_RESPONSES_MESSAGE_ID_KEY: &str = "message_id";
2356
2357/// Converts output text or a refusal to a Rig text block, retaining nonempty
2358/// output-text extras under the Responses key.
2359pub(crate) fn text_block(value: AssistantContent) -> Text {
2360    match value {
2361        AssistantContent::Refusal { refusal } => Text::new(refusal),
2362        // Keep this destructuring exhaustive so new wire fields force an
2363        // explicit capture-or-drop decision.
2364        AssistantContent::OutputText(OutputText { text, extras }) => {
2365            // Empty metadata must not change replayed request bytes.
2366            let extras: Map<String, Value> = extras
2367                .into_iter()
2368                .filter(|(_, value)| {
2369                    !(value.is_null()
2370                        || value.as_array().is_some_and(Vec::is_empty)
2371                        || value.as_object().is_some_and(Map::is_empty))
2372                })
2373                .collect();
2374            Text {
2375                text,
2376                additional_params: crate::message::AdditionalParams::from_entries(
2377                    (!extras.is_empty())
2378                        .then_some((OPENAI_RESPONSES_EXTRAS_KEY, Value::Object(extras))),
2379                ),
2380            }
2381        }
2382    }
2383}
2384
2385impl From<AssistantContent> for completion::AssistantContent {
2386    fn from(value: AssistantContent) -> Self {
2387        completion::AssistantContent::Text(text_block(value))
2388    }
2389}
2390
2391/// The type of assistant content.
2392#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
2393#[serde(untagged)]
2394pub enum AssistantContentType {
2395    Text(AssistantContent),
2396    ToolCall(OutputFunctionCall),
2397    Reasoning(OpenAIReasoning),
2398}
2399
2400/// System content for the OpenAI Responses API.
2401/// Uses `input_text` type to match the Responses API format.
2402#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
2403#[serde(tag = "type", rename_all = "snake_case")]
2404pub enum SystemContent {
2405    InputText { text: String },
2406}
2407
2408impl From<String> for SystemContent {
2409    fn from(s: String) -> Self {
2410        SystemContent::InputText { text: s }
2411    }
2412}
2413
2414impl std::str::FromStr for SystemContent {
2415    type Err = std::convert::Infallible;
2416
2417    fn from_str(s: &str) -> Result<Self, Self::Err> {
2418        Ok(SystemContent::InputText {
2419            text: s.to_string(),
2420        })
2421    }
2422}
2423
2424/// Different types of user content.
2425#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
2426#[serde(tag = "type", rename_all = "snake_case")]
2427pub enum UserContent {
2428    InputText {
2429        text: String,
2430    },
2431    InputImage {
2432        image_url: String,
2433        #[serde(default)]
2434        detail: ImageDetail,
2435    },
2436    InputFile {
2437        #[serde(skip_serializing_if = "Option::is_none")]
2438        file_id: Option<String>,
2439        #[serde(skip_serializing_if = "Option::is_none")]
2440        file_url: Option<String>,
2441        #[serde(skip_serializing_if = "Option::is_none")]
2442        file_data: Option<String>,
2443        #[serde(skip_serializing_if = "Option::is_none")]
2444        filename: Option<String>,
2445    },
2446}
2447
2448impl FromStr for UserContent {
2449    type Err = Infallible;
2450
2451    fn from_str(s: &str) -> Result<Self, Self::Err> {
2452        Ok(UserContent::InputText {
2453            text: s.to_string(),
2454        })
2455    }
2456}
2457
2458#[cfg(test)]
2459mod stateless_replay_tests;
2460#[cfg(test)]
2461mod tests;