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