Skip to main content

rig/providers/openai/responses_api/
mod.rs

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