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.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, MessageError, 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                        _ => {
245                            return Err(CompletionError::ProviderError(
246                                "This API only supports text and tool results at the moment"
247                                    .to_string(),
248                            ));
249                        }
250                    }
251                }
252
253                Ok(items)
254            }
255            crate::completion::Message::Assistant { id, content } => {
256                let mut items = Vec::new();
257
258                for assistant_content in content {
259                    match assistant_content {
260                        crate::message::AssistantContent::Text(Text { text }) => {
261                            let id = id.as_ref().unwrap_or(&String::default()).clone();
262                            items.push(InputItem {
263                                role: Some(Role::Assistant),
264                                input: InputContent::Message(Message::Assistant {
265                                    content: OneOrMany::one(AssistantContentType::Text(
266                                        AssistantContent::OutputText(Text { text }),
267                                    )),
268                                    id,
269                                    name: None,
270                                    status: ToolStatus::Completed,
271                                }),
272                            });
273                        }
274                        crate::message::AssistantContent::ToolCall(crate::message::ToolCall {
275                            id: tool_id,
276                            call_id,
277                            function,
278                        }) => {
279                            items.push(InputItem {
280                                role: None,
281                                input: InputContent::FunctionCall(OutputFunctionCall {
282                                    arguments: function.arguments,
283                                    call_id: call_id.expect("The tool call ID should exist!"),
284                                    id: tool_id,
285                                    name: function.name,
286                                    status: ToolStatus::Completed,
287                                }),
288                            });
289                        }
290                        crate::message::AssistantContent::Reasoning(
291                            crate::message::Reasoning { id, reasoning },
292                        ) => {
293                            items.push(InputItem {
294                                role: None,
295                                input: InputContent::Reasoning(OpenAIReasoning {
296                                    id: id
297                                        .expect("An OpenAI-generated ID is required when using OpenAI reasoning items"),
298                                    summary: reasoning.into_iter().map(|x| ReasoningSummary::new(&x)).collect(),
299                                    encrypted_content: None,
300                                    status: None,
301                                }),
302                            });
303                        }
304                    }
305                }
306
307                Ok(items)
308            }
309        }
310    }
311}
312
313impl From<OneOrMany<String>> for Vec<ReasoningSummary> {
314    fn from(value: OneOrMany<String>) -> Self {
315        value.iter().map(|x| ReasoningSummary::new(x)).collect()
316    }
317}
318
319/// The definition of a tool response, repurposed for OpenAI's Responses API.
320#[derive(Debug, Deserialize, Serialize, Clone)]
321pub struct ResponsesToolDefinition {
322    /// Tool name
323    pub name: String,
324    /// 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).
325    pub parameters: serde_json::Value,
326    /// Whether to use strict mode. Enabled by default as it allows for improved efficiency.
327    pub strict: bool,
328    /// The type of tool. This should always be "function".
329    #[serde(rename = "type")]
330    pub kind: String,
331    /// Tool description.
332    pub description: String,
333}
334
335impl From<completion::ToolDefinition> for ResponsesToolDefinition {
336    fn from(value: completion::ToolDefinition) -> Self {
337        let completion::ToolDefinition {
338            name,
339            mut parameters,
340            description,
341        } = value;
342
343        let parameters = parameters
344            .as_object_mut()
345            .expect("parameters should be a JSON object");
346        parameters.insert(
347            "additionalProperties".to_string(),
348            serde_json::Value::Bool(false),
349        );
350
351        let parameters = serde_json::Value::Object(parameters.clone());
352
353        Self {
354            name,
355            parameters,
356            description,
357            kind: "function".to_string(),
358            strict: true,
359        }
360    }
361}
362
363/// Token usage.
364/// 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.
365#[derive(Clone, Debug, Serialize, Deserialize)]
366pub struct ResponsesUsage {
367    /// Input tokens
368    pub input_tokens: u64,
369    /// In-depth detail on input tokens (cached tokens)
370    #[serde(skip_serializing_if = "Option::is_none")]
371    pub input_tokens_details: Option<InputTokensDetails>,
372    /// Output tokens
373    pub output_tokens: u64,
374    /// In-depth detail on output tokens (reasoning tokens)
375    pub output_tokens_details: OutputTokensDetails,
376    /// Total tokens used (for a given prompt)
377    pub total_tokens: u64,
378}
379
380impl ResponsesUsage {
381    /// Create a new ResponsesUsage instance
382    pub(crate) fn new() -> Self {
383        Self {
384            input_tokens: 0,
385            input_tokens_details: Some(InputTokensDetails::new()),
386            output_tokens: 0,
387            output_tokens_details: OutputTokensDetails::new(),
388            total_tokens: 0,
389        }
390    }
391}
392
393impl Add for ResponsesUsage {
394    type Output = Self;
395
396    fn add(self, rhs: Self) -> Self::Output {
397        let input_tokens = self.input_tokens + rhs.input_tokens;
398        let input_tokens_details = self.input_tokens_details.map(|lhs| {
399            if let Some(tokens) = rhs.input_tokens_details {
400                lhs + tokens
401            } else {
402                lhs
403            }
404        });
405        let output_tokens = self.output_tokens + rhs.output_tokens;
406        let output_tokens_details = self.output_tokens_details + rhs.output_tokens_details;
407        let total_tokens = self.total_tokens + rhs.total_tokens;
408        Self {
409            input_tokens,
410            input_tokens_details,
411            output_tokens,
412            output_tokens_details,
413            total_tokens,
414        }
415    }
416}
417
418/// In-depth details on input tokens.
419#[derive(Clone, Debug, Serialize, Deserialize)]
420pub struct InputTokensDetails {
421    /// Cached tokens from OpenAI
422    pub cached_tokens: u64,
423}
424
425impl InputTokensDetails {
426    pub(crate) fn new() -> Self {
427        Self { cached_tokens: 0 }
428    }
429}
430
431impl Add for InputTokensDetails {
432    type Output = Self;
433    fn add(self, rhs: Self) -> Self::Output {
434        Self {
435            cached_tokens: self.cached_tokens + rhs.cached_tokens,
436        }
437    }
438}
439
440/// In-depth details on output tokens.
441#[derive(Clone, Debug, Serialize, Deserialize)]
442pub struct OutputTokensDetails {
443    /// Reasoning tokens
444    pub reasoning_tokens: u64,
445}
446
447impl OutputTokensDetails {
448    pub(crate) fn new() -> Self {
449        Self {
450            reasoning_tokens: 0,
451        }
452    }
453}
454
455impl Add for OutputTokensDetails {
456    type Output = Self;
457    fn add(self, rhs: Self) -> Self::Output {
458        Self {
459            reasoning_tokens: self.reasoning_tokens + rhs.reasoning_tokens,
460        }
461    }
462}
463
464/// Occasionally, when using OpenAI's Responses API you may get an incomplete response. This struct holds the reason as to why it happened.
465#[derive(Clone, Debug, Default, Serialize, Deserialize)]
466pub struct IncompleteDetailsReason {
467    /// The reason for an incomplete [`CompletionResponse`].
468    pub reason: String,
469}
470
471/// A response error from OpenAI's Response API.
472#[derive(Clone, Debug, Default, Serialize, Deserialize)]
473pub struct ResponseError {
474    /// Error code
475    pub code: String,
476    /// Error message
477    pub message: String,
478}
479
480/// A response object as an enum (ensures type validation)
481#[derive(Clone, Debug, Deserialize, Serialize)]
482#[serde(rename_all = "snake_case")]
483pub enum ResponseObject {
484    Response,
485}
486
487/// The response status as an enum (ensures type validation)
488#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
489#[serde(rename_all = "snake_case")]
490pub enum ResponseStatus {
491    InProgress,
492    Completed,
493    Failed,
494    Cancelled,
495    Queued,
496    Incomplete,
497}
498
499/// Attempt to try and create a `NewCompletionRequest` from a model name and [`crate::completion::CompletionRequest`]
500impl TryFrom<(String, crate::completion::CompletionRequest)> for CompletionRequest {
501    type Error = CompletionError;
502    fn try_from(
503        (model, req): (String, crate::completion::CompletionRequest),
504    ) -> Result<Self, Self::Error> {
505        let input = {
506            let mut partial_history = vec![];
507            if let Some(docs) = req.normalized_documents() {
508                partial_history.push(docs);
509            }
510            partial_history.extend(req.chat_history);
511
512            // Initialize full history with preamble (or empty if non-existent)
513            let mut full_history: Vec<InputItem> = Vec::new();
514
515            // Convert and extend the rest of the history
516            full_history.extend(
517                partial_history
518                    .into_iter()
519                    .map(|x| <Vec<InputItem>>::try_from(x).unwrap())
520                    .collect::<Vec<Vec<InputItem>>>()
521                    .into_iter()
522                    .flatten()
523                    .collect::<Vec<InputItem>>(),
524            );
525
526            full_history
527        };
528
529        let input = OneOrMany::many(input)
530            .expect("This should never panic - if it does, please file a bug report");
531
532        let stream = req
533            .additional_params
534            .clone()
535            .unwrap_or(Value::Null)
536            .as_bool();
537
538        let additional_parameters = if let Some(map) = req.additional_params {
539            serde_json::from_value::<AdditionalParameters>(map).expect("Converting additional parameters to AdditionalParameters should never fail as every field is an Option")
540        } else {
541            // If there's no additional parameters, initialise an empty object
542            AdditionalParameters::default()
543        };
544
545        Ok(Self {
546            input,
547            model,
548            instructions: req.preamble,
549            max_output_tokens: req.max_tokens,
550            stream,
551            tools: req
552                .tools
553                .into_iter()
554                .map(ResponsesToolDefinition::from)
555                .collect(),
556            temperature: req.temperature,
557            additional_parameters,
558        })
559    }
560}
561
562/// The completion model struct for OpenAI's response API.
563#[derive(Clone)]
564pub struct ResponsesCompletionModel {
565    /// The OpenAI client
566    pub(crate) client: Client,
567    /// Name of the model (e.g.: gpt-3.5-turbo-1106)
568    pub model: String,
569}
570
571impl ResponsesCompletionModel {
572    /// Creates a new [`ResponsesCompletionModel`].
573    pub fn new(client: Client, model: &str) -> Self {
574        Self {
575            client,
576            model: model.to_string(),
577        }
578    }
579
580    /// Use the Completions API instead of Responses.
581    pub fn completions_api(self) -> crate::providers::openai::completion::CompletionModel {
582        crate::providers::openai::completion::CompletionModel::new(self.client, &self.model)
583    }
584
585    /// Attempt to create a completion request from [`crate::completion::CompletionRequest`].
586    pub(crate) fn create_completion_request(
587        &self,
588        completion_request: crate::completion::CompletionRequest,
589    ) -> Result<CompletionRequest, CompletionError> {
590        let req = CompletionRequest::try_from((self.model.clone(), completion_request))?;
591
592        Ok(req)
593    }
594}
595
596/// The standard response format from OpenAI's Responses API.
597#[derive(Clone, Debug, Serialize, Deserialize)]
598pub struct CompletionResponse {
599    /// The ID of a completion response.
600    pub id: String,
601    /// The type of the object.
602    pub object: ResponseObject,
603    /// The time at which a given response has been created, in seconds from the UNIX epoch (01/01/1970 00:00:00).
604    pub created_at: u64,
605    /// The status of the response.
606    pub status: ResponseStatus,
607    /// Response error (optional)
608    pub error: Option<ResponseError>,
609    /// Incomplete response details (optional)
610    pub incomplete_details: Option<IncompleteDetailsReason>,
611    /// System prompt/preamble
612    pub instructions: Option<String>,
613    /// The maximum number of tokens the model should output
614    pub max_output_tokens: Option<u64>,
615    /// The model name
616    pub model: String,
617    /// Token usage
618    pub usage: Option<ResponsesUsage>,
619    /// The model output (messages, etc will go here)
620    pub output: Vec<Output>,
621    /// Tools
622    pub tools: Vec<ResponsesToolDefinition>,
623    /// Additional parameters
624    #[serde(flatten)]
625    pub additional_parameters: AdditionalParameters,
626}
627
628/// Additional parameters for the completion request type for OpenAI's Response API: <https://platform.openai.com/docs/api-reference/responses/create>
629/// Intended to be derived from [`crate::completion::request::CompletionRequest`].
630#[derive(Clone, Debug, Deserialize, Serialize, Default)]
631pub struct AdditionalParameters {
632    /// Whether or not a given model task should run in the background (ie a detached process).
633    #[serde(skip_serializing_if = "Option::is_none")]
634    pub background: Option<bool>,
635    /// The text response format. This is where you would add structured outputs (if you want them).
636    #[serde(skip_serializing_if = "Option::is_none")]
637    pub text: Option<TextConfig>,
638    /// 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!
639    #[serde(skip_serializing_if = "Option::is_none")]
640    pub include: Option<Vec<Include>>,
641    /// `top_p`. Mutually exclusive with the `temperature` argument.
642    #[serde(skip_serializing_if = "Option::is_none")]
643    pub top_p: Option<f64>,
644    /// Whether or not the response should be truncated.
645    #[serde(skip_serializing_if = "Option::is_none")]
646    pub truncation: Option<TruncationStrategy>,
647    /// The username of the user (that you want to use).
648    #[serde(skip_serializing_if = "Option::is_none")]
649    pub user: Option<String>,
650    /// Any additional metadata you'd like to add. This will additionally be returned by the response.
651    #[serde(skip_serializing_if = "Map::is_empty", default)]
652    pub metadata: serde_json::Map<String, serde_json::Value>,
653    /// Whether or not you want tool calls to run in parallel.
654    #[serde(skip_serializing_if = "Option::is_none")]
655    pub parallel_tool_calls: Option<bool>,
656    /// Previous response ID. If you are not sending a full conversation, this can help to track the message flow.
657    #[serde(skip_serializing_if = "Option::is_none")]
658    pub previous_response_id: Option<String>,
659    /// Add thinking/reasoning to your response. The response will be emitted as a list member of the `output` field.
660    #[serde(skip_serializing_if = "Option::is_none")]
661    pub reasoning: Option<Reasoning>,
662    /// The service tier you're using.
663    #[serde(skip_serializing_if = "Option::is_none")]
664    pub service_tier: Option<OpenAIServiceTier>,
665    /// Whether or not to store the response for later retrieval by API.
666    #[serde(skip_serializing_if = "Option::is_none")]
667    pub store: Option<bool>,
668}
669
670impl AdditionalParameters {
671    pub fn to_json(self) -> serde_json::Value {
672        serde_json::to_value(self).expect("this should never fail since a struct that impls Deserialize will always be valid JSON")
673    }
674}
675
676/// The truncation strategy.
677/// 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.
678/// Otherwise, does nothing (and is disabled by default).
679#[derive(Clone, Debug, Default, Serialize, Deserialize)]
680#[serde(rename_all = "snake_case")]
681pub enum TruncationStrategy {
682    Auto,
683    #[default]
684    Disabled,
685}
686
687/// The model output format configuration.
688/// You can either have plain text by default, or attach a JSON schema for the purposes of structured outputs.
689#[derive(Clone, Debug, Serialize, Deserialize)]
690pub struct TextConfig {
691    pub format: TextFormat,
692}
693
694impl TextConfig {
695    pub(crate) fn structured_output<S>(name: S, schema: serde_json::Value) -> Self
696    where
697        S: Into<String>,
698    {
699        Self {
700            format: TextFormat::JsonSchema(StructuredOutputsInput {
701                name: name.into(),
702                schema,
703                strict: true,
704            }),
705        }
706    }
707}
708
709/// The text format (contained by [`TextConfig`]).
710/// You can either have plain text by default, or attach a JSON schema for the purposes of structured outputs.
711#[derive(Clone, Debug, Serialize, Deserialize, Default)]
712#[serde(tag = "type")]
713#[serde(rename_all = "snake_case")]
714pub enum TextFormat {
715    JsonSchema(StructuredOutputsInput),
716    #[default]
717    Text,
718}
719
720/// The inputs required for adding structured outputs.
721#[derive(Clone, Debug, Serialize, Deserialize)]
722pub struct StructuredOutputsInput {
723    /// The name of your schema.
724    pub name: String,
725    /// 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>.
726    pub schema: serde_json::Value,
727    /// 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.
728    pub strict: bool,
729}
730
731/// Add reasoning to a [`CompletionRequest`].
732#[derive(Clone, Debug, Default, Serialize, Deserialize)]
733pub struct Reasoning {
734    /// How much effort you want the model to put into thinking/reasoning.
735    pub effort: Option<ReasoningEffort>,
736    /// How much effort you want the model to put into writing the reasoning summary.
737    #[serde(skip_serializing_if = "Option::is_none")]
738    pub summary: Option<ReasoningSummaryLevel>,
739}
740
741impl Reasoning {
742    /// Creates a new Reasoning instantiation (with empty values).
743    pub fn new() -> Self {
744        Self {
745            effort: None,
746            summary: None,
747        }
748    }
749
750    /// Adds reasoning effort.
751    pub fn with_effort(mut self, reasoning_effort: ReasoningEffort) -> Self {
752        self.effort = Some(reasoning_effort);
753
754        self
755    }
756
757    /// Adds summary level (how detailed the reasoning summary will be).
758    pub fn with_summary_level(mut self, reasoning_summary_level: ReasoningSummaryLevel) -> Self {
759        self.summary = Some(reasoning_summary_level);
760
761        self
762    }
763}
764
765/// The billing service tier that will be used. On auto by default.
766#[derive(Clone, Debug, Default, Serialize, Deserialize)]
767#[serde(rename_all = "snake_case")]
768pub enum OpenAIServiceTier {
769    #[default]
770    Auto,
771    Default,
772    Flex,
773}
774
775/// The amount of reasoning effort that will be used by a given model.
776#[derive(Clone, Debug, Default, Serialize, Deserialize)]
777#[serde(rename_all = "snake_case")]
778pub enum ReasoningEffort {
779    Low,
780    #[default]
781    Medium,
782    High,
783}
784
785/// The amount of effort that will go into a reasoning summary by a given model.
786#[derive(Clone, Debug, Default, Serialize, Deserialize)]
787#[serde(rename_all = "snake_case")]
788pub enum ReasoningSummaryLevel {
789    #[default]
790    Auto,
791    Concise,
792    Detailed,
793}
794
795/// Results to additionally include in the OpenAI Responses API.
796/// Note that most of these are currently unsupported, but have been added for completeness.
797#[derive(Clone, Debug, Deserialize, Serialize)]
798pub enum Include {
799    #[serde(rename = "file_search_call.results")]
800    FileSearchCallResults,
801    #[serde(rename = "message.input_image.image_url")]
802    MessageInputImageImageUrl,
803    #[serde(rename = "computer_call.output.image_url")]
804    ComputerCallOutputOutputImageUrl,
805    #[serde(rename = "reasoning.encrypted_content")]
806    ReasoningEncryptedContent,
807    #[serde(rename = "code_interpreter_call.outputs")]
808    CodeInterpreterCallOutputs,
809}
810
811/// A currently non-exhaustive list of output types.
812#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
813#[serde(tag = "type")]
814#[serde(rename_all = "snake_case")]
815pub enum Output {
816    Message(OutputMessage),
817    #[serde(alias = "function_call")]
818    FunctionCall(OutputFunctionCall),
819    Reasoning {
820        id: String,
821        summary: Vec<ReasoningSummary>,
822    },
823}
824
825impl From<Output> for Vec<completion::AssistantContent> {
826    fn from(value: Output) -> Self {
827        let res: Vec<completion::AssistantContent> = match value {
828            Output::Message(OutputMessage { content, .. }) => content
829                .into_iter()
830                .map(completion::AssistantContent::from)
831                .collect(),
832            Output::FunctionCall(OutputFunctionCall {
833                id,
834                arguments,
835                call_id,
836                name,
837                ..
838            }) => vec![completion::AssistantContent::tool_call_with_call_id(
839                id, call_id, name, arguments,
840            )],
841            Output::Reasoning { id, summary } => {
842                let summary: Vec<String> = summary.into_iter().map(|x| x.text()).collect();
843
844                vec![completion::AssistantContent::Reasoning(
845                    message::Reasoning::multi(summary).with_id(id),
846                )]
847            }
848        };
849
850        res
851    }
852}
853
854#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
855pub struct OutputReasoning {
856    id: String,
857    summary: Vec<ReasoningSummary>,
858    status: ToolStatus,
859}
860
861/// 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.
862#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
863pub struct OutputFunctionCall {
864    pub id: String,
865    #[serde(with = "json_utils::stringified_json")]
866    pub arguments: serde_json::Value,
867    pub call_id: String,
868    pub name: String,
869    pub status: ToolStatus,
870}
871
872/// The status of a given tool.
873#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
874#[serde(rename_all = "snake_case")]
875pub enum ToolStatus {
876    InProgress,
877    Completed,
878    Incomplete,
879}
880
881/// An output message from OpenAI's Responses API.
882#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
883pub struct OutputMessage {
884    /// The message ID. Must be included when sending the message back to OpenAI
885    pub id: String,
886    /// The role (currently only Assistant is available as this struct is only created when receiving an LLM message as a response)
887    pub role: OutputRole,
888    /// The status of the response
889    pub status: ResponseStatus,
890    /// The actual message content
891    pub content: Vec<AssistantContent>,
892}
893
894/// The role of an output message.
895#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
896#[serde(rename_all = "snake_case")]
897pub enum OutputRole {
898    Assistant,
899}
900
901impl completion::CompletionModel for ResponsesCompletionModel {
902    type Response = CompletionResponse;
903    type StreamingResponse = StreamingCompletionResponse;
904
905    #[cfg_attr(feature = "worker", worker::send)]
906    async fn completion(
907        &self,
908        completion_request: crate::completion::CompletionRequest,
909    ) -> Result<completion::CompletionResponse<Self::Response>, CompletionError> {
910        let request = self.create_completion_request(completion_request)?;
911        let request = serde_json::to_value(request)?;
912
913        tracing::debug!("OpenAI input: {}", serde_json::to_string_pretty(&request)?);
914
915        let response = self.client.post("/responses").json(&request).send().await?;
916
917        if response.status().is_success() {
918            let t = response.text().await?;
919            tracing::debug!(target: "rig", "OpenAI response: {}", t);
920
921            let response = serde_json::from_str::<Self::Response>(&t)?;
922            response.try_into()
923        } else {
924            Err(CompletionError::ProviderError(response.text().await?))
925        }
926    }
927
928    #[cfg_attr(feature = "worker", worker::send)]
929    async fn stream(
930        &self,
931        request: crate::completion::CompletionRequest,
932    ) -> Result<
933        crate::streaming::StreamingCompletionResponse<Self::StreamingResponse>,
934        CompletionError,
935    > {
936        Self::stream(self, request).await
937    }
938}
939
940impl TryFrom<CompletionResponse> for completion::CompletionResponse<CompletionResponse> {
941    type Error = CompletionError;
942
943    fn try_from(response: CompletionResponse) -> Result<Self, Self::Error> {
944        if response.output.is_empty() {
945            return Err(CompletionError::ResponseError(
946                "Response contained no parts".to_owned(),
947            ));
948        }
949
950        let content: Vec<completion::AssistantContent> = response
951            .output
952            .iter()
953            .cloned()
954            .flat_map(<Vec<completion::AssistantContent>>::from)
955            .collect();
956
957        let choice = OneOrMany::many(content).map_err(|_| {
958            CompletionError::ResponseError(
959                "Response contained no message or tool call (empty)".to_owned(),
960            )
961        })?;
962
963        let usage = response
964            .usage
965            .as_ref()
966            .map(|usage| completion::Usage {
967                input_tokens: usage.input_tokens,
968                output_tokens: usage.output_tokens,
969                total_tokens: usage.total_tokens,
970            })
971            .unwrap_or_default();
972
973        Ok(completion::CompletionResponse {
974            choice,
975            usage,
976            raw_response: response,
977        })
978    }
979}
980
981/// An OpenAI Responses API message.
982#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
983#[serde(tag = "role", rename_all = "lowercase")]
984pub enum Message {
985    #[serde(alias = "developer")]
986    System {
987        #[serde(deserialize_with = "string_or_one_or_many")]
988        content: OneOrMany<SystemContent>,
989        #[serde(skip_serializing_if = "Option::is_none")]
990        name: Option<String>,
991    },
992    User {
993        #[serde(deserialize_with = "string_or_one_or_many")]
994        content: OneOrMany<UserContent>,
995        #[serde(skip_serializing_if = "Option::is_none")]
996        name: Option<String>,
997    },
998    Assistant {
999        content: OneOrMany<AssistantContentType>,
1000        #[serde(skip_serializing_if = "String::is_empty")]
1001        id: String,
1002        #[serde(skip_serializing_if = "Option::is_none")]
1003        name: Option<String>,
1004        status: ToolStatus,
1005    },
1006    #[serde(rename = "tool")]
1007    ToolResult {
1008        tool_call_id: String,
1009        output: String,
1010    },
1011}
1012
1013/// The type of a tool result content item.
1014#[derive(Default, Debug, Serialize, Deserialize, PartialEq, Clone)]
1015#[serde(rename_all = "lowercase")]
1016pub enum ToolResultContentType {
1017    #[default]
1018    Text,
1019}
1020
1021impl Message {
1022    pub fn system(content: &str) -> Self {
1023        Message::System {
1024            content: OneOrMany::one(content.to_owned().into()),
1025            name: None,
1026        }
1027    }
1028}
1029
1030/// Text assistant content.
1031/// Note that the text type in comparison to the Completions API is actually `output_text` rather than `text`.
1032#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
1033#[serde(tag = "type", rename_all = "snake_case")]
1034pub enum AssistantContent {
1035    OutputText(Text),
1036    Refusal { refusal: String },
1037}
1038
1039impl From<AssistantContent> for completion::AssistantContent {
1040    fn from(value: AssistantContent) -> Self {
1041        match value {
1042            AssistantContent::Refusal { refusal } => {
1043                completion::AssistantContent::Text(Text { text: refusal })
1044            }
1045            AssistantContent::OutputText(Text { text }) => {
1046                completion::AssistantContent::Text(Text { text })
1047            }
1048        }
1049    }
1050}
1051
1052/// The type of assistant content.
1053#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
1054#[serde(untagged)]
1055pub enum AssistantContentType {
1056    Text(AssistantContent),
1057    ToolCall(OutputFunctionCall),
1058    Reasoning(OpenAIReasoning),
1059}
1060
1061/// Different types of user content.
1062#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
1063#[serde(tag = "type", rename_all = "snake_case")]
1064pub enum UserContent {
1065    InputText {
1066        text: String,
1067    },
1068    #[serde(rename = "image_url")]
1069    Image {
1070        image_url: ImageUrl,
1071    },
1072    Audio {
1073        input_audio: InputAudio,
1074    },
1075    #[serde(rename = "tool")]
1076    ToolResult {
1077        tool_call_id: String,
1078        output: String,
1079    },
1080}
1081
1082impl TryFrom<message::Message> for Vec<Message> {
1083    type Error = message::MessageError;
1084
1085    fn try_from(message: message::Message) -> Result<Self, Self::Error> {
1086        match message {
1087            message::Message::User { content } => {
1088                let (tool_results, other_content): (Vec<_>, Vec<_>) = content
1089                    .into_iter()
1090                    .partition(|content| matches!(content, message::UserContent::ToolResult(_)));
1091
1092                // If there are messages with both tool results and user content, openai will only
1093                //  handle tool results. It's unlikely that there will be both.
1094                if !tool_results.is_empty() {
1095                    tool_results
1096                        .into_iter()
1097                        .map(|content| match content {
1098                            message::UserContent::ToolResult(message::ToolResult {
1099                                call_id,
1100                                content,
1101                                ..
1102                            }) => Ok::<_, message::MessageError>(Message::ToolResult {
1103                                tool_call_id: call_id.expect("The tool call ID should exist"),
1104                                output: {
1105                                    let res = content.first();
1106                                    match res {
1107                                        completion::message::ToolResultContent::Text(Text {
1108                                            text,
1109                                        }) => text,
1110                                        _ => return  Err(MessageError::ConversionError("This API only currently supports text tool results".into()))
1111                                    }
1112                                },
1113                            }),
1114                            _ => unreachable!(),
1115                        })
1116                        .collect::<Result<Vec<_>, _>>()
1117                } else {
1118                    let other_content = OneOrMany::many(other_content).expect(
1119                        "There must be other content here if there were no tool result content",
1120                    );
1121
1122                    Ok(vec![Message::User {
1123                        content: other_content.map(|content| match content {
1124                            message::UserContent::Text(message::Text { text }) => {
1125                                UserContent::InputText { text }
1126                            }
1127                            message::UserContent::Image(message::Image {
1128                                data, detail, ..
1129                            }) => UserContent::Image {
1130                                image_url: ImageUrl {
1131                                    url: data,
1132                                    detail: detail.unwrap_or_default(),
1133                                },
1134                            },
1135                            message::UserContent::Document(message::Document { data, .. }) => {
1136                                UserContent::InputText { text: data }
1137                            }
1138                            message::UserContent::Audio(message::Audio {
1139                                data,
1140                                media_type,
1141                                ..
1142                            }) => UserContent::Audio {
1143                                input_audio: InputAudio {
1144                                    data,
1145                                    format: match media_type {
1146                                        Some(media_type) => media_type,
1147                                        None => AudioMediaType::MP3,
1148                                    },
1149                                },
1150                            },
1151                            _ => unreachable!(),
1152                        }),
1153                        name: None,
1154                    }])
1155                }
1156            }
1157            message::Message::Assistant { content, id } => {
1158                let assistant_message_id = id;
1159
1160                match content.first() {
1161                    crate::message::AssistantContent::Text(Text { text }) => {
1162                        Ok(vec![Message::Assistant {
1163                            id: assistant_message_id
1164                                .expect("The assistant message ID should exist"),
1165                            status: ToolStatus::Completed,
1166                            content: OneOrMany::one(AssistantContentType::Text(
1167                                AssistantContent::OutputText(Text { text }),
1168                            )),
1169                            name: None,
1170                        }])
1171                    }
1172                    crate::message::AssistantContent::ToolCall(crate::message::ToolCall {
1173                        id,
1174                        call_id,
1175                        function,
1176                    }) => Ok(vec![Message::Assistant {
1177                        content: OneOrMany::one(AssistantContentType::ToolCall(
1178                            OutputFunctionCall {
1179                                call_id: call_id.expect("The call ID should exist"),
1180                                arguments: function.arguments,
1181                                id,
1182                                name: function.name,
1183                                status: ToolStatus::Completed,
1184                            },
1185                        )),
1186                        id: assistant_message_id.expect("The assistant message ID should exist!"),
1187                        name: None,
1188                        status: ToolStatus::Completed,
1189                    }]),
1190                    crate::message::AssistantContent::Reasoning(crate::message::Reasoning {
1191                        id,
1192                        reasoning,
1193                    }) => Ok(vec![Message::Assistant {
1194                        content: OneOrMany::one(AssistantContentType::Reasoning(OpenAIReasoning {
1195                            id: id.expect("An OpenAI-generated ID is required when using OpenAI reasoning items"),
1196                            summary: reasoning.into_iter().map(|x| ReasoningSummary::SummaryText { text: x }).collect(),
1197                            encrypted_content: None,
1198                            status: Some(ToolStatus::Completed),
1199                        })),
1200                        id: assistant_message_id.expect("The assistant message ID should exist!"),
1201                        name: None,
1202                        status: (ToolStatus::Completed),
1203                    }]),
1204                }
1205            }
1206        }
1207    }
1208}
1209
1210impl FromStr for UserContent {
1211    type Err = Infallible;
1212
1213    fn from_str(s: &str) -> Result<Self, Self::Err> {
1214        Ok(UserContent::InputText {
1215            text: s.to_string(),
1216        })
1217    }
1218}