Skip to main content

openai_protocol/
responses.rs

1// OpenAI Responses API types
2// https://platform.openai.com/docs/api-reference/responses
3
4use std::collections::{HashMap, HashSet};
5
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8use validator::{Validate, ValidationError};
9
10use super::{
11    common::{
12        default_true, validate_stop, ChatLogProbs, ContextManagementEntry, ConversationRef, Detail,
13        Function, FunctionChoice, GenerationRequest, PromptCacheRetention, PromptTokenUsageInfo,
14        ResponsePrompt, StreamOptions, StringOrArray, ToolChoice as ChatToolChoice,
15        ToolChoiceValue as ChatToolChoiceValue, ToolReference, UsageInfo,
16    },
17    sampling_params::{validate_top_k_value, validate_top_p_value},
18};
19use crate::{builders::ResponsesResponseBuilder, validated::Normalizable};
20
21// ============================================================================
22// Responses API Tool Choice
23// ============================================================================
24
25/// Simple tool-choice option strings supported by the Responses API.
26///
27/// Spec: `tool_choice` may be the bare string `"none"`, `"auto"`, or `"required"`.
28/// Shared chat/responses semantics but the Responses type owns its own enum so
29/// chat-path validators cannot accept unknown Responses variants by accident.
30#[derive(Debug, Clone, Copy, Deserialize, Serialize, schemars::JsonSchema, PartialEq, Eq)]
31#[serde(rename_all = "snake_case")]
32pub enum ToolChoiceOptions {
33    None,
34    Auto,
35    Required,
36}
37
38/// Single-value tag used to force the `"type": "function"` discriminator on the
39/// flat function-selection variant so the untagged outer enum can distinguish
40/// `Function` from `AllowedTools` / `Mcp` / `Custom` / `Types`.
41///
42/// Responses spec: `{"type": "function", "name": "..."}` — note the **flat**
43/// shape (no nested `function` object). This differs from Chat Completions
44/// which wraps the name in `{"function": {"name": "..."}}`.
45#[derive(Debug, Clone, Copy, Deserialize, Serialize, schemars::JsonSchema, PartialEq, Eq)]
46pub enum FunctionToolChoiceTag {
47    #[serde(rename = "function")]
48    Function,
49}
50
51/// Tag enum forcing the `"type": "allowed_tools"` discriminator.
52#[derive(Debug, Clone, Copy, Deserialize, Serialize, schemars::JsonSchema, PartialEq, Eq)]
53pub enum AllowedToolsToolChoiceTag {
54    #[serde(rename = "allowed_tools")]
55    AllowedTools,
56}
57
58/// Tag enum forcing the `"type": "mcp"` discriminator.
59#[derive(Debug, Clone, Copy, Deserialize, Serialize, schemars::JsonSchema, PartialEq, Eq)]
60pub enum McpToolChoiceTag {
61    #[serde(rename = "mcp")]
62    Mcp,
63}
64
65/// Tag enum forcing the `"type": "custom"` discriminator.
66#[derive(Debug, Clone, Copy, Deserialize, Serialize, schemars::JsonSchema, PartialEq, Eq)]
67pub enum CustomToolChoiceTag {
68    #[serde(rename = "custom")]
69    Custom,
70}
71
72/// Tag enum forcing the `"type": "apply_patch"` discriminator.
73#[derive(Debug, Clone, Copy, Deserialize, Serialize, schemars::JsonSchema, PartialEq, Eq)]
74pub enum ApplyPatchToolChoiceTag {
75    #[serde(rename = "apply_patch")]
76    ApplyPatch,
77}
78
79/// Tag enum forcing the `"type": "shell"` discriminator.
80#[derive(Debug, Clone, Copy, Deserialize, Serialize, schemars::JsonSchema, PartialEq, Eq)]
81pub enum ShellToolChoiceTag {
82    #[serde(rename = "shell")]
83    Shell,
84}
85
86/// Built-in (hosted) tool types that can be referenced directly by
87/// `tool_choice: {"type": "..."}` in the Responses API.
88///
89/// Each variant is a distinct string per the spec — we keep multiple
90/// `web_search_preview*` forms because the spec enumerates them separately
91/// and older clients may still send the versioned aliases.
92#[derive(Debug, Clone, Copy, Deserialize, Serialize, schemars::JsonSchema, PartialEq, Eq)]
93#[serde(rename_all = "snake_case")]
94pub enum BuiltInToolChoiceType {
95    FileSearch,
96    WebSearch,
97    WebSearchPreview,
98    #[serde(rename = "web_search_preview_2025_03_11")]
99    WebSearchPreview20250311,
100    ImageGeneration,
101    ComputerUsePreview,
102    CodeInterpreter,
103}
104
105/// Canonical payload for the Responses API `Function` tool-choice variant.
106///
107/// Serializes as the spec-flat shape `{"type": "function", "name": "..."}`.
108///
109/// Deserialization accepts **both** wire shapes for backward compatibility
110/// with smg clients written against the pre-split shared `ToolChoice` type
111/// (which used the Chat-style nested `{"function": {"name": "..."}}` layout
112/// on the Responses endpoint):
113///
114/// * Canonical flat: `{"type": "function", "name": "..."}`
115/// * Legacy nested:  `{"type": "function", "function": {"name": "..."}}`
116///
117/// Either shape normalizes to `name: String` at deserialize time so the rest
118/// of the gateway only ever sees the canonical form.
119#[derive(Debug, Clone, Serialize, schemars::JsonSchema)]
120pub struct ResponsesFunctionToolChoice {
121    #[serde(rename = "type")]
122    pub tool_type: FunctionToolChoiceTag,
123    pub name: String,
124}
125
126impl<'de> Deserialize<'de> for ResponsesFunctionToolChoice {
127    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
128    where
129        D: serde::Deserializer<'de>,
130    {
131        // Accept either the spec-flat `{type, name}` or the legacy
132        // Chat-nested `{type, function: {name}}` shape. The helper lets
133        // both fields be absent so serde can bind whichever wire form
134        // the caller sent; we then pick one and fail loudly if neither
135        // provides a function name.
136        #[derive(Deserialize)]
137        struct Helper {
138            #[serde(rename = "type")]
139            tool_type: FunctionToolChoiceTag,
140            #[serde(default)]
141            name: Option<String>,
142            #[serde(default)]
143            function: Option<FunctionChoice>,
144        }
145
146        let helper = Helper::deserialize(deserializer)?;
147        let name = helper
148            .name
149            .or_else(|| helper.function.map(|f| f.name))
150            .ok_or_else(|| {
151                serde::de::Error::custom(
152                    "tool_choice function requires a `name` field or a `function.name` field",
153                )
154            })?;
155        Ok(Self {
156            tool_type: helper.tool_type,
157            name,
158        })
159    }
160}
161
162/// `tool_choice` accepted on the Responses API (`POST /v1/responses`).
163///
164/// The Responses spec enumerates eight concrete wire shapes, each with a
165/// distinct discriminator — see `ResponsesToolChoice` variants below.
166/// Deserialised via `#[serde(untagged)]` because the outermost JSON is
167/// either a bare string (`Options`) or an object whose `"type"` picks
168/// the variant.
169///
170/// Each object variant pins the discriminator through a single-value tag
171/// enum (`FunctionToolChoiceTag`, etc.) so serde cannot match a payload
172/// whose `type` does not belong to that variant. Without the tag pinning,
173/// the `#[serde(untagged)]` enum would accept any object shape that
174/// happened to fit the field set of an earlier variant.
175///
176/// This type deliberately does NOT live in `common.rs`: Chat Completions
177/// has its own `ToolChoice` with a different `Function` wire shape
178/// (nested `{"function": {"name": ...}}`) and does not accept the
179/// `Types` / `Mcp` / `Custom` / `ApplyPatch` / `Shell` variants at all.
180/// Sharing one enum across both APIs would silently accept spec-invalid
181/// payloads on `/v1/chat/completions`.
182#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
183#[serde(untagged)]
184pub enum ResponsesToolChoice {
185    /// `"none"` | `"auto"` | `"required"`.
186    Options(ToolChoiceOptions),
187
188    /// `{"type": "file_search" | "web_search" | "web_search_preview" |
189    /// "web_search_preview_2025_03_11" | "image_generation" |
190    /// "computer_use_preview" | "code_interpreter"}` — select a built-in
191    /// hosted tool by type alone (no additional payload).
192    Types {
193        #[serde(rename = "type")]
194        tool_type: BuiltInToolChoiceType,
195    },
196
197    /// `{"type": "function", "name": "..."}` — Responses spec flat shape.
198    ///
199    /// Accepts both the spec-canonical flat wire shape and the legacy
200    /// Chat-style nested shape (`{"type": "function", "function": {"name": "..."}}`)
201    /// on deserialize to preserve backward compatibility with smg clients
202    /// written against the pre-split shared `ToolChoice` type. Always
203    /// serializes as the canonical flat shape per the OpenAI Responses spec
204    /// — Postel's law: liberal on input, conservative on output.
205    ///
206    /// The nested legacy shape is gated behind a custom `Deserialize` impl on
207    /// `ResponsesFunctionToolChoice`; the untagged outer enum still pins the
208    /// `"type": "function"` discriminator via `FunctionToolChoiceTag` so
209    /// payloads without that tag cannot reach this variant.
210    Function(ResponsesFunctionToolChoice),
211
212    /// `{"type": "allowed_tools", "mode": "auto"|"required", "tools": [...]}`.
213    ///
214    /// `tools` is an array of `ToolReference` items — the same type reused
215    /// from Chat's Allowed Tools payload because the Responses spec also
216    /// allows function / mcp / file_search / web_search_preview /
217    /// computer_use_preview / code_interpreter / image_generation entries.
218    AllowedTools {
219        #[serde(rename = "type")]
220        tool_type: AllowedToolsToolChoiceTag,
221        /// `"auto"` or `"required"`. Validated at request-normalisation time
222        /// (see `validate_tool_choice_with_tools`).
223        mode: String,
224        tools: Vec<ToolReference>,
225    },
226
227    /// `{"type": "mcp", "server_label": "...", "name"?: "..."}` — force
228    /// routing to a specific MCP server, optionally pinning a tool name.
229    Mcp {
230        #[serde(rename = "type")]
231        tool_type: McpToolChoiceTag,
232        server_label: String,
233        #[serde(skip_serializing_if = "Option::is_none")]
234        name: Option<String>,
235    },
236
237    /// `{"type": "custom", "name": "..."}` — pin a user-registered
238    /// custom tool by name.
239    Custom {
240        #[serde(rename = "type")]
241        tool_type: CustomToolChoiceTag,
242        name: String,
243    },
244
245    /// `{"type": "apply_patch"}` — force the built-in `apply_patch` tool.
246    ApplyPatch {
247        #[serde(rename = "type")]
248        tool_type: ApplyPatchToolChoiceTag,
249    },
250
251    /// `{"type": "shell"}` — force the built-in `shell` tool.
252    Shell {
253        #[serde(rename = "type")]
254        tool_type: ShellToolChoiceTag,
255    },
256}
257
258impl Default for ResponsesToolChoice {
259    fn default() -> Self {
260        Self::Options(ToolChoiceOptions::Auto)
261    }
262}
263
264impl ResponsesToolChoice {
265    /// Serialize tool_choice to string for ResponsesResponse payloads.
266    ///
267    /// Returns the JSON-serialized tool_choice or `"auto"` as default.
268    pub fn serialize_to_string(tool_choice: Option<&ResponsesToolChoice>) -> String {
269        tool_choice
270            .map(|tc| serde_json::to_string(tc).unwrap_or_else(|_| "auto".to_string()))
271            .unwrap_or_else(|| "auto".to_string())
272    }
273
274    /// Return the pinned function name for the `Function` variant, regardless
275    /// of which wire shape (spec-flat `name` or legacy nested `function.name`)
276    /// was used at deserialize time. `None` for any non-`Function` variant.
277    ///
278    /// Consumers that need to project / validate the function name should go
279    /// through this accessor rather than pattern-matching so future wire
280    /// shapes can be added without touching call sites.
281    pub fn function_name(&self) -> Option<&str> {
282        match self {
283            Self::Function(payload) => Some(payload.name.as_str()),
284            _ => None,
285        }
286    }
287
288    /// Project the Responses-level tool_choice onto a Chat Completions
289    /// tool_choice when a Responses request is being routed through the
290    /// Chat Completions gRPC pipeline.
291    ///
292    /// Mapping rules:
293    /// - `Options(None|Auto|Required)` → `ChatToolChoice::Value(...)` — shared
294    ///   semantics.
295    /// - `Function { name }` → `ChatToolChoice::Function { nested name }` —
296    ///   shape translation (flat Responses → nested Chat wire form).
297    /// - `AllowedTools { mode, tools }` → `ChatToolChoice::AllowedTools {...}`.
298    /// - Hosted / custom / apply_patch / shell / mcp — Chat Completions has no
299    ///   equivalent spec variant; fall back to `Auto` so the downstream chat
300    ///   backend still runs with tool-calling enabled.
301    pub fn to_chat_tool_choice(&self) -> ChatToolChoice {
302        match self {
303            Self::Options(ToolChoiceOptions::None) => {
304                ChatToolChoice::Value(ChatToolChoiceValue::None)
305            }
306            Self::Options(ToolChoiceOptions::Auto) => {
307                ChatToolChoice::Value(ChatToolChoiceValue::Auto)
308            }
309            Self::Options(ToolChoiceOptions::Required) => {
310                ChatToolChoice::Value(ChatToolChoiceValue::Required)
311            }
312            Self::Function(payload) => ChatToolChoice::Function {
313                tool_type: "function".to_string(),
314                function: FunctionChoice {
315                    name: payload.name.clone(),
316                },
317            },
318            Self::AllowedTools { mode, tools, .. } => ChatToolChoice::AllowedTools {
319                tool_type: "allowed_tools".to_string(),
320                mode: mode.clone(),
321                tools: tools.clone(),
322            },
323            // No matching Chat spec variant — fall through to `auto` so
324            // downstream Chat backends still see tool-calling enabled.
325            Self::Types { .. }
326            | Self::Mcp { .. }
327            | Self::Custom { .. }
328            | Self::ApplyPatch { .. }
329            | Self::Shell { .. } => ChatToolChoice::Value(ChatToolChoiceValue::Auto),
330        }
331    }
332}
333
334// ============================================================================
335// Response Tools (MCP and others)
336// ============================================================================
337
338#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
339#[serde(tag = "type")]
340#[serde(rename_all = "snake_case")]
341pub enum ResponseTool {
342    /// Function tool.
343    #[serde(rename = "function")]
344    Function(FunctionTool),
345
346    /// Built-in tool.
347    #[serde(rename = "web_search_preview")]
348    WebSearchPreview(WebSearchPreviewTool),
349
350    /// Built-in non-preview hosted web search tool.
351    ///
352    /// Spec: `{ type: "web_search" | "web_search_2025_08_26", filters? { allowed_domains? },
353    /// search_context_size?: "low"|"medium"|"high", user_location? }`. Distinct from
354    /// `web_search_preview` — non-preview adds `filters.allowed_domains` and constrains
355    /// `search_context_size` to a typed enum.
356    #[serde(rename = "web_search", alias = "web_search_2025_08_26")]
357    WebSearch(WebSearchTool),
358
359    /// Built-in tool.
360    #[serde(rename = "code_interpreter")]
361    CodeInterpreter(CodeInterpreterTool),
362
363    /// MCP server tool.
364    #[serde(rename = "mcp")]
365    Mcp(McpTool),
366
367    /// Built-in file search tool over vector stores.
368    #[serde(rename = "file_search")]
369    FileSearch(FileSearchTool),
370
371    /// Built-in image generation tool. Spec:
372    /// `{ type: "image_generation", action?, background?, input_fidelity?,
373    ///    input_image_mask?, model?, moderation?, output_compression?,
374    ///    output_format?, partial_images?, quality?, size? }`.
375    #[serde(rename = "image_generation")]
376    ImageGeneration(ImageGenerationTool),
377
378    /// Generic computer tool — `{ type: "computer" }`.
379    ///
380    /// Spec (openai-responses-api-spec.md §tools): `Computer { type: "computer" }`.
381    /// Carries no payload; the model is told that a computer-control surface is
382    /// available without committing to display dimensions or environment.
383    #[serde(rename = "computer")]
384    Computer,
385
386    /// Computer-use preview tool — `{ type: "computer_use_preview",
387    /// display_height, display_width, environment }`.
388    ///
389    /// Spec (openai-responses-api-spec.md §tools): `ComputerUsePreview
390    /// { display_height, display_width, environment: "browser"|"ubuntu"|
391    /// "windows"|"mac", type: "computer_use_preview" }`.
392    #[serde(rename = "computer_use_preview")]
393    ComputerUsePreview(ComputerUsePreviewTool),
394
395    /// User-defined custom tool with optional grammar-constrained input.
396    ///
397    /// Spec: `{ name, type: "custom", defer_loading?, description?, format? }`.
398    /// `format` constrains the model's free-form `input` payload — `Text` is
399    /// unconstrained, `Grammar` enforces a Lark or regex production at decode
400    /// time. The model returns a `custom_tool_call` output item carrying the
401    /// raw `input` string back to the client; the client owns execution.
402    #[serde(rename = "custom")]
403    Custom(CustomTool),
404
405    /// Grouping of `Function` / `Custom` tools under a shared namespace.
406    ///
407    /// Spec (openai-responses-api-spec.md §tools L475):
408    /// `Namespace { description, name, tools: array of Function | Custom,
409    /// type: "namespace" }` — inner elements share the top-level shape but
410    /// are restricted to `Function` or `Custom`. Nested namespaces and
411    /// hosted/built-in tools are explicitly not permitted as elements.
412    #[serde(rename = "namespace")]
413    Namespace(NamespaceToolDef),
414
415    /// Containerized `shell` tool. Distinct from `local_shell` — the tool
416    /// definition itself may carry an optional [`ShellEnvironment`]
417    /// (container-auto, local, or existing container reference) that the
418    /// platform resolves into a concrete execution target. Emitted
419    /// `shell_call` items use the narrower call-side unions
420    /// [`ShellCallEnvironment`] (input path) and
421    /// [`ResponseShellCallEnvironment`] (response path), which drop the
422    /// `container_auto` variant.
423    ///
424    /// Spec (openai-responses-api-spec.md §tools, L463-470):
425    /// `Shell { type: "shell", environment? }`.
426    #[serde(rename = "shell")]
427    Shell(ShellTool),
428
429    /// Built-in `apply_patch` tool — `{ type: "apply_patch" }`.
430    ///
431    /// Spec (openai-responses-api-spec.md §tools, L478): `ApplyPatch
432    /// { type: "apply_patch" }`. Unit variant with no payload — the model is
433    /// simply told the apply_patch surface is available and subsequently emits
434    /// `apply_patch_call` items carrying file-edit operations (see
435    /// [`ResponseInputOutputItem::ApplyPatchCall`]). Pairs with
436    /// [`ResponsesToolChoice::ApplyPatch`] when callers want to force usage.
437    #[serde(rename = "apply_patch")]
438    ApplyPatch,
439
440    /// Built-in host-execute shell tool — `{ type: "local_shell" }`.
441    ///
442    /// Spec (openai-responses-api-spec.md §tools L462): `LocalShell { type:
443    /// "local_shell" }` — carries no payload. Distinct from `shell` (T6),
444    /// which carries a containerized `environment`. The model emits
445    /// `local_shell_call` output items carrying a `LocalShellExec` action;
446    /// the client executes the command on the host and replies with a
447    /// matching `local_shell_call_output` item.
448    #[serde(rename = "local_shell")]
449    LocalShell,
450}
451
452impl ResponseTool {
453    /// Wire `type` tag for this variant — matches the variant's
454    /// `#[serde(rename = ...)]` attribute. Stable across serde
455    /// roundtrips and safe to use as a discriminator string.
456    pub fn as_str(&self) -> &'static str {
457        match self {
458            ResponseTool::Function(_) => "function",
459            ResponseTool::WebSearchPreview(_) => "web_search_preview",
460            ResponseTool::WebSearch(_) => "web_search",
461            ResponseTool::CodeInterpreter(_) => "code_interpreter",
462            ResponseTool::Mcp(_) => "mcp",
463            ResponseTool::FileSearch(_) => "file_search",
464            ResponseTool::ImageGeneration(_) => "image_generation",
465            ResponseTool::Computer => "computer",
466            ResponseTool::ComputerUsePreview(_) => "computer_use_preview",
467            ResponseTool::Custom(_) => "custom",
468            ResponseTool::Namespace(_) => "namespace",
469            ResponseTool::Shell(_) => "shell",
470            ResponseTool::ApplyPatch => "apply_patch",
471            ResponseTool::LocalShell => "local_shell",
472        }
473    }
474}
475
476/// Payload carried by [`ResponseTool::Namespace`].
477///
478/// Using a dedicated struct (rather than inline struct-variant fields) lets
479/// us apply `#[serde(deny_unknown_fields)]`, matching sibling variants like
480/// [`CustomTool`] and [`FunctionTool`] so unrecognized namespace-level keys
481/// are rejected instead of silently swallowed.
482#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
483#[serde(deny_unknown_fields)]
484pub struct NamespaceToolDef {
485    /// Human-readable description surfaced to the model alongside the group.
486    pub description: String,
487    /// Stable identifier the model uses to address the namespace in
488    /// `function_call` / `custom_tool_call` items (via the `namespace` field).
489    pub name: String,
490    /// Tools in this namespace. Spec restricts elements to `Function` or
491    /// `Custom`; the dedicated [`NamespaceTool`] enum prevents nested
492    /// namespaces and hosted-tool leakage that the parent `ResponseTool`
493    /// enum would otherwise allow.
494    pub tools: Vec<NamespaceTool>,
495}
496
497/// Element type accepted inside a [`NamespaceToolDef`]'s `tools` array.
498///
499/// Spec (openai-responses-api-spec.md §tools L475): namespace elements must be
500/// either a `Function` or a `Custom` tool. Using a dedicated enum rather than
501/// `ResponseTool` prevents recursive nesting (`Namespace` inside `Namespace`)
502/// and hosted/built-in tool leakage that the spec forbids.
503#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
504#[serde(tag = "type")]
505#[serde(rename_all = "snake_case")]
506pub enum NamespaceTool {
507    /// Function tool — same shape as [`ResponseTool::Function`].
508    #[serde(rename = "function")]
509    Function(FunctionTool),
510
511    /// Custom tool — same shape as [`ResponseTool::Custom`].
512    #[serde(rename = "custom")]
513    Custom(CustomTool),
514}
515
516#[serde_with::skip_serializing_none]
517#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
518#[serde(deny_unknown_fields)]
519pub struct FunctionTool {
520    /// Flatten to match Responses API tool JSON shape.
521    #[serde(flatten)]
522    pub function: Function,
523}
524
525/// File search tool configuration.
526///
527/// Spec: `{ type: "file_search", vector_store_ids, filters?, max_num_results?, ranking_options? }`.
528#[serde_with::skip_serializing_none]
529#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
530#[serde(deny_unknown_fields)]
531pub struct FileSearchTool {
532    /// Vector store IDs to search over.
533    pub vector_store_ids: Vec<String>,
534    /// Optional filter applied to candidate documents.
535    pub filters: Option<FileSearchFilter>,
536    /// Maximum number of results to return.
537    pub max_num_results: Option<u32>,
538    /// Ranking options for the search.
539    pub ranking_options: Option<FileSearchRankingOptions>,
540}
541
542/// Filter expression for file search.
543///
544/// Either a single comparison or a boolean compound (`and` / `or`) over nested filters.
545#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
546#[serde(tag = "type")]
547pub enum FileSearchFilter {
548    #[serde(rename = "eq")]
549    Eq(ComparisonFilter),
550    #[serde(rename = "ne")]
551    Ne(ComparisonFilter),
552    #[serde(rename = "gt")]
553    Gt(ComparisonFilter),
554    #[serde(rename = "gte")]
555    Gte(ComparisonFilter),
556    #[serde(rename = "lt")]
557    Lt(ComparisonFilter),
558    #[serde(rename = "lte")]
559    Lte(ComparisonFilter),
560    #[serde(rename = "in")]
561    In(ComparisonFilter),
562    #[serde(rename = "nin")]
563    Nin(ComparisonFilter),
564    #[serde(rename = "and")]
565    And(CompoundFilter),
566    #[serde(rename = "or")]
567    Or(CompoundFilter),
568}
569
570/// Key/value comparison used by the `eq`/`ne`/`gt`/`gte`/`lt`/`lte`/`in`/`nin` filter variants.
571#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
572#[serde(deny_unknown_fields)]
573pub struct ComparisonFilter {
574    pub key: String,
575    /// Spec allows `string | number | boolean | array of string | number`.
576    pub value: Value,
577}
578
579/// Boolean composition over nested filters (used by `and` / `or` variants).
580#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
581#[serde(deny_unknown_fields)]
582pub struct CompoundFilter {
583    pub filters: Vec<FileSearchFilter>,
584}
585
586/// Ranking options for file search results.
587#[serde_with::skip_serializing_none]
588#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
589#[serde(deny_unknown_fields)]
590pub struct FileSearchRankingOptions {
591    pub hybrid_search: Option<HybridSearchOptions>,
592    pub ranker: Option<FileSearchRanker>,
593    pub score_threshold: Option<f64>,
594}
595
596/// Weights combining embedding-based and text-based similarity.
597#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
598#[serde(deny_unknown_fields)]
599pub struct HybridSearchOptions {
600    #[serde(default, skip_serializing_if = "Option::is_none")]
601    pub embedding_weight: Option<f64>,
602    #[serde(default, skip_serializing_if = "Option::is_none")]
603    pub text_weight: Option<f64>,
604}
605
606/// Ranker selection for file search.
607#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, schemars::JsonSchema)]
608pub enum FileSearchRanker {
609    #[serde(rename = "auto")]
610    Auto,
611    #[serde(rename = "default-2024-11-15")]
612    Default20241115,
613}
614
615/// User-defined custom tool definition.
616///
617/// Spec: `{ name, type: "custom", defer_loading?, description?, format? }`.
618/// The discriminator (`type: "custom"`) is enforced by the parent
619/// [`ResponseTool`] enum; this struct only carries the payload fields so the
620/// `flatten`-style wire shape survives a round-trip.
621#[serde_with::skip_serializing_none]
622#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
623#[serde(deny_unknown_fields)]
624pub struct CustomTool {
625    /// Stable identifier the model uses to address this tool in
626    /// `custom_tool_call` items.
627    pub name: String,
628    /// Optional human-readable description supplied to the model.
629    pub description: Option<String>,
630    /// When `true`, the tool definition is deferred to a later
631    /// `tool_search`-style fetch instead of being loaded inline.
632    pub defer_loading: Option<bool>,
633    /// Optional input-format constraint — `text` (unconstrained) or
634    /// `grammar` (Lark / regex production).
635    pub format: Option<CustomToolInputFormat>,
636}
637
638/// Input-format constraint applied to a [`CustomTool`].
639///
640/// Spec: `CustomToolInputFormat = Text { type: "text" } |
641/// Grammar { definition, syntax: "lark" | "regex", type: "grammar" }`.
642#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
643#[serde(tag = "type")]
644#[serde(rename_all = "snake_case")]
645pub enum CustomToolInputFormat {
646    /// Unconstrained free-form text input.
647    #[serde(rename = "text")]
648    Text,
649    /// Grammar-constrained input. The model's free-form output must match
650    /// `definition` interpreted under `syntax`.
651    #[serde(rename = "grammar")]
652    Grammar(CustomToolGrammar),
653}
654
655/// Grammar payload carried by [`CustomToolInputFormat::Grammar`].
656#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
657#[serde(deny_unknown_fields)]
658pub struct CustomToolGrammar {
659    /// Grammar source text. Interpretation depends on `syntax`.
660    pub definition: String,
661    /// `"lark"` or `"regex"` per spec.
662    pub syntax: CustomToolGrammarSyntax,
663}
664
665/// Grammar dialect for [`CustomToolGrammar::syntax`].
666#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, schemars::JsonSchema)]
667#[serde(rename_all = "snake_case")]
668pub enum CustomToolGrammarSyntax {
669    /// Lark grammar (https://lark-parser.readthedocs.io/).
670    Lark,
671    /// Regular expression.
672    Regex,
673}
674
675/// Input-only content part accepted inside a `custom_tool_call_output` array.
676///
677/// Spec (openai-responses-api-spec.md L269): the array form of
678/// `custom_tool_call_output.output` permits only `ResponseInputText |
679/// ResponseInputImage | ResponseInputFile` — assistant-facing shapes such as
680/// `output_text` and `refusal` are explicitly not allowed here. This enum
681/// mirrors the three input variants of [`ResponseContentPart`] with identical
682/// field shapes so spec-compliant payloads round-trip unchanged, while
683/// deserialization of `output_text`/`refusal` fails loudly instead of being
684/// silently coerced.
685///
686/// Other call sites that legitimately carry mixed input/output content parts
687/// continue to use [`ResponseContentPart`] (Postel-of-liberality preserved for
688/// cross-tool reuse).
689#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
690#[serde(tag = "type")]
691#[serde(rename_all = "snake_case")]
692// Variant names intentionally mirror the spec's `input_*` tag set; the shared
693// prefix communicates the input-only restriction that justifies this enum's
694// existence (see type-level docs above).
695#[expect(
696    clippy::enum_variant_names,
697    reason = "variant names mirror spec `input_*` tags by design"
698)]
699pub enum CustomToolInputContentPart {
700    /// `type: "input_text"` — inline textual input.
701    #[serde(rename = "input_text")]
702    InputText { text: String },
703    /// `type: "input_image"` — reference to an image supplied by the client.
704    /// Exactly one of `file_id` / `image_url` is typically set; both may be
705    /// absent when only `detail` is being conveyed.
706    #[serde(rename = "input_image")]
707    InputImage {
708        #[serde(skip_serializing_if = "Option::is_none")]
709        detail: Option<Detail>,
710        #[serde(skip_serializing_if = "Option::is_none")]
711        file_id: Option<String>,
712        #[serde(skip_serializing_if = "Option::is_none")]
713        image_url: Option<String>,
714    },
715    /// `type: "input_file"` — reference to an attached file. `file_data` is a
716    /// base64 blob; `file_url` / `file_id` reference external/uploaded files.
717    #[serde(rename = "input_file")]
718    InputFile {
719        #[serde(skip_serializing_if = "Option::is_none")]
720        detail: Option<FileDetail>,
721        #[serde(skip_serializing_if = "Option::is_none")]
722        file_data: Option<String>,
723        #[serde(skip_serializing_if = "Option::is_none")]
724        file_id: Option<String>,
725        #[serde(skip_serializing_if = "Option::is_none")]
726        file_url: Option<String>,
727        #[serde(skip_serializing_if = "Option::is_none")]
728        filename: Option<String>,
729    },
730}
731
732/// Output payload variant accepted by `custom_tool_call_output`.
733///
734/// Spec: `output: string or array of ResponseInputText | ResponseInputImage |
735/// ResponseInputFile`. The array variant uses the restricted
736/// [`CustomToolInputContentPart`] type so assistant-only shapes
737/// (`output_text`, `refusal`) are rejected at the type boundary rather than
738/// accepted and silently reinterpreted.
739#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
740#[serde(untagged)]
741pub enum CustomToolCallOutputContent {
742    /// Plain string output.
743    Text(String),
744    /// Array of input-typed content parts (`input_text` / `input_image` /
745    /// `input_file`) per the spec's `output` array shape.
746    Parts(Vec<CustomToolInputContentPart>),
747}
748
749/// Containerized `shell` tool. Spec
750/// (openai-responses-api-spec.md §tools, L463-470):
751/// `Shell { type: "shell", environment? }`.
752///
753/// The discriminator (`type: "shell"`) is enforced by the parent
754/// [`ResponseTool`] enum; this struct carries only the optional environment
755/// payload so the flatten-style wire shape survives a round-trip.
756#[serde_with::skip_serializing_none]
757#[derive(Debug, Clone, Default, Deserialize, Serialize, schemars::JsonSchema)]
758#[serde(deny_unknown_fields)]
759pub struct ShellTool {
760    /// Optional environment scope for the shell tool. When omitted, the
761    /// model runs commands inside the platform-default environment.
762    pub environment: Option<ShellEnvironment>,
763}
764
765/// Tool-side environment union carried on [`ShellTool::environment`].
766///
767/// Spec (openai-responses-api-spec.md §tools, L464-470):
768/// `environment: ContainerAuto | LocalEnvironment | ContainerReference`.
769///
770/// Distinct from the call-side environment unions
771/// [`ShellCallEnvironment`] (input-side call form, reuses
772/// [`LocalShellEnvironment`]) and
773/// [`ResponseShellCallEnvironment`] (response-side call form, carries the
774/// narrower [`ResponseLocalShellEnvironment`]): the tool
775/// form permits the `container_auto` variant, which asks the platform to
776/// provision a new container; both call forms only carry the resolved
777/// `local` / `container_reference` shape that the model echoes back.
778#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
779#[serde(tag = "type")]
780pub enum ShellEnvironment {
781    /// `type: "container_auto"` — spec L465-468. Requests a
782    /// platform-provisioned container with optional `file_ids`,
783    /// `memory_limit`, and `network_policy`.
784    #[serde(rename = "container_auto")]
785    ContainerAuto(ContainerAutoEnvironment),
786    /// `type: "local"` — spec L469. Runs in the caller-owned environment.
787    #[serde(rename = "local")]
788    Local(LocalShellEnvironment),
789    /// `type: "container_reference"` — spec L470. Pins execution to an
790    /// existing container by id.
791    #[serde(rename = "container_reference")]
792    ContainerReference(ContainerReferenceEnvironment),
793}
794
795/// Payload for [`ShellEnvironment::ContainerAuto`]. All fields are optional
796/// per spec.
797#[serde_with::skip_serializing_none]
798#[derive(Debug, Clone, Default, Deserialize, Serialize, schemars::JsonSchema)]
799#[serde(deny_unknown_fields)]
800pub struct ContainerAutoEnvironment {
801    /// Files pre-mounted into the container by id.
802    pub file_ids: Option<Vec<String>>,
803    /// Memory budget. Spec (CodeInterpreter §447): `"1g"|"4g"|"16g"|"64g"`
804    /// — kept `String`-typed here for forward compatibility with future tiers.
805    pub memory_limit: Option<String>,
806    /// Network isolation policy.
807    pub network_policy: Option<ContainerNetworkPolicy>,
808}
809
810/// Payload for [`ShellEnvironment::Local`]. Carries no fields.
811#[serde_with::skip_serializing_none]
812#[derive(Debug, Clone, Default, Deserialize, Serialize, schemars::JsonSchema)]
813#[serde(deny_unknown_fields)]
814pub struct LocalShellEnvironment {}
815
816/// Payload for [`ShellEnvironment::ContainerReference`]. Pins the tool to
817/// an existing container id.
818#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
819#[serde(deny_unknown_fields)]
820pub struct ContainerReferenceEnvironment {
821    /// Existing container id.
822    pub container_id: String,
823}
824
825/// Network isolation policy for [`ContainerAutoEnvironment::network_policy`].
826///
827/// Spec (openai-responses-api-spec.md §tools, L448):
828/// `ContainerNetworkPolicyDisabled { type: "disabled" } |
829/// ContainerNetworkPolicyAllowlist { allowed_domains, type: "allowlist",
830/// domain_secrets? }`.
831#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
832#[serde(tag = "type")]
833pub enum ContainerNetworkPolicy {
834    /// `type: "disabled"` — no outbound network access.
835    #[serde(rename = "disabled")]
836    Disabled,
837    /// `type: "allowlist"` — only the listed domains are reachable.
838    #[serde(rename = "allowlist")]
839    Allowlist(ContainerNetworkAllowlist),
840}
841
842/// Payload for [`ContainerNetworkPolicy::Allowlist`].
843///
844/// Spec (openai-responses-api-spec.md §tools, L448-449):
845/// `{ allowed_domains, type: "allowlist", domain_secrets? }` where
846/// `domain_secrets: array of { domain, name, value }`.
847#[serde_with::skip_serializing_none]
848#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
849#[serde(deny_unknown_fields)]
850pub struct ContainerNetworkAllowlist {
851    /// Outbound-reachable hostnames.
852    pub allowed_domains: Vec<String>,
853    /// Optional per-domain secret bindings.
854    pub domain_secrets: Option<Vec<ContainerDomainSecret>>,
855}
856
857/// Per-domain secret binding carried by
858/// [`ContainerNetworkAllowlist::domain_secrets`].
859#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
860#[serde(deny_unknown_fields)]
861pub struct ContainerDomainSecret {
862    /// Target domain the secret applies to.
863    pub domain: String,
864    /// Secret identifier / lookup name.
865    pub name: String,
866    /// Secret value.
867    pub value: String,
868}
869
870/// Input-side environment union carried on
871/// [`ResponseInputOutputItem::ShellCall`].
872///
873/// Spec (openai-responses-api-spec.md §ShellCall, L228-230): the input-side
874/// call form carries `environment: optional LocalEnvironment { type: "local"
875/// } | ContainerReference { container_id, type: "container_reference"
876/// }`. `container_auto` is rejected here — it is a request-side *tool*
877/// hint (§tools L465) that the platform resolves into `local` /
878/// `container_reference` before a call is surfaced, not a call-form value.
879///
880/// The tool-side [`LocalShellEnvironment`] is intentionally reused so
881/// spec-compliant replay flows that echo back input call items round-trip
882/// losslessly. Response-side emissions use the narrower
883/// [`ResponseShellCallEnvironment`] instead.
884#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
885#[serde(tag = "type")]
886pub enum ShellCallEnvironment {
887    /// `type: "local"` — input-side local environment. Reuses the tool-form
888    /// [`LocalShellEnvironment`] verbatim.
889    #[serde(rename = "local")]
890    Local(LocalShellEnvironment),
891    /// `type: "container_reference"` — resolved container binding.
892    #[serde(rename = "container_reference")]
893    ContainerReference(ContainerReferenceEnvironment),
894}
895
896/// Response-side environment union carried on
897/// [`ResponseOutputItem::ShellCall`].
898///
899/// Spec (openai-responses-api-spec.md §returns L512-513): the ShellCall
900/// response form has `environment: ResponseLocalEnvironment { type: "local"
901/// } | ResponseContainerReference { container_id, type: "container_reference"
902/// }`. The response-side local arm is
903/// `ResponseLocalEnvironment { type: "local" }`.
904#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
905#[serde(tag = "type")]
906pub enum ResponseShellCallEnvironment {
907    /// `type: "local"` — resolved local environment on the response-side
908    /// call form. Per spec L513 this is `ResponseLocalEnvironment { type:
909    /// "local" }`.
910    #[serde(rename = "local")]
911    Local(ResponseLocalShellEnvironment),
912    /// `type: "container_reference"` — resolved container binding.
913    /// Structurally identical to the input-side variant; reused directly.
914    #[serde(rename = "container_reference")]
915    ContainerReference(ContainerReferenceEnvironment),
916}
917
918/// Payload for [`ResponseShellCallEnvironment::Local`].
919///
920/// Spec (openai-responses-api-spec.md §returns L513): response-side local
921/// environment is `ResponseLocalEnvironment { type: "local" }` — the
922/// discriminator is the only field the model echoes back.
923#[derive(Debug, Clone, Default, Deserialize, Serialize, schemars::JsonSchema)]
924#[serde(deny_unknown_fields)]
925pub struct ResponseLocalShellEnvironment {}
926
927/// Action payload for [`ResponseInputOutputItem::ShellCall`] /
928/// [`ResponseOutputItem::ShellCall`].
929///
930/// Spec (openai-responses-api-spec.md §ShellCall, L229):
931/// `action: { commands, max_output_length?, timeout_ms? }`.
932#[serde_with::skip_serializing_none]
933#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
934#[serde(deny_unknown_fields)]
935pub struct ShellCallAction {
936    /// Command line to run inside the environment, as a positional-argv
937    /// array (equivalent to `argv` of `execve`).
938    pub commands: Vec<String>,
939    /// Optional cap on captured stdout / stderr bytes.
940    pub max_output_length: Option<u64>,
941    /// Optional per-command timeout in milliseconds.
942    pub timeout_ms: Option<u64>,
943}
944
945/// Status for [`ResponseInputOutputItem::ShellCall`] /
946/// [`ResponseOutputItem::ShellCall`] and their `*_output` siblings.
947///
948/// Spec (openai-responses-api-spec.md §ShellCall, L221+L238):
949/// `status: "in_progress" | "completed" | "incomplete"`.
950#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, schemars::JsonSchema)]
951#[serde(rename_all = "snake_case")]
952pub enum ShellCallStatus {
953    /// `in_progress` — call is still executing.
954    InProgress,
955    /// `completed` — call finished and `output` chunks are final.
956    Completed,
957    /// `incomplete` — call aborted or never reached completion.
958    Incomplete,
959}
960
961/// One entry of a `shell_call_output.output` array.
962///
963/// Spec (openai-responses-api-spec.md §ShellCallOutput, L234-238):
964/// `{ outcome, stderr, stdout }` plus the optional `created_by` marker
965/// mirroring the same tag on `FunctionCallOutput`/`ComputerCallOutput` for
966/// provenance.
967#[serde_with::skip_serializing_none]
968#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
969#[serde(deny_unknown_fields)]
970pub struct ShellOutputChunk {
971    /// Call outcome — timeout or numeric exit.
972    pub outcome: ShellOutcome,
973    /// Captured stderr bytes (UTF-8 where possible).
974    pub stderr: String,
975    /// Captured stdout bytes (UTF-8 where possible).
976    pub stdout: String,
977    /// Optional provenance marker — `"system"`, `"user"`, or similar per
978    /// spec. Kept `Option<String>` for forward-compatibility with future
979    /// `created_by` tags.
980    pub created_by: Option<String>,
981}
982
983/// Outcome of a shell call. Spec (openai-responses-api-spec.md
984/// §ShellCallOutput, L235):
985/// `outcome: Timeout { type: "timeout" } | Exit { exit_code, type: "exit" }`.
986#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
987#[serde(tag = "type")]
988pub enum ShellOutcome {
989    /// `type: "timeout"` — the call exceeded `action.timeout_ms` and was
990    /// killed by the environment.
991    #[serde(rename = "timeout")]
992    Timeout,
993    /// `type: "exit"` — the process exited, carrying the numeric exit code.
994    #[serde(rename = "exit")]
995    Exit(ShellExit),
996}
997
998/// Exit payload for [`ShellOutcome::Exit`]. Spec: `{ exit_code, type: "exit" }`.
999#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
1000#[serde(deny_unknown_fields)]
1001pub struct ShellExit {
1002    /// Process exit code. Signed to preserve negative error codes on
1003    /// platforms that report them.
1004    pub exit_code: i32,
1005}
1006
1007/// File-edit operation payload carried by
1008/// [`ResponseInputOutputItem::ApplyPatchCall`] /
1009/// [`ResponseOutputItem::ApplyPatchCall`].
1010///
1011/// Spec (openai-responses-api-spec.md §ApplyPatchCall L240-L246): the
1012/// `operation` field is a `type`-tagged union of three shapes —
1013/// `CreateFile { diff, path, type: "create_file" }`,
1014/// `DeleteFile { path, type: "delete_file" }`, and
1015/// `UpdateFile { diff, path, type: "update_file" }`. `DeleteFile` carries no
1016/// diff because the whole file is removed; the other two carry a unified
1017/// diff payload describing the edit.
1018///
1019/// `deny_unknown_fields` is applied so variants fail fast on foreign keys —
1020/// e.g. `{"type":"delete_file","path":"x","diff":"..."}` is rejected rather
1021/// than silently dropping the stray `diff`, matching the P5 fail-fast
1022/// contract applied elsewhere on protocol-surface structs.
1023#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
1024#[serde(tag = "type", deny_unknown_fields)]
1025#[serde(rename_all = "snake_case")]
1026// Variant names intentionally mirror the spec's `*_file` tag set; the shared
1027// `File` postfix tracks the spec verbatim and keeps the type discriminator
1028// symmetric with the JSON wire shape.
1029#[expect(
1030    clippy::enum_variant_names,
1031    reason = "variant names mirror spec `create_file` / `delete_file` / `update_file` tags by design"
1032)]
1033pub enum ApplyPatchOperation {
1034    /// `{ type: "create_file", diff, path }` — create a new file whose
1035    /// contents are described by `diff`.
1036    CreateFile { diff: String, path: String },
1037    /// `{ type: "delete_file", path }` — remove an existing file.
1038    DeleteFile { path: String },
1039    /// `{ type: "update_file", diff, path }` — apply a unified diff to an
1040    /// existing file at `path`.
1041    UpdateFile { diff: String, path: String },
1042}
1043
1044/// Status for a [`ResponseInputOutputItem::ApplyPatchCall`] /
1045/// [`ResponseOutputItem::ApplyPatchCall`] item.
1046///
1047/// Spec (openai-responses-api-spec.md §ApplyPatchCall L245):
1048/// `status: "in_progress" | "completed"`. Distinct from
1049/// [`ApplyPatchCallOutputStatus`] which adds `"failed"` for the output item.
1050#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, schemars::JsonSchema)]
1051#[serde(rename_all = "snake_case")]
1052pub enum ApplyPatchCallStatus {
1053    InProgress,
1054    Completed,
1055}
1056
1057/// Status for a [`ResponseInputOutputItem::ApplyPatchCallOutput`] /
1058/// [`ResponseOutputItem::ApplyPatchCallOutput`] item.
1059///
1060/// Spec (openai-responses-api-spec.md §ApplyPatchCallOutput L249):
1061/// `status: "completed" | "failed"`. The output-side status intentionally
1062/// drops `"in_progress"` because the output only materialises once the
1063/// apply_patch attempt has terminated — either the edit applied cleanly or
1064/// it failed — so an in-progress output would be spec-invalid.
1065#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, schemars::JsonSchema)]
1066#[serde(rename_all = "snake_case")]
1067pub enum ApplyPatchCallOutputStatus {
1068    Completed,
1069    Failed,
1070}
1071
1072#[serde_with::skip_serializing_none]
1073#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
1074#[serde(deny_unknown_fields)]
1075pub struct McpTool {
1076    pub server_url: Option<String>,
1077    pub authorization: Option<String>,
1078    /// Custom headers to send to MCP server (from request payload, not HTTP headers)
1079    pub headers: Option<HashMap<String, String>>,
1080    pub server_label: String,
1081    pub server_description: Option<String>,
1082    /// Approval requirement configuration for MCP tools.
1083    pub require_approval: Option<RequireApproval>,
1084    /// List of allowed tool names, or a filter object with `read_only` /
1085    /// `tool_names` fields. Spec (openai-responses-api-spec.md L442):
1086    /// `allowed_tools: McpAllowedTools = array of string | McpToolFilter`.
1087    /// Backward-compat: legacy `["a","b"]` wire shape still deserializes via
1088    /// the untagged `List` variant.
1089    pub allowed_tools: Option<McpAllowedTools>,
1090    /// Identifier for service connectors (e.g. Dropbox, Gmail). One of
1091    /// `server_url` or `connector_id` must be provided per spec
1092    /// (openai-responses-api-spec.md L441-445).
1093    pub connector_id: Option<McpConnectorId>,
1094    /// When `true`, the MCP server's tool list is fetched lazily on first
1095    /// use rather than eagerly at request time. Spec
1096    /// (openai-responses-api-spec.md L441): `defer_loading?: bool`.
1097    /// Not yet present in OpenAI Python SDK 2.8.1 `types/responses/tool.py`;
1098    /// included here to track the documented Responses API surface.
1099    pub defer_loading: Option<bool>,
1100}
1101
1102/// Allowed-tools filter for an MCP tool.
1103///
1104/// Spec (openai-responses-api-spec.md L442): `array of string | McpToolFilter`.
1105///
1106/// Variant order matters for `#[serde(untagged)]`: serde tries `List` first
1107/// (JSON array) so a bare `["foo","bar"]` wire shape keeps deserializing into
1108/// `List(vec!["foo","bar"])`. A JSON object falls through to `Filter`.
1109#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, schemars::JsonSchema)]
1110#[serde(untagged)]
1111pub enum McpAllowedTools {
1112    /// Flat allow-list of tool names.
1113    List(Vec<String>),
1114    /// Object filter selecting tools by read-only flag and/or explicit names.
1115    Filter(McpToolFilter),
1116}
1117
1118/// Object filter selecting MCP tools by read-only flag and/or explicit names.
1119///
1120/// Spec (openai-responses-api-spec.md L442): `McpToolFilter { read_only?, tool_names? }`.
1121///
1122/// `deny_unknown_fields` is applied so typoed keys (e.g. `"tool_namse"`) fail
1123/// fast at deserialize time rather than silently collapsing to an empty filter
1124/// — an empty filter would be projected by the router to "no name constraint",
1125/// unexpectedly broadening MCP tool exposure for payloads that meant to scope
1126/// it down.
1127#[serde_with::skip_serializing_none]
1128#[derive(Debug, Clone, Deserialize, Serialize, Default, PartialEq, Eq, schemars::JsonSchema)]
1129#[serde(deny_unknown_fields)]
1130pub struct McpToolFilter {
1131    /// Match tools flagged read-only via MCP `readOnlyHint` annotation.
1132    #[serde(default, skip_serializing_if = "Option::is_none")]
1133    pub read_only: Option<bool>,
1134    /// Explicit list of allowed tool names.
1135    #[serde(default, skip_serializing_if = "Option::is_none")]
1136    pub tool_names: Option<Vec<String>>,
1137}
1138
1139/// Service-connector identifier for hosted MCP tools.
1140///
1141/// Spec (openai-responses-api-spec.md L443): exactly one of `server_url` or
1142/// `connector_id` is required on `McpTool`. The wire values are literal
1143/// snake-case strings such as `"connector_dropbox"`.
1144///
1145/// Enum values mirror OpenAI Python SDK 2.8.1
1146/// `types/responses/tool.py::Mcp.connector_id`.
1147#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, schemars::JsonSchema)]
1148pub enum McpConnectorId {
1149    #[serde(rename = "connector_dropbox")]
1150    Dropbox,
1151    #[serde(rename = "connector_gmail")]
1152    Gmail,
1153    #[serde(rename = "connector_googlecalendar")]
1154    GoogleCalendar,
1155    #[serde(rename = "connector_googledrive")]
1156    GoogleDrive,
1157    #[serde(rename = "connector_microsoftteams")]
1158    MicrosoftTeams,
1159    #[serde(rename = "connector_outlookcalendar")]
1160    OutlookCalendar,
1161    #[serde(rename = "connector_outlookemail")]
1162    OutlookEmail,
1163    #[serde(rename = "connector_sharepoint")]
1164    SharePoint,
1165}
1166
1167#[serde_with::skip_serializing_none]
1168#[derive(Debug, Clone, Deserialize, Serialize, Default, schemars::JsonSchema)]
1169#[serde(deny_unknown_fields)]
1170pub struct WebSearchPreviewTool {
1171    pub search_context_size: Option<String>,
1172    pub user_location: Option<Value>,
1173}
1174
1175/// Non-preview hosted web search tool configuration.
1176///
1177/// Spec: `{ type: "web_search" | "web_search_2025_08_26", filters? { allowed_domains? },
1178/// return_token_budget?: "default"|"unlimited", search_context_size?: "low"|"medium"|"high",
1179/// user_location? }`.
1180///
1181/// Distinct from `WebSearchPreviewTool`: adds `filters.allowed_domains` (domain
1182/// allowlist), `return_token_budget`, and pins `search_context_size` to the spec-listed enum.
1183#[serde_with::skip_serializing_none]
1184#[derive(Debug, Clone, Deserialize, Serialize, Default, schemars::JsonSchema)]
1185#[serde(deny_unknown_fields)]
1186pub struct WebSearchTool {
1187    /// Optional domain allowlist applied to candidate sources.
1188    pub filters: Option<WebSearchFilters>,
1189    /// Search-result context token budget. Spec enum: `"default" | "unlimited"`.
1190    pub return_token_budget: Option<WebSearchReturnTokenBudget>,
1191    /// Search context budget. Spec enum: `"low" | "medium" | "high"`.
1192    pub search_context_size: Option<WebSearchContextSize>,
1193    /// Approximate user location used to bias results.
1194    pub user_location: Option<WebSearchUserLocation>,
1195}
1196
1197/// Filters for the non-preview `web_search` tool.
1198///
1199/// Spec: `filters? { allowed_domains? }`.
1200#[serde_with::skip_serializing_none]
1201#[derive(Debug, Clone, Deserialize, Serialize, Default, schemars::JsonSchema)]
1202#[serde(deny_unknown_fields)]
1203pub struct WebSearchFilters {
1204    /// Optional list of domains to restrict search results to.
1205    pub allowed_domains: Option<Vec<String>>,
1206}
1207
1208/// Search context budget for the non-preview `web_search` tool.
1209///
1210/// Spec: `search_context_size?: "low" | "medium" | "high"`.
1211#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, schemars::JsonSchema)]
1212#[serde(rename_all = "snake_case")]
1213pub enum WebSearchContextSize {
1214    Low,
1215    Medium,
1216    High,
1217}
1218
1219/// Search-result token budget for the non-preview `web_search` tool.
1220///
1221/// Spec: `return_token_budget?: "default" | "unlimited"`.
1222#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, schemars::JsonSchema)]
1223#[serde(rename_all = "snake_case")]
1224pub enum WebSearchReturnTokenBudget {
1225    Default,
1226    Unlimited,
1227}
1228
1229/// Approximate user location for the non-preview `web_search` tool.
1230///
1231/// Spec: `user_location: { city?, country?: ISO2, region?, timezone?: IANA, type?: "approximate" }`.
1232#[serde_with::skip_serializing_none]
1233#[derive(Debug, Clone, Deserialize, Serialize, Default, schemars::JsonSchema)]
1234#[serde(deny_unknown_fields)]
1235pub struct WebSearchUserLocation {
1236    /// City name.
1237    pub city: Option<String>,
1238    /// ISO-3166-1 alpha-2 country code (e.g. `"US"`).
1239    pub country: Option<String>,
1240    /// Region / state / province name.
1241    pub region: Option<String>,
1242    /// IANA timezone identifier (e.g. `"America/Los_Angeles"`).
1243    pub timezone: Option<String>,
1244    /// Discriminator. Spec only enumerates `"approximate"`.
1245    #[serde(rename = "type")]
1246    pub location_type: Option<String>,
1247}
1248
1249#[serde_with::skip_serializing_none]
1250#[derive(Debug, Clone, Deserialize, Serialize, Default, schemars::JsonSchema)]
1251#[serde(deny_unknown_fields)]
1252pub struct CodeInterpreterTool {
1253    pub container: Option<Value>,
1254    pub environment: Option<ResponseToolEnvironment>,
1255}
1256
1257#[serde_with::skip_serializing_none]
1258#[derive(Debug, Clone, Deserialize, Serialize, Default, schemars::JsonSchema)]
1259#[serde(deny_unknown_fields)]
1260pub struct ResponseToolEnvironment {}
1261
1262/// Configuration payload for the `image_generation` built-in tool.
1263///
1264/// Spec: `{ type: "image_generation", action?, background?, input_fidelity?,
1265/// input_image_mask?, model?, moderation?, output_compression?, output_format?,
1266/// partial_images?, quality?, size? }`. All inner fields are optional; the
1267/// model picks defaults documented in the spec.
1268#[serde_with::skip_serializing_none]
1269#[derive(Debug, Clone, Deserialize, Serialize, Default, schemars::JsonSchema)]
1270#[serde(deny_unknown_fields)]
1271pub struct ImageGenerationTool {
1272    /// `"generate" | "edit" | "auto"` (default `auto`). Free-form string here so
1273    /// future actions added by OpenAI deserialize without a wire break.
1274    pub action: Option<String>,
1275    /// `"transparent" | "opaque" | "auto"` (default `auto`).
1276    pub background: Option<String>,
1277    /// `"high" | "low"` — gpt-image-1 / gpt-image-1.5 only (not 1-mini).
1278    pub input_fidelity: Option<String>,
1279    /// Reference image used by `edit` action; mask either by uploaded file or URL.
1280    pub input_image_mask: Option<ImageInputMask>,
1281    /// `string | "gpt-image-1" | "gpt-image-1-mini" | "gpt-image-1.5"` — kept as
1282    /// `String` so unknown model identifiers passed through unchanged.
1283    pub model: Option<String>,
1284    /// `"auto" | "low"`.
1285    pub moderation: Option<String>,
1286    /// Output compression level. Spec default `100` when omitted; we keep
1287    /// `Option` so an unset field round-trips as absent (not `null`, via
1288    /// `#[serde_with::skip_serializing_none]`) rather than forcing 100.
1289    pub output_compression: Option<u32>,
1290    /// `"png" | "webp" | "jpeg"`.
1291    pub output_format: Option<String>,
1292    /// `0..3` — number of partial images to stream.
1293    pub partial_images: Option<u32>,
1294    /// `"low" | "medium" | "high" | "auto"`.
1295    pub quality: Option<String>,
1296    /// `"1024x1024" | "1024x1536" | "1536x1024" | "auto"`.
1297    pub size: Option<String>,
1298}
1299
1300/// Mask reference for image-generation `edit` calls. Spec: `{ file_id?, image_url? }`.
1301/// Reuses the same upload conventions as P1 `InputImage`.
1302#[serde_with::skip_serializing_none]
1303#[derive(Debug, Clone, Deserialize, Serialize, Default, schemars::JsonSchema)]
1304#[serde(deny_unknown_fields)]
1305pub struct ImageInputMask {
1306    pub file_id: Option<String>,
1307    pub image_url: Option<String>,
1308}
1309
1310/// Status values for an `image_generation_call` output item.
1311///
1312/// Spec: `"in_progress" | "completed" | "generating" | "failed"`.
1313#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, schemars::JsonSchema)]
1314#[serde(rename_all = "snake_case")]
1315pub enum ImageGenerationCallStatus {
1316    InProgress,
1317    Completed,
1318    Generating,
1319    Failed,
1320}
1321
1322/// `require_approval` values.
1323#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, schemars::JsonSchema)]
1324#[serde(untagged)]
1325pub enum RequireApproval {
1326    Mode(RequireApprovalMode),
1327    Rules(RequireApprovalRules),
1328}
1329
1330#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, schemars::JsonSchema)]
1331#[serde(rename_all = "snake_case")]
1332pub enum RequireApprovalMode {
1333    Always,
1334    Never,
1335}
1336
1337#[serde_with::skip_serializing_none]
1338#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, schemars::JsonSchema)]
1339#[serde(deny_unknown_fields)]
1340pub struct RequireApprovalRules {
1341    pub always: Option<RequireApprovalFilter>,
1342    pub never: Option<RequireApprovalFilter>,
1343}
1344
1345#[serde_with::skip_serializing_none]
1346#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, schemars::JsonSchema)]
1347#[serde(deny_unknown_fields)]
1348pub struct RequireApprovalFilter {
1349    pub tool_names: Option<Vec<String>>,
1350    pub read_only: Option<bool>,
1351}
1352
1353// ============================================================================
1354// Computer Tool (T2)
1355// ============================================================================
1356
1357/// Computer-use preview tool payload.
1358///
1359/// Spec (openai-responses-api-spec.md §tools): `ComputerUsePreview
1360/// { display_height, display_width, environment, type: "computer_use_preview" }`.
1361#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
1362#[serde(deny_unknown_fields)]
1363pub struct ComputerUsePreviewTool {
1364    /// Height of the simulated display in pixels.
1365    pub display_height: u32,
1366    /// Width of the simulated display in pixels.
1367    pub display_width: u32,
1368    /// Operating environment the model should target.
1369    pub environment: ComputerEnvironment,
1370}
1371
1372/// Environment selector for [`ComputerUsePreviewTool`].
1373///
1374/// Spec (openai-responses-api-spec.md §tools): `environment:
1375/// "windows"|"mac"|"linux"|"ubuntu"|"browser"`.
1376#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, schemars::JsonSchema)]
1377#[serde(rename_all = "snake_case")]
1378pub enum ComputerEnvironment {
1379    Windows,
1380    Mac,
1381    Linux,
1382    Ubuntu,
1383    Browser,
1384}
1385
1386/// Mouse button used by the `Click` action.
1387///
1388/// Spec (openai-responses-api-spec.md §ComputerAction): `button:
1389/// "left"|"right"|"wheel"|"back"|"forward"`. Only the `Click` action carries a
1390/// `button` field; `Scroll` uses `scroll_x`/`scroll_y` offsets.
1391#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, schemars::JsonSchema)]
1392#[serde(rename_all = "snake_case")]
1393pub enum MouseButton {
1394    Left,
1395    Right,
1396    Wheel,
1397    Back,
1398    Forward,
1399}
1400
1401/// `(x, y)` coordinate pair used in `Drag.path` and `Move/Click/Scroll` actions.
1402#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, schemars::JsonSchema)]
1403#[serde(deny_unknown_fields)]
1404pub struct ComputerCoordinate {
1405    pub x: i32,
1406    pub y: i32,
1407}
1408
1409/// Discriminated union of computer-use actions the model may emit.
1410///
1411/// Spec (openai-responses-api-spec.md §ComputerAction):
1412/// `Click | DoubleClick | Drag | Keypress | Move | Screenshot | Scroll | Type | Wait`.
1413#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
1414#[serde(tag = "type", rename_all = "snake_case")]
1415pub enum ComputerAction {
1416    /// `{ type: "click", button, x, y, keys? }`.
1417    Click {
1418        button: MouseButton,
1419        x: i32,
1420        y: i32,
1421        #[serde(default, skip_serializing_if = "Option::is_none")]
1422        keys: Option<Vec<String>>,
1423    },
1424    /// `{ type: "double_click", x, y, keys? }`.
1425    DoubleClick {
1426        x: i32,
1427        y: i32,
1428        #[serde(default, skip_serializing_if = "Option::is_none")]
1429        keys: Option<Vec<String>>,
1430    },
1431    /// `{ type: "drag", path, keys? }`.
1432    Drag {
1433        path: Vec<ComputerCoordinate>,
1434        #[serde(default, skip_serializing_if = "Option::is_none")]
1435        keys: Option<Vec<String>>,
1436    },
1437    /// `{ type: "keypress", keys }`.
1438    Keypress { keys: Vec<String> },
1439    /// `{ type: "move", x, y, keys? }`.
1440    Move {
1441        x: i32,
1442        y: i32,
1443        #[serde(default, skip_serializing_if = "Option::is_none")]
1444        keys: Option<Vec<String>>,
1445    },
1446    /// `{ type: "screenshot" }`.
1447    Screenshot,
1448    /// `{ type: "scroll", scroll_x, scroll_y, x, y, keys? }`.
1449    Scroll {
1450        scroll_x: i32,
1451        scroll_y: i32,
1452        x: i32,
1453        y: i32,
1454        #[serde(default, skip_serializing_if = "Option::is_none")]
1455        keys: Option<Vec<String>>,
1456    },
1457    /// `{ type: "type", text }`.
1458    Type { text: String },
1459    /// `{ type: "wait" }`.
1460    Wait,
1461}
1462
1463/// Status for a [`ResponseInputOutputItem::ComputerCall`] /
1464/// [`ResponseOutputItem::ComputerCall`] item.
1465///
1466/// Spec (openai-responses-api-spec.md §ComputerCall): `status: "in_progress"
1467/// | "completed" | "incomplete"`.
1468#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, schemars::JsonSchema)]
1469#[serde(rename_all = "snake_case")]
1470pub enum ComputerCallStatus {
1471    InProgress,
1472    Completed,
1473    Incomplete,
1474}
1475
1476/// One pending or acknowledged safety check attached to a computer-use call.
1477///
1478/// Spec (openai-responses-api-spec.md §ComputerCall): `pending_safety_checks:
1479/// array of { id, code, message }`. Per SDK v2.8.1
1480/// (`types/responses/response_computer_tool_call.py::PendingSafetyCheck`),
1481/// `code` and `message` are `Optional[str] = None`, so we mirror that here to
1482/// round-trip payloads that omit either field.
1483#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, schemars::JsonSchema)]
1484#[serde(deny_unknown_fields)]
1485pub struct ComputerSafetyCheck {
1486    pub id: String,
1487    #[serde(default, skip_serializing_if = "Option::is_none")]
1488    pub code: Option<String>,
1489    #[serde(default, skip_serializing_if = "Option::is_none")]
1490    pub message: Option<String>,
1491}
1492
1493/// Output payload of a [`ResponseInputOutputItem::ComputerCallOutput`] item.
1494///
1495/// Spec (openai-responses-api-spec.md §ComputerCallOutput.output):
1496/// `ResponseComputerToolCallOutputScreenshot { type: "computer_screenshot",
1497/// file_id?, image_url? }`.
1498#[serde_with::skip_serializing_none]
1499#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
1500#[serde(tag = "type", rename_all = "snake_case")]
1501pub enum ComputerCallOutputContent {
1502    /// `{ type: "computer_screenshot", file_id?, image_url? }`.
1503    ComputerScreenshot {
1504        #[serde(default, skip_serializing_if = "Option::is_none")]
1505        file_id: Option<String>,
1506        #[serde(default, skip_serializing_if = "Option::is_none")]
1507        image_url: Option<String>,
1508    },
1509}
1510
1511// ============================================================================
1512// Reasoning Parameters
1513// ============================================================================
1514
1515#[serde_with::skip_serializing_none]
1516#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
1517pub struct ResponseReasoningParam {
1518    #[serde(default = "default_reasoning_effort")]
1519    pub effort: Option<ReasoningEffort>,
1520    pub summary: Option<ReasoningSummary>,
1521}
1522
1523#[expect(
1524    clippy::unnecessary_wraps,
1525    reason = "serde default function must match field type Option<T>"
1526)]
1527fn default_reasoning_effort() -> Option<ReasoningEffort> {
1528    Some(ReasoningEffort::Medium)
1529}
1530
1531#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
1532#[serde(rename_all = "snake_case")]
1533pub enum ReasoningEffort {
1534    Minimal,
1535    Low,
1536    Medium,
1537    High,
1538}
1539
1540#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
1541#[serde(rename_all = "snake_case")]
1542pub enum ReasoningSummary {
1543    Auto,
1544    Concise,
1545    Detailed,
1546}
1547
1548// ============================================================================
1549// Input/Output Items
1550// ============================================================================
1551
1552/// Content can be either a simple string or array of content parts (for SimpleInputMessage)
1553#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
1554#[serde(untagged)]
1555pub enum StringOrContentParts {
1556    String(String),
1557    Array(Vec<ResponseContentPart>),
1558}
1559
1560/// Phase label for assistant messages in the Responses API.
1561///
1562/// For gpt-5.3-codex+ multi-turn conversations, preserving and resending the
1563/// original `phase` value avoids quality / latency degradation — the model
1564/// relies on it to disambiguate commentary-style reasoning from the final
1565/// answer. Opaque to SMG otherwise; preserved verbatim through store+retrieve,
1566/// SSE output, and upstream passthrough.
1567#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, schemars::JsonSchema)]
1568#[serde(rename_all = "snake_case")]
1569pub enum MessagePhase {
1570    Commentary,
1571    FinalAnswer,
1572}
1573
1574#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
1575#[serde(tag = "type")]
1576#[serde(rename_all = "snake_case")]
1577pub enum ResponseInputOutputItem {
1578    #[serde(rename = "message")]
1579    Message {
1580        id: String,
1581        role: String,
1582        content: Vec<ResponseContentPart>,
1583        #[serde(skip_serializing_if = "Option::is_none")]
1584        status: Option<String>,
1585        /// Optional phase label, preserved from previous assistant output so
1586        /// gpt-5.3-codex+ multi-turn does not degrade (spec: ResponseOutputMessage.phase).
1587        #[serde(default, skip_serializing_if = "Option::is_none")]
1588        phase: Option<MessagePhase>,
1589    },
1590    #[serde(rename = "reasoning")]
1591    #[non_exhaustive]
1592    Reasoning {
1593        id: String,
1594        #[serde(default, skip_serializing_if = "Vec::is_empty")]
1595        summary: Vec<SummaryTextContent>,
1596        #[serde(skip_serializing_if = "Vec::is_empty")]
1597        #[serde(default)]
1598        content: Vec<ResponseReasoningContent>,
1599        /// Encrypted reasoning payload for gpt-5 / o-series round-trip via
1600        /// `previous_response_id`. Opaque to SMG; preserved verbatim.
1601        #[serde(skip_serializing_if = "Option::is_none")]
1602        #[serde(default)]
1603        encrypted_content: Option<String>,
1604        #[serde(skip_serializing_if = "Option::is_none")]
1605        status: Option<String>,
1606    },
1607    #[serde(rename = "function_call")]
1608    FunctionToolCall {
1609        id: String,
1610        call_id: String,
1611        name: String,
1612        arguments: String,
1613        #[serde(skip_serializing_if = "Option::is_none")]
1614        output: Option<String>,
1615        #[serde(skip_serializing_if = "Option::is_none")]
1616        status: Option<String>,
1617    },
1618    #[serde(rename = "function_call_output")]
1619    FunctionCallOutput {
1620        id: Option<String>,
1621        call_id: String,
1622        output: String,
1623        #[serde(skip_serializing_if = "Option::is_none")]
1624        status: Option<String>,
1625    },
1626    #[serde(rename = "mcp_approval_request")]
1627    McpApprovalRequest {
1628        id: String,
1629        server_label: String,
1630        name: String,
1631        arguments: String,
1632    },
1633    #[serde(rename = "mcp_approval_response")]
1634    McpApprovalResponse {
1635        #[serde(skip_serializing_if = "Option::is_none")]
1636        id: Option<String>,
1637        approval_request_id: String,
1638        approve: bool,
1639        #[serde(skip_serializing_if = "Option::is_none")]
1640        reason: Option<String>,
1641    },
1642    /// `type: "image_generation_call"` — round-trip form for an image generated
1643    /// in a prior turn. Spec (OpenAI Responses API, multi-turn image-edit
1644    /// flow): clients may resubmit only `{ type, id }` to reference a prior
1645    /// generation by identifier, so `result` and `status` are accepted as
1646    /// absent on the input side. The full shape is
1647    /// `{ id, action?, background?, output_format?, quality?, result?: base64,
1648    /// revised_prompt?, size?, status?, type }`.
1649    ///
1650    /// This mirrors the OpenAI Python SDK 2.8.x
1651    /// `response_input_item_param.ImageGenerationCall` TypedDict: while the
1652    /// TypedDict types those fields as `Required[Optional[...]]`, the HTTP
1653    /// API itself documents the id-only multi-turn reference form (see the
1654    /// image-generation tool guide), and `skip_serializing_if` keeps the
1655    /// serialized form spec-compatible when a full item is round-tripped.
1656    /// The server-side `ResponseOutputItem::ImageGenerationCall` variant
1657    /// carries the same metadata so real OpenAI responses
1658    /// (`action`/`background`/`output_format`/`quality`/`size`) survive
1659    /// cloud-passthrough and persistence round-trips.
1660    ///
1661    /// The metadata fields (`action`, `background`, `output_format`,
1662    /// `quality`, `size`) are typed as `Option<String>` rather than
1663    /// narrow enums so unknown or future-added values pass through
1664    /// unchanged — this mirrors `ImageGenerationTool` on the input-tool
1665    /// side.
1666    #[serde(rename = "image_generation_call")]
1667    ImageGenerationCall {
1668        id: String,
1669        /// `"generate" | "edit" | "auto"` — which image-generation action the
1670        /// prior turn dispatched. Preserved free-form so future actions pass
1671        /// through without a wire break.
1672        #[serde(default, skip_serializing_if = "Option::is_none")]
1673        action: Option<String>,
1674        /// `"transparent" | "opaque" | "auto"`. Matches the
1675        /// `image_generation` tool input knob of the same name.
1676        #[serde(default, skip_serializing_if = "Option::is_none")]
1677        background: Option<String>,
1678        /// `"png" | "webp" | "jpeg"`. Matches the `image_generation` tool
1679        /// input knob of the same name.
1680        #[serde(default, skip_serializing_if = "Option::is_none")]
1681        output_format: Option<String>,
1682        /// `"auto" | "low" | "medium" | "high" | "standard" | "hd"`. Matches
1683        /// the `image_generation` tool input knob of the same name.
1684        #[serde(default, skip_serializing_if = "Option::is_none")]
1685        quality: Option<String>,
1686        /// Base64-encoded image bytes. Omitted on id-only references.
1687        #[serde(default, skip_serializing_if = "Option::is_none")]
1688        result: Option<String>,
1689        /// Prompt text the mainline model rewrote before dispatching the
1690        /// image-generation call. Preserved so downstream turns/storage do
1691        /// not drop it on replay.
1692        #[serde(default, skip_serializing_if = "Option::is_none")]
1693        revised_prompt: Option<String>,
1694        /// `"auto" | "1024x1024" | "1024x1536" | "1536x1024"`. Matches the
1695        /// `image_generation` tool input knob of the same name.
1696        #[serde(default, skip_serializing_if = "Option::is_none")]
1697        size: Option<String>,
1698        /// Generation status. Omitted on id-only references.
1699        #[serde(default, skip_serializing_if = "Option::is_none")]
1700        status: Option<ImageGenerationCallStatus>,
1701    },
1702    /// `type: "compaction"` — opaque compacted-history payload generated by
1703    /// the `/v1/responses/compact` API. Spec
1704    /// (openai-responses-api-spec.md §InputItemList L203-205):
1705    /// `Compaction { encrypted_content, type, id }`. `id` is optional on the
1706    /// input wire so newly-minted client-side compactions can omit it; it is
1707    /// always present on items round-tripped from a previous response.
1708    #[serde(rename = "compaction")]
1709    Compaction {
1710        encrypted_content: String,
1711        #[serde(default, skip_serializing_if = "Option::is_none")]
1712        id: Option<String>,
1713    },
1714    /// `{ type: "computer_call", id, call_id, action?, actions?, status,
1715    /// pending_safety_checks }`.
1716    ///
1717    /// Spec (openai-responses-api-spec.md §ComputerCall): single-action
1718    /// `action` is the legacy shape; `actions` carries the flattened batch
1719    /// for `computer_use`. Both fields are optional independently, so callers
1720    /// can roundtrip either form.
1721    #[serde(rename = "computer_call")]
1722    ComputerCall {
1723        id: String,
1724        call_id: String,
1725        #[serde(default, skip_serializing_if = "Option::is_none")]
1726        action: Option<ComputerAction>,
1727        #[serde(default, skip_serializing_if = "Option::is_none")]
1728        actions: Option<Vec<ComputerAction>>,
1729        status: ComputerCallStatus,
1730        /// Always serialized (including an empty `[]`). The official OpenAI
1731        /// Python SDK (`openai==2.8.1`,
1732        /// `types/responses/response_computer_tool_call.py`) declares this as a
1733        /// non-`Optional` `List[PendingSafetyCheck]`, so the field must always
1734        /// appear on the wire — an empty array is semantically distinct from
1735        /// omitting the field.
1736        #[serde(default)]
1737        pending_safety_checks: Vec<ComputerSafetyCheck>,
1738    },
1739    /// `{ type: "computer_call_output", id?, call_id, output,
1740    /// acknowledged_safety_checks?, status? }`.
1741    ///
1742    /// Spec (openai-responses-api-spec.md §ComputerCallOutput): `output` is the
1743    /// [`ComputerCallOutputContent::ComputerScreenshot`] payload;
1744    /// `acknowledged_safety_checks` and `status` are both optional per spec.
1745    #[serde(rename = "computer_call_output")]
1746    ComputerCallOutput {
1747        #[serde(default, skip_serializing_if = "Option::is_none")]
1748        id: Option<String>,
1749        call_id: String,
1750        output: ComputerCallOutputContent,
1751        #[serde(default, skip_serializing_if = "Vec::is_empty")]
1752        acknowledged_safety_checks: Vec<ComputerSafetyCheck>,
1753        #[serde(default, skip_serializing_if = "Option::is_none")]
1754        status: Option<ComputerCallStatus>,
1755    },
1756    /// `type: "custom_tool_call"` — assistant's call into a registered
1757    /// custom tool. Spec: `{ call_id, input, name, type, id?, namespace? }`.
1758    /// `id` / `namespace` are modelled as `Option<String>` so newly-minted
1759    /// client-side calls can omit them; they are populated on items
1760    /// round-tripped from a previous response. `input` is the model's
1761    /// free-form payload (constrained by the tool's `format` if grammar is
1762    /// set); the client owns execution and replies with a matching
1763    /// [`Self::CustomToolCallOutput`].
1764    #[serde(rename = "custom_tool_call")]
1765    CustomToolCall {
1766        call_id: String,
1767        input: String,
1768        name: String,
1769        #[serde(skip_serializing_if = "Option::is_none")]
1770        id: Option<String>,
1771        #[serde(skip_serializing_if = "Option::is_none")]
1772        namespace: Option<String>,
1773    },
1774    /// `type: "custom_tool_call_output"` — client's response to a
1775    /// `custom_tool_call`. Spec: `{ call_id, output, type, id? }` (no
1776    /// `status` field per spec — see Drift Log entry for T8). `id` is
1777    /// `Option<String>` for the same reason as `CustomToolCall.id` above.
1778    /// `output` is either a plain string or an array of input-typed content
1779    /// parts (`input_text` / `input_image` / `input_file`).
1780    #[serde(rename = "custom_tool_call_output")]
1781    CustomToolCallOutput {
1782        call_id: String,
1783        output: CustomToolCallOutputContent,
1784        #[serde(skip_serializing_if = "Option::is_none")]
1785        id: Option<String>,
1786    },
1787    /// `type: "shell_call"` — assistant's call into the containerized
1788    /// [`ResponseTool::Shell`] tool.
1789    ///
1790    /// Spec (openai-responses-api-spec.md §ShellCall, L228-231) +
1791    /// OpenAI SDK v2.8.1 `ResponseFunctionShellToolCall`:
1792    /// `{ action, call_id, type, id, environment, status, created_by? }`.
1793    /// `id` is `Option<String>` so newly-minted client-side calls can omit
1794    /// it; the model populates it on items round-tripped from a previous
1795    /// response. `created_by` carries provenance metadata the SDK types
1796    /// as `Optional[str]` — present when the item was emitted by the
1797    /// platform, absent on client-authored calls.
1798    #[serde(rename = "shell_call")]
1799    ShellCall {
1800        action: ShellCallAction,
1801        call_id: String,
1802        #[serde(default, skip_serializing_if = "Option::is_none")]
1803        id: Option<String>,
1804        /// Resolved execution environment. Spec constrains this to
1805        /// `local` or `container_reference` on the call form (see
1806        /// [`ShellCallEnvironment`] docs).
1807        #[serde(default, skip_serializing_if = "Option::is_none")]
1808        environment: Option<ShellCallEnvironment>,
1809        #[serde(default, skip_serializing_if = "Option::is_none")]
1810        status: Option<ShellCallStatus>,
1811        /// Provenance tag mirroring the SDK's `created_by: Optional[str]`
1812        /// on `ResponseFunctionShellToolCall`. Dropped at serialize time
1813        /// when absent so client-authored calls do not carry a null
1814        /// placeholder.
1815        #[serde(default, skip_serializing_if = "Option::is_none")]
1816        created_by: Option<String>,
1817    },
1818    /// `type: "shell_call_output"` — client's reply to a `shell_call`.
1819    ///
1820    /// Spec (openai-responses-api-spec.md §ShellCallOutput, L233-238) +
1821    /// OpenAI SDK v2.8.1 `ResponseFunctionShellToolCallOutput`:
1822    /// `{ call_id, output, type, id, max_output_length?, status, created_by? }`.
1823    /// `id`, `max_output_length`, and `created_by` are modelled as
1824    /// `Option` per the SDK's `Optional[...]` typing; the server populates
1825    /// them on items round-tripped from a previous response.
1826    #[serde(rename = "shell_call_output")]
1827    ShellCallOutput {
1828        call_id: String,
1829        output: Vec<ShellOutputChunk>,
1830        #[serde(default, skip_serializing_if = "Option::is_none")]
1831        id: Option<String>,
1832        #[serde(default, skip_serializing_if = "Option::is_none")]
1833        max_output_length: Option<u64>,
1834        #[serde(default, skip_serializing_if = "Option::is_none")]
1835        status: Option<ShellCallStatus>,
1836        /// Provenance tag mirroring the SDK's `created_by: Optional[str]`
1837        /// on `ResponseFunctionShellToolCallOutput`.
1838        #[serde(default, skip_serializing_if = "Option::is_none")]
1839        created_by: Option<String>,
1840    },
1841    /// `type: "apply_patch_call"` — model-issued file-edit request. Spec
1842    /// (openai-responses-api-spec.md §ApplyPatchCall L240-L246):
1843    /// `{ call_id, operation, status, type, id }`. `id` is `Option<String>`
1844    /// so newly-minted client-side calls can omit it; it is always present on
1845    /// items round-tripped from a previous response. The `operation` union is
1846    /// `CreateFile | DeleteFile | UpdateFile` per
1847    /// [`ApplyPatchOperation`]; the client owns execution (apply the diff on
1848    /// disk) and replies with a matching [`Self::ApplyPatchCallOutput`].
1849    #[serde(rename = "apply_patch_call")]
1850    ApplyPatchCall {
1851        call_id: String,
1852        operation: ApplyPatchOperation,
1853        status: ApplyPatchCallStatus,
1854        #[serde(skip_serializing_if = "Option::is_none")]
1855        id: Option<String>,
1856    },
1857    /// `type: "apply_patch_call_output"` — client's response to an
1858    /// `apply_patch_call`. Spec (openai-responses-api-spec.md
1859    /// §ApplyPatchCallOutput L248-L251): `{ call_id, status, type, id,
1860    /// output }` where `output` is optional log text. `id` is
1861    /// `Option<String>` for the same reason as `ApplyPatchCall.id` above;
1862    /// `output` uses `skip_serializing_if` so a no-log success round-trips
1863    /// without emitting an explicit `null`.
1864    #[serde(rename = "apply_patch_call_output")]
1865    ApplyPatchCallOutput {
1866        call_id: String,
1867        status: ApplyPatchCallOutputStatus,
1868        #[serde(skip_serializing_if = "Option::is_none")]
1869        id: Option<String>,
1870        #[serde(skip_serializing_if = "Option::is_none")]
1871        output: Option<String>,
1872    },
1873    /// `type: "local_shell_call"` — assistant's call into the
1874    /// `local_shell` built-in tool. Spec
1875    /// (openai-responses-api-spec.md §LocalShellCall L219-222):
1876    /// `{ id, action, call_id, status, type }` where `action` is a
1877    /// [`LocalShellExec`] payload describing the command to run on the
1878    /// host. The client executes the command and replies with a
1879    /// matching [`Self::LocalShellCallOutput`].
1880    #[serde(rename = "local_shell_call")]
1881    LocalShellCall {
1882        id: String,
1883        call_id: String,
1884        action: LocalShellExec,
1885        status: LocalShellCallStatus,
1886    },
1887    /// `type: "local_shell_call_output"` — client's response to a
1888    /// `local_shell_call`. Spec
1889    /// (openai-responses-api-spec.md §LocalShellCallOutput L224-226):
1890    /// `{ id, output, type, status }`. `output` is a single string
1891    /// carrying the command's serialized JSON output; `status` is
1892    /// optional per SDK v2.8.1 (`openai==2.8.1`,
1893    /// `types/responses/response_input_item_param.py`
1894    /// `LocalShellCallOutput` — `Optional` on `status`).
1895    #[serde(rename = "local_shell_call_output")]
1896    LocalShellCallOutput {
1897        id: String,
1898        output: String,
1899        #[serde(default, skip_serializing_if = "Option::is_none")]
1900        status: Option<LocalShellCallStatus>,
1901    },
1902    /// `type: "mcp_call"` — assistant-emitted hosted-MCP tool call replayed
1903    /// as an input item for stateless multi-turn (`store=false`) flows.
1904    ///
1905    /// Spec (openai-responses-api-spec.md §McpCall L264-266):
1906    /// `{ id, arguments, name, server_label, type, approval_request_id?, error?, output?, status? }`.
1907    /// Shape mirrors [`ResponseOutputItem::McpCall`] but `approval_request_id`,
1908    /// `error`, `output`, and `status` are optional on the input side so
1909    /// replay of an abridged or in-flight call (no output yet) stays
1910    /// lossless. Matches OpenAI Python SDK 2.8.1
1911    /// `types/responses/response_input_item.py::McpCall`.
1912    #[serde(rename = "mcp_call")]
1913    McpCall {
1914        id: String,
1915        arguments: String,
1916        name: String,
1917        server_label: String,
1918        #[serde(default, skip_serializing_if = "Option::is_none")]
1919        approval_request_id: Option<String>,
1920        #[serde(default, skip_serializing_if = "Option::is_none")]
1921        error: Option<String>,
1922        #[serde(default, skip_serializing_if = "Option::is_none")]
1923        output: Option<String>,
1924        #[serde(default, skip_serializing_if = "Option::is_none")]
1925        status: Option<String>,
1926    },
1927    /// `type: "mcp_list_tools"` — hosted-MCP server's tool listing replayed
1928    /// as an input item.
1929    ///
1930    /// Spec (openai-responses-api-spec.md §McpListTools L253-255):
1931    /// `{ id, server_label, tools, type, error? }` where each `tools` entry
1932    /// is `{ input_schema, name, annotations?, description? }`. Shape
1933    /// mirrors [`ResponseOutputItem::McpListTools`] with `error` optional
1934    /// per SDK v2.8.1
1935    /// `types/responses/response_input_item.py::McpListTools`.
1936    #[serde(rename = "mcp_list_tools")]
1937    McpListTools {
1938        id: String,
1939        server_label: String,
1940        tools: Vec<McpToolInfo>,
1941        #[serde(default, skip_serializing_if = "Option::is_none")]
1942        error: Option<String>,
1943    },
1944    #[serde(untagged)]
1945    SimpleInputMessage {
1946        content: StringOrContentParts,
1947        role: String,
1948        /// Spec: `EasyInputMessage.type` is `optional "message"`. Constrained
1949        /// to a single-value tag enum so payloads with an unknown `type`
1950        /// (e.g. `"input_file"`, `"totally_made_up"`) do not silently land
1951        /// in this untagged catch-all variant — P5 fail-fast contract.
1952        #[serde(default, skip_serializing_if = "Option::is_none")]
1953        #[serde(rename = "type")]
1954        r#type: Option<SimpleInputMessageTypeTag>,
1955        /// Optional phase label (spec: EasyInputMessage.phase).
1956        ///
1957        /// Preserved through conversation storage so gpt-5.3-codex+ does not
1958        /// lose the commentary/final_answer distinction across turns.
1959        #[serde(default, skip_serializing_if = "Option::is_none")]
1960        phase: Option<MessagePhase>,
1961    },
1962    /// `type: "item_reference"` — pointer to a previously-stored item in the
1963    /// active conversation. Spec (openai-responses-api-spec.md §InputItemList
1964    /// L275-276): `ItemReference { id, type }` where `type` is
1965    /// `optional "item_reference"`. The variant is declared as
1966    /// `#[serde(untagged)]` because the `type` discriminator is optional on
1967    /// the wire; `r#type` is pinned to [`ItemReferenceTypeTag`] so payloads
1968    /// whose `type` is not `"item_reference"` (e.g. `"totally_made_up"`) do
1969    /// not silently land in this catch-all variant — P5 fail-fast contract.
1970    ///
1971    /// Declared AFTER [`Self::SimpleInputMessage`] so a `{id, role, content}`
1972    /// payload (the id-carrying shape of `SimpleInputMessage`) still lands in
1973    /// `SimpleInputMessage` first; only `{id}` / `{id, type: "item_reference"}`
1974    /// payloads — which fail `SimpleInputMessage`'s required-field check —
1975    /// fall through to this arm.
1976    ///
1977    /// Backend resolution (router looks up `id` from conversation history and
1978    /// substitutes the referenced item inline) is deferred to a future R
1979    /// task; this variant only adds the schema surface.
1980    #[serde(untagged)]
1981    ItemReference {
1982        id: String,
1983        #[serde(default, skip_serializing_if = "Option::is_none")]
1984        #[serde(rename = "type")]
1985        r#type: Option<ItemReferenceTypeTag>,
1986    },
1987}
1988
1989/// Single-value tag enum pinning [`ResponseInputOutputItem::ItemReference`]'s
1990/// optional `type` discriminator to the spec's only permitted value,
1991/// `"item_reference"`. Used because the outer enum is `type`-tagged and the
1992/// `ItemReference` variant is declared `#[serde(untagged)]` to accept payloads
1993/// that omit `type` entirely — without this pin the catch-all would silently
1994/// swallow payloads whose `type` discriminator is an unknown string.
1995#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, schemars::JsonSchema)]
1996#[serde(rename_all = "snake_case")]
1997pub enum ItemReferenceTypeTag {
1998    ItemReference,
1999}
2000
2001/// Single-value tag enum pinning `EasyInputMessage.type` to the spec's only
2002/// permitted value, `"message"`. Used to keep [`ResponseInputOutputItem::SimpleInputMessage`]
2003/// — which is the `#[serde(untagged)]` fallback in the outer `type`-tagged enum
2004/// — from silently swallowing payloads whose `type` discriminator is unknown
2005/// (P5 fail-fast contract).
2006#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, schemars::JsonSchema)]
2007#[serde(rename_all = "snake_case")]
2008pub enum SimpleInputMessageTypeTag {
2009    Message,
2010}
2011
2012/// Detail level for [`ResponseContentPart::InputFile`]. Spec restricts this
2013/// to `"low" | "high"` (defaults to `low`); it is narrower than [`Detail`]
2014/// used for images which also admits `auto` / `original`.
2015#[derive(Debug, Clone, Default, Serialize, Deserialize, schemars::JsonSchema)]
2016#[serde(rename_all = "snake_case")]
2017pub enum FileDetail {
2018    #[default]
2019    Low,
2020    High,
2021}
2022
2023/// Typed annotation attached to [`ResponseContentPart::OutputText`]. Matches
2024/// the OpenAI Responses API `Annotation` union; a `type` discriminator selects
2025/// the variant on the wire.
2026#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
2027#[serde(tag = "type", rename_all = "snake_case")]
2028pub enum Annotation {
2029    /// `type: "file_citation"` — points at a file previously uploaded.
2030    FileCitation {
2031        file_id: String,
2032        filename: String,
2033        index: u32,
2034    },
2035    /// `type: "url_citation"` — citation back to a URL in a web-search result.
2036    UrlCitation {
2037        url: String,
2038        title: String,
2039        start_index: u32,
2040        end_index: u32,
2041    },
2042    /// `type: "container_file_citation"` — citation to a file inside a
2043    /// code-interpreter / computer-use container.
2044    ContainerFileCitation {
2045        container_id: String,
2046        file_id: String,
2047        filename: String,
2048        start_index: u32,
2049        end_index: u32,
2050    },
2051    /// `type: "file_path"` — reference to a generated file path.
2052    FilePath { file_id: String, index: u32 },
2053}
2054
2055#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
2056#[serde(tag = "type")]
2057#[serde(rename_all = "snake_case")]
2058pub enum ResponseContentPart {
2059    #[serde(rename = "output_text")]
2060    OutputText {
2061        text: String,
2062        #[serde(default)]
2063        annotations: Vec<Annotation>,
2064        #[serde(skip_serializing_if = "Option::is_none")]
2065        logprobs: Option<ChatLogProbs>,
2066    },
2067    #[serde(rename = "input_text")]
2068    InputText { text: String },
2069    /// `type: "input_image"` — reference to an image supplied by the client.
2070    /// Exactly one of `file_id` / `image_url` is typically set; both may be
2071    /// absent when only `detail` is being conveyed.
2072    #[serde(rename = "input_image")]
2073    InputImage {
2074        #[serde(skip_serializing_if = "Option::is_none")]
2075        detail: Option<Detail>,
2076        #[serde(skip_serializing_if = "Option::is_none")]
2077        file_id: Option<String>,
2078        #[serde(skip_serializing_if = "Option::is_none")]
2079        image_url: Option<String>,
2080    },
2081    /// `type: "input_file"` — reference to an attached file. `file_data` is a
2082    /// base64 blob; `file_url` / `file_id` reference external/uploaded files.
2083    #[serde(rename = "input_file")]
2084    InputFile {
2085        #[serde(skip_serializing_if = "Option::is_none")]
2086        detail: Option<FileDetail>,
2087        #[serde(skip_serializing_if = "Option::is_none")]
2088        file_data: Option<String>,
2089        #[serde(skip_serializing_if = "Option::is_none")]
2090        file_id: Option<String>,
2091        #[serde(skip_serializing_if = "Option::is_none")]
2092        file_url: Option<String>,
2093        #[serde(skip_serializing_if = "Option::is_none")]
2094        filename: Option<String>,
2095    },
2096    /// `type: "refusal"` — model refusal surfaced as a content part (spec's
2097    /// `ResponseOutputRefusal`).
2098    #[serde(rename = "refusal")]
2099    Refusal { refusal: String },
2100}
2101
2102#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
2103#[serde(tag = "type")]
2104#[serde(rename_all = "snake_case")]
2105pub enum ResponseReasoningContent {
2106    #[serde(rename = "reasoning_text")]
2107    ReasoningText { text: String },
2108}
2109
2110/// Tagged content element carried in `Reasoning.summary`.
2111///
2112/// OpenAI spec: `summary: array of SummaryTextContent { text, type: "summary_text" }`.
2113/// Replaces the prior `Vec<String>` wire-type that broke bidirectional
2114/// interoperability with spec-compliant clients.
2115#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
2116#[serde(tag = "type")]
2117#[serde(rename_all = "snake_case")]
2118pub enum SummaryTextContent {
2119    #[serde(rename = "summary_text")]
2120    SummaryText { text: String },
2121}
2122
2123/// MCP Tool information for the mcp_list_tools output item
2124#[serde_with::skip_serializing_none]
2125#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
2126pub struct McpToolInfo {
2127    pub name: String,
2128    pub description: Option<String>,
2129    pub input_schema: Value,
2130    pub annotations: Option<Value>,
2131}
2132
2133#[serde_with::skip_serializing_none]
2134#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
2135#[serde(tag = "type")]
2136#[serde(rename_all = "snake_case")]
2137pub enum ResponseOutputItem {
2138    #[serde(rename = "message")]
2139    Message {
2140        id: String,
2141        role: String,
2142        content: Vec<ResponseContentPart>,
2143        status: String,
2144        /// Optional phase label (spec: ResponseOutputMessage.phase).
2145        ///
2146        /// Labels assistant messages; for gpt-5.3-codex+ we must preserve and
2147        /// resend this on subsequent turns to avoid perf degradation.
2148        #[serde(default, skip_serializing_if = "Option::is_none")]
2149        phase: Option<MessagePhase>,
2150    },
2151    #[serde(rename = "reasoning")]
2152    #[non_exhaustive]
2153    Reasoning {
2154        id: String,
2155        #[serde(default, skip_serializing_if = "Vec::is_empty")]
2156        summary: Vec<SummaryTextContent>,
2157        content: Vec<ResponseReasoningContent>,
2158        /// Encrypted reasoning payload for gpt-5 / o-series round-trip.
2159        /// Opaque to SMG; preserved verbatim.
2160        #[serde(skip_serializing_if = "Option::is_none")]
2161        #[serde(default)]
2162        encrypted_content: Option<String>,
2163        status: Option<String>,
2164    },
2165    #[serde(rename = "function_call")]
2166    FunctionToolCall {
2167        id: String,
2168        call_id: String,
2169        name: String,
2170        arguments: String,
2171        output: Option<String>,
2172        status: String,
2173    },
2174    #[serde(rename = "mcp_list_tools")]
2175    McpListTools {
2176        id: String,
2177        server_label: String,
2178        tools: Vec<McpToolInfo>,
2179        /// Spec (openai-responses-api-spec.md §McpListTools L253-255):
2180        /// `error?: string`. Preserves the failure message when the MCP
2181        /// server could not list tools; symmetric with the matching field
2182        /// on `ResponseInputOutputItem::McpListTools` so emit↔replay is
2183        /// lossless.
2184        #[serde(default, skip_serializing_if = "Option::is_none")]
2185        error: Option<String>,
2186    },
2187    #[serde(rename = "mcp_call")]
2188    McpCall {
2189        id: String,
2190        status: String,
2191        approval_request_id: Option<String>,
2192        arguments: String,
2193        error: Option<String>,
2194        name: String,
2195        output: String,
2196        server_label: String,
2197    },
2198    #[serde(rename = "mcp_approval_request")]
2199    McpApprovalRequest {
2200        id: String,
2201        server_label: String,
2202        name: String,
2203        arguments: String,
2204    },
2205    #[serde(rename = "web_search_call")]
2206    WebSearchCall {
2207        id: String,
2208        status: WebSearchCallStatus,
2209        action: WebSearchAction,
2210        /// Search hits surfaced when callers request `web_search_call.results`
2211        /// via the top-level `include[]` array. Mirrors the `file_search_call.results`
2212        /// shape — array of typed entries when populated, omitted otherwise so the
2213        /// default wire shape (`{id, action, status, type}`) stays spec-byte-identical.
2214        #[serde(default, skip_serializing_if = "Option::is_none")]
2215        results: Option<Vec<WebSearchResult>>,
2216    },
2217    #[serde(rename = "code_interpreter_call")]
2218    CodeInterpreterCall {
2219        id: String,
2220        status: CodeInterpreterCallStatus,
2221        container_id: String,
2222        code: Option<String>,
2223        outputs: Option<Vec<CodeInterpreterOutput>>,
2224    },
2225    #[serde(rename = "file_search_call")]
2226    FileSearchCall {
2227        id: String,
2228        status: FileSearchCallStatus,
2229        queries: Vec<String>,
2230        results: Option<Vec<FileSearchResult>>,
2231    },
2232    /// `type: "image_generation_call"` — output item carrying a base64 image
2233    /// produced by the `image_generation` built-in tool. Spec:
2234    /// `{ id, action?, background?, output_format?, quality?, result: base64,
2235    /// revised_prompt?, size?, status, type }`.
2236    ///
2237    /// Real OpenAI production responses include the five metadata fields
2238    /// (`action`, `background`, `output_format`, `quality`, `size`) even
2239    /// though the OpenAI Rust SDK v2.8.1 omits them. We carry them as
2240    /// `Option<String>` so cloud passthrough and persistence round-trips
2241    /// preserve them verbatim — and so downstream consumers can read them
2242    /// without a second round-trip to the provider.
2243    ///
2244    /// The metadata fields are typed as `Option<String>` rather than narrow
2245    /// enums so unknown or future-added values pass through unchanged;
2246    /// this mirrors `ImageGenerationTool` on the input-tool side.
2247    #[serde(rename = "image_generation_call")]
2248    ImageGenerationCall {
2249        id: String,
2250        /// `"generate" | "edit" | "auto"` — which image-generation action
2251        /// this call dispatched. Preserved free-form so future actions pass
2252        /// through without a wire break.
2253        #[serde(default, skip_serializing_if = "Option::is_none")]
2254        action: Option<String>,
2255        /// `"transparent" | "opaque" | "auto"`. Mirrors the
2256        /// `image_generation` tool input knob of the same name.
2257        #[serde(default, skip_serializing_if = "Option::is_none")]
2258        background: Option<String>,
2259        /// `"png" | "webp" | "jpeg"`. Mirrors the `image_generation` tool
2260        /// input knob of the same name.
2261        #[serde(default, skip_serializing_if = "Option::is_none")]
2262        output_format: Option<String>,
2263        /// `"auto" | "low" | "medium" | "high" | "standard" | "hd"`. Mirrors
2264        /// the `image_generation` tool input knob of the same name.
2265        #[serde(default, skip_serializing_if = "Option::is_none")]
2266        quality: Option<String>,
2267        /// Base64-encoded image bytes.
2268        result: String,
2269        /// Prompt text the mainline model rewrote before dispatching the
2270        /// image-generation call. Preserved so downstream turns/storage do
2271        /// not drop it on replay.
2272        #[serde(default, skip_serializing_if = "Option::is_none")]
2273        revised_prompt: Option<String>,
2274        /// `"auto" | "1024x1024" | "1024x1536" | "1536x1024"`. Mirrors the
2275        /// `image_generation` tool input knob of the same name.
2276        #[serde(default, skip_serializing_if = "Option::is_none")]
2277        size: Option<String>,
2278        status: ImageGenerationCallStatus,
2279    },
2280    /// `type: "compaction"` — server-emitted item carrying an opaque
2281    /// compacted-history payload. Spec
2282    /// (openai-responses-api-spec.md §InputItemList L203-205,
2283    /// §`output: array of ResponseOutputItem`): `{ encrypted_content, type, id }`.
2284    /// `id` is required on the output wire because the server always assigns
2285    /// one when emitting the compaction item.
2286    #[serde(rename = "compaction")]
2287    Compaction {
2288        id: String,
2289        encrypted_content: String,
2290    },
2291    /// `{ type: "computer_call", id, call_id, action?, actions?, status,
2292    /// pending_safety_checks }`.
2293    ///
2294    /// Spec (openai-responses-api-spec.md §ComputerCall): output-side mirror of
2295    /// the input variant — emitted when the model issues a computer-use action.
2296    /// See [`ComputerAction`].
2297    #[serde(rename = "computer_call")]
2298    ComputerCall {
2299        id: String,
2300        call_id: String,
2301        #[serde(default, skip_serializing_if = "Option::is_none")]
2302        action: Option<ComputerAction>,
2303        #[serde(default, skip_serializing_if = "Option::is_none")]
2304        actions: Option<Vec<ComputerAction>>,
2305        status: ComputerCallStatus,
2306        /// Always serialized (including an empty `[]`). The official OpenAI
2307        /// Python SDK (`openai==2.8.1`,
2308        /// `types/responses/response_computer_tool_call.py`) declares this as a
2309        /// non-`Optional` `List[PendingSafetyCheck]`, so the field must always
2310        /// appear on the wire — an empty array is semantically distinct from
2311        /// omitting the field.
2312        #[serde(default)]
2313        pending_safety_checks: Vec<ComputerSafetyCheck>,
2314    },
2315    /// `{ type: "computer_call_output", id?, call_id, output,
2316    /// acknowledged_safety_checks?, status? }`.
2317    ///
2318    /// Spec (openai-responses-api-spec.md §ComputerCallOutput).
2319    #[serde(rename = "computer_call_output")]
2320    ComputerCallOutput {
2321        #[serde(default, skip_serializing_if = "Option::is_none")]
2322        id: Option<String>,
2323        call_id: String,
2324        output: ComputerCallOutputContent,
2325        #[serde(default, skip_serializing_if = "Vec::is_empty")]
2326        acknowledged_safety_checks: Vec<ComputerSafetyCheck>,
2327        #[serde(default, skip_serializing_if = "Option::is_none")]
2328        status: Option<ComputerCallStatus>,
2329    },
2330    /// `type: "shell_call"` — output-side mirror of the input variant.
2331    ///
2332    /// Spec (openai-responses-api-spec.md §ShellCall, L228-231 and §returns
2333    /// L512-513) + OpenAI SDK v2.8.1 `ResponseFunctionShellToolCall`:
2334    /// emitted when the model issues a containerized shell action. The
2335    /// environment echoed back is restricted to `local` /
2336    /// `container_reference` via [`ResponseShellCallEnvironment`] — per spec
2337    /// L513 the response form uses `ResponseLocalEnvironment { type: "local" }`
2338    /// only.
2339    ///
2340    /// `id` and `status` are required on the output wire — the SDK types
2341    /// them as non-`Optional` on `ResponseFunctionShellToolCall`, mirroring
2342    /// the `ComputerCall` treatment above. `created_by` is the SDK's
2343    /// `Optional[str]` provenance tag, populated when the platform stamps
2344    /// the item.
2345    #[serde(rename = "shell_call")]
2346    ShellCall {
2347        id: String,
2348        call_id: String,
2349        action: ShellCallAction,
2350        #[serde(default, skip_serializing_if = "Option::is_none")]
2351        environment: Option<ResponseShellCallEnvironment>,
2352        status: ShellCallStatus,
2353        #[serde(default, skip_serializing_if = "Option::is_none")]
2354        created_by: Option<String>,
2355    },
2356    /// `type: "shell_call_output"` — output-side mirror of the input
2357    /// variant.
2358    ///
2359    /// Spec (openai-responses-api-spec.md §ShellCallOutput, L233-238) +
2360    /// OpenAI SDK v2.8.1 `ResponseFunctionShellToolCallOutput`:
2361    /// `{ call_id, output, type, id, max_output_length?, status, created_by? }`.
2362    /// Emitted when the platform surfaces captured stdout/stderr plus an
2363    /// [`ShellOutcome`] for a prior shell call.
2364    ///
2365    /// `id` and `status` are required per the SDK's non-`Optional` typing.
2366    /// `max_output_length` is `Optional[int]` in the SDK (the platform may
2367    /// emit shell outputs when the originating `shell_call.action` did not
2368    /// specify a cap) and `created_by` is `Optional[str]` — both dropped at
2369    /// serialize time when absent so downstream consumers do not see null
2370    /// placeholders.
2371    #[serde(rename = "shell_call_output")]
2372    ShellCallOutput {
2373        id: String,
2374        call_id: String,
2375        output: Vec<ShellOutputChunk>,
2376        #[serde(default, skip_serializing_if = "Option::is_none")]
2377        max_output_length: Option<u64>,
2378        status: ShellCallStatus,
2379        #[serde(default, skip_serializing_if = "Option::is_none")]
2380        created_by: Option<String>,
2381    },
2382    /// `type: "apply_patch_call"` — server-emitted mirror of the input
2383    /// variant. Spec (openai-responses-api-spec.md §ApplyPatchCall L240-L246):
2384    /// `{ call_id, operation, status, type, id }`. `id` is required on the
2385    /// output wire because the server always assigns one when emitting the
2386    /// apply_patch call item.
2387    #[serde(rename = "apply_patch_call")]
2388    ApplyPatchCall {
2389        id: String,
2390        call_id: String,
2391        operation: ApplyPatchOperation,
2392        status: ApplyPatchCallStatus,
2393    },
2394    /// `type: "apply_patch_call_output"` — server-emitted mirror of the
2395    /// input variant. Spec (openai-responses-api-spec.md
2396    /// §ApplyPatchCallOutput L248-L251): `{ call_id, status, type, id,
2397    /// output }`. `id` is required on the output wire; `output` is optional
2398    /// log text surfaced by the upstream apply_patch executor.
2399    #[serde(rename = "apply_patch_call_output")]
2400    ApplyPatchCallOutput {
2401        id: String,
2402        call_id: String,
2403        status: ApplyPatchCallOutputStatus,
2404        #[serde(default, skip_serializing_if = "Option::is_none")]
2405        output: Option<String>,
2406    },
2407    /// `type: "local_shell_call"` — output-side mirror of the input
2408    /// variant — emitted when the model issues a `local_shell` tool call.
2409    /// Spec (openai-responses-api-spec.md §LocalShellCall L219-222):
2410    /// `{ id, action, call_id, status, type }` with `action` as a
2411    /// [`LocalShellExec`] payload. See [`ResponseInputOutputItem::LocalShellCall`].
2412    #[serde(rename = "local_shell_call")]
2413    LocalShellCall {
2414        id: String,
2415        call_id: String,
2416        action: LocalShellExec,
2417        status: LocalShellCallStatus,
2418    },
2419    /// `type: "local_shell_call_output"` — output-side mirror of the
2420    /// input variant. Spec
2421    /// (openai-responses-api-spec.md §LocalShellCallOutput L224-226):
2422    /// `{ id, output, type, status }` with `status` optional per SDK
2423    /// v2.8.1. See [`ResponseInputOutputItem::LocalShellCallOutput`].
2424    #[serde(rename = "local_shell_call_output")]
2425    LocalShellCallOutput {
2426        id: String,
2427        output: String,
2428        #[serde(default, skip_serializing_if = "Option::is_none")]
2429        status: Option<LocalShellCallStatus>,
2430    },
2431}
2432
2433// ============================================================================
2434// Built-in Tool Call Types
2435// ============================================================================
2436
2437/// Status for web search tool calls.
2438#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, schemars::JsonSchema)]
2439#[serde(rename_all = "snake_case")]
2440pub enum WebSearchCallStatus {
2441    InProgress,
2442    Searching,
2443    Completed,
2444    Failed,
2445}
2446
2447/// Action performed during a web search.
2448#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
2449#[serde(tag = "type", rename_all = "snake_case")]
2450pub enum WebSearchAction {
2451    Search {
2452        #[serde(skip_serializing_if = "Option::is_none")]
2453        query: Option<String>,
2454        #[serde(default, skip_serializing_if = "Vec::is_empty")]
2455        queries: Vec<String>,
2456        #[serde(default, skip_serializing_if = "Vec::is_empty")]
2457        sources: Vec<WebSearchSource>,
2458    },
2459    OpenPage {
2460        url: String,
2461    },
2462    Find {
2463        url: String,
2464        pattern: String,
2465    },
2466}
2467
2468/// A source returned from web search.
2469#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
2470pub struct WebSearchSource {
2471    #[serde(rename = "type")]
2472    pub source_type: String,
2473    pub url: String,
2474}
2475
2476/// A single search result attached to a `WebSearchCall` when the caller
2477/// requested `web_search_call.results` via the top-level `include[]` array.
2478///
2479/// Optional fields mirror the `FileSearchResult` shape — only `url` is
2480/// guaranteed; titles, snippets, and scores ride along when the upstream
2481/// search backend supplies them.
2482#[serde_with::skip_serializing_none]
2483#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
2484pub struct WebSearchResult {
2485    /// Canonical URL of the result.
2486    pub url: String,
2487    /// Page or document title, when surfaced by the search backend.
2488    pub title: Option<String>,
2489    /// Short text snippet excerpted from the result.
2490    pub snippet: Option<String>,
2491    /// Relevance score in `[0, 1]`, when the backend supplies one.
2492    pub score: Option<f32>,
2493}
2494
2495/// Status for code interpreter tool calls.
2496#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, schemars::JsonSchema)]
2497#[serde(rename_all = "snake_case")]
2498pub enum CodeInterpreterCallStatus {
2499    InProgress,
2500    Completed,
2501    Incomplete,
2502    Interpreting,
2503    Failed,
2504}
2505
2506/// Output from code interpreter execution.
2507#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
2508#[serde(tag = "type", rename_all = "snake_case")]
2509pub enum CodeInterpreterOutput {
2510    Logs { logs: String },
2511    Image { url: String },
2512}
2513
2514/// Status for file search tool calls.
2515#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, schemars::JsonSchema)]
2516#[serde(rename_all = "snake_case")]
2517pub enum FileSearchCallStatus {
2518    InProgress,
2519    Searching,
2520    Completed,
2521    Incomplete,
2522    Failed,
2523}
2524
2525/// A result from file search.
2526#[serde_with::skip_serializing_none]
2527#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
2528pub struct FileSearchResult {
2529    pub file_id: String,
2530    pub filename: String,
2531    pub text: Option<String>,
2532    pub score: Option<f32>,
2533    pub attributes: Option<Value>,
2534}
2535
2536/// Status for `local_shell` tool calls.
2537///
2538/// Spec (openai-responses-api-spec.md §LocalShellCall L221): `"in_progress"
2539/// | "completed" | "incomplete"`.
2540#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, schemars::JsonSchema)]
2541#[serde(rename_all = "snake_case")]
2542pub enum LocalShellCallStatus {
2543    InProgress,
2544    Completed,
2545    Incomplete,
2546}
2547
2548/// `action` payload carried by a [`ResponseInputOutputItem::LocalShellCall`] /
2549/// [`ResponseOutputItem::LocalShellCall`] item.
2550///
2551/// Spec (openai-responses-api-spec.md §LocalShellCall L220):
2552/// `{ command: array of string, env: map[string], type: "exec",
2553///   timeout_ms?, user?, working_directory? }`. `env` is always present
2554/// (an empty object is semantically distinct from omitting the field),
2555/// matching the OpenAI Python SDK (`openai==2.8.1`,
2556/// `types/responses/response_input_item_param.py` `LocalShellCallAction`
2557/// — non-`Optional` `Dict[str, str]`).
2558#[serde_with::skip_serializing_none]
2559#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, schemars::JsonSchema)]
2560#[serde(tag = "type", rename_all = "snake_case")]
2561pub enum LocalShellExec {
2562    /// `type: "exec"` — the only action kind defined by the spec today.
2563    #[serde(rename = "exec")]
2564    Exec {
2565        /// Argv of the command to run on the host.
2566        command: Vec<String>,
2567        /// Environment variables overlaid on the host process env.
2568        /// Always serialized (possibly empty) to match SDK shape.
2569        env: std::collections::BTreeMap<String, String>,
2570        /// Hard timeout in milliseconds.
2571        #[serde(default, skip_serializing_if = "Option::is_none")]
2572        timeout_ms: Option<u64>,
2573        /// User to run the command as.
2574        #[serde(default, skip_serializing_if = "Option::is_none")]
2575        user: Option<String>,
2576        /// Working directory for the command.
2577        #[serde(default, skip_serializing_if = "Option::is_none")]
2578        working_directory: Option<String>,
2579    },
2580}
2581
2582// ============================================================================
2583// Configuration Enums
2584// ============================================================================
2585
2586#[derive(Debug, Clone, Deserialize, Serialize, Default, schemars::JsonSchema)]
2587#[serde(rename_all = "snake_case")]
2588#[schemars(rename = "ResponsesServiceTier")]
2589pub enum ServiceTier {
2590    #[default]
2591    Auto,
2592    Default,
2593    Flex,
2594    Scale,
2595    Priority,
2596}
2597
2598#[derive(Debug, Clone, Deserialize, Serialize, Default, schemars::JsonSchema)]
2599#[serde(rename_all = "snake_case")]
2600pub enum Truncation {
2601    Auto,
2602    #[default]
2603    Disabled,
2604}
2605
2606#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, schemars::JsonSchema)]
2607#[serde(rename_all = "snake_case")]
2608#[non_exhaustive]
2609pub enum ResponseStatus {
2610    Queued,
2611    InProgress,
2612    Completed,
2613    Incomplete,
2614    Failed,
2615    Cancelled,
2616}
2617
2618/// Why a response stopped before producing complete output.
2619///
2620/// Mirrors OpenAI's `incomplete_details.reason`: reserved strictly for the two
2621/// truncation semantics. Any other stop condition (wall-clock timeout,
2622/// `max_tool_calls` exhaustion, provider errors) is surfaced as a `failed`
2623/// status with an `error` payload instead.
2624#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, schemars::JsonSchema)]
2625#[serde(rename_all = "snake_case")]
2626#[non_exhaustive]
2627pub enum IncompleteReason {
2628    /// Output was truncated because it hit `max_output_tokens`.
2629    MaxOutputTokens,
2630    /// Output was truncated by the content filter.
2631    ContentFilter,
2632}
2633
2634/// Structured detail attached to a response whose status is `incomplete`.
2635///
2636/// Wire shape: `{ "reason": "max_output_tokens" | "content_filter" }`.
2637#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, schemars::JsonSchema)]
2638pub struct IncompleteDetails {
2639    /// The reason the response is incomplete.
2640    pub reason: IncompleteReason,
2641}
2642
2643#[serde_with::skip_serializing_none]
2644#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
2645pub struct ReasoningInfo {
2646    pub effort: Option<String>,
2647    pub summary: Option<String>,
2648}
2649
2650// ============================================================================
2651// Text Format (structured outputs)
2652// ============================================================================
2653
2654/// Text configuration for structured output requests
2655#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
2656pub struct TextConfig {
2657    #[serde(skip_serializing_if = "Option::is_none")]
2658    pub format: Option<TextFormat>,
2659}
2660
2661/// Text format: text (default), json_object (legacy), or json_schema (recommended)
2662#[serde_with::skip_serializing_none]
2663#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
2664#[serde(tag = "type")]
2665pub enum TextFormat {
2666    #[serde(rename = "text")]
2667    Text,
2668
2669    #[serde(rename = "json_object")]
2670    JsonObject,
2671
2672    #[serde(rename = "json_schema")]
2673    JsonSchema {
2674        name: String,
2675        schema: Value,
2676        description: Option<String>,
2677        strict: Option<bool>,
2678    },
2679}
2680
2681#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, schemars::JsonSchema)]
2682#[serde(rename_all = "snake_case")]
2683pub enum IncludeField {
2684    #[serde(rename = "code_interpreter_call.outputs")]
2685    CodeInterpreterCallOutputs,
2686    #[serde(rename = "computer_call_output.output.image_url")]
2687    ComputerCallOutputImageUrl,
2688    #[serde(rename = "file_search_call.results")]
2689    FileSearchCallResults,
2690    #[serde(rename = "message.input_image.image_url")]
2691    MessageInputImageUrl,
2692    #[serde(rename = "message.output_text.logprobs")]
2693    MessageOutputTextLogprobs,
2694    #[serde(rename = "reasoning.encrypted_content")]
2695    ReasoningEncryptedContent,
2696    #[serde(rename = "web_search_call.action.sources")]
2697    WebSearchCallActionSources,
2698    #[serde(rename = "web_search_call.results")]
2699    WebSearchCallResults,
2700}
2701
2702// ============================================================================
2703// Usage Types (Responses API format)
2704// ============================================================================
2705
2706/// OpenAI Responses API usage format (different from standard UsageInfo)
2707#[serde_with::skip_serializing_none]
2708#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
2709pub struct ResponseUsage {
2710    pub input_tokens: u32,
2711    pub output_tokens: u32,
2712    pub total_tokens: u32,
2713    pub input_tokens_details: Option<InputTokensDetails>,
2714    pub output_tokens_details: Option<OutputTokensDetails>,
2715}
2716
2717#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
2718#[serde(untagged)]
2719pub enum ResponsesUsage {
2720    Classic(UsageInfo),
2721    Modern(ResponseUsage),
2722}
2723
2724#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
2725pub struct InputTokensDetails {
2726    pub cached_tokens: u32,
2727}
2728
2729impl From<&PromptTokenUsageInfo> for InputTokensDetails {
2730    fn from(d: &PromptTokenUsageInfo) -> Self {
2731        Self {
2732            cached_tokens: d.cached_tokens,
2733        }
2734    }
2735}
2736
2737#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
2738pub struct OutputTokensDetails {
2739    pub reasoning_tokens: u32,
2740}
2741
2742impl UsageInfo {
2743    /// Convert to OpenAI Responses API format
2744    pub fn to_response_usage(&self) -> ResponseUsage {
2745        ResponseUsage {
2746            input_tokens: self.prompt_tokens,
2747            output_tokens: self.completion_tokens,
2748            total_tokens: self.total_tokens,
2749            input_tokens_details: self
2750                .prompt_tokens_details
2751                .as_ref()
2752                .map(InputTokensDetails::from),
2753            output_tokens_details: self.reasoning_tokens.map(|tokens| OutputTokensDetails {
2754                reasoning_tokens: tokens,
2755            }),
2756        }
2757    }
2758}
2759
2760impl From<UsageInfo> for ResponseUsage {
2761    fn from(usage: UsageInfo) -> Self {
2762        usage.to_response_usage()
2763    }
2764}
2765
2766impl ResponseUsage {
2767    /// Convert back to standard UsageInfo format
2768    pub fn to_usage_info(&self) -> UsageInfo {
2769        UsageInfo {
2770            prompt_tokens: self.input_tokens,
2771            completion_tokens: self.output_tokens,
2772            total_tokens: self.total_tokens,
2773            reasoning_tokens: self
2774                .output_tokens_details
2775                .as_ref()
2776                .map(|details| details.reasoning_tokens),
2777            prompt_tokens_details: self.input_tokens_details.as_ref().map(|details| {
2778                PromptTokenUsageInfo {
2779                    cached_tokens: details.cached_tokens,
2780                }
2781            }),
2782        }
2783    }
2784}
2785
2786impl ResponsesUsage {
2787    pub fn to_response_usage(&self) -> ResponseUsage {
2788        match self {
2789            ResponsesUsage::Classic(usage) => usage.to_response_usage(),
2790            ResponsesUsage::Modern(usage) => usage.clone(),
2791        }
2792    }
2793
2794    pub fn to_usage_info(&self) -> UsageInfo {
2795        match self {
2796            ResponsesUsage::Classic(usage) => usage.clone(),
2797            ResponsesUsage::Modern(usage) => usage.to_usage_info(),
2798        }
2799    }
2800}
2801
2802// ============================================================================
2803// Helper Functions for Defaults
2804// ============================================================================
2805
2806fn default_top_k() -> i32 {
2807    -1
2808}
2809
2810fn default_repetition_penalty() -> f32 {
2811    1.0
2812}
2813
2814#[expect(
2815    clippy::unnecessary_wraps,
2816    reason = "serde default function must match field type Option<T>"
2817)]
2818fn default_temperature() -> Option<f32> {
2819    Some(1.0)
2820}
2821
2822// ============================================================================
2823// Request/Response Types
2824// ============================================================================
2825
2826#[derive(Debug, Clone, Deserialize, Serialize, Validate, schemars::JsonSchema)]
2827#[validate(schema(function = "validate_responses_cross_parameters"))]
2828pub struct ResponsesRequest {
2829    /// Fields to include in the response
2830    #[serde(skip_serializing_if = "Option::is_none")]
2831    pub include: Option<Vec<IncludeField>>,
2832
2833    /// Input content - can be string or structured items
2834    #[validate(custom(function = "validate_response_input"))]
2835    pub input: ResponseInput,
2836
2837    /// System instructions for the model
2838    #[serde(skip_serializing_if = "Option::is_none")]
2839    pub instructions: Option<String>,
2840
2841    /// Maximum number of output tokens
2842    #[serde(skip_serializing_if = "Option::is_none")]
2843    #[validate(range(min = 1))]
2844    pub max_output_tokens: Option<u32>,
2845
2846    /// Maximum number of tool calls
2847    #[serde(skip_serializing_if = "Option::is_none")]
2848    #[validate(range(min = 1))]
2849    pub max_tool_calls: Option<u32>,
2850
2851    /// Additional metadata
2852    #[serde(skip_serializing_if = "Option::is_none")]
2853    pub metadata: Option<HashMap<String, Value>>,
2854
2855    /// Model to use
2856    pub model: String,
2857
2858    /// Optional conversation reference to persist input/output as items.
2859    ///
2860    /// Spec: `conversation` accepts either a bare ID string or
2861    /// `ResponseConversationParam { id }`. Both wire shapes deserialize into
2862    /// [`ConversationRef`]; downstream code reads the id via
2863    /// [`ConversationRef::as_id`].
2864    #[serde(skip_serializing_if = "Option::is_none")]
2865    #[validate(custom(function = "validate_conversation_id"))]
2866    pub conversation: Option<ConversationRef>,
2867
2868    /// Whether to enable parallel tool calls
2869    #[serde(skip_serializing_if = "Option::is_none")]
2870    pub parallel_tool_calls: Option<bool>,
2871
2872    /// ID of previous response to continue from
2873    #[serde(skip_serializing_if = "Option::is_none")]
2874    pub previous_response_id: Option<String>,
2875
2876    /// Reasoning configuration
2877    #[serde(skip_serializing_if = "Option::is_none")]
2878    pub reasoning: Option<ResponseReasoningParam>,
2879
2880    /// Service tier
2881    #[serde(skip_serializing_if = "Option::is_none")]
2882    pub service_tier: Option<ServiceTier>,
2883
2884    /// Whether to store the response
2885    #[serde(skip_serializing_if = "Option::is_none")]
2886    pub store: Option<bool>,
2887
2888    /// Whether to stream the response
2889    #[serde(default)]
2890    pub stream: Option<bool>,
2891
2892    /// Temperature for sampling
2893    #[serde(
2894        default = "default_temperature",
2895        skip_serializing_if = "Option::is_none"
2896    )]
2897    #[validate(range(min = 0.0, max = 2.0))]
2898    pub temperature: Option<f32>,
2899
2900    /// Tool choice behavior (Responses-spec enum — see `ResponsesToolChoice`).
2901    #[serde(skip_serializing_if = "Option::is_none")]
2902    pub tool_choice: Option<ResponsesToolChoice>,
2903
2904    /// Available tools
2905    #[serde(skip_serializing_if = "Option::is_none")]
2906    #[validate(custom(function = "validate_response_tools"))]
2907    pub tools: Option<Vec<ResponseTool>>,
2908
2909    /// Number of top logprobs to return
2910    #[serde(skip_serializing_if = "Option::is_none")]
2911    #[validate(range(min = 0, max = 20))]
2912    pub top_logprobs: Option<u32>,
2913
2914    /// Top-p sampling parameter
2915    #[serde(skip_serializing_if = "Option::is_none")]
2916    #[validate(custom(function = "validate_top_p_value"))]
2917    pub top_p: Option<f32>,
2918
2919    /// Truncation behavior
2920    #[serde(skip_serializing_if = "Option::is_none")]
2921    pub truncation: Option<Truncation>,
2922
2923    /// Text format for structured outputs (text, json_object, json_schema)
2924    #[serde(skip_serializing_if = "Option::is_none")]
2925    #[validate(custom(function = "validate_text_format"))]
2926    pub text: Option<TextConfig>,
2927
2928    /// User identifier
2929    #[serde(skip_serializing_if = "Option::is_none")]
2930    pub user: Option<String>,
2931
2932    /// Request ID
2933    #[serde(skip_serializing_if = "Option::is_none")]
2934    pub request_id: Option<String>,
2935
2936    /// Request priority
2937    #[serde(default)]
2938    pub priority: i32,
2939
2940    /// Frequency penalty
2941    #[serde(skip_serializing_if = "Option::is_none")]
2942    #[validate(range(min = -2.0, max = 2.0))]
2943    pub frequency_penalty: Option<f32>,
2944
2945    /// Presence penalty
2946    #[serde(skip_serializing_if = "Option::is_none")]
2947    #[validate(range(min = -2.0, max = 2.0))]
2948    pub presence_penalty: Option<f32>,
2949
2950    /// Stop sequences
2951    #[serde(skip_serializing_if = "Option::is_none")]
2952    #[validate(custom(function = "validate_stop"))]
2953    pub stop: Option<StringOrArray>,
2954
2955    /// Reference to a prompt template and its variables.
2956    /// Spec: body param `prompt` (ResponsePrompt).
2957    #[serde(skip_serializing_if = "Option::is_none")]
2958    pub prompt: Option<ResponsePrompt>,
2959
2960    /// Stable cache key used by upstream to share prompt-prefix caches across
2961    /// requests. Spec: body param `prompt_cache_key` (replaces `user`).
2962    #[serde(skip_serializing_if = "Option::is_none")]
2963    pub prompt_cache_key: Option<String>,
2964
2965    /// Retention policy for prompt-cache entries.
2966    /// Spec: body param `prompt_cache_retention` (`"in-memory"` | `"24h"`).
2967    #[serde(skip_serializing_if = "Option::is_none")]
2968    pub prompt_cache_retention: Option<PromptCacheRetention>,
2969
2970    /// Stable user identifier for policy/abuse detection (max 64 chars on the
2971    /// spec, but we do not enforce length here — routers may pass through).
2972    /// Spec: body param `safety_identifier` (replaces `user` on request side).
2973    #[serde(skip_serializing_if = "Option::is_none")]
2974    pub safety_identifier: Option<String>,
2975
2976    /// Streaming-only options. Spec: body param `stream_options`.
2977    /// On the Responses API the only documented field is `include_obfuscation`.
2978    #[serde(skip_serializing_if = "Option::is_none")]
2979    pub stream_options: Option<StreamOptions>,
2980
2981    /// Per-request context-management configuration.
2982    /// Spec: body param `context_management` — array of entries describing how
2983    /// the upstream should compact context for this request.
2984    #[serde(skip_serializing_if = "Option::is_none")]
2985    pub context_management: Option<Vec<ContextManagementEntry>>,
2986
2987    /// Top-k sampling parameter (SGLang extension)
2988    #[serde(default = "default_top_k")]
2989    #[validate(custom(function = "validate_top_k_value"))]
2990    pub top_k: i32,
2991
2992    /// Min-p sampling parameter (SGLang extension)
2993    #[serde(default)]
2994    #[validate(range(min = 0.0, max = 1.0))]
2995    pub min_p: f32,
2996
2997    /// Repetition penalty (SGLang extension)
2998    #[serde(default = "default_repetition_penalty")]
2999    #[validate(range(min = 0.0, max = 2.0))]
3000    pub repetition_penalty: f32,
3001}
3002
3003#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
3004#[serde(untagged)]
3005pub enum ResponseInput {
3006    Items(Vec<ResponseInputOutputItem>),
3007    Text(String),
3008}
3009
3010impl Default for ResponsesRequest {
3011    fn default() -> Self {
3012        Self {
3013            include: None,
3014            input: ResponseInput::Text(String::new()),
3015            instructions: None,
3016            max_output_tokens: None,
3017            max_tool_calls: None,
3018            metadata: None,
3019            model: String::new(),
3020            conversation: None,
3021            parallel_tool_calls: None,
3022            previous_response_id: None,
3023            reasoning: None,
3024            service_tier: None,
3025            store: None,
3026            stream: None,
3027            temperature: None,
3028            tool_choice: None,
3029            tools: None,
3030            top_logprobs: None,
3031            top_p: None,
3032            truncation: None,
3033            text: None,
3034            user: None,
3035            request_id: None,
3036            priority: 0,
3037            frequency_penalty: None,
3038            presence_penalty: None,
3039            stop: None,
3040            prompt: None,
3041            prompt_cache_key: None,
3042            prompt_cache_retention: None,
3043            safety_identifier: None,
3044            stream_options: None,
3045            context_management: None,
3046            top_k: default_top_k(),
3047            min_p: 0.0,
3048            repetition_penalty: default_repetition_penalty(),
3049        }
3050    }
3051}
3052
3053impl Normalizable for ResponsesRequest {
3054    /// Normalize the request by applying defaults:
3055    /// 1. Apply tool_choice defaults based on tools presence
3056    /// 2. Apply parallel_tool_calls defaults
3057    /// 3. Apply store field defaults
3058    fn normalize(&mut self) {
3059        // 1. Apply tool_choice defaults
3060        if self.tool_choice.is_none() {
3061            if let Some(tools) = &self.tools {
3062                let choice_value = if tools.is_empty() {
3063                    ToolChoiceOptions::None
3064                } else {
3065                    ToolChoiceOptions::Auto
3066                };
3067                self.tool_choice = Some(ResponsesToolChoice::Options(choice_value));
3068            }
3069            // If tools is None, leave tool_choice as None (don't set it)
3070        }
3071
3072        // 2. Apply default for parallel_tool_calls if tools are present
3073        if self.parallel_tool_calls.is_none() && self.tools.is_some() {
3074            self.parallel_tool_calls = Some(true);
3075        }
3076
3077        // 3. Ensure store defaults to true if not specified
3078        if self.store.is_none() {
3079            self.store = Some(true);
3080        }
3081    }
3082}
3083
3084impl GenerationRequest for ResponsesRequest {
3085    fn is_stream(&self) -> bool {
3086        self.stream.unwrap_or(false)
3087    }
3088
3089    fn get_model(&self) -> Option<&str> {
3090        Some(self.model.as_str())
3091    }
3092
3093    fn extract_text_for_routing(&self) -> String {
3094        match &self.input {
3095            ResponseInput::Text(text) => text.clone(),
3096            ResponseInput::Items(items) => {
3097                let mut result = String::with_capacity(256);
3098                let mut has_parts = false;
3099
3100                let mut append_text = |text: &str| {
3101                    if has_parts {
3102                        result.push(' ');
3103                    }
3104                    has_parts = true;
3105                    result.push_str(text);
3106                };
3107
3108                for item in items {
3109                    match item {
3110                        ResponseInputOutputItem::Message { content, .. } => {
3111                            for part in content {
3112                                let text = match part {
3113                                    ResponseContentPart::OutputText { text, .. } => {
3114                                        Some(text.as_str())
3115                                    }
3116                                    ResponseContentPart::InputText { text } => Some(text.as_str()),
3117                                    // Non-text parts (images, files, refusals) contribute no
3118                                    // prompt text; skip without appending.
3119                                    ResponseContentPart::InputImage { .. }
3120                                    | ResponseContentPart::InputFile { .. }
3121                                    | ResponseContentPart::Refusal { .. } => None,
3122                                };
3123                                if let Some(t) = text {
3124                                    append_text(t);
3125                                }
3126                            }
3127                        }
3128                        ResponseInputOutputItem::SimpleInputMessage { content, .. } => {
3129                            match content {
3130                                StringOrContentParts::String(s) => {
3131                                    append_text(s.as_str());
3132                                }
3133                                StringOrContentParts::Array(parts) => {
3134                                    for part in parts {
3135                                        let text = match part {
3136                                            ResponseContentPart::OutputText { text, .. } => {
3137                                                Some(text.as_str())
3138                                            }
3139                                            ResponseContentPart::InputText { text } => {
3140                                                Some(text.as_str())
3141                                            }
3142                                            ResponseContentPart::InputImage { .. }
3143                                            | ResponseContentPart::InputFile { .. }
3144                                            | ResponseContentPart::Refusal { .. } => None,
3145                                        };
3146                                        if let Some(t) = text {
3147                                            append_text(t);
3148                                        }
3149                                    }
3150                                }
3151                            }
3152                        }
3153                        ResponseInputOutputItem::Reasoning { content, .. } => {
3154                            for part in content {
3155                                match part {
3156                                    ResponseReasoningContent::ReasoningText { text } => {
3157                                        append_text(text.as_str());
3158                                    }
3159                                }
3160                            }
3161                        }
3162                        ResponseInputOutputItem::FunctionToolCall { .. }
3163                        | ResponseInputOutputItem::FunctionCallOutput { .. }
3164                        | ResponseInputOutputItem::McpApprovalRequest { .. }
3165                        | ResponseInputOutputItem::McpApprovalResponse { .. }
3166                        | ResponseInputOutputItem::ImageGenerationCall { .. }
3167                        | ResponseInputOutputItem::Compaction { .. }
3168                        | ResponseInputOutputItem::ComputerCall { .. }
3169                        | ResponseInputOutputItem::ComputerCallOutput { .. }
3170                        | ResponseInputOutputItem::CustomToolCall { .. }
3171                        | ResponseInputOutputItem::CustomToolCallOutput { .. }
3172                        | ResponseInputOutputItem::ShellCall { .. }
3173                        | ResponseInputOutputItem::ShellCallOutput { .. }
3174                        | ResponseInputOutputItem::ItemReference { .. }
3175                        | ResponseInputOutputItem::ApplyPatchCall { .. }
3176                        | ResponseInputOutputItem::ApplyPatchCallOutput { .. }
3177                        | ResponseInputOutputItem::LocalShellCall { .. }
3178                        | ResponseInputOutputItem::LocalShellCallOutput { .. }
3179                        | ResponseInputOutputItem::McpCall { .. }
3180                        | ResponseInputOutputItem::McpListTools { .. } => {}
3181                    }
3182                }
3183
3184                result
3185            }
3186        }
3187    }
3188}
3189
3190/// Validate the conversation reference's ID format.
3191///
3192/// The validator crate auto-unwraps `Option<ConversationRef>` for the
3193/// `#[validate(custom(...))]` attribute, so this function only runs when
3194/// the field is present. Both wire shapes (bare string or `{ id }` object)
3195/// are validated against the same rule by extracting the underlying id via
3196/// [`ConversationRef::as_id`].
3197pub fn validate_conversation_id(conv: &ConversationRef) -> Result<(), ValidationError> {
3198    let conv_id = conv.as_id();
3199    if !conv_id.starts_with("conv_") {
3200        let mut error = ValidationError::new("invalid_conversation_id");
3201        error.message = Some(std::borrow::Cow::Owned(format!(
3202            "Invalid 'conversation': '{conv_id}'. Expected an ID that begins with 'conv_'."
3203        )));
3204        return Err(error);
3205    }
3206
3207    // Check if the conversation ID contains only valid characters
3208    let is_valid = conv_id
3209        .chars()
3210        .all(|c| c.is_alphanumeric() || c == '_' || c == '-');
3211
3212    if !is_valid {
3213        let mut error = ValidationError::new("invalid_conversation_id");
3214        error.message = Some(std::borrow::Cow::Owned(format!(
3215            "Invalid 'conversation': '{conv_id}'. Expected an ID that contains letters, numbers, underscores, or dashes, but this value contained additional characters."
3216        )));
3217        return Err(error);
3218    }
3219    Ok(())
3220}
3221
3222/// Validates tool_choice requires tools and references exist
3223fn validate_tool_choice_with_tools(request: &ResponsesRequest) -> Result<(), ValidationError> {
3224    let Some(tool_choice) = &request.tool_choice else {
3225        return Ok(());
3226    };
3227
3228    let has_tools = request.tools.as_ref().is_some_and(|t| !t.is_empty());
3229    let is_some_choice = !matches!(
3230        tool_choice,
3231        ResponsesToolChoice::Options(ToolChoiceOptions::None)
3232    );
3233
3234    // Check if tool_choice requires tools but none are provided
3235    if is_some_choice && !has_tools {
3236        let mut e = ValidationError::new("tool_choice_requires_tools");
3237        e.message = Some("Invalid value for 'tool_choice': 'tool_choice' is only allowed when 'tools' are specified.".into());
3238        return Err(e);
3239    }
3240
3241    // Validate tool references exist when tools are present
3242    if !has_tools {
3243        return Ok(());
3244    }
3245
3246    // Extract function tool names from ResponseTools
3247    // INVARIANT: has_tools is true here, so tools is Some and non-empty
3248    let Some(tools) = request.tools.as_ref() else {
3249        return Ok(());
3250    };
3251    let function_tool_names: Vec<&str> = tools
3252        .iter()
3253        .filter_map(|t| match t {
3254            ResponseTool::Function(ft) => Some(ft.function.name.as_str()),
3255            _ => None,
3256        })
3257        .collect();
3258
3259    // Validate tool references exist
3260    match tool_choice {
3261        ResponsesToolChoice::Function(_) => {
3262            // Accessor goes through `function_name()` so we stay agnostic to
3263            // the underlying wire shape (flat vs. legacy nested) — both are
3264            // normalized at deserialize time.
3265            if let Some(name) = tool_choice.function_name() {
3266                if !function_tool_names.contains(&name) {
3267                    let mut e = ValidationError::new("tool_choice_function_not_found");
3268                    e.message = Some(
3269                        format!(
3270                            "Invalid value for 'tool_choice': function '{name}' not found in 'tools'.",
3271                        )
3272                        .into(),
3273                    );
3274                    return Err(e);
3275                }
3276            }
3277        }
3278        ResponsesToolChoice::AllowedTools {
3279            mode,
3280            tools: allowed_tools,
3281            ..
3282        } => {
3283            // Validate mode is "auto" or "required"
3284            if mode != "auto" && mode != "required" {
3285                let mut e = ValidationError::new("tool_choice_invalid_mode");
3286                e.message = Some(
3287                    format!(
3288                        "Invalid value for 'tool_choice.mode': must be 'auto' or 'required', got '{mode}'."
3289                    )
3290                    .into(),
3291                );
3292                return Err(e);
3293            }
3294
3295            // Validate that all function tool references exist
3296            for tool_ref in allowed_tools {
3297                if let ToolReference::Function { name } = tool_ref {
3298                    if !function_tool_names.contains(&name.as_str()) {
3299                        let mut e = ValidationError::new("tool_choice_tool_not_found");
3300                        e.message = Some(
3301                            format!(
3302                                "Invalid value for 'tool_choice.tools': tool '{name}' not found in 'tools'."
3303                            )
3304                            .into(),
3305                        );
3306                        return Err(e);
3307                    }
3308                }
3309                // Note: MCP and hosted tools don't need existence validation here
3310                // as they are resolved dynamically at runtime
3311            }
3312        }
3313        // Remaining variants have no cross-field existence constraints —
3314        // hosted built-ins, MCP server selection, custom tool names, and
3315        // `apply_patch` / `shell` are resolved at routing time.
3316        ResponsesToolChoice::Options(_)
3317        | ResponsesToolChoice::Types { .. }
3318        | ResponsesToolChoice::Mcp { .. }
3319        | ResponsesToolChoice::Custom { .. }
3320        | ResponsesToolChoice::ApplyPatch { .. }
3321        | ResponsesToolChoice::Shell { .. } => {}
3322    }
3323
3324    Ok(())
3325}
3326
3327/// Schema-level validation for cross-field dependencies
3328fn validate_responses_cross_parameters(request: &ResponsesRequest) -> Result<(), ValidationError> {
3329    // 1. Validate tool_choice requires tools (enhanced)
3330    validate_tool_choice_with_tools(request)?;
3331
3332    // 2. Validate top_logprobs requires include field
3333    if request.top_logprobs.is_some() {
3334        let has_logprobs_include = request
3335            .include
3336            .as_ref()
3337            .is_some_and(|inc| inc.contains(&IncludeField::MessageOutputTextLogprobs));
3338
3339        if !has_logprobs_include {
3340            let mut e = ValidationError::new("top_logprobs_requires_include");
3341            e.message = Some(
3342                "top_logprobs requires include field with 'message.output_text.logprobs'".into(),
3343            );
3344            return Err(e);
3345        }
3346    }
3347
3348    // 3. Validate conversation and previous_response_id are mutually exclusive
3349    if request.conversation.is_some() && request.previous_response_id.is_some() {
3350        let mut e = ValidationError::new("mutually_exclusive_parameters");
3351        e.message = Some("Mutually exclusive parameters. Ensure you are only providing one of: 'previous_response_id' or 'conversation'.".into());
3352        return Err(e);
3353    }
3354
3355    // 4. Validate input items structure
3356    if let ResponseInput::Items(items) = &request.input {
3357        // Check for at least one valid input message
3358        let has_valid_input = items.iter().any(|item| {
3359            matches!(
3360                item,
3361                ResponseInputOutputItem::Message { .. }
3362                    | ResponseInputOutputItem::SimpleInputMessage { .. }
3363            )
3364        });
3365
3366        if !has_valid_input {
3367            let mut e = ValidationError::new("input_missing_user_message");
3368            e.message = Some("Input items must contain at least one message".into());
3369            return Err(e);
3370        }
3371    }
3372
3373    // 5. Validate text format conflicts (for future structured output constraints)
3374    // Currently, Responses API doesn't have regex/ebnf like Chat API,
3375    // but this is here for completeness and future-proofing
3376
3377    Ok(())
3378}
3379
3380// ============================================================================
3381// Field-Level Validation Functions
3382// ============================================================================
3383
3384/// Validates response input is not empty and has valid content
3385fn validate_response_input(input: &ResponseInput) -> Result<(), ValidationError> {
3386    match input {
3387        ResponseInput::Text(text) => {
3388            if text.is_empty() {
3389                let mut e = ValidationError::new("input_text_empty");
3390                e.message = Some("Input text cannot be empty".into());
3391                return Err(e);
3392            }
3393        }
3394        ResponseInput::Items(items) => {
3395            if items.is_empty() {
3396                let mut e = ValidationError::new("input_items_empty");
3397                e.message = Some("Input items cannot be empty".into());
3398                return Err(e);
3399            }
3400            // Validate each item has valid content
3401            for item in items {
3402                validate_input_item(item)?;
3403            }
3404        }
3405    }
3406    Ok(())
3407}
3408
3409/// Validates individual input items have valid content
3410fn validate_input_item(item: &ResponseInputOutputItem) -> Result<(), ValidationError> {
3411    match item {
3412        ResponseInputOutputItem::Message { content, .. } => {
3413            if content.is_empty() {
3414                let mut e = ValidationError::new("message_content_empty");
3415                e.message = Some("Message content cannot be empty".into());
3416                return Err(e);
3417            }
3418        }
3419        ResponseInputOutputItem::SimpleInputMessage { content, .. } => match content {
3420            StringOrContentParts::String(s) if s.is_empty() => {
3421                let mut e = ValidationError::new("message_content_empty");
3422                e.message = Some("Message content cannot be empty".into());
3423                return Err(e);
3424            }
3425            StringOrContentParts::Array(parts) if parts.is_empty() => {
3426                let mut e = ValidationError::new("message_content_empty");
3427                e.message = Some("Message content parts cannot be empty".into());
3428                return Err(e);
3429            }
3430            _ => {}
3431        },
3432        ResponseInputOutputItem::Reasoning { .. } => {
3433            // Reasoning content can be empty - no validation needed
3434        }
3435        ResponseInputOutputItem::FunctionCallOutput { output, .. } => {
3436            if output.is_empty() {
3437                let mut e = ValidationError::new("function_output_empty");
3438                e.message = Some("Function call output cannot be empty".into());
3439                return Err(e);
3440            }
3441        }
3442        ResponseInputOutputItem::FunctionToolCall { .. } => {}
3443        ResponseInputOutputItem::McpApprovalRequest { .. } => {}
3444        ResponseInputOutputItem::McpApprovalResponse { .. } => {}
3445        ResponseInputOutputItem::ImageGenerationCall { .. } => {}
3446        ResponseInputOutputItem::Compaction { .. } => {}
3447        ResponseInputOutputItem::ComputerCall { .. } => {}
3448        ResponseInputOutputItem::ComputerCallOutput { .. } => {}
3449        // CustomToolCall is model-generated and echoed back on multi-turn
3450        // replay; matches the FunctionToolCall arm above with no content
3451        // validation so a parameterless custom tool with empty input can
3452        // round-trip cleanly.
3453        ResponseInputOutputItem::CustomToolCall { .. } => {}
3454        ResponseInputOutputItem::CustomToolCallOutput { output, .. } => match output {
3455            CustomToolCallOutputContent::Text(s) if s.is_empty() => {
3456                let mut e = ValidationError::new("custom_tool_call_output_empty");
3457                e.message = Some("Custom tool call output cannot be empty".into());
3458                return Err(e);
3459            }
3460            CustomToolCallOutputContent::Parts(parts) if parts.is_empty() => {
3461                let mut e = ValidationError::new("custom_tool_call_output_empty");
3462                e.message = Some("Custom tool call output parts cannot be empty".into());
3463                return Err(e);
3464            }
3465            _ => {}
3466        },
3467        // ShellCall is model-generated and echoed back on multi-turn replay;
3468        // mirrors FunctionToolCall above with no content validation so a
3469        // parameterless shell call can round-trip cleanly.
3470        ResponseInputOutputItem::ShellCall { .. } => {}
3471        ResponseInputOutputItem::ShellCallOutput { .. } => {
3472            // Backend execution is out of scope for T6 (schema-only); the
3473            // router returns 501 for shell calls, so SMG never synthesises
3474            // a ShellCallOutput itself. Skip content validation here so
3475            // round-tripping a previously-recorded response (even with an
3476            // empty chunk list) stays lossless — the cross-turn replay
3477            // contract is the motivating use case for keeping this arm
3478            // content-agnostic.
3479        }
3480        // I2: schema-only; backend resolution (history lookup +
3481        // substitution) is deferred to a future R task.
3482        ResponseInputOutputItem::ItemReference { .. } => {}
3483        // ApplyPatchCall is model-generated and echoed back on multi-turn
3484        // replay; matches the FunctionToolCall / CustomToolCall arms with no
3485        // diff/path validation — the operation payload is structurally
3486        // enforced by the `ApplyPatchOperation` enum, and accepting empty
3487        // diffs for `create_file` / `update_file` preserves round-trip
3488        // fidelity with items emitted by upstream providers.
3489        ResponseInputOutputItem::ApplyPatchCall { .. } => {}
3490        // ApplyPatchCallOutput.output is optional log text per spec
3491        // (openai-responses-api-spec.md §ApplyPatchCallOutput L251); an
3492        // absent or empty log is spec-legal (a clean `completed` with no
3493        // output, or a `failed` where the executor had nothing to log) so
3494        // no emptiness check applies here.
3495        ResponseInputOutputItem::ApplyPatchCallOutput { .. } => {}
3496        // Schema-only pass-through: T5 adds the protocol variants for the
3497        // `local_shell` built-in tool. Validation mirrors `ComputerCall` /
3498        // `ImageGenerationCall` above (no payload-level content checks).
3499        ResponseInputOutputItem::LocalShellCall { .. } => {}
3500        ResponseInputOutputItem::LocalShellCallOutput { .. } => {}
3501        // T11 schema-only: MCP call/list-tools input items replayed for
3502        // stateless multi-turn. Matches `McpApprovalRequest` above with no
3503        // content validation so an abridged or in-flight call (output /
3504        // error absent) can round-trip cleanly.
3505        ResponseInputOutputItem::McpCall { .. } => {}
3506        ResponseInputOutputItem::McpListTools { .. } => {}
3507    }
3508    Ok(())
3509}
3510
3511/// Validates ResponseTool structure based on tool type
3512fn validate_response_tools(tools: &[ResponseTool]) -> Result<(), ValidationError> {
3513    // MCP server_label must be present and unique (case-insensitive).
3514    let mut seen_mcp_labels: HashSet<String> = HashSet::new();
3515
3516    for (idx, tool) in tools.iter().enumerate() {
3517        if let ResponseTool::Mcp(mcp) = tool {
3518            let raw_label = mcp.server_label.as_str();
3519            if raw_label.is_empty() {
3520                let mut e = ValidationError::new("missing_required_parameter");
3521                e.message = Some(
3522                    format!("Missing required parameter: 'tools[{idx}].server_label'.").into(),
3523                );
3524                return Err(e);
3525            }
3526
3527            // OpenAI spec-compatible validation: require a non-empty label that starts with a
3528            // letter and contains only letters, digits, '-' and '_'.
3529            let valid = raw_label.starts_with(|c: char| c.is_ascii_alphabetic())
3530                && raw_label
3531                    .chars()
3532                    .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_');
3533            if !valid {
3534                let mut e = ValidationError::new("invalid_server_label");
3535                e.message = Some(
3536                    format!(
3537                        "Invalid input {raw_label}: 'server_label' must start with a letter and consist of only letters, digits, '-' and '_'"
3538                    )
3539                    .into(),
3540                );
3541                return Err(e);
3542            }
3543
3544            let normalized = raw_label.to_lowercase();
3545            if !seen_mcp_labels.insert(normalized) {
3546                let mut e = ValidationError::new("mcp_tool_duplicate_server_label");
3547                e.message = Some(
3548                    format!("Duplicate MCP server_label '{raw_label}' found in 'tools' parameter.")
3549                        .into(),
3550                );
3551                return Err(e);
3552            }
3553
3554            // T11 spec contract (openai-responses-api-spec.md L441, L445): one
3555            // of `server_url` or `connector_id` is required, and the two are
3556            // mutually exclusive. Reject payloads that set both so downstream
3557            // target resolution is unambiguous.
3558            if mcp.server_url.is_some() && mcp.connector_id.is_some() {
3559                let mut e = ValidationError::new("mcp_tool_conflicting_targets");
3560                e.message = Some(
3561                    format!(
3562                        "MCP tool with server_label '{raw_label}' sets both 'server_url' and 'connector_id'; exactly one is required."
3563                    )
3564                    .into(),
3565                );
3566                return Err(e);
3567            }
3568        }
3569    }
3570    Ok(())
3571}
3572
3573/// Validates text format configuration (JSON schema name cannot be empty)
3574fn validate_text_format(text: &TextConfig) -> Result<(), ValidationError> {
3575    if let Some(TextFormat::JsonSchema { name, .. }) = &text.format {
3576        if name.is_empty() {
3577            let mut e = ValidationError::new("json_schema_name_empty");
3578            e.message = Some("JSON schema name cannot be empty".into());
3579            return Err(e);
3580        }
3581    }
3582    Ok(())
3583}
3584
3585/// Normalize a SimpleInputMessage to a proper Message item
3586///
3587/// This helper converts SimpleInputMessage (which can have flexible content)
3588/// into a fully-structured Message item with a generated ID, role, and content array.
3589///
3590/// SimpleInputMessage items are converted to Message items with IDs generated using
3591/// the centralized ID generation pattern with "msg_" prefix for consistency.
3592///
3593/// # Arguments
3594/// * `item` - The input item to normalize
3595///
3596/// # Returns
3597/// A normalized ResponseInputOutputItem (either Message if converted, or original if not SimpleInputMessage)
3598pub fn normalize_input_item(item: &ResponseInputOutputItem) -> ResponseInputOutputItem {
3599    match item {
3600        ResponseInputOutputItem::SimpleInputMessage {
3601            content,
3602            role,
3603            phase,
3604            ..
3605        } => {
3606            let content_vec = match content {
3607                StringOrContentParts::String(s) => {
3608                    vec![ResponseContentPart::InputText { text: s.clone() }]
3609                }
3610                StringOrContentParts::Array(parts) => parts.clone(),
3611            };
3612
3613            ResponseInputOutputItem::Message {
3614                id: generate_id("msg"),
3615                role: role.clone(),
3616                content: content_vec,
3617                status: Some("completed".to_string()),
3618                phase: *phase,
3619            }
3620        }
3621        _ => item.clone(),
3622    }
3623}
3624
3625pub fn generate_id(prefix: &str) -> String {
3626    use rand::RngCore;
3627    let mut rng = rand::rng();
3628    // Generate exactly 50 hex characters (25 bytes) for the part after the underscore
3629    let mut bytes = [0u8; 25];
3630    rng.fill_bytes(&mut bytes);
3631    let hex_string: String = bytes.iter().map(|b| format!("{b:02x}")).collect();
3632    format!("{prefix}_{hex_string}")
3633}
3634
3635#[serde_with::skip_serializing_none]
3636#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
3637#[non_exhaustive]
3638pub struct ResponsesResponse {
3639    /// Response ID
3640    pub id: String,
3641
3642    /// Object type
3643    #[serde(default = "default_object_type")]
3644    pub object: String,
3645
3646    /// Creation timestamp (unix seconds)
3647    pub created_at: i64,
3648
3649    /// Completion timestamp (unix seconds). `None` until the response reaches
3650    /// a terminal state (`completed`, `incomplete`, `failed`, `cancelled`).
3651    #[serde(default)]
3652    pub completed_at: Option<i64>,
3653
3654    /// Whether the response was created in background mode.
3655    #[serde(default)]
3656    pub background: Option<bool>,
3657
3658    /// Conversation this response is linked to, if any.
3659    #[serde(default)]
3660    pub conversation: Option<String>,
3661
3662    /// Response status
3663    pub status: ResponseStatus,
3664
3665    /// Error information if status is failed
3666    pub error: Option<Value>,
3667
3668    /// Incomplete details if the response was truncated (`incomplete` status).
3669    pub incomplete_details: Option<IncompleteDetails>,
3670
3671    /// System instructions used
3672    pub instructions: Option<String>,
3673
3674    /// Max output tokens setting
3675    pub max_output_tokens: Option<u32>,
3676
3677    /// Model name
3678    pub model: String,
3679
3680    /// Output items
3681    #[serde(default)]
3682    pub output: Vec<ResponseOutputItem>,
3683
3684    /// Whether parallel tool calls are enabled
3685    #[serde(default = "default_true")]
3686    pub parallel_tool_calls: bool,
3687
3688    /// Previous response ID if this is a continuation
3689    pub previous_response_id: Option<String>,
3690
3691    /// Reasoning information
3692    pub reasoning: Option<ReasoningInfo>,
3693
3694    /// Whether the response is stored
3695    #[serde(default = "default_true")]
3696    pub store: bool,
3697
3698    /// Temperature setting used
3699    pub temperature: Option<f32>,
3700
3701    /// Text format settings
3702    pub text: Option<TextConfig>,
3703
3704    /// Tool choice setting
3705    #[serde(default = "default_tool_choice")]
3706    pub tool_choice: String,
3707
3708    /// Available tools
3709    #[serde(default)]
3710    pub tools: Vec<ResponseTool>,
3711
3712    /// Top-p setting used
3713    pub top_p: Option<f32>,
3714
3715    /// Truncation strategy used
3716    pub truncation: Option<String>,
3717
3718    /// Usage information
3719    pub usage: Option<ResponsesUsage>,
3720
3721    /// User identifier
3722    pub user: Option<String>,
3723
3724    /// Safety identifier for content moderation
3725    pub safety_identifier: Option<String>,
3726
3727    /// Additional metadata
3728    #[serde(default)]
3729    pub metadata: HashMap<String, Value>,
3730}
3731
3732fn default_object_type() -> String {
3733    "response".to_string()
3734}
3735
3736fn default_tool_choice() -> String {
3737    "auto".to_string()
3738}
3739
3740impl ResponsesResponse {
3741    /// Create a builder for constructing a ResponsesResponse
3742    pub fn builder(id: impl Into<String>, model: impl Into<String>) -> ResponsesResponseBuilder {
3743        ResponsesResponseBuilder::new(id, model)
3744    }
3745
3746    /// Check if the response is complete
3747    pub fn is_complete(&self) -> bool {
3748        matches!(self.status, ResponseStatus::Completed)
3749    }
3750
3751    /// Check if the response is in progress
3752    pub fn is_in_progress(&self) -> bool {
3753        matches!(self.status, ResponseStatus::InProgress)
3754    }
3755
3756    /// Check if the response failed
3757    pub fn is_failed(&self) -> bool {
3758        matches!(self.status, ResponseStatus::Failed)
3759    }
3760
3761    /// Check if the response terminated as incomplete (max_output_tokens / content_filter)
3762    pub fn is_incomplete(&self) -> bool {
3763        matches!(self.status, ResponseStatus::Incomplete)
3764    }
3765}
3766
3767impl ResponseInputOutputItem {
3768    /// Create a new reasoning input/output item.
3769    ///
3770    /// `encrypted_content` defaults to `None`; use
3771    /// [`Self::new_reasoning_encrypted`] when round-tripping gpt-5 /
3772    /// o-series encrypted reasoning.
3773    pub fn new_reasoning(
3774        id: String,
3775        summary: Vec<SummaryTextContent>,
3776        content: Vec<ResponseReasoningContent>,
3777        status: Option<String>,
3778    ) -> Self {
3779        Self::Reasoning {
3780            id,
3781            summary,
3782            content,
3783            encrypted_content: None,
3784            status,
3785        }
3786    }
3787
3788    /// Create a new reasoning input/output item carrying an encrypted
3789    /// reasoning payload. The `encrypted_content` must be the opaque
3790    /// ciphertext.
3791    pub fn new_reasoning_encrypted(
3792        id: String,
3793        summary: Vec<SummaryTextContent>,
3794        content: Vec<ResponseReasoningContent>,
3795        encrypted_content: String,
3796        status: Option<String>,
3797    ) -> Self {
3798        Self::Reasoning {
3799            id,
3800            summary,
3801            content,
3802            encrypted_content: Some(encrypted_content),
3803            status,
3804        }
3805    }
3806}
3807
3808impl ResponseOutputItem {
3809    /// Create a new message output item (no phase).
3810    pub fn new_message(
3811        id: String,
3812        role: String,
3813        content: Vec<ResponseContentPart>,
3814        status: String,
3815    ) -> Self {
3816        Self::Message {
3817            id,
3818            role,
3819            content,
3820            status,
3821            phase: None,
3822        }
3823    }
3824
3825    /// Create a new reasoning output item.
3826    ///
3827    /// `encrypted_content` defaults to `None`; use
3828    /// [`Self::new_reasoning_encrypted`] when carrying gpt-5 / o-series
3829    /// encrypted reasoning.
3830    pub fn new_reasoning(
3831        id: String,
3832        summary: Vec<SummaryTextContent>,
3833        content: Vec<ResponseReasoningContent>,
3834        status: Option<String>,
3835    ) -> Self {
3836        Self::Reasoning {
3837            id,
3838            summary,
3839            content,
3840            encrypted_content: None,
3841            status,
3842        }
3843    }
3844
3845    /// Create a new reasoning output item carrying an encrypted reasoning payload.
3846    ///
3847    /// The `encrypted_content` must be the opaque ciphertext; a `None` value
3848    /// would defeat the purpose of the `_encrypted` constructor — callers
3849    /// without ciphertext should use [`Self::new_reasoning`] instead.
3850    pub fn new_reasoning_encrypted(
3851        id: String,
3852        summary: Vec<SummaryTextContent>,
3853        content: Vec<ResponseReasoningContent>,
3854        encrypted_content: String,
3855        status: Option<String>,
3856    ) -> Self {
3857        Self::Reasoning {
3858            id,
3859            summary,
3860            content,
3861            encrypted_content: Some(encrypted_content),
3862            status,
3863        }
3864    }
3865
3866    /// Create a new function tool call output item
3867    pub fn new_function_tool_call(
3868        id: String,
3869        call_id: String,
3870        name: String,
3871        arguments: String,
3872        output: Option<String>,
3873        status: String,
3874    ) -> Self {
3875        Self::FunctionToolCall {
3876            id,
3877            call_id,
3878            name,
3879            arguments,
3880            output,
3881            status,
3882        }
3883    }
3884}
3885
3886impl ResponseContentPart {
3887    /// Create a new `output_text` content part.
3888    pub fn new_text(
3889        text: String,
3890        annotations: Vec<Annotation>,
3891        logprobs: Option<ChatLogProbs>,
3892    ) -> Self {
3893        Self::OutputText {
3894            text,
3895            annotations,
3896            logprobs,
3897        }
3898    }
3899}
3900
3901impl ResponseReasoningContent {
3902    /// Create a new reasoning text content
3903    pub fn new_reasoning_text(text: String) -> Self {
3904        Self::ReasoningText { text }
3905    }
3906}
3907
3908#[cfg(test)]
3909mod tests {
3910    use super::*;
3911
3912    /// Lock `as_str()` to the canonical serde tag for the unit variants
3913    /// — drift between the two would produce inconsistent wire labels
3914    /// across dispatch paths and serializers.
3915    #[test]
3916    fn response_tool_as_str_matches_serde_tag_for_unit_variants() {
3917        for tool in [
3918            ResponseTool::Computer,
3919            ResponseTool::ApplyPatch,
3920            ResponseTool::LocalShell,
3921        ] {
3922            let serialized = serde_json::to_value(&tool).unwrap();
3923            let serde_tag = serialized.get("type").and_then(|v| v.as_str()).unwrap();
3924            assert_eq!(tool.as_str(), serde_tag);
3925        }
3926    }
3927}