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