Skip to main content

rig_core/providers/openai/extension/
responses.rs

1//! The Responses section of OpenAI's options, and the reply fields only a
2//! Responses reply carries.
3
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6
7use crate::completion::provider_options::reply_field;
8
9/// The fields only OpenAI's Responses route sends, each at the top level of
10/// the body. Serialize-only; an unset field is not sent.
11#[non_exhaustive]
12#[derive(Clone, Debug, Default, PartialEq, Serialize)]
13pub struct OpenAiResponsesOptions {
14    /// `reasoning.summary`, `.mode` and `.context`, merged beside the
15    /// `reasoning.effort` that `GenerationOptions::reasoning` sends.
16    #[serde(skip_serializing_if = "Option::is_none")]
17    pub reasoning: Option<ReasoningOptions>,
18    /// `include`: extra output to return. The wire adds
19    /// `reasoning.encrypted_content` itself when it replays reasoning.
20    #[serde(skip_serializing_if = "Vec::is_empty")]
21    pub include: Vec<Include>,
22    /// `conversation`: the id of a stored conversation the request joins.
23    /// The history before it is then the provider's to hold.
24    #[serde(skip_serializing_if = "Option::is_none")]
25    pub conversation: Option<String>,
26    /// `truncation`: what the provider does when the input outgrows the
27    /// context window.
28    #[serde(skip_serializing_if = "Option::is_none")]
29    pub truncation: Option<Truncation>,
30    /// `context_management`: server-side compaction of the context.
31    #[serde(skip_serializing_if = "Vec::is_empty")]
32    pub context_management: Vec<ContextManagement>,
33    /// `prompt_cache_options.comparison_response_id`, merged beside the
34    /// `ttl` and `mode` that `GenerationOptions::cache` sends.
35    #[serde(skip_serializing_if = "Option::is_none")]
36    pub prompt_cache_options: Option<PromptCacheOptions>,
37    /// `background`: run the response asynchronously. HTTP only: a
38    /// WebSocket session refuses it through the request's
39    /// [`OnUnsupported`](crate::completion::OnUnsupported) policy.
40    #[serde(skip_serializing_if = "Option::is_none")]
41    pub background: Option<bool>,
42    /// `max_tool_calls`: the most built-in tool calls the response makes.
43    #[serde(skip_serializing_if = "Option::is_none")]
44    pub max_tool_calls: Option<u32>,
45    /// `top_logprobs`: 0 to 20 most likely tokens per position. A model
46    /// that samples only without reasoning refuses it while it reasons.
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub top_logprobs: Option<u8>,
49    /// `access_programs`: the access programs the request runs under.
50    #[serde(skip_serializing_if = "Option::is_none")]
51    pub access_programs: Option<AccessPrograms>,
52}
53
54impl OpenAiResponsesOptions {
55    /// Send `reasoning.summary`.
56    #[must_use]
57    pub fn reasoning_summary(mut self, summary: ReasoningSummary) -> Self {
58        self.reasoning.get_or_insert_with(Default::default).summary = Some(summary);
59        self
60    }
61
62    /// Send `reasoning.mode`.
63    #[must_use]
64    pub fn reasoning_mode(mut self, mode: ReasoningMode) -> Self {
65        self.reasoning.get_or_insert_with(Default::default).mode = Some(mode);
66        self
67    }
68
69    /// Send `reasoning.context`.
70    #[must_use]
71    pub fn reasoning_context(mut self, context: ReasoningContext) -> Self {
72        self.reasoning.get_or_insert_with(Default::default).context = Some(context);
73        self
74    }
75
76    /// Add `include` entries.
77    #[must_use]
78    pub fn include(mut self, include: impl IntoIterator<Item = Include>) -> Self {
79        self.include.extend(include);
80        self
81    }
82
83    /// Send `conversation`.
84    #[must_use]
85    pub fn conversation(mut self, id: impl Into<String>) -> Self {
86        self.conversation = Some(id.into());
87        self
88    }
89
90    /// Send `truncation`.
91    #[must_use]
92    pub fn truncation(mut self, truncation: Truncation) -> Self {
93        self.truncation = Some(truncation);
94        self
95    }
96
97    /// Add `context_management` entries.
98    #[must_use]
99    pub fn context_management(
100        mut self,
101        entries: impl IntoIterator<Item = ContextManagement>,
102    ) -> Self {
103        self.context_management.extend(entries);
104        self
105    }
106
107    /// Send `prompt_cache_options.comparison_response_id`.
108    #[must_use]
109    pub fn prompt_cache_comparison(mut self, response_id: impl Into<String>) -> Self {
110        self.prompt_cache_options = Some(PromptCacheOptions {
111            comparison_response_id: Some(response_id.into()),
112        });
113        self
114    }
115
116    /// Send `background`.
117    #[must_use]
118    pub fn background(mut self, background: bool) -> Self {
119        self.background = Some(background);
120        self
121    }
122
123    /// Send `max_tool_calls`.
124    #[must_use]
125    pub fn max_tool_calls(mut self, max: u32) -> Self {
126        self.max_tool_calls = Some(max);
127        self
128    }
129
130    /// Send `top_logprobs`.
131    #[must_use]
132    pub fn top_logprobs(mut self, count: u8) -> Self {
133        self.top_logprobs = Some(count);
134        self
135    }
136
137    /// Send `access_programs`.
138    #[must_use]
139    pub fn access_programs(mut self, programs: AccessPrograms) -> Self {
140        self.access_programs = Some(programs);
141        self
142    }
143}
144
145/// The `reasoning` fields beside its effort.
146#[non_exhaustive]
147#[derive(Clone, Debug, Default, PartialEq, Serialize)]
148pub struct ReasoningOptions {
149    /// `reasoning.summary`.
150    #[serde(skip_serializing_if = "Option::is_none")]
151    pub summary: Option<ReasoningSummary>,
152    /// `reasoning.mode`.
153    #[serde(skip_serializing_if = "Option::is_none")]
154    pub mode: Option<ReasoningMode>,
155    /// `reasoning.context`.
156    #[serde(skip_serializing_if = "Option::is_none")]
157    pub context: Option<ReasoningContext>,
158}
159
160/// How much of its reasoning the model summarizes.
161#[non_exhaustive]
162#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
163#[serde(rename_all = "snake_case")]
164pub enum ReasoningSummary {
165    /// The model's choice.
166    Auto,
167    /// A short summary.
168    Concise,
169    /// A detailed summary.
170    Detailed,
171}
172
173/// The reasoning mode, independent of the effort. GPT-5.6 and later take
174/// it.
175#[non_exhaustive]
176#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
177#[serde(rename_all = "snake_case")]
178pub enum ReasoningMode {
179    /// The default mode.
180    Standard,
181    /// Pro mode.
182    Pro,
183}
184
185/// Which earlier turns' reasoning the model reuses. GPT-5.6 and later take
186/// it.
187#[non_exhaustive]
188#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
189#[serde(rename_all = "snake_case")]
190pub enum ReasoningContext {
191    /// The model's choice.
192    Auto,
193    /// Reasoning from every earlier turn.
194    AllTurns,
195    /// Only the current turn's reasoning.
196    CurrentTurn,
197}
198
199/// Extra output a response returns.
200#[non_exhaustive]
201#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
202pub enum Include {
203    /// `file_search_call.results`.
204    #[serde(rename = "file_search_call.results")]
205    FileSearchCallResults,
206    /// `web_search_call.results`.
207    #[serde(rename = "web_search_call.results")]
208    WebSearchCallResults,
209    /// `web_search_call.action.sources`.
210    #[serde(rename = "web_search_call.action.sources")]
211    WebSearchCallActionSources,
212    /// `message.input_image.image_url`.
213    #[serde(rename = "message.input_image.image_url")]
214    MessageInputImageImageUrl,
215    /// `computer_call_output.output.image_url`.
216    #[serde(rename = "computer_call_output.output.image_url")]
217    ComputerCallOutputOutputImageUrl,
218    /// `code_interpreter_call.outputs`.
219    #[serde(rename = "code_interpreter_call.outputs")]
220    CodeInterpreterCallOutputs,
221    /// `reasoning.encrypted_content`.
222    #[serde(rename = "reasoning.encrypted_content")]
223    ReasoningEncryptedContent,
224    /// `message.output_text.logprobs`.
225    #[serde(rename = "message.output_text.logprobs")]
226    MessageOutputTextLogprobs,
227}
228
229/// What the provider does when the input outgrows the context window.
230#[non_exhaustive]
231#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
232#[serde(rename_all = "snake_case")]
233pub enum Truncation {
234    /// Drop input items from the middle of the conversation.
235    Auto,
236    /// Fail the request.
237    Disabled,
238}
239
240/// One `context_management` entry.
241#[non_exhaustive]
242#[derive(Clone, Debug, PartialEq, Serialize)]
243#[serde(tag = "type", rename_all = "snake_case")]
244pub enum ContextManagement {
245    /// Compact the context once it reaches `compact_threshold` tokens, or
246    /// at the provider's threshold.
247    Compaction {
248        /// The token count compaction starts at.
249        #[serde(skip_serializing_if = "Option::is_none")]
250        compact_threshold: Option<u32>,
251    },
252}
253
254impl ContextManagement {
255    /// Compaction at `threshold` tokens, or at the provider's threshold.
256    pub fn compaction(threshold: Option<u32>) -> Self {
257        Self::Compaction {
258            compact_threshold: threshold,
259        }
260    }
261}
262
263/// The `prompt_cache_options` fields `GenerationOptions::cache` does not
264/// send.
265#[non_exhaustive]
266#[derive(Clone, Debug, Default, PartialEq, Serialize)]
267pub struct PromptCacheOptions {
268    /// `comparison_response_id`: an earlier response whose cache use this
269    /// one is compared with.
270    #[serde(skip_serializing_if = "Option::is_none")]
271    pub comparison_response_id: Option<String>,
272}
273
274/// The access programs a request runs under.
275#[non_exhaustive]
276#[derive(Clone, Debug, PartialEq, Serialize)]
277pub struct AccessPrograms {
278    /// `cyber`: the cyber-security access program.
279    pub cyber: CyberAccess,
280}
281
282impl AccessPrograms {
283    /// The `cyber` program at `access`.
284    pub fn cyber(access: CyberAccess) -> Self {
285        Self { cyber: access }
286    }
287}
288
289/// The cyber-security access program's level.
290#[non_exhaustive]
291#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
292#[serde(rename_all = "snake_case")]
293pub enum CyberAccess {
294    /// `standard`.
295    Standard,
296    /// `daybreak_blue`.
297    DaybreakBlue,
298    /// `daybreak_red`.
299    DaybreakRed,
300}
301
302/// The `phase` of one `message` item of a Responses reply.
303#[non_exhaustive]
304#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
305pub struct ItemPhase {
306    /// The item's `id`.
307    pub id: String,
308    /// The item's `phase`, such as `final_answer`, when it names one.
309    #[serde(default)]
310    pub phase: Option<String>,
311}
312
313/// The fields a Responses reply's envelope carries, as the OpenAI and
314/// ChatGPT extras read them.
315pub(crate) struct Envelope {
316    pub(crate) service_tier: Option<String>,
317    pub(crate) reasoning_effort: Option<String>,
318    pub(crate) reasoning_summary: Option<String>,
319    pub(crate) reasoning_mode: Option<String>,
320    pub(crate) reasoning_context: Option<String>,
321    pub(crate) prompt_cache_retention: Option<String>,
322    pub(crate) incomplete_reason: Option<String>,
323    pub(crate) phases: Option<Vec<ItemPhase>>,
324}
325
326impl Envelope {
327    /// The envelope of `raw`. A `reasoning` that is not an object, as a
328    /// compatible server's text reasoning, gives no reasoning fields.
329    pub(crate) fn from_reply(raw: &Value) -> Result<Self, serde_json::Error> {
330        let reasoning = raw.get("reasoning").filter(|value| value.is_object());
331        let reasoning = |key: &str| match reasoning {
332            Some(reasoning) => reply_field::<String>(reasoning, &format!("/{key}")),
333            None => Ok(None),
334        };
335        let messages = raw
336            .get("output")
337            .and_then(Value::as_array)
338            .into_iter()
339            .flatten()
340            .filter(|item| item.get("type").and_then(Value::as_str) == Some("message"))
341            .map(ItemPhase::deserialize)
342            .collect::<Result<Vec<_>, _>>()?;
343        Ok(Self {
344            service_tier: reply_field(raw, "/service_tier")?,
345            reasoning_effort: reasoning("effort")?,
346            reasoning_summary: reasoning("summary")?,
347            reasoning_mode: reasoning("mode")?,
348            reasoning_context: reasoning("context")?,
349            prompt_cache_retention: reply_field(raw, "/prompt_cache_retention")?,
350            incomplete_reason: reply_field(raw, "/incomplete_details/reason")?,
351            phases: (!messages.is_empty()).then_some(messages),
352        })
353    }
354}
355
356#[cfg(test)]
357mod tests;