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::{Client, responses_api::streaming::StreamingCompletionResponse};
11use super::{ImageUrl, InputAudio, SystemContent};
12use crate::completion::CompletionError;
13use crate::json_utils;
14use crate::message::{AudioMediaType, Document, DocumentSourceKind, MessageError, MimeType, Text};
15use crate::one_or_many::string_or_one_or_many;
16
17use crate::{OneOrMany, completion, message};
18use serde::{Deserialize, Serialize};
19use serde_json::{Map, Value};
20
21use std::convert::Infallible;
22use std::ops::Add;
23use std::str::FromStr;
24
25pub mod streaming;
26
27/// The completion request type for OpenAI's Response API: <https://platform.openai.com/docs/api-reference/responses/create>
28/// Intended to be derived from [`crate::completion::request::CompletionRequest`].
29#[derive(Debug, Deserialize, Serialize, Clone)]
30pub struct CompletionRequest {
31    /// Message inputs
32    pub input: OneOrMany<InputItem>,
33    /// The model name
34    pub model: String,
35    /// Instructions (also referred to as preamble, although in other APIs this would be the "system prompt")
36    #[serde(skip_serializing_if = "Option::is_none")]
37    pub instructions: Option<String>,
38    /// The maximum number of output tokens.
39    #[serde(skip_serializing_if = "Option::is_none")]
40    pub max_output_tokens: Option<u64>,
41    /// Toggle to true for streaming responses.
42    #[serde(skip_serializing_if = "Option::is_none")]
43    pub stream: Option<bool>,
44    /// The temperature. Set higher (up to a max of 1.0) for more creative responses.
45    #[serde(skip_serializing_if = "Option::is_none")]
46    pub temperature: Option<f64>,
47    // TODO: Fix this before opening a PR!
48    // tool_choice: Option<T>,
49    /// The tools you want to use. Currently this is limited to functions, but will be expanded on in future.
50    #[serde(skip_serializing_if = "Vec::is_empty")]
51    pub tools: Vec<ResponsesToolDefinition>,
52    /// Additional parameters
53    #[serde(flatten)]
54    pub additional_parameters: AdditionalParameters,
55}
56
57impl CompletionRequest {
58    pub fn with_structured_outputs<S>(mut self, schema_name: S, schema: serde_json::Value) -> Self
59    where
60        S: Into<String>,
61    {
62        self.additional_parameters.text = Some(TextConfig::structured_output(schema_name, schema));
63
64        self
65    }
66
67    pub fn with_reasoning(mut self, reasoning: Reasoning) -> Self {
68        self.additional_parameters.reasoning = Some(reasoning);
69
70        self
71    }
72}
73
74/// An input item for [`CompletionRequest`].
75#[derive(Debug, Deserialize, Serialize, Clone)]
76pub struct InputItem {
77    /// The role of an input item/message.
78    /// Input messages should be Some(Role::User), and output messages should be Some(Role::Assistant).
79    /// Everything else should be None.
80    #[serde(skip_serializing_if = "Option::is_none")]
81    role: Option<Role>,
82    /// The input content itself.
83    #[serde(flatten)]
84    input: InputContent,
85}
86
87/// Message roles. Used by OpenAI Responses API to determine who created a given message.
88#[derive(Debug, Deserialize, Serialize, Clone)]
89#[serde(rename_all = "lowercase")]
90pub enum Role {
91    User,
92    Assistant,
93    System,
94}
95
96/// The type of content used in an [`InputItem`]. Additionally holds data for each type of input content.
97#[derive(Debug, Deserialize, Serialize, Clone)]
98#[serde(tag = "type", rename_all = "snake_case")]
99pub enum InputContent {
100    Message(Message),
101    Reasoning(OpenAIReasoning),
102    FunctionCall(OutputFunctionCall),
103    FunctionCallOutput(ToolResult),
104}
105
106#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
107pub struct OpenAIReasoning {
108    id: String,
109    pub summary: Vec<ReasoningSummary>,
110    pub encrypted_content: Option<String>,
111    #[serde(skip_serializing_if = "Option::is_none")]
112    pub status: Option<ToolStatus>,
113}
114
115#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
116#[serde(tag = "type", rename_all = "snake_case")]
117pub enum ReasoningSummary {
118    SummaryText { text: String },
119}
120
121impl ReasoningSummary {
122    fn new(input: &str) -> Self {
123        Self::SummaryText {
124            text: input.to_string(),
125        }
126    }
127
128    pub fn text(&self) -> String {
129        let ReasoningSummary::SummaryText { text } = self;
130        text.clone()
131    }
132}
133
134/// A tool result.
135#[derive(Debug, Deserialize, Serialize, Clone)]
136pub struct ToolResult {
137    /// The call ID of a tool (this should be linked to the call ID for a tool call, otherwise an error will be received)
138    call_id: String,
139    /// The result of a tool call.
140    output: String,
141    /// The status of a tool call (if used in a completion request, this should always be Completed)
142    status: ToolStatus,
143}
144
145impl From<Message> for InputItem {
146    fn from(value: Message) -> Self {
147        match value {
148            Message::User { .. } => Self {
149                role: Some(Role::User),
150                input: InputContent::Message(value),
151            },
152            Message::Assistant { ref content, .. } => {
153                let role = if content
154                    .clone()
155                    .iter()
156                    .any(|x| matches!(x, AssistantContentType::Reasoning(_)))
157                {
158                    None
159                } else {
160                    Some(Role::Assistant)
161                };
162                Self {
163                    role,
164                    input: InputContent::Message(value),
165                }
166            }
167            Message::System { .. } => Self {
168                role: Some(Role::System),
169                input: InputContent::Message(value),
170            },
171            Message::ToolResult {
172                tool_call_id,
173                output,
174            } => Self {
175                role: None,
176                input: InputContent::FunctionCallOutput(ToolResult {
177                    call_id: tool_call_id,
178                    output,
179                    status: ToolStatus::Completed,
180                }),
181            },
182        }
183    }
184}
185
186impl TryFrom<crate::completion::Message> for Vec<InputItem> {
187    type Error = CompletionError;
188
189    fn try_from(value: crate::completion::Message) -> Result<Self, Self::Error> {
190        match value {
191            crate::completion::Message::User { content } => {
192                let mut items = Vec::new();
193
194                for user_content in content {
195                    match user_content {
196                        crate::message::UserContent::Text(Text { text }) => {
197                            items.push(InputItem {
198                                role: Some(Role::User),
199                                input: InputContent::Message(Message::User {
200                                    content: OneOrMany::one(UserContent::InputText { text }),
201                                    name: None,
202                                }),
203                            });
204                        }
205                        crate::message::UserContent::ToolResult(
206                            crate::completion::message::ToolResult {
207                                call_id,
208                                content: tool_content,
209                                ..
210                            },
211                        ) => {
212                            for tool_result_content in tool_content {
213                                let crate::completion::message::ToolResultContent::Text(Text {
214                                    text,
215                                }) = tool_result_content
216                                else {
217                                    return Err(CompletionError::ProviderError(
218                                        "This thing only supports text!".to_string(),
219                                    ));
220                                };
221                                // let output = serde_json::from_str(&text)?;
222                                items.push(InputItem {
223                                    role: None,
224                                    input: InputContent::FunctionCallOutput(ToolResult {
225                                        call_id: call_id
226                                            .clone()
227                                            .expect("The call ID of this tool should exist!"),
228                                        output: text,
229                                        status: ToolStatus::Completed,
230                                    }),
231                                });
232                            }
233                        }
234                        // todo: should we ensure this takes into account file size?
235                        crate::message::UserContent::Document(Document { data, .. }) => {
236                            items.push(InputItem {
237                                role: Some(Role::User),
238                                input: InputContent::Message(Message::User {
239                                    content: OneOrMany::one(UserContent::InputText { text: data }),
240                                    name: None,
241                                }),
242                            })
243                        }
244                        crate::message::UserContent::Image(crate::message::Image {
245                            data,
246                            media_type,
247                            detail,
248                            ..
249                        }) => {
250                            let url = match data {
251                                DocumentSourceKind::Base64(data) => {
252                                    let media_type = if let Some(media_type) = media_type {
253                                        media_type.to_mime_type().to_string()
254                                    } else {
255                                        String::new()
256                                    };
257                                    format!("data:{media_type};base64,{data}")
258                                }
259                                DocumentSourceKind::Url(url) => url,
260                                DocumentSourceKind::Unknown => return Err(CompletionError::RequestError("Attempted to create an OpenAI Responses AI image input from unknown variant".into()))
261                            };
262                            items.push(InputItem {
263                                role: Some(Role::User),
264                                input: InputContent::Message(Message::User {
265                                    content: OneOrMany::one(UserContent::InputImage {
266                                        image_url: ImageUrl {
267                                            url,
268                                            detail: detail.unwrap_or_default(),
269                                        },
270                                    }),
271                                    name: None,
272                                }),
273                            });
274                        }
275                        message => {
276                            return Err(CompletionError::ProviderError(format!(
277                                "Unsupported message: {message:?}"
278                            )));
279                        }
280                    }
281                }
282
283                Ok(items)
284            }
285            crate::completion::Message::Assistant { id, content } => {
286                let mut items = Vec::new();
287
288                for assistant_content in content {
289                    match assistant_content {
290                        crate::message::AssistantContent::Text(Text { text }) => {
291                            let id = id.as_ref().unwrap_or(&String::default()).clone();
292                            items.push(InputItem {
293                                role: Some(Role::Assistant),
294                                input: InputContent::Message(Message::Assistant {
295                                    content: OneOrMany::one(AssistantContentType::Text(
296                                        AssistantContent::OutputText(Text { text }),
297                                    )),
298                                    id,
299                                    name: None,
300                                    status: ToolStatus::Completed,
301                                }),
302                            });
303                        }
304                        crate::message::AssistantContent::ToolCall(crate::message::ToolCall {
305                            id: tool_id,
306                            call_id,
307                            function,
308                        }) => {
309                            items.push(InputItem {
310                                role: None,
311                                input: InputContent::FunctionCall(OutputFunctionCall {
312                                    arguments: function.arguments,
313                                    call_id: call_id.expect("The tool call ID should exist!"),
314                                    id: tool_id,
315                                    name: function.name,
316                                    status: ToolStatus::Completed,
317                                }),
318                            });
319                        }
320                        crate::message::AssistantContent::Reasoning(
321                            crate::message::Reasoning { id, reasoning },
322                        ) => {
323                            items.push(InputItem {
324                                role: None,
325                                input: InputContent::Reasoning(OpenAIReasoning {
326                                    id: id
327                                        .expect("An OpenAI-generated ID is required when using OpenAI reasoning items"),
328                                    summary: reasoning.into_iter().map(|x| ReasoningSummary::new(&x)).collect(),
329                                    encrypted_content: None,
330                                    status: None,
331                                }),
332                            });
333                        }
334                    }
335                }
336
337                Ok(items)
338            }
339        }
340    }
341}
342
343impl From<OneOrMany<String>> for Vec<ReasoningSummary> {
344    fn from(value: OneOrMany<String>) -> Self {
345        value.iter().map(|x| ReasoningSummary::new(x)).collect()
346    }
347}
348
349/// The definition of a tool response, repurposed for OpenAI's Responses API.
350#[derive(Debug, Deserialize, Serialize, Clone)]
351pub struct ResponsesToolDefinition {
352    /// Tool name
353    pub name: String,
354    /// 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).
355    pub parameters: serde_json::Value,
356    /// Whether to use strict mode. Enabled by default as it allows for improved efficiency.
357    pub strict: bool,
358    /// The type of tool. This should always be "function".
359    #[serde(rename = "type")]
360    pub kind: String,
361    /// Tool description.
362    pub description: String,
363}
364
365/// Recursively ensures all object schemas in a JSON schema have `additionalProperties: false`.
366/// Nested arrays, schema $defs, object properties and enums should be handled through this method
367/// This seems to be required by OpenAI's Responses API when using strict mode.
368fn add_props_false(schema: &mut serde_json::Value) {
369    if let Value::Object(obj) = schema {
370        let is_object_schema = obj.get("type") == Some(&Value::String("object".to_string()))
371            || obj.contains_key("properties");
372
373        if is_object_schema && !obj.contains_key("additionalProperties") {
374            obj.insert("additionalProperties".to_string(), Value::Bool(false));
375        }
376
377        if let Some(defs) = obj.get_mut("$defs")
378            && let Value::Object(defs_obj) = defs
379        {
380            for (_, def_schema) in defs_obj.iter_mut() {
381                add_props_false(def_schema);
382            }
383        }
384
385        if let Some(properties) = obj.get_mut("properties")
386            && let Value::Object(props) = properties
387        {
388            for (_, prop_value) in props.iter_mut() {
389                add_props_false(prop_value);
390            }
391        }
392
393        if let Some(items) = obj.get_mut("items") {
394            add_props_false(items);
395        }
396
397        // should handle Enums (anyOf/oneOf)
398        for key in ["anyOf", "oneOf", "allOf"] {
399            if let Some(variants) = obj.get_mut(key)
400                && let Value::Array(variants_array) = variants
401            {
402                for variant in variants_array.iter_mut() {
403                    add_props_false(variant);
404                }
405            }
406        }
407    }
408}
409
410impl From<completion::ToolDefinition> for ResponsesToolDefinition {
411    fn from(value: completion::ToolDefinition) -> Self {
412        let completion::ToolDefinition {
413            name,
414            mut parameters,
415            description,
416        } = value;
417
418        add_props_false(&mut parameters);
419
420        Self {
421            name,
422            parameters,
423            description,
424            kind: "function".to_string(),
425            strict: true,
426        }
427    }
428}
429
430/// Token usage.
431/// 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.
432#[derive(Clone, Debug, Serialize, Deserialize)]
433pub struct ResponsesUsage {
434    /// Input tokens
435    pub input_tokens: u64,
436    /// In-depth detail on input tokens (cached tokens)
437    #[serde(skip_serializing_if = "Option::is_none")]
438    pub input_tokens_details: Option<InputTokensDetails>,
439    /// Output tokens
440    pub output_tokens: u64,
441    /// In-depth detail on output tokens (reasoning tokens)
442    pub output_tokens_details: OutputTokensDetails,
443    /// Total tokens used (for a given prompt)
444    pub total_tokens: u64,
445}
446
447impl ResponsesUsage {
448    /// Create a new ResponsesUsage instance
449    pub(crate) fn new() -> Self {
450        Self {
451            input_tokens: 0,
452            input_tokens_details: Some(InputTokensDetails::new()),
453            output_tokens: 0,
454            output_tokens_details: OutputTokensDetails::new(),
455            total_tokens: 0,
456        }
457    }
458}
459
460impl Add for ResponsesUsage {
461    type Output = Self;
462
463    fn add(self, rhs: Self) -> Self::Output {
464        let input_tokens = self.input_tokens + rhs.input_tokens;
465        let input_tokens_details = self.input_tokens_details.map(|lhs| {
466            if let Some(tokens) = rhs.input_tokens_details {
467                lhs + tokens
468            } else {
469                lhs
470            }
471        });
472        let output_tokens = self.output_tokens + rhs.output_tokens;
473        let output_tokens_details = self.output_tokens_details + rhs.output_tokens_details;
474        let total_tokens = self.total_tokens + rhs.total_tokens;
475        Self {
476            input_tokens,
477            input_tokens_details,
478            output_tokens,
479            output_tokens_details,
480            total_tokens,
481        }
482    }
483}
484
485/// In-depth details on input tokens.
486#[derive(Clone, Debug, Serialize, Deserialize)]
487pub struct InputTokensDetails {
488    /// Cached tokens from OpenAI
489    pub cached_tokens: u64,
490}
491
492impl InputTokensDetails {
493    pub(crate) fn new() -> Self {
494        Self { cached_tokens: 0 }
495    }
496}
497
498impl Add for InputTokensDetails {
499    type Output = Self;
500    fn add(self, rhs: Self) -> Self::Output {
501        Self {
502            cached_tokens: self.cached_tokens + rhs.cached_tokens,
503        }
504    }
505}
506
507/// In-depth details on output tokens.
508#[derive(Clone, Debug, Serialize, Deserialize)]
509pub struct OutputTokensDetails {
510    /// Reasoning tokens
511    pub reasoning_tokens: u64,
512}
513
514impl OutputTokensDetails {
515    pub(crate) fn new() -> Self {
516        Self {
517            reasoning_tokens: 0,
518        }
519    }
520}
521
522impl Add for OutputTokensDetails {
523    type Output = Self;
524    fn add(self, rhs: Self) -> Self::Output {
525        Self {
526            reasoning_tokens: self.reasoning_tokens + rhs.reasoning_tokens,
527        }
528    }
529}
530
531/// Occasionally, when using OpenAI's Responses API you may get an incomplete response. This struct holds the reason as to why it happened.
532#[derive(Clone, Debug, Default, Serialize, Deserialize)]
533pub struct IncompleteDetailsReason {
534    /// The reason for an incomplete [`CompletionResponse`].
535    pub reason: String,
536}
537
538/// A response error from OpenAI's Response API.
539#[derive(Clone, Debug, Default, Serialize, Deserialize)]
540pub struct ResponseError {
541    /// Error code
542    pub code: String,
543    /// Error message
544    pub message: String,
545}
546
547/// A response object as an enum (ensures type validation)
548#[derive(Clone, Debug, Deserialize, Serialize)]
549#[serde(rename_all = "snake_case")]
550pub enum ResponseObject {
551    Response,
552}
553
554/// The response status as an enum (ensures type validation)
555#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
556#[serde(rename_all = "snake_case")]
557pub enum ResponseStatus {
558    InProgress,
559    Completed,
560    Failed,
561    Cancelled,
562    Queued,
563    Incomplete,
564}
565
566/// Attempt to try and create a `NewCompletionRequest` from a model name and [`crate::completion::CompletionRequest`]
567impl TryFrom<(String, crate::completion::CompletionRequest)> for CompletionRequest {
568    type Error = CompletionError;
569    fn try_from(
570        (model, req): (String, crate::completion::CompletionRequest),
571    ) -> Result<Self, Self::Error> {
572        let input = {
573            let mut partial_history = vec![];
574            if let Some(docs) = req.normalized_documents() {
575                partial_history.push(docs);
576            }
577            partial_history.extend(req.chat_history);
578
579            // Initialize full history with preamble (or empty if non-existent)
580            let mut full_history: Vec<InputItem> = Vec::new();
581
582            // Convert and extend the rest of the history
583            full_history.extend(
584                partial_history
585                    .into_iter()
586                    .map(|x| <Vec<InputItem>>::try_from(x).unwrap())
587                    .collect::<Vec<Vec<InputItem>>>()
588                    .into_iter()
589                    .flatten()
590                    .collect::<Vec<InputItem>>(),
591            );
592
593            full_history
594        };
595
596        let input = OneOrMany::many(input)
597            .expect("This should never panic - if it does, please file a bug report");
598
599        let stream = req
600            .additional_params
601            .clone()
602            .unwrap_or(Value::Null)
603            .as_bool();
604
605        let additional_parameters = if let Some(map) = req.additional_params {
606            serde_json::from_value::<AdditionalParameters>(map).expect("Converting additional parameters to AdditionalParameters should never fail as every field is an Option")
607        } else {
608            // If there's no additional parameters, initialise an empty object
609            AdditionalParameters::default()
610        };
611
612        Ok(Self {
613            input,
614            model,
615            instructions: req.preamble,
616            max_output_tokens: req.max_tokens,
617            stream,
618            tools: req
619                .tools
620                .into_iter()
621                .map(ResponsesToolDefinition::from)
622                .collect(),
623            temperature: req.temperature,
624            additional_parameters,
625        })
626    }
627}
628
629/// The completion model struct for OpenAI's response API.
630#[derive(Clone)]
631pub struct ResponsesCompletionModel {
632    /// The OpenAI client
633    pub(crate) client: Client,
634    /// Name of the model (e.g.: gpt-3.5-turbo-1106)
635    pub model: String,
636}
637
638impl ResponsesCompletionModel {
639    /// Creates a new [`ResponsesCompletionModel`].
640    pub fn new(client: Client, model: &str) -> Self {
641        Self {
642            client,
643            model: model.to_string(),
644        }
645    }
646
647    /// Use the Completions API instead of Responses.
648    pub fn completions_api(self) -> crate::providers::openai::completion::CompletionModel {
649        crate::providers::openai::completion::CompletionModel::new(self.client, &self.model)
650    }
651
652    /// Attempt to create a completion request from [`crate::completion::CompletionRequest`].
653    pub(crate) fn create_completion_request(
654        &self,
655        completion_request: crate::completion::CompletionRequest,
656    ) -> Result<CompletionRequest, CompletionError> {
657        let req = CompletionRequest::try_from((self.model.clone(), completion_request))?;
658
659        Ok(req)
660    }
661}
662
663/// The standard response format from OpenAI's Responses API.
664#[derive(Clone, Debug, Serialize, Deserialize)]
665pub struct CompletionResponse {
666    /// The ID of a completion response.
667    pub id: String,
668    /// The type of the object.
669    pub object: ResponseObject,
670    /// The time at which a given response has been created, in seconds from the UNIX epoch (01/01/1970 00:00:00).
671    pub created_at: u64,
672    /// The status of the response.
673    pub status: ResponseStatus,
674    /// Response error (optional)
675    pub error: Option<ResponseError>,
676    /// Incomplete response details (optional)
677    pub incomplete_details: Option<IncompleteDetailsReason>,
678    /// System prompt/preamble
679    pub instructions: Option<String>,
680    /// The maximum number of tokens the model should output
681    pub max_output_tokens: Option<u64>,
682    /// The model name
683    pub model: String,
684    /// Token usage
685    pub usage: Option<ResponsesUsage>,
686    /// The model output (messages, etc will go here)
687    pub output: Vec<Output>,
688    /// Tools
689    pub tools: Vec<ResponsesToolDefinition>,
690    /// Additional parameters
691    #[serde(flatten)]
692    pub additional_parameters: AdditionalParameters,
693}
694
695/// Additional parameters for the completion request type for OpenAI's Response API: <https://platform.openai.com/docs/api-reference/responses/create>
696/// Intended to be derived from [`crate::completion::request::CompletionRequest`].
697#[derive(Clone, Debug, Deserialize, Serialize, Default)]
698pub struct AdditionalParameters {
699    /// Whether or not a given model task should run in the background (ie a detached process).
700    #[serde(skip_serializing_if = "Option::is_none")]
701    pub background: Option<bool>,
702    /// The text response format. This is where you would add structured outputs (if you want them).
703    #[serde(skip_serializing_if = "Option::is_none")]
704    pub text: Option<TextConfig>,
705    /// 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!
706    #[serde(skip_serializing_if = "Option::is_none")]
707    pub include: Option<Vec<Include>>,
708    /// `top_p`. Mutually exclusive with the `temperature` argument.
709    #[serde(skip_serializing_if = "Option::is_none")]
710    pub top_p: Option<f64>,
711    /// Whether or not the response should be truncated.
712    #[serde(skip_serializing_if = "Option::is_none")]
713    pub truncation: Option<TruncationStrategy>,
714    /// The username of the user (that you want to use).
715    #[serde(skip_serializing_if = "Option::is_none")]
716    pub user: Option<String>,
717    /// Any additional metadata you'd like to add. This will additionally be returned by the response.
718    #[serde(skip_serializing_if = "Map::is_empty", default)]
719    pub metadata: serde_json::Map<String, serde_json::Value>,
720    /// Whether or not you want tool calls to run in parallel.
721    #[serde(skip_serializing_if = "Option::is_none")]
722    pub parallel_tool_calls: Option<bool>,
723    /// Previous response ID. If you are not sending a full conversation, this can help to track the message flow.
724    #[serde(skip_serializing_if = "Option::is_none")]
725    pub previous_response_id: Option<String>,
726    /// Add thinking/reasoning to your response. The response will be emitted as a list member of the `output` field.
727    #[serde(skip_serializing_if = "Option::is_none")]
728    pub reasoning: Option<Reasoning>,
729    /// The service tier you're using.
730    #[serde(skip_serializing_if = "Option::is_none")]
731    pub service_tier: Option<OpenAIServiceTier>,
732    /// Whether or not to store the response for later retrieval by API.
733    #[serde(skip_serializing_if = "Option::is_none")]
734    pub store: Option<bool>,
735}
736
737impl AdditionalParameters {
738    pub fn to_json(self) -> serde_json::Value {
739        serde_json::to_value(self).expect("this should never fail since a struct that impls Deserialize will always be valid JSON")
740    }
741}
742
743/// The truncation strategy.
744/// 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.
745/// Otherwise, does nothing (and is disabled by default).
746#[derive(Clone, Debug, Default, Serialize, Deserialize)]
747#[serde(rename_all = "snake_case")]
748pub enum TruncationStrategy {
749    Auto,
750    #[default]
751    Disabled,
752}
753
754/// The model output format configuration.
755/// You can either have plain text by default, or attach a JSON schema for the purposes of structured outputs.
756#[derive(Clone, Debug, Serialize, Deserialize)]
757pub struct TextConfig {
758    pub format: TextFormat,
759}
760
761impl TextConfig {
762    pub(crate) fn structured_output<S>(name: S, schema: serde_json::Value) -> Self
763    where
764        S: Into<String>,
765    {
766        Self {
767            format: TextFormat::JsonSchema(StructuredOutputsInput {
768                name: name.into(),
769                schema,
770                strict: true,
771            }),
772        }
773    }
774}
775
776/// The text format (contained by [`TextConfig`]).
777/// You can either have plain text by default, or attach a JSON schema for the purposes of structured outputs.
778#[derive(Clone, Debug, Serialize, Deserialize, Default)]
779#[serde(tag = "type")]
780#[serde(rename_all = "snake_case")]
781pub enum TextFormat {
782    JsonSchema(StructuredOutputsInput),
783    #[default]
784    Text,
785}
786
787/// The inputs required for adding structured outputs.
788#[derive(Clone, Debug, Serialize, Deserialize)]
789pub struct StructuredOutputsInput {
790    /// The name of your schema.
791    pub name: String,
792    /// 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>.
793    pub schema: serde_json::Value,
794    /// 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.
795    pub strict: bool,
796}
797
798/// Add reasoning to a [`CompletionRequest`].
799#[derive(Clone, Debug, Default, Serialize, Deserialize)]
800pub struct Reasoning {
801    /// How much effort you want the model to put into thinking/reasoning.
802    pub effort: Option<ReasoningEffort>,
803    /// How much effort you want the model to put into writing the reasoning summary.
804    #[serde(skip_serializing_if = "Option::is_none")]
805    pub summary: Option<ReasoningSummaryLevel>,
806}
807
808impl Reasoning {
809    /// Creates a new Reasoning instantiation (with empty values).
810    pub fn new() -> Self {
811        Self {
812            effort: None,
813            summary: None,
814        }
815    }
816
817    /// Adds reasoning effort.
818    pub fn with_effort(mut self, reasoning_effort: ReasoningEffort) -> Self {
819        self.effort = Some(reasoning_effort);
820
821        self
822    }
823
824    /// Adds summary level (how detailed the reasoning summary will be).
825    pub fn with_summary_level(mut self, reasoning_summary_level: ReasoningSummaryLevel) -> Self {
826        self.summary = Some(reasoning_summary_level);
827
828        self
829    }
830}
831
832/// The billing service tier that will be used. On auto by default.
833#[derive(Clone, Debug, Default, Serialize, Deserialize)]
834#[serde(rename_all = "snake_case")]
835pub enum OpenAIServiceTier {
836    #[default]
837    Auto,
838    Default,
839    Flex,
840}
841
842/// The amount of reasoning effort that will be used by a given model.
843#[derive(Clone, Debug, Default, Serialize, Deserialize)]
844#[serde(rename_all = "snake_case")]
845pub enum ReasoningEffort {
846    Minimal,
847    Low,
848    #[default]
849    Medium,
850    High,
851}
852
853/// The amount of effort that will go into a reasoning summary by a given model.
854#[derive(Clone, Debug, Default, Serialize, Deserialize)]
855#[serde(rename_all = "snake_case")]
856pub enum ReasoningSummaryLevel {
857    #[default]
858    Auto,
859    Concise,
860    Detailed,
861}
862
863/// Results to additionally include in the OpenAI Responses API.
864/// Note that most of these are currently unsupported, but have been added for completeness.
865#[derive(Clone, Debug, Deserialize, Serialize)]
866pub enum Include {
867    #[serde(rename = "file_search_call.results")]
868    FileSearchCallResults,
869    #[serde(rename = "message.input_image.image_url")]
870    MessageInputImageImageUrl,
871    #[serde(rename = "computer_call.output.image_url")]
872    ComputerCallOutputOutputImageUrl,
873    #[serde(rename = "reasoning.encrypted_content")]
874    ReasoningEncryptedContent,
875    #[serde(rename = "code_interpreter_call.outputs")]
876    CodeInterpreterCallOutputs,
877}
878
879/// A currently non-exhaustive list of output types.
880#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
881#[serde(tag = "type")]
882#[serde(rename_all = "snake_case")]
883pub enum Output {
884    Message(OutputMessage),
885    #[serde(alias = "function_call")]
886    FunctionCall(OutputFunctionCall),
887    Reasoning {
888        id: String,
889        summary: Vec<ReasoningSummary>,
890    },
891}
892
893impl From<Output> for Vec<completion::AssistantContent> {
894    fn from(value: Output) -> Self {
895        let res: Vec<completion::AssistantContent> = match value {
896            Output::Message(OutputMessage { content, .. }) => content
897                .into_iter()
898                .map(completion::AssistantContent::from)
899                .collect(),
900            Output::FunctionCall(OutputFunctionCall {
901                id,
902                arguments,
903                call_id,
904                name,
905                ..
906            }) => vec![completion::AssistantContent::tool_call_with_call_id(
907                id, call_id, name, arguments,
908            )],
909            Output::Reasoning { id, summary } => {
910                let summary: Vec<String> = summary.into_iter().map(|x| x.text()).collect();
911
912                vec![completion::AssistantContent::Reasoning(
913                    message::Reasoning::multi(summary).with_id(id),
914                )]
915            }
916        };
917
918        res
919    }
920}
921
922#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
923pub struct OutputReasoning {
924    id: String,
925    summary: Vec<ReasoningSummary>,
926    status: ToolStatus,
927}
928
929/// 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.
930#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
931pub struct OutputFunctionCall {
932    pub id: String,
933    #[serde(with = "json_utils::stringified_json")]
934    pub arguments: serde_json::Value,
935    pub call_id: String,
936    pub name: String,
937    pub status: ToolStatus,
938}
939
940/// The status of a given tool.
941#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
942#[serde(rename_all = "snake_case")]
943pub enum ToolStatus {
944    InProgress,
945    Completed,
946    Incomplete,
947}
948
949/// An output message from OpenAI's Responses API.
950#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
951pub struct OutputMessage {
952    /// The message ID. Must be included when sending the message back to OpenAI
953    pub id: String,
954    /// The role (currently only Assistant is available as this struct is only created when receiving an LLM message as a response)
955    pub role: OutputRole,
956    /// The status of the response
957    pub status: ResponseStatus,
958    /// The actual message content
959    pub content: Vec<AssistantContent>,
960}
961
962/// The role of an output message.
963#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
964#[serde(rename_all = "snake_case")]
965pub enum OutputRole {
966    Assistant,
967}
968
969impl completion::CompletionModel for ResponsesCompletionModel {
970    type Response = CompletionResponse;
971    type StreamingResponse = StreamingCompletionResponse;
972
973    #[cfg_attr(feature = "worker", worker::send)]
974    async fn completion(
975        &self,
976        completion_request: crate::completion::CompletionRequest,
977    ) -> Result<completion::CompletionResponse<Self::Response>, CompletionError> {
978        let request = self.create_completion_request(completion_request)?;
979        let request = serde_json::to_value(request)?;
980
981        tracing::debug!("OpenAI input: {}", serde_json::to_string_pretty(&request)?);
982
983        let response = self.client.post("/responses").json(&request).send().await?;
984
985        if response.status().is_success() {
986            let t = response.text().await?;
987            tracing::debug!(target: "rig", "OpenAI response: {}", t);
988
989            let response = serde_json::from_str::<Self::Response>(&t)?;
990            response.try_into()
991        } else {
992            Err(CompletionError::ProviderError(response.text().await?))
993        }
994    }
995
996    #[cfg_attr(feature = "worker", worker::send)]
997    async fn stream(
998        &self,
999        request: crate::completion::CompletionRequest,
1000    ) -> Result<
1001        crate::streaming::StreamingCompletionResponse<Self::StreamingResponse>,
1002        CompletionError,
1003    > {
1004        Self::stream(self, request).await
1005    }
1006}
1007
1008impl TryFrom<CompletionResponse> for completion::CompletionResponse<CompletionResponse> {
1009    type Error = CompletionError;
1010
1011    fn try_from(response: CompletionResponse) -> Result<Self, Self::Error> {
1012        if response.output.is_empty() {
1013            return Err(CompletionError::ResponseError(
1014                "Response contained no parts".to_owned(),
1015            ));
1016        }
1017
1018        let content: Vec<completion::AssistantContent> = response
1019            .output
1020            .iter()
1021            .cloned()
1022            .flat_map(<Vec<completion::AssistantContent>>::from)
1023            .collect();
1024
1025        let choice = OneOrMany::many(content).map_err(|_| {
1026            CompletionError::ResponseError(
1027                "Response contained no message or tool call (empty)".to_owned(),
1028            )
1029        })?;
1030
1031        let usage = response
1032            .usage
1033            .as_ref()
1034            .map(|usage| completion::Usage {
1035                input_tokens: usage.input_tokens,
1036                output_tokens: usage.output_tokens,
1037                total_tokens: usage.total_tokens,
1038            })
1039            .unwrap_or_default();
1040
1041        Ok(completion::CompletionResponse {
1042            choice,
1043            usage,
1044            raw_response: response,
1045        })
1046    }
1047}
1048
1049/// An OpenAI Responses API message.
1050#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
1051#[serde(tag = "role", rename_all = "lowercase")]
1052pub enum Message {
1053    #[serde(alias = "developer")]
1054    System {
1055        #[serde(deserialize_with = "string_or_one_or_many")]
1056        content: OneOrMany<SystemContent>,
1057        #[serde(skip_serializing_if = "Option::is_none")]
1058        name: Option<String>,
1059    },
1060    User {
1061        #[serde(deserialize_with = "string_or_one_or_many")]
1062        content: OneOrMany<UserContent>,
1063        #[serde(skip_serializing_if = "Option::is_none")]
1064        name: Option<String>,
1065    },
1066    Assistant {
1067        content: OneOrMany<AssistantContentType>,
1068        #[serde(skip_serializing_if = "String::is_empty")]
1069        id: String,
1070        #[serde(skip_serializing_if = "Option::is_none")]
1071        name: Option<String>,
1072        status: ToolStatus,
1073    },
1074    #[serde(rename = "tool")]
1075    ToolResult {
1076        tool_call_id: String,
1077        output: String,
1078    },
1079}
1080
1081/// The type of a tool result content item.
1082#[derive(Default, Debug, Serialize, Deserialize, PartialEq, Clone)]
1083#[serde(rename_all = "lowercase")]
1084pub enum ToolResultContentType {
1085    #[default]
1086    Text,
1087}
1088
1089impl Message {
1090    pub fn system(content: &str) -> Self {
1091        Message::System {
1092            content: OneOrMany::one(content.to_owned().into()),
1093            name: None,
1094        }
1095    }
1096}
1097
1098/// Text assistant content.
1099/// Note that the text type in comparison to the Completions API is actually `output_text` rather than `text`.
1100#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
1101#[serde(tag = "type", rename_all = "snake_case")]
1102pub enum AssistantContent {
1103    OutputText(Text),
1104    Refusal { refusal: String },
1105}
1106
1107impl From<AssistantContent> for completion::AssistantContent {
1108    fn from(value: AssistantContent) -> Self {
1109        match value {
1110            AssistantContent::Refusal { refusal } => {
1111                completion::AssistantContent::Text(Text { text: refusal })
1112            }
1113            AssistantContent::OutputText(Text { text }) => {
1114                completion::AssistantContent::Text(Text { text })
1115            }
1116        }
1117    }
1118}
1119
1120/// The type of assistant content.
1121#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
1122#[serde(untagged)]
1123pub enum AssistantContentType {
1124    Text(AssistantContent),
1125    ToolCall(OutputFunctionCall),
1126    Reasoning(OpenAIReasoning),
1127}
1128
1129/// Different types of user content.
1130#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
1131#[serde(tag = "type", rename_all = "snake_case")]
1132pub enum UserContent {
1133    InputText {
1134        text: String,
1135    },
1136    InputImage {
1137        image_url: ImageUrl,
1138    },
1139    Audio {
1140        input_audio: InputAudio,
1141    },
1142    #[serde(rename = "tool")]
1143    ToolResult {
1144        tool_call_id: String,
1145        output: String,
1146    },
1147}
1148
1149impl TryFrom<message::Message> for Vec<Message> {
1150    type Error = message::MessageError;
1151
1152    fn try_from(message: message::Message) -> Result<Self, Self::Error> {
1153        match message {
1154            message::Message::User { content } => {
1155                let (tool_results, other_content): (Vec<_>, Vec<_>) = content
1156                    .into_iter()
1157                    .partition(|content| matches!(content, message::UserContent::ToolResult(_)));
1158
1159                // If there are messages with both tool results and user content, openai will only
1160                //  handle tool results. It's unlikely that there will be both.
1161                if !tool_results.is_empty() {
1162                    tool_results
1163                        .into_iter()
1164                        .map(|content| match content {
1165                            message::UserContent::ToolResult(message::ToolResult {
1166                                call_id,
1167                                content,
1168                                ..
1169                            }) => Ok::<_, message::MessageError>(Message::ToolResult {
1170                                tool_call_id: call_id.expect("The tool call ID should exist"),
1171                                output: {
1172                                    let res = content.first();
1173                                    match res {
1174                                        completion::message::ToolResultContent::Text(Text {
1175                                            text,
1176                                        }) => text,
1177                                        _ => return  Err(MessageError::ConversionError("This API only currently supports text tool results".into()))
1178                                    }
1179                                },
1180                            }),
1181                            _ => unreachable!(),
1182                        })
1183                        .collect::<Result<Vec<_>, _>>()
1184                } else {
1185                    let other_content = other_content
1186                        .into_iter()
1187                        .map(|content| match content {
1188                            message::UserContent::Text(message::Text { text }) => {
1189                                Ok(UserContent::InputText { text })
1190                            }
1191                            message::UserContent::Image(message::Image {
1192                                data,
1193                                detail,
1194                                media_type,
1195                                ..
1196                            }) => {
1197                                let url = match data {
1198                                    DocumentSourceKind::Base64(data) => {
1199                                        let media_type = if let Some(media_type) = media_type {
1200                                            media_type.to_mime_type().to_string()
1201                                        } else {
1202                                            String::new()
1203                                        };
1204                                        format!("data:{media_type};base64,{data}")
1205                                    }
1206                                    DocumentSourceKind::Url(url) => url,
1207                                    DocumentSourceKind::Unknown => return Err(MessageError::ConversionError("Attempted to convert unknown image type to OpenAI image input".to_string()))
1208                                };
1209
1210                                Ok(UserContent::InputImage {
1211                                    image_url: ImageUrl {
1212                                        url,
1213                                        detail: detail.unwrap_or_default(),
1214                                    },
1215                                })
1216                            }
1217                            message::UserContent::Document(message::Document { data, .. }) => {
1218                                Ok(UserContent::InputText { text: data })
1219                            }
1220                            message::UserContent::Audio(message::Audio {
1221                                data,
1222                                media_type,
1223                                ..
1224                            }) => Ok(UserContent::Audio {
1225                                input_audio: InputAudio {
1226                                    data,
1227                                    format: match media_type {
1228                                        Some(media_type) => media_type,
1229                                        None => AudioMediaType::MP3,
1230                                    },
1231                                },
1232                            }),
1233                            _ => unreachable!(),
1234                        })
1235                        .collect::<Result<Vec<_>, _>>()?;
1236
1237                    let other_content = OneOrMany::many(other_content).expect(
1238                        "There must be other content here if there were no tool result content",
1239                    );
1240
1241                    Ok(vec![Message::User {
1242                        content: other_content,
1243                        name: None,
1244                    }])
1245                }
1246            }
1247            message::Message::Assistant { content, id } => {
1248                let assistant_message_id = id;
1249
1250                match content.first() {
1251                    crate::message::AssistantContent::Text(Text { text }) => {
1252                        Ok(vec![Message::Assistant {
1253                            id: assistant_message_id
1254                                .expect("The assistant message ID should exist"),
1255                            status: ToolStatus::Completed,
1256                            content: OneOrMany::one(AssistantContentType::Text(
1257                                AssistantContent::OutputText(Text { text }),
1258                            )),
1259                            name: None,
1260                        }])
1261                    }
1262                    crate::message::AssistantContent::ToolCall(crate::message::ToolCall {
1263                        id,
1264                        call_id,
1265                        function,
1266                    }) => Ok(vec![Message::Assistant {
1267                        content: OneOrMany::one(AssistantContentType::ToolCall(
1268                            OutputFunctionCall {
1269                                call_id: call_id.expect("The call ID should exist"),
1270                                arguments: function.arguments,
1271                                id,
1272                                name: function.name,
1273                                status: ToolStatus::Completed,
1274                            },
1275                        )),
1276                        id: assistant_message_id.expect("The assistant message ID should exist!"),
1277                        name: None,
1278                        status: ToolStatus::Completed,
1279                    }]),
1280                    crate::message::AssistantContent::Reasoning(crate::message::Reasoning {
1281                        id,
1282                        reasoning,
1283                    }) => Ok(vec![Message::Assistant {
1284                        content: OneOrMany::one(AssistantContentType::Reasoning(OpenAIReasoning {
1285                            id: id.expect("An OpenAI-generated ID is required when using OpenAI reasoning items"),
1286                            summary: reasoning.into_iter().map(|x| ReasoningSummary::SummaryText { text: x }).collect(),
1287                            encrypted_content: None,
1288                            status: Some(ToolStatus::Completed),
1289                        })),
1290                        id: assistant_message_id.expect("The assistant message ID should exist!"),
1291                        name: None,
1292                        status: (ToolStatus::Completed),
1293                    }]),
1294                }
1295            }
1296        }
1297    }
1298}
1299
1300impl FromStr for UserContent {
1301    type Err = Infallible;
1302
1303    fn from_str(s: &str) -> Result<Self, Self::Err> {
1304        Ok(UserContent::InputText {
1305            text: s.to_string(),
1306        })
1307    }
1308}