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`,
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
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)]
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        #[serde(default, skip_serializing_if = "Option::is_none")]
1610        id: Option<String>,
1611        call_id: String,
1612        name: String,
1613        arguments: String,
1614        #[serde(skip_serializing_if = "Option::is_none")]
1615        output: Option<String>,
1616        #[serde(skip_serializing_if = "Option::is_none")]
1617        status: Option<String>,
1618    },
1619    #[serde(rename = "function_call_output")]
1620    FunctionCallOutput {
1621        #[serde(default, skip_serializing_if = "Option::is_none")]
1622        id: Option<String>,
1623        call_id: String,
1624        output: String,
1625        #[serde(skip_serializing_if = "Option::is_none")]
1626        status: Option<String>,
1627    },
1628    #[serde(rename = "mcp_approval_request")]
1629    McpApprovalRequest {
1630        id: String,
1631        server_label: String,
1632        name: String,
1633        arguments: String,
1634    },
1635    #[serde(rename = "mcp_approval_response")]
1636    McpApprovalResponse {
1637        #[serde(skip_serializing_if = "Option::is_none")]
1638        id: Option<String>,
1639        approval_request_id: String,
1640        approve: bool,
1641        #[serde(skip_serializing_if = "Option::is_none")]
1642        reason: Option<String>,
1643    },
1644    /// `type: "image_generation_call"` — round-trip form for an image generated
1645    /// in a prior turn. Spec (OpenAI Responses API, multi-turn image-edit
1646    /// flow): clients may resubmit only `{ type, id }` to reference a prior
1647    /// generation by identifier, so `result` and `status` are accepted as
1648    /// absent on the input side. The full shape is
1649    /// `{ id, action?, background?, output_format?, quality?, result?: base64,
1650    /// revised_prompt?, size?, status?, type }`.
1651    ///
1652    /// This mirrors the OpenAI Python SDK 2.8.x
1653    /// `response_input_item_param.ImageGenerationCall` TypedDict: while the
1654    /// TypedDict types those fields as `Required[Optional[...]]`, the HTTP
1655    /// API itself documents the id-only multi-turn reference form (see the
1656    /// image-generation tool guide), and `skip_serializing_if` keeps the
1657    /// serialized form spec-compatible when a full item is round-tripped.
1658    /// The server-side `ResponseOutputItem::ImageGenerationCall` variant
1659    /// carries the same metadata so real OpenAI responses
1660    /// (`action`/`background`/`output_format`/`quality`/`size`) survive
1661    /// cloud-passthrough and persistence round-trips.
1662    ///
1663    /// The metadata fields (`action`, `background`, `output_format`,
1664    /// `quality`, `size`) are typed as `Option<String>` rather than
1665    /// narrow enums so unknown or future-added values pass through
1666    /// unchanged — this mirrors `ImageGenerationTool` on the input-tool
1667    /// side.
1668    #[serde(rename = "image_generation_call")]
1669    ImageGenerationCall {
1670        id: String,
1671        /// `"generate" | "edit" | "auto"` — which image-generation action the
1672        /// prior turn dispatched. Preserved free-form so future actions pass
1673        /// through without a wire break.
1674        #[serde(default, skip_serializing_if = "Option::is_none")]
1675        action: Option<String>,
1676        /// `"transparent" | "opaque" | "auto"`. Matches the
1677        /// `image_generation` tool input knob of the same name.
1678        #[serde(default, skip_serializing_if = "Option::is_none")]
1679        background: Option<String>,
1680        /// `"png" | "webp" | "jpeg"`. Matches the `image_generation` tool
1681        /// input knob of the same name.
1682        #[serde(default, skip_serializing_if = "Option::is_none")]
1683        output_format: Option<String>,
1684        /// `"auto" | "low" | "medium" | "high" | "standard" | "hd"`. Matches
1685        /// the `image_generation` tool input knob of the same name.
1686        #[serde(default, skip_serializing_if = "Option::is_none")]
1687        quality: Option<String>,
1688        /// Base64-encoded image bytes. Omitted on id-only references.
1689        #[serde(default, skip_serializing_if = "Option::is_none")]
1690        result: Option<String>,
1691        /// Prompt text the mainline model rewrote before dispatching the
1692        /// image-generation call. Preserved so downstream turns/storage do
1693        /// not drop it on replay.
1694        #[serde(default, skip_serializing_if = "Option::is_none")]
1695        revised_prompt: Option<String>,
1696        /// `"auto" | "1024x1024" | "1024x1536" | "1536x1024"`. Matches the
1697        /// `image_generation` tool input knob of the same name.
1698        #[serde(default, skip_serializing_if = "Option::is_none")]
1699        size: Option<String>,
1700        /// Generation status. Omitted on id-only references.
1701        #[serde(default, skip_serializing_if = "Option::is_none")]
1702        status: Option<ImageGenerationCallStatus>,
1703    },
1704    /// `type: "compaction"` — opaque compacted-history payload generated by
1705    /// the `/v1/responses/compact` API. Spec
1706    /// (openai-responses-api-spec.md §InputItemList L203-205):
1707    /// `Compaction { encrypted_content, type, id }`. `id` is optional on the
1708    /// input wire so newly-minted client-side compactions can omit it; it is
1709    /// always present on items round-tripped from a previous response.
1710    #[serde(rename = "compaction")]
1711    Compaction {
1712        encrypted_content: String,
1713        #[serde(default, skip_serializing_if = "Option::is_none")]
1714        id: Option<String>,
1715    },
1716    /// `{ type: "computer_call", id, call_id, action?, actions?, status,
1717    /// pending_safety_checks }`.
1718    ///
1719    /// Spec (openai-responses-api-spec.md §ComputerCall): single-action
1720    /// `action` is the legacy shape; `actions` carries the flattened batch
1721    /// for `computer_use`. Both fields are optional independently, so callers
1722    /// can roundtrip either form.
1723    #[serde(rename = "computer_call")]
1724    ComputerCall {
1725        id: String,
1726        call_id: String,
1727        #[serde(default, skip_serializing_if = "Option::is_none")]
1728        action: Option<ComputerAction>,
1729        #[serde(default, skip_serializing_if = "Option::is_none")]
1730        actions: Option<Vec<ComputerAction>>,
1731        status: ComputerCallStatus,
1732        /// Always serialized (including an empty `[]`). The official OpenAI
1733        /// Python SDK (`openai==2.8.1`,
1734        /// `types/responses/response_computer_tool_call.py`) declares this as a
1735        /// non-`Optional` `List[PendingSafetyCheck]`, so the field must always
1736        /// appear on the wire — an empty array is semantically distinct from
1737        /// omitting the field.
1738        #[serde(default)]
1739        pending_safety_checks: Vec<ComputerSafetyCheck>,
1740    },
1741    /// `{ type: "computer_call_output", id?, call_id, output,
1742    /// acknowledged_safety_checks?, status? }`.
1743    ///
1744    /// Spec (openai-responses-api-spec.md §ComputerCallOutput): `output` is the
1745    /// [`ComputerCallOutputContent::ComputerScreenshot`] payload;
1746    /// `acknowledged_safety_checks` and `status` are both optional per spec.
1747    #[serde(rename = "computer_call_output")]
1748    ComputerCallOutput {
1749        #[serde(default, skip_serializing_if = "Option::is_none")]
1750        id: Option<String>,
1751        call_id: String,
1752        output: ComputerCallOutputContent,
1753        #[serde(default, skip_serializing_if = "Vec::is_empty")]
1754        acknowledged_safety_checks: Vec<ComputerSafetyCheck>,
1755        #[serde(default, skip_serializing_if = "Option::is_none")]
1756        status: Option<ComputerCallStatus>,
1757    },
1758    /// `type: "custom_tool_call"` — assistant's call into a registered
1759    /// custom tool. Spec: `{ call_id, input, name, type, id?, namespace? }`.
1760    /// `id` / `namespace` are modelled as `Option<String>` so newly-minted
1761    /// client-side calls can omit them; they are populated on items
1762    /// round-tripped from a previous response. `input` is the model's
1763    /// free-form payload (constrained by the tool's `format` if grammar is
1764    /// set); the client owns execution and replies with a matching
1765    /// [`Self::CustomToolCallOutput`].
1766    #[serde(rename = "custom_tool_call")]
1767    CustomToolCall {
1768        call_id: String,
1769        input: String,
1770        name: String,
1771        #[serde(skip_serializing_if = "Option::is_none")]
1772        id: Option<String>,
1773        #[serde(skip_serializing_if = "Option::is_none")]
1774        namespace: Option<String>,
1775    },
1776    /// `type: "custom_tool_call_output"` — client's response to a
1777    /// `custom_tool_call`. Spec: `{ call_id, output, type, id? }` (no
1778    /// `status` field per spec). `id` is
1779    /// `Option<String>` for the same reason as `CustomToolCall.id` above.
1780    /// `output` is either a plain string or an array of input-typed content
1781    /// parts (`input_text` / `input_image` / `input_file`).
1782    #[serde(rename = "custom_tool_call_output")]
1783    CustomToolCallOutput {
1784        call_id: String,
1785        output: CustomToolCallOutputContent,
1786        #[serde(skip_serializing_if = "Option::is_none")]
1787        id: Option<String>,
1788    },
1789    /// `type: "shell_call"` — assistant's call into the containerized
1790    /// [`ResponseTool::Shell`] tool.
1791    ///
1792    /// Spec (openai-responses-api-spec.md §ShellCall, L228-231) +
1793    /// OpenAI SDK v2.8.1 `ResponseFunctionShellToolCall`:
1794    /// `{ action, call_id, type, id, environment, status, created_by? }`.
1795    /// `id` is `Option<String>` so newly-minted client-side calls can omit
1796    /// it; the model populates it on items round-tripped from a previous
1797    /// response. `created_by` carries provenance metadata the SDK types
1798    /// as `Optional[str]` — present when the item was emitted by the
1799    /// platform, absent on client-authored calls.
1800    #[serde(rename = "shell_call")]
1801    ShellCall {
1802        action: ShellCallAction,
1803        call_id: String,
1804        #[serde(default, skip_serializing_if = "Option::is_none")]
1805        id: Option<String>,
1806        /// Resolved execution environment. Spec constrains this to
1807        /// `local` or `container_reference` on the call form (see
1808        /// [`ShellCallEnvironment`] docs).
1809        #[serde(default, skip_serializing_if = "Option::is_none")]
1810        environment: Option<ShellCallEnvironment>,
1811        #[serde(default, skip_serializing_if = "Option::is_none")]
1812        status: Option<ShellCallStatus>,
1813        /// Provenance tag mirroring the SDK's `created_by: Optional[str]`
1814        /// on `ResponseFunctionShellToolCall`. Dropped at serialize time
1815        /// when absent so client-authored calls do not carry a null
1816        /// placeholder.
1817        #[serde(default, skip_serializing_if = "Option::is_none")]
1818        created_by: Option<String>,
1819    },
1820    /// `type: "shell_call_output"` — client's reply to a `shell_call`.
1821    ///
1822    /// Spec (openai-responses-api-spec.md §ShellCallOutput, L233-238) +
1823    /// OpenAI SDK v2.8.1 `ResponseFunctionShellToolCallOutput`:
1824    /// `{ call_id, output, type, id, max_output_length?, status, created_by? }`.
1825    /// `id`, `max_output_length`, and `created_by` are modelled as
1826    /// `Option` per the SDK's `Optional[...]` typing; the server populates
1827    /// them on items round-tripped from a previous response.
1828    #[serde(rename = "shell_call_output")]
1829    ShellCallOutput {
1830        call_id: String,
1831        output: Vec<ShellOutputChunk>,
1832        #[serde(default, skip_serializing_if = "Option::is_none")]
1833        id: Option<String>,
1834        #[serde(default, skip_serializing_if = "Option::is_none")]
1835        max_output_length: Option<u64>,
1836        #[serde(default, skip_serializing_if = "Option::is_none")]
1837        status: Option<ShellCallStatus>,
1838        /// Provenance tag mirroring the SDK's `created_by: Optional[str]`
1839        /// on `ResponseFunctionShellToolCallOutput`.
1840        #[serde(default, skip_serializing_if = "Option::is_none")]
1841        created_by: Option<String>,
1842    },
1843    /// `type: "apply_patch_call"` — model-issued file-edit request. Spec
1844    /// (openai-responses-api-spec.md §ApplyPatchCall L240-L246):
1845    /// `{ call_id, operation, status, type, id }`. `id` is `Option<String>`
1846    /// so newly-minted client-side calls can omit it; it is always present on
1847    /// items round-tripped from a previous response. The `operation` union is
1848    /// `CreateFile | DeleteFile | UpdateFile` per
1849    /// [`ApplyPatchOperation`]; the client owns execution (apply the diff on
1850    /// disk) and replies with a matching [`Self::ApplyPatchCallOutput`].
1851    #[serde(rename = "apply_patch_call")]
1852    ApplyPatchCall {
1853        call_id: String,
1854        operation: ApplyPatchOperation,
1855        status: ApplyPatchCallStatus,
1856        #[serde(skip_serializing_if = "Option::is_none")]
1857        id: Option<String>,
1858    },
1859    /// `type: "apply_patch_call_output"` — client's response to an
1860    /// `apply_patch_call`. Spec (openai-responses-api-spec.md
1861    /// §ApplyPatchCallOutput L248-L251): `{ call_id, status, type, id,
1862    /// output }` where `output` is optional log text. `id` is
1863    /// `Option<String>` for the same reason as `ApplyPatchCall.id` above;
1864    /// `output` uses `skip_serializing_if` so a no-log success round-trips
1865    /// without emitting an explicit `null`.
1866    #[serde(rename = "apply_patch_call_output")]
1867    ApplyPatchCallOutput {
1868        call_id: String,
1869        status: ApplyPatchCallOutputStatus,
1870        #[serde(skip_serializing_if = "Option::is_none")]
1871        id: Option<String>,
1872        #[serde(skip_serializing_if = "Option::is_none")]
1873        output: Option<String>,
1874    },
1875    /// `type: "local_shell_call"` — assistant's call into the
1876    /// `local_shell` built-in tool. Spec
1877    /// (openai-responses-api-spec.md §LocalShellCall L219-222):
1878    /// `{ id, action, call_id, status, type }` where `action` is a
1879    /// [`LocalShellExec`] payload describing the command to run on the
1880    /// host. The client executes the command and replies with a
1881    /// matching [`Self::LocalShellCallOutput`].
1882    #[serde(rename = "local_shell_call")]
1883    LocalShellCall {
1884        id: String,
1885        call_id: String,
1886        action: LocalShellExec,
1887        status: LocalShellCallStatus,
1888    },
1889    /// `type: "local_shell_call_output"` — client's response to a
1890    /// `local_shell_call`. Spec
1891    /// (openai-responses-api-spec.md §LocalShellCallOutput L224-226):
1892    /// `{ id, output, type, status }`. `output` is a single string
1893    /// carrying the command's serialized JSON output; `status` is
1894    /// optional per SDK v2.8.1 (`openai==2.8.1`,
1895    /// `types/responses/response_input_item_param.py`
1896    /// `LocalShellCallOutput` — `Optional` on `status`).
1897    #[serde(rename = "local_shell_call_output")]
1898    LocalShellCallOutput {
1899        id: String,
1900        output: String,
1901        #[serde(default, skip_serializing_if = "Option::is_none")]
1902        status: Option<LocalShellCallStatus>,
1903    },
1904    /// `type: "mcp_call"` — assistant-emitted hosted-MCP tool call replayed
1905    /// as an input item for stateless multi-turn (`store=false`) flows.
1906    ///
1907    /// Spec (openai-responses-api-spec.md §McpCall L264-266):
1908    /// `{ id, arguments, name, server_label, type, approval_request_id?, error?, output?, status? }`.
1909    /// Shape mirrors [`ResponseOutputItem::McpCall`] but `approval_request_id`,
1910    /// `error`, `output`, and `status` are optional on the input side so
1911    /// replay of an abridged or in-flight call (no output yet) stays
1912    /// lossless.
1913    #[serde(rename = "mcp_call")]
1914    McpCall {
1915        id: String,
1916        arguments: String,
1917        name: String,
1918        server_label: String,
1919        #[serde(default, skip_serializing_if = "Option::is_none")]
1920        approval_request_id: Option<String>,
1921        #[serde(
1922            default,
1923            deserialize_with = "mcp_call_error_compat",
1924            skip_serializing_if = "Option::is_none"
1925        )]
1926        error: Option<McpToolCallError>,
1927        #[serde(default, skip_serializing_if = "Option::is_none")]
1928        output: Option<String>,
1929        #[serde(default, skip_serializing_if = "Option::is_none")]
1930        status: Option<String>,
1931    },
1932    /// `type: "mcp_list_tools"` — hosted-MCP server's tool listing replayed
1933    /// as an input item.
1934    ///
1935    /// Spec (openai-responses-api-spec.md §McpListTools L253-255):
1936    /// `{ id, server_label, tools, type, error? }` where each `tools` entry
1937    /// is `{ input_schema, name, annotations?, description? }`. Shape
1938    /// mirrors [`ResponseOutputItem::McpListTools`] with `error` optional
1939    /// per SDK v2.8.1
1940    /// `types/responses/response_input_item.py::McpListTools`.
1941    #[serde(rename = "mcp_list_tools")]
1942    McpListTools {
1943        id: String,
1944        server_label: String,
1945        tools: Vec<McpToolInfo>,
1946        #[serde(default, skip_serializing_if = "Option::is_none")]
1947        error: Option<String>,
1948    },
1949    #[serde(untagged)]
1950    SimpleInputMessage {
1951        content: StringOrContentParts,
1952        role: String,
1953        /// Spec: `EasyInputMessage.type` is `optional "message"`. Constrained
1954        /// to a single-value tag enum so payloads with an unknown `type`
1955        /// (e.g. `"input_file"`, `"totally_made_up"`) do not silently land
1956        /// in this untagged catch-all variant — P5 fail-fast contract.
1957        #[serde(default, skip_serializing_if = "Option::is_none")]
1958        #[serde(rename = "type")]
1959        r#type: Option<SimpleInputMessageTypeTag>,
1960        /// Optional phase label (spec: EasyInputMessage.phase).
1961        ///
1962        /// Preserved through conversation storage so gpt-5.3-codex+ does not
1963        /// lose the commentary/final_answer distinction across turns.
1964        #[serde(default, skip_serializing_if = "Option::is_none")]
1965        phase: Option<MessagePhase>,
1966    },
1967    /// `type: "item_reference"` — pointer to a previously-stored item in the
1968    /// active conversation. Spec (openai-responses-api-spec.md §InputItemList
1969    /// L275-276): `ItemReference { id, type }` where `type` is
1970    /// `optional "item_reference"`. The variant is declared as
1971    /// `#[serde(untagged)]` because the `type` discriminator is optional on
1972    /// the wire; `r#type` is pinned to [`ItemReferenceTypeTag`] so payloads
1973    /// whose `type` is not `"item_reference"` (e.g. `"totally_made_up"`) do
1974    /// not silently land in this catch-all variant — P5 fail-fast contract.
1975    ///
1976    /// Declared AFTER [`Self::SimpleInputMessage`] so a `{id, role, content}`
1977    /// payload (the id-carrying shape of `SimpleInputMessage`) still lands in
1978    /// `SimpleInputMessage` first; only `{id}` / `{id, type: "item_reference"}`
1979    /// payloads — which fail `SimpleInputMessage`'s required-field check —
1980    /// fall through to this arm.
1981    ///
1982    /// Backend resolution (router looks up `id` from conversation history and
1983    /// substitutes the referenced item inline) is deferred to a future R
1984    /// task; this variant only adds the schema surface.
1985    #[serde(untagged)]
1986    ItemReference {
1987        id: String,
1988        #[serde(default, skip_serializing_if = "Option::is_none")]
1989        #[serde(rename = "type")]
1990        r#type: Option<ItemReferenceTypeTag>,
1991    },
1992}
1993
1994/// Single-value tag enum pinning [`ResponseInputOutputItem::ItemReference`]'s
1995/// optional `type` discriminator to the spec's only permitted value,
1996/// `"item_reference"`. Used because the outer enum is `type`-tagged and the
1997/// `ItemReference` variant is declared `#[serde(untagged)]` to accept payloads
1998/// that omit `type` entirely — without this pin the catch-all would silently
1999/// swallow payloads whose `type` discriminator is an unknown string.
2000#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, schemars::JsonSchema)]
2001#[serde(rename_all = "snake_case")]
2002pub enum ItemReferenceTypeTag {
2003    ItemReference,
2004}
2005
2006/// Single-value tag enum pinning `EasyInputMessage.type` to the spec's only
2007/// permitted value, `"message"`. Used to keep [`ResponseInputOutputItem::SimpleInputMessage`]
2008/// — which is the `#[serde(untagged)]` fallback in the outer `type`-tagged enum
2009/// — from silently swallowing payloads whose `type` discriminator is unknown
2010/// (P5 fail-fast contract).
2011#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, schemars::JsonSchema)]
2012#[serde(rename_all = "snake_case")]
2013pub enum SimpleInputMessageTypeTag {
2014    Message,
2015}
2016
2017/// Detail level for [`ResponseContentPart::InputFile`]. Spec restricts this
2018/// to `"low" | "high"` (defaults to `low`); it is narrower than [`Detail`]
2019/// used for images which also admits `auto` / `original`.
2020#[derive(Debug, Clone, Default, Serialize, Deserialize, schemars::JsonSchema)]
2021#[serde(rename_all = "snake_case")]
2022pub enum FileDetail {
2023    #[default]
2024    Low,
2025    High,
2026}
2027
2028/// Typed annotation attached to [`ResponseContentPart::OutputText`]. Matches
2029/// the OpenAI Responses API `Annotation` union; a `type` discriminator selects
2030/// the variant on the wire.
2031#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
2032#[serde(tag = "type", rename_all = "snake_case")]
2033pub enum Annotation {
2034    /// `type: "file_citation"` — points at a file previously uploaded.
2035    FileCitation {
2036        file_id: String,
2037        filename: String,
2038        index: u32,
2039    },
2040    /// `type: "url_citation"` — citation back to a URL in a web-search result.
2041    UrlCitation {
2042        url: String,
2043        title: String,
2044        start_index: u32,
2045        end_index: u32,
2046    },
2047    /// `type: "container_file_citation"` — citation to a file inside a
2048    /// code-interpreter / computer-use container.
2049    ContainerFileCitation {
2050        container_id: String,
2051        file_id: String,
2052        filename: String,
2053        start_index: u32,
2054        end_index: u32,
2055    },
2056    /// `type: "file_path"` — reference to a generated file path.
2057    FilePath { file_id: String, index: u32 },
2058}
2059
2060#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
2061#[serde(tag = "type")]
2062#[serde(rename_all = "snake_case")]
2063pub enum ResponseContentPart {
2064    #[serde(rename = "output_text")]
2065    OutputText {
2066        text: String,
2067        #[serde(default)]
2068        annotations: Vec<Annotation>,
2069        #[serde(skip_serializing_if = "Option::is_none")]
2070        logprobs: Option<ChatLogProbs>,
2071    },
2072    #[serde(rename = "input_text")]
2073    InputText { text: String },
2074    /// `type: "input_image"` — reference to an image supplied by the client.
2075    /// Exactly one of `file_id` / `image_url` is typically set; both may be
2076    /// absent when only `detail` is being conveyed.
2077    #[serde(rename = "input_image")]
2078    InputImage {
2079        #[serde(skip_serializing_if = "Option::is_none")]
2080        detail: Option<Detail>,
2081        #[serde(skip_serializing_if = "Option::is_none")]
2082        file_id: Option<String>,
2083        #[serde(skip_serializing_if = "Option::is_none")]
2084        image_url: Option<String>,
2085    },
2086    /// `type: "input_file"` — reference to an attached file. `file_data` is a
2087    /// base64 blob; `file_url` / `file_id` reference external/uploaded files.
2088    #[serde(rename = "input_file")]
2089    InputFile {
2090        #[serde(skip_serializing_if = "Option::is_none")]
2091        detail: Option<FileDetail>,
2092        #[serde(skip_serializing_if = "Option::is_none")]
2093        file_data: Option<String>,
2094        #[serde(skip_serializing_if = "Option::is_none")]
2095        file_id: Option<String>,
2096        #[serde(skip_serializing_if = "Option::is_none")]
2097        file_url: Option<String>,
2098        #[serde(skip_serializing_if = "Option::is_none")]
2099        filename: Option<String>,
2100    },
2101    /// `type: "refusal"` — model refusal surfaced as a content part (spec's
2102    /// `ResponseOutputRefusal`).
2103    #[serde(rename = "refusal")]
2104    Refusal { refusal: String },
2105}
2106
2107#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
2108#[serde(tag = "type")]
2109#[serde(rename_all = "snake_case")]
2110pub enum ResponseReasoningContent {
2111    #[serde(rename = "reasoning_text")]
2112    ReasoningText { text: String },
2113}
2114
2115/// Tagged content element carried in `Reasoning.summary`.
2116///
2117/// OpenAI spec: `summary: array of SummaryTextContent { text, type: "summary_text" }`.
2118/// Replaces the prior `Vec<String>` wire-type that broke bidirectional
2119/// interoperability with spec-compliant clients.
2120#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
2121#[serde(tag = "type")]
2122#[serde(rename_all = "snake_case")]
2123pub enum SummaryTextContent {
2124    #[serde(rename = "summary_text")]
2125    SummaryText { text: String },
2126}
2127
2128/// MCP Tool information for the mcp_list_tools output item
2129#[serde_with::skip_serializing_none]
2130#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
2131pub struct McpToolInfo {
2132    pub name: String,
2133    pub description: Option<String>,
2134    pub input_schema: Value,
2135    pub annotations: Option<Value>,
2136}
2137
2138/// Structured `mcp_call.error`: a `type`-tagged union of protocol,
2139/// tool-execution, and HTTP failures. OpenAI removed the legacy plain-string
2140/// shape from the wire; stored/replayed string errors still deserialize via
2141/// [`mcp_call_error_compat`].
2142#[expect(
2143    clippy::enum_variant_names,
2144    reason = "variant names mirror the spec's `*_error` tag set"
2145)]
2146#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, schemars::JsonSchema)]
2147#[serde(tag = "type")]
2148pub enum McpToolCallError {
2149    #[serde(rename = "mcp_protocol_error")]
2150    ProtocolError { code: i64, message: String },
2151    #[serde(rename = "mcp_tool_execution_error")]
2152    ToolExecutionError { content: Value },
2153    #[serde(rename = "http_error")]
2154    HttpError { code: i64, message: String },
2155}
2156
2157impl McpToolCallError {
2158    /// Wrap a bare failure message in the tool-execution variant.
2159    pub fn execution(message: impl Into<String>) -> Self {
2160        Self::ToolExecutionError {
2161            content: Value::String(message.into()),
2162        }
2163    }
2164}
2165
2166/// Accept both the structured union and the legacy plain-string error shape.
2167fn mcp_call_error_compat<'de, D>(deserializer: D) -> Result<Option<McpToolCallError>, D::Error>
2168where
2169    D: serde::Deserializer<'de>,
2170{
2171    #[derive(Deserialize)]
2172    #[serde(untagged)]
2173    enum Compat {
2174        Structured(McpToolCallError),
2175        Legacy(String),
2176    }
2177    Ok(
2178        Option::<Compat>::deserialize(deserializer)?.map(|error| match error {
2179            Compat::Structured(error) => error,
2180            Compat::Legacy(message) => McpToolCallError::execution(message),
2181        }),
2182    )
2183}
2184
2185#[serde_with::skip_serializing_none]
2186#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
2187#[serde(tag = "type")]
2188#[serde(rename_all = "snake_case")]
2189pub enum ResponseOutputItem {
2190    #[serde(rename = "message")]
2191    Message {
2192        id: String,
2193        role: String,
2194        content: Vec<ResponseContentPart>,
2195        status: String,
2196        /// Optional phase label (spec: ResponseOutputMessage.phase).
2197        ///
2198        /// Labels assistant messages; for gpt-5.3-codex+ we must preserve and
2199        /// resend this on subsequent turns to avoid perf degradation.
2200        #[serde(default, skip_serializing_if = "Option::is_none")]
2201        phase: Option<MessagePhase>,
2202    },
2203    #[serde(rename = "reasoning")]
2204    #[non_exhaustive]
2205    Reasoning {
2206        id: String,
2207        #[serde(default)]
2208        summary: Vec<SummaryTextContent>,
2209        content: Vec<ResponseReasoningContent>,
2210        /// Encrypted reasoning payload for gpt-5 / o-series round-trip.
2211        /// Opaque to SMG; preserved verbatim.
2212        #[serde(skip_serializing_if = "Option::is_none")]
2213        #[serde(default)]
2214        encrypted_content: Option<String>,
2215        status: Option<String>,
2216    },
2217    #[serde(rename = "function_call")]
2218    FunctionToolCall {
2219        #[serde(default, skip_serializing_if = "Option::is_none")]
2220        id: Option<String>,
2221        call_id: String,
2222        name: String,
2223        arguments: String,
2224        #[serde(default, skip_serializing_if = "Option::is_none")]
2225        output: Option<String>,
2226        status: String,
2227    },
2228    #[serde(rename = "mcp_list_tools")]
2229    McpListTools {
2230        id: String,
2231        server_label: String,
2232        tools: Vec<McpToolInfo>,
2233        /// Spec (openai-responses-api-spec.md §McpListTools L253-255):
2234        /// `error?: string`. Preserves the failure message when the MCP
2235        /// server could not list tools; symmetric with the matching field
2236        /// on `ResponseInputOutputItem::McpListTools` so emit↔replay is
2237        /// lossless.
2238        #[serde(default, skip_serializing_if = "Option::is_none")]
2239        error: Option<String>,
2240    },
2241    #[serde(rename = "mcp_call")]
2242    McpCall {
2243        id: String,
2244        status: String,
2245        approval_request_id: Option<String>,
2246        arguments: String,
2247        #[serde(default, deserialize_with = "mcp_call_error_compat")]
2248        error: Option<McpToolCallError>,
2249        name: String,
2250        output: String,
2251        server_label: String,
2252    },
2253    #[serde(rename = "mcp_approval_request")]
2254    McpApprovalRequest {
2255        id: String,
2256        server_label: String,
2257        name: String,
2258        arguments: String,
2259    },
2260    #[serde(rename = "web_search_call")]
2261    WebSearchCall {
2262        id: String,
2263        status: WebSearchCallStatus,
2264        action: WebSearchAction,
2265        /// Search hits surfaced when callers request `web_search_call.results`
2266        /// via the top-level `include[]` array. Mirrors the `file_search_call.results`
2267        /// shape — array of typed entries when populated, omitted otherwise so the
2268        /// default wire shape (`{id, action, status, type}`) stays spec-byte-identical.
2269        #[serde(default, skip_serializing_if = "Option::is_none")]
2270        results: Option<Vec<WebSearchResult>>,
2271    },
2272    #[serde(rename = "code_interpreter_call")]
2273    CodeInterpreterCall {
2274        id: String,
2275        status: CodeInterpreterCallStatus,
2276        container_id: String,
2277        code: Option<String>,
2278        outputs: Option<Vec<CodeInterpreterOutput>>,
2279    },
2280    #[serde(rename = "file_search_call")]
2281    FileSearchCall {
2282        id: String,
2283        status: FileSearchCallStatus,
2284        queries: Vec<String>,
2285        results: Option<Vec<FileSearchResult>>,
2286    },
2287    /// `type: "image_generation_call"` — output item carrying a base64 image
2288    /// produced by the `image_generation` built-in tool. Spec:
2289    /// `{ id, action?, background?, output_format?, quality?, result: base64,
2290    /// revised_prompt?, size?, status, type }`.
2291    ///
2292    /// Real OpenAI production responses include the five metadata fields
2293    /// (`action`, `background`, `output_format`, `quality`, `size`) even
2294    /// though the OpenAI Rust SDK v2.8.1 omits them. We carry them as
2295    /// `Option<String>` so cloud passthrough and persistence round-trips
2296    /// preserve them verbatim — and so downstream consumers can read them
2297    /// without a second round-trip to the provider.
2298    ///
2299    /// The metadata fields are typed as `Option<String>` rather than narrow
2300    /// enums so unknown or future-added values pass through unchanged;
2301    /// this mirrors `ImageGenerationTool` on the input-tool side.
2302    #[serde(rename = "image_generation_call")]
2303    ImageGenerationCall {
2304        id: String,
2305        /// `"generate" | "edit" | "auto"` — which image-generation action
2306        /// this call dispatched. Preserved free-form so future actions pass
2307        /// through without a wire break.
2308        #[serde(default, skip_serializing_if = "Option::is_none")]
2309        action: Option<String>,
2310        /// `"transparent" | "opaque" | "auto"`. Mirrors the
2311        /// `image_generation` tool input knob of the same name.
2312        #[serde(default, skip_serializing_if = "Option::is_none")]
2313        background: Option<String>,
2314        /// `"png" | "webp" | "jpeg"`. Mirrors the `image_generation` tool
2315        /// input knob of the same name.
2316        #[serde(default, skip_serializing_if = "Option::is_none")]
2317        output_format: Option<String>,
2318        /// `"auto" | "low" | "medium" | "high" | "standard" | "hd"`. Mirrors
2319        /// the `image_generation` tool input knob of the same name.
2320        #[serde(default, skip_serializing_if = "Option::is_none")]
2321        quality: Option<String>,
2322        /// Base64-encoded image bytes.
2323        result: String,
2324        /// Prompt text the mainline model rewrote before dispatching the
2325        /// image-generation call. Preserved so downstream turns/storage do
2326        /// not drop it on replay.
2327        #[serde(default, skip_serializing_if = "Option::is_none")]
2328        revised_prompt: Option<String>,
2329        /// `"auto" | "1024x1024" | "1024x1536" | "1536x1024"`. Mirrors the
2330        /// `image_generation` tool input knob of the same name.
2331        #[serde(default, skip_serializing_if = "Option::is_none")]
2332        size: Option<String>,
2333        status: ImageGenerationCallStatus,
2334    },
2335    /// `type: "compaction"` — server-emitted item carrying an opaque
2336    /// compacted-history payload. Spec
2337    /// (openai-responses-api-spec.md §InputItemList L203-205,
2338    /// §`output: array of ResponseOutputItem`): `{ encrypted_content, type, id }`.
2339    /// `id` is required on the output wire because the server always assigns
2340    /// one when emitting the compaction item.
2341    #[serde(rename = "compaction")]
2342    Compaction {
2343        id: String,
2344        encrypted_content: String,
2345    },
2346    /// `{ type: "computer_call", id, call_id, action?, actions?, status,
2347    /// pending_safety_checks }`.
2348    ///
2349    /// Spec (openai-responses-api-spec.md §ComputerCall): output-side mirror of
2350    /// the input variant — emitted when the model issues a computer-use action.
2351    /// See [`ComputerAction`].
2352    #[serde(rename = "computer_call")]
2353    ComputerCall {
2354        id: String,
2355        call_id: String,
2356        #[serde(default, skip_serializing_if = "Option::is_none")]
2357        action: Option<ComputerAction>,
2358        #[serde(default, skip_serializing_if = "Option::is_none")]
2359        actions: Option<Vec<ComputerAction>>,
2360        status: ComputerCallStatus,
2361        /// Always serialized (including an empty `[]`). The official OpenAI
2362        /// Python SDK (`openai==2.8.1`,
2363        /// `types/responses/response_computer_tool_call.py`) declares this as a
2364        /// non-`Optional` `List[PendingSafetyCheck]`, so the field must always
2365        /// appear on the wire — an empty array is semantically distinct from
2366        /// omitting the field.
2367        #[serde(default)]
2368        pending_safety_checks: Vec<ComputerSafetyCheck>,
2369    },
2370    /// `{ type: "computer_call_output", id?, call_id, output,
2371    /// acknowledged_safety_checks?, status? }`.
2372    ///
2373    /// Spec (openai-responses-api-spec.md §ComputerCallOutput).
2374    #[serde(rename = "computer_call_output")]
2375    ComputerCallOutput {
2376        #[serde(default, skip_serializing_if = "Option::is_none")]
2377        id: Option<String>,
2378        call_id: String,
2379        output: ComputerCallOutputContent,
2380        #[serde(default, skip_serializing_if = "Vec::is_empty")]
2381        acknowledged_safety_checks: Vec<ComputerSafetyCheck>,
2382        #[serde(default, skip_serializing_if = "Option::is_none")]
2383        status: Option<ComputerCallStatus>,
2384    },
2385    /// `type: "shell_call"` — output-side mirror of the input variant.
2386    ///
2387    /// Spec (openai-responses-api-spec.md §ShellCall, L228-231 and §returns
2388    /// L512-513) + OpenAI SDK v2.8.1 `ResponseFunctionShellToolCall`:
2389    /// emitted when the model issues a containerized shell action. The
2390    /// environment echoed back is restricted to `local` /
2391    /// `container_reference` via [`ResponseShellCallEnvironment`] — per spec
2392    /// L513 the response form uses `ResponseLocalEnvironment { type: "local" }`
2393    /// only.
2394    ///
2395    /// `id` and `status` are required on the output wire — the SDK types
2396    /// them as non-`Optional` on `ResponseFunctionShellToolCall`, mirroring
2397    /// the `ComputerCall` treatment above. `created_by` is the SDK's
2398    /// `Optional[str]` provenance tag, populated when the platform stamps
2399    /// the item.
2400    #[serde(rename = "shell_call")]
2401    ShellCall {
2402        id: String,
2403        call_id: String,
2404        action: ShellCallAction,
2405        #[serde(default, skip_serializing_if = "Option::is_none")]
2406        environment: Option<ResponseShellCallEnvironment>,
2407        status: ShellCallStatus,
2408        #[serde(default, skip_serializing_if = "Option::is_none")]
2409        created_by: Option<String>,
2410    },
2411    /// `type: "shell_call_output"` — output-side mirror of the input
2412    /// variant.
2413    ///
2414    /// Spec (openai-responses-api-spec.md §ShellCallOutput, L233-238) +
2415    /// OpenAI SDK v2.8.1 `ResponseFunctionShellToolCallOutput`:
2416    /// `{ call_id, output, type, id, max_output_length?, status, created_by? }`.
2417    /// Emitted when the platform surfaces captured stdout/stderr plus an
2418    /// [`ShellOutcome`] for a prior shell call.
2419    ///
2420    /// `id` and `status` are required per the SDK's non-`Optional` typing.
2421    /// `max_output_length` is `Optional[int]` in the SDK (the platform may
2422    /// emit shell outputs when the originating `shell_call.action` did not
2423    /// specify a cap) and `created_by` is `Optional[str]` — both dropped at
2424    /// serialize time when absent so downstream consumers do not see null
2425    /// placeholders.
2426    #[serde(rename = "shell_call_output")]
2427    ShellCallOutput {
2428        id: String,
2429        call_id: String,
2430        output: Vec<ShellOutputChunk>,
2431        #[serde(default, skip_serializing_if = "Option::is_none")]
2432        max_output_length: Option<u64>,
2433        status: ShellCallStatus,
2434        #[serde(default, skip_serializing_if = "Option::is_none")]
2435        created_by: Option<String>,
2436    },
2437    /// `type: "apply_patch_call"` — server-emitted mirror of the input
2438    /// variant. Spec (openai-responses-api-spec.md §ApplyPatchCall L240-L246):
2439    /// `{ call_id, operation, status, type, id }`. `id` is required on the
2440    /// output wire because the server always assigns one when emitting the
2441    /// apply_patch call item.
2442    #[serde(rename = "apply_patch_call")]
2443    ApplyPatchCall {
2444        id: String,
2445        call_id: String,
2446        operation: ApplyPatchOperation,
2447        status: ApplyPatchCallStatus,
2448    },
2449    /// `type: "apply_patch_call_output"` — server-emitted mirror of the
2450    /// input variant. Spec (openai-responses-api-spec.md
2451    /// §ApplyPatchCallOutput L248-L251): `{ call_id, status, type, id,
2452    /// output }`. `id` is required on the output wire; `output` is optional
2453    /// log text surfaced by the upstream apply_patch executor.
2454    #[serde(rename = "apply_patch_call_output")]
2455    ApplyPatchCallOutput {
2456        id: String,
2457        call_id: String,
2458        status: ApplyPatchCallOutputStatus,
2459        #[serde(default, skip_serializing_if = "Option::is_none")]
2460        output: Option<String>,
2461    },
2462    /// `type: "local_shell_call"` — output-side mirror of the input
2463    /// variant — emitted when the model issues a `local_shell` tool call.
2464    /// Spec (openai-responses-api-spec.md §LocalShellCall L219-222):
2465    /// `{ id, action, call_id, status, type }` with `action` as a
2466    /// [`LocalShellExec`] payload. See [`ResponseInputOutputItem::LocalShellCall`].
2467    #[serde(rename = "local_shell_call")]
2468    LocalShellCall {
2469        id: String,
2470        call_id: String,
2471        action: LocalShellExec,
2472        status: LocalShellCallStatus,
2473    },
2474    /// `type: "local_shell_call_output"` — output-side mirror of the
2475    /// input variant. Spec
2476    /// (openai-responses-api-spec.md §LocalShellCallOutput L224-226):
2477    /// `{ id, output, type, status }` with `status` optional per SDK
2478    /// v2.8.1. See [`ResponseInputOutputItem::LocalShellCallOutput`].
2479    #[serde(rename = "local_shell_call_output")]
2480    LocalShellCallOutput {
2481        id: String,
2482        output: String,
2483        #[serde(default, skip_serializing_if = "Option::is_none")]
2484        status: Option<LocalShellCallStatus>,
2485    },
2486}
2487
2488// ============================================================================
2489// Built-in Tool Call Types
2490// ============================================================================
2491
2492/// Status for web search tool calls.
2493#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, schemars::JsonSchema)]
2494#[serde(rename_all = "snake_case")]
2495pub enum WebSearchCallStatus {
2496    InProgress,
2497    Searching,
2498    Completed,
2499    Failed,
2500}
2501
2502/// Action performed during a web search.
2503#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
2504#[serde(tag = "type", rename_all = "snake_case")]
2505pub enum WebSearchAction {
2506    Search {
2507        #[serde(skip_serializing_if = "Option::is_none")]
2508        query: Option<String>,
2509        #[serde(default, skip_serializing_if = "Vec::is_empty")]
2510        queries: Vec<String>,
2511        #[serde(default, skip_serializing_if = "Vec::is_empty")]
2512        sources: Vec<WebSearchSource>,
2513    },
2514    OpenPage {
2515        url: String,
2516    },
2517    Find {
2518        url: String,
2519        pattern: String,
2520    },
2521}
2522
2523/// A source returned from web search.
2524#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
2525pub struct WebSearchSource {
2526    #[serde(rename = "type")]
2527    pub source_type: String,
2528    pub url: String,
2529}
2530
2531/// A single search result attached to a `WebSearchCall` when the caller
2532/// requested `web_search_call.results` via the top-level `include[]` array.
2533///
2534/// Optional fields mirror the `FileSearchResult` shape — only `url` is
2535/// guaranteed; titles, snippets, and scores ride along when the upstream
2536/// search backend supplies them.
2537#[serde_with::skip_serializing_none]
2538#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
2539pub struct WebSearchResult {
2540    /// Canonical URL of the result.
2541    pub url: String,
2542    /// Page or document title, when surfaced by the search backend.
2543    pub title: Option<String>,
2544    /// Short text snippet excerpted from the result.
2545    pub snippet: Option<String>,
2546    /// Relevance score in `[0, 1]`, when the backend supplies one.
2547    pub score: Option<f32>,
2548}
2549
2550/// Status for code interpreter tool calls.
2551#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, schemars::JsonSchema)]
2552#[serde(rename_all = "snake_case")]
2553pub enum CodeInterpreterCallStatus {
2554    InProgress,
2555    Completed,
2556    Incomplete,
2557    Interpreting,
2558    Failed,
2559}
2560
2561/// Output from code interpreter execution.
2562#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
2563#[serde(tag = "type", rename_all = "snake_case")]
2564pub enum CodeInterpreterOutput {
2565    Logs { logs: String },
2566    Image { url: String },
2567}
2568
2569/// Status for file search tool calls.
2570#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, schemars::JsonSchema)]
2571#[serde(rename_all = "snake_case")]
2572pub enum FileSearchCallStatus {
2573    InProgress,
2574    Searching,
2575    Completed,
2576    Incomplete,
2577    Failed,
2578}
2579
2580/// A result from file search.
2581#[serde_with::skip_serializing_none]
2582#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
2583pub struct FileSearchResult {
2584    pub file_id: String,
2585    pub filename: String,
2586    pub text: Option<String>,
2587    pub score: Option<f32>,
2588    pub attributes: Option<Value>,
2589}
2590
2591/// Status for `local_shell` tool calls.
2592///
2593/// Spec (openai-responses-api-spec.md §LocalShellCall L221): `"in_progress"
2594/// | "completed" | "incomplete"`.
2595#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, schemars::JsonSchema)]
2596#[serde(rename_all = "snake_case")]
2597pub enum LocalShellCallStatus {
2598    InProgress,
2599    Completed,
2600    Incomplete,
2601}
2602
2603/// `action` payload carried by a [`ResponseInputOutputItem::LocalShellCall`] /
2604/// [`ResponseOutputItem::LocalShellCall`] item.
2605///
2606/// Spec (openai-responses-api-spec.md §LocalShellCall L220):
2607/// `{ command: array of string, env: map[string], type: "exec",
2608///   timeout_ms?, user?, working_directory? }`. `env` is always present
2609/// (an empty object is semantically distinct from omitting the field),
2610/// matching the OpenAI Python SDK (`openai==2.8.1`,
2611/// `types/responses/response_input_item_param.py` `LocalShellCallAction`
2612/// — non-`Optional` `Dict[str, str]`).
2613#[serde_with::skip_serializing_none]
2614#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, schemars::JsonSchema)]
2615#[serde(tag = "type", rename_all = "snake_case")]
2616pub enum LocalShellExec {
2617    /// `type: "exec"` — the only action kind defined by the spec today.
2618    #[serde(rename = "exec")]
2619    Exec {
2620        /// Argv of the command to run on the host.
2621        command: Vec<String>,
2622        /// Environment variables overlaid on the host process env.
2623        /// Always serialized (possibly empty) to match SDK shape.
2624        env: std::collections::BTreeMap<String, String>,
2625        /// Hard timeout in milliseconds.
2626        #[serde(default, skip_serializing_if = "Option::is_none")]
2627        timeout_ms: Option<u64>,
2628        /// User to run the command as.
2629        #[serde(default, skip_serializing_if = "Option::is_none")]
2630        user: Option<String>,
2631        /// Working directory for the command.
2632        #[serde(default, skip_serializing_if = "Option::is_none")]
2633        working_directory: Option<String>,
2634    },
2635}
2636
2637// ============================================================================
2638// Configuration Enums
2639// ============================================================================
2640
2641#[derive(Debug, Clone, Deserialize, Serialize, Default, schemars::JsonSchema)]
2642#[serde(rename_all = "snake_case")]
2643#[schemars(rename = "ResponsesServiceTier")]
2644pub enum ServiceTier {
2645    #[default]
2646    Auto,
2647    Default,
2648    Flex,
2649    Scale,
2650    Priority,
2651}
2652
2653#[derive(Debug, Clone, Deserialize, Serialize, Default, schemars::JsonSchema)]
2654#[serde(rename_all = "snake_case")]
2655pub enum Truncation {
2656    Auto,
2657    #[default]
2658    Disabled,
2659}
2660
2661#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, schemars::JsonSchema)]
2662#[serde(rename_all = "snake_case")]
2663#[non_exhaustive]
2664pub enum ResponseStatus {
2665    Queued,
2666    InProgress,
2667    Completed,
2668    Incomplete,
2669    Failed,
2670    Cancelled,
2671}
2672
2673/// Why a response stopped before producing complete output.
2674///
2675/// Mirrors OpenAI's `incomplete_details.reason`: reserved strictly for the two
2676/// truncation semantics. Any other stop condition (wall-clock timeout,
2677/// `max_tool_calls` exhaustion, provider errors) is surfaced as a `failed`
2678/// status with an `error` payload instead.
2679#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, schemars::JsonSchema)]
2680#[serde(rename_all = "snake_case")]
2681#[non_exhaustive]
2682pub enum IncompleteReason {
2683    /// Output was truncated because it hit `max_output_tokens`.
2684    MaxOutputTokens,
2685    /// Output was truncated by the content filter.
2686    ContentFilter,
2687}
2688
2689/// Structured detail attached to a response whose status is `incomplete`.
2690///
2691/// Wire shape: `{ "reason": "max_output_tokens" | "content_filter" }`.
2692#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, schemars::JsonSchema)]
2693pub struct IncompleteDetails {
2694    /// The reason the response is incomplete.
2695    pub reason: IncompleteReason,
2696}
2697
2698#[serde_with::skip_serializing_none]
2699#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
2700pub struct ReasoningInfo {
2701    pub effort: Option<String>,
2702    pub summary: Option<String>,
2703}
2704
2705// ============================================================================
2706// Text Format (structured outputs)
2707// ============================================================================
2708
2709/// Text configuration for structured output requests
2710#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
2711pub struct TextConfig {
2712    #[serde(skip_serializing_if = "Option::is_none")]
2713    pub format: Option<TextFormat>,
2714}
2715
2716/// Text format: text (default), json_object (legacy), or json_schema (recommended)
2717#[serde_with::skip_serializing_none]
2718#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
2719#[serde(tag = "type")]
2720pub enum TextFormat {
2721    #[serde(rename = "text")]
2722    Text,
2723
2724    #[serde(rename = "json_object")]
2725    JsonObject,
2726
2727    #[serde(rename = "json_schema")]
2728    JsonSchema {
2729        name: String,
2730        schema: Value,
2731        description: Option<String>,
2732        strict: Option<bool>,
2733    },
2734}
2735
2736#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, schemars::JsonSchema)]
2737#[serde(rename_all = "snake_case")]
2738pub enum IncludeField {
2739    #[serde(rename = "code_interpreter_call.outputs")]
2740    CodeInterpreterCallOutputs,
2741    #[serde(rename = "computer_call_output.output.image_url")]
2742    ComputerCallOutputImageUrl,
2743    #[serde(rename = "file_search_call.results")]
2744    FileSearchCallResults,
2745    #[serde(rename = "message.input_image.image_url")]
2746    MessageInputImageUrl,
2747    #[serde(rename = "message.output_text.logprobs")]
2748    MessageOutputTextLogprobs,
2749    #[serde(rename = "reasoning.encrypted_content")]
2750    ReasoningEncryptedContent,
2751    #[serde(rename = "web_search_call.action.sources")]
2752    WebSearchCallActionSources,
2753    #[serde(rename = "web_search_call.results")]
2754    WebSearchCallResults,
2755}
2756
2757// ============================================================================
2758// Usage Types (Responses API format)
2759// ============================================================================
2760
2761/// OpenAI Responses API usage format (different from standard UsageInfo)
2762#[serde_with::skip_serializing_none]
2763#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
2764pub struct ResponseUsage {
2765    pub input_tokens: u32,
2766    pub output_tokens: u32,
2767    pub total_tokens: u32,
2768    pub input_tokens_details: Option<InputTokensDetails>,
2769    pub output_tokens_details: Option<OutputTokensDetails>,
2770}
2771
2772#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
2773#[serde(untagged)]
2774pub enum ResponsesUsage {
2775    Classic(UsageInfo),
2776    Modern(ResponseUsage),
2777}
2778
2779#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
2780pub struct InputTokensDetails {
2781    pub cached_tokens: u32,
2782}
2783
2784impl From<&PromptTokenUsageInfo> for InputTokensDetails {
2785    fn from(d: &PromptTokenUsageInfo) -> Self {
2786        Self {
2787            cached_tokens: d.cached_tokens,
2788        }
2789    }
2790}
2791
2792#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
2793pub struct OutputTokensDetails {
2794    pub reasoning_tokens: u32,
2795}
2796
2797impl UsageInfo {
2798    /// Convert to OpenAI Responses API format
2799    pub fn to_response_usage(&self) -> ResponseUsage {
2800        ResponseUsage {
2801            input_tokens: self.prompt_tokens,
2802            output_tokens: self.completion_tokens,
2803            total_tokens: self.total_tokens,
2804            input_tokens_details: self
2805                .prompt_tokens_details
2806                .as_ref()
2807                .map(InputTokensDetails::from),
2808            output_tokens_details: self.reasoning_tokens.map(|tokens| OutputTokensDetails {
2809                reasoning_tokens: tokens,
2810            }),
2811        }
2812    }
2813}
2814
2815impl From<UsageInfo> for ResponseUsage {
2816    fn from(usage: UsageInfo) -> Self {
2817        usage.to_response_usage()
2818    }
2819}
2820
2821impl ResponseUsage {
2822    /// Convert back to standard UsageInfo format
2823    pub fn to_usage_info(&self) -> UsageInfo {
2824        UsageInfo {
2825            prompt_tokens: self.input_tokens,
2826            completion_tokens: self.output_tokens,
2827            total_tokens: self.total_tokens,
2828            reasoning_tokens: self
2829                .output_tokens_details
2830                .as_ref()
2831                .map(|details| details.reasoning_tokens),
2832            prompt_tokens_details: self.input_tokens_details.as_ref().map(|details| {
2833                PromptTokenUsageInfo {
2834                    cached_tokens: details.cached_tokens,
2835                }
2836            }),
2837        }
2838    }
2839}
2840
2841impl ResponsesUsage {
2842    pub fn to_response_usage(&self) -> ResponseUsage {
2843        match self {
2844            ResponsesUsage::Classic(usage) => usage.to_response_usage(),
2845            ResponsesUsage::Modern(usage) => usage.clone(),
2846        }
2847    }
2848
2849    pub fn to_usage_info(&self) -> UsageInfo {
2850        match self {
2851            ResponsesUsage::Classic(usage) => usage.clone(),
2852            ResponsesUsage::Modern(usage) => usage.to_usage_info(),
2853        }
2854    }
2855}
2856
2857// ============================================================================
2858// Helper Functions for Defaults
2859// ============================================================================
2860
2861fn default_top_k() -> i32 {
2862    -1
2863}
2864
2865fn default_repetition_penalty() -> f32 {
2866    1.0
2867}
2868
2869#[expect(
2870    clippy::unnecessary_wraps,
2871    reason = "serde default function must match field type Option<T>"
2872)]
2873fn default_temperature() -> Option<f32> {
2874    Some(1.0)
2875}
2876
2877// ============================================================================
2878// Request/Response Types
2879// ============================================================================
2880
2881#[derive(Debug, Clone, Deserialize, Serialize, Validate, schemars::JsonSchema)]
2882#[validate(schema(function = "validate_responses_cross_parameters"))]
2883pub struct ResponsesRequest {
2884    /// Fields to include in the response
2885    #[serde(skip_serializing_if = "Option::is_none")]
2886    pub include: Option<Vec<IncludeField>>,
2887
2888    /// Input content - can be string or structured items
2889    #[validate(custom(function = "validate_response_input"))]
2890    pub input: ResponseInput,
2891
2892    /// System instructions for the model
2893    #[serde(skip_serializing_if = "Option::is_none")]
2894    pub instructions: Option<String>,
2895
2896    /// Maximum number of output tokens
2897    #[serde(skip_serializing_if = "Option::is_none")]
2898    #[validate(range(min = 1))]
2899    pub max_output_tokens: Option<u32>,
2900
2901    /// Maximum number of tool calls
2902    #[serde(skip_serializing_if = "Option::is_none")]
2903    #[validate(range(min = 1))]
2904    pub max_tool_calls: Option<u32>,
2905
2906    /// Additional metadata
2907    #[serde(skip_serializing_if = "Option::is_none")]
2908    pub metadata: Option<HashMap<String, Value>>,
2909
2910    /// Model to use
2911    pub model: String,
2912
2913    /// Optional conversation reference to persist input/output as items.
2914    ///
2915    /// Spec: `conversation` accepts either a bare ID string or
2916    /// `ResponseConversationParam { id }`. Both wire shapes deserialize into
2917    /// [`ConversationRef`]; downstream code reads the id via
2918    /// [`ConversationRef::as_id`].
2919    #[serde(skip_serializing_if = "Option::is_none")]
2920    #[validate(custom(function = "validate_conversation_id"))]
2921    pub conversation: Option<ConversationRef>,
2922
2923    /// Whether to enable parallel tool calls
2924    #[serde(skip_serializing_if = "Option::is_none")]
2925    pub parallel_tool_calls: Option<bool>,
2926
2927    /// ID of previous response to continue from
2928    #[serde(skip_serializing_if = "Option::is_none")]
2929    pub previous_response_id: Option<String>,
2930
2931    /// Reasoning configuration
2932    #[serde(skip_serializing_if = "Option::is_none")]
2933    pub reasoning: Option<ResponseReasoningParam>,
2934
2935    /// Service tier
2936    #[serde(skip_serializing_if = "Option::is_none")]
2937    pub service_tier: Option<ServiceTier>,
2938
2939    /// Whether to store the response
2940    #[serde(skip_serializing_if = "Option::is_none")]
2941    pub store: Option<bool>,
2942
2943    /// Whether to stream the response
2944    #[serde(default, skip_serializing_if = "Option::is_none")]
2945    pub stream: Option<bool>,
2946
2947    /// Temperature for sampling
2948    #[serde(
2949        default = "default_temperature",
2950        skip_serializing_if = "Option::is_none"
2951    )]
2952    #[validate(range(min = 0.0, max = 2.0))]
2953    pub temperature: Option<f32>,
2954
2955    /// Tool choice behavior (Responses-spec enum — see `ResponsesToolChoice`).
2956    #[serde(skip_serializing_if = "Option::is_none")]
2957    pub tool_choice: Option<ResponsesToolChoice>,
2958
2959    /// Available tools
2960    #[serde(skip_serializing_if = "Option::is_none")]
2961    #[validate(custom(function = "validate_response_tools"))]
2962    pub tools: Option<Vec<ResponseTool>>,
2963
2964    /// Number of top logprobs to return
2965    #[serde(skip_serializing_if = "Option::is_none")]
2966    #[validate(range(min = 0, max = 20))]
2967    pub top_logprobs: Option<u32>,
2968
2969    /// Top-p sampling parameter
2970    #[serde(skip_serializing_if = "Option::is_none")]
2971    #[validate(custom(function = "validate_top_p_value"))]
2972    pub top_p: Option<f32>,
2973
2974    /// Truncation behavior
2975    #[serde(skip_serializing_if = "Option::is_none")]
2976    pub truncation: Option<Truncation>,
2977
2978    /// Text format for structured outputs (text, json_object, json_schema)
2979    #[serde(skip_serializing_if = "Option::is_none")]
2980    #[validate(custom(function = "validate_text_format"))]
2981    pub text: Option<TextConfig>,
2982
2983    /// User identifier
2984    #[serde(skip_serializing_if = "Option::is_none")]
2985    pub user: Option<String>,
2986
2987    /// Request ID
2988    #[serde(skip_serializing_if = "Option::is_none")]
2989    pub request_id: Option<String>,
2990
2991    /// Request priority
2992    #[serde(default)]
2993    pub priority: i32,
2994
2995    /// Frequency penalty
2996    #[serde(skip_serializing_if = "Option::is_none")]
2997    #[validate(range(min = -2.0, max = 2.0))]
2998    pub frequency_penalty: Option<f32>,
2999
3000    /// Presence penalty
3001    #[serde(skip_serializing_if = "Option::is_none")]
3002    #[validate(range(min = -2.0, max = 2.0))]
3003    pub presence_penalty: Option<f32>,
3004
3005    /// Stop sequences
3006    #[serde(skip_serializing_if = "Option::is_none")]
3007    #[validate(custom(function = "validate_stop"))]
3008    pub stop: Option<StringOrArray>,
3009
3010    /// Reference to a prompt template and its variables.
3011    /// Spec: body param `prompt` (ResponsePrompt).
3012    #[serde(skip_serializing_if = "Option::is_none")]
3013    pub prompt: Option<ResponsePrompt>,
3014
3015    /// Stable cache key used by upstream to share prompt-prefix caches across
3016    /// requests. Spec: body param `prompt_cache_key` (replaces `user`).
3017    #[serde(skip_serializing_if = "Option::is_none")]
3018    pub prompt_cache_key: Option<String>,
3019
3020    /// Retention policy for prompt-cache entries.
3021    /// Spec: body param `prompt_cache_retention` (`"in-memory"` | `"24h"`).
3022    #[serde(skip_serializing_if = "Option::is_none")]
3023    pub prompt_cache_retention: Option<PromptCacheRetention>,
3024
3025    /// Stable user identifier for policy/abuse detection (max 64 chars on the
3026    /// spec, but we do not enforce length here — routers may pass through).
3027    /// Spec: body param `safety_identifier` (replaces `user` on request side).
3028    #[serde(skip_serializing_if = "Option::is_none")]
3029    pub safety_identifier: Option<String>,
3030
3031    /// Streaming-only options. Spec: body param `stream_options`.
3032    /// On the Responses API the only documented field is `include_obfuscation`.
3033    #[serde(skip_serializing_if = "Option::is_none")]
3034    pub stream_options: Option<StreamOptions>,
3035
3036    /// Per-request context-management configuration.
3037    /// Spec: body param `context_management` — array of entries describing how
3038    /// the upstream should compact context for this request.
3039    #[serde(skip_serializing_if = "Option::is_none")]
3040    pub context_management: Option<Vec<ContextManagementEntry>>,
3041
3042    /// Top-k sampling parameter (SGLang extension)
3043    #[serde(default = "default_top_k")]
3044    #[validate(custom(function = "validate_top_k_value"))]
3045    pub top_k: i32,
3046
3047    /// Min-p sampling parameter (SGLang extension)
3048    #[serde(default)]
3049    #[validate(range(min = 0.0, max = 1.0))]
3050    pub min_p: f32,
3051
3052    /// Repetition penalty (SGLang extension)
3053    #[serde(default = "default_repetition_penalty")]
3054    #[validate(range(min = 0.0, max = 2.0))]
3055    pub repetition_penalty: f32,
3056}
3057
3058#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
3059#[serde(untagged)]
3060pub enum ResponseInput {
3061    Items(Vec<ResponseInputOutputItem>),
3062    Text(String),
3063}
3064
3065impl Default for ResponsesRequest {
3066    fn default() -> Self {
3067        Self {
3068            include: None,
3069            input: ResponseInput::Text(String::new()),
3070            instructions: None,
3071            max_output_tokens: None,
3072            max_tool_calls: None,
3073            metadata: None,
3074            model: String::new(),
3075            conversation: None,
3076            parallel_tool_calls: None,
3077            previous_response_id: None,
3078            reasoning: None,
3079            service_tier: None,
3080            store: None,
3081            stream: None,
3082            temperature: None,
3083            tool_choice: None,
3084            tools: None,
3085            top_logprobs: None,
3086            top_p: None,
3087            truncation: None,
3088            text: None,
3089            user: None,
3090            request_id: None,
3091            priority: 0,
3092            frequency_penalty: None,
3093            presence_penalty: None,
3094            stop: None,
3095            prompt: None,
3096            prompt_cache_key: None,
3097            prompt_cache_retention: None,
3098            safety_identifier: None,
3099            stream_options: None,
3100            context_management: None,
3101            top_k: default_top_k(),
3102            min_p: 0.0,
3103            repetition_penalty: default_repetition_penalty(),
3104        }
3105    }
3106}
3107
3108impl Normalizable for ResponsesRequest {
3109    /// Normalize the request by applying defaults:
3110    /// 1. Apply tool_choice defaults based on tools presence
3111    /// 2. Apply parallel_tool_calls defaults
3112    /// 3. Apply store field defaults
3113    fn normalize(&mut self) {
3114        // 1. Apply tool_choice defaults
3115        if self.tool_choice.is_none() {
3116            if let Some(tools) = &self.tools {
3117                let choice_value = if tools.is_empty() {
3118                    ToolChoiceOptions::None
3119                } else {
3120                    ToolChoiceOptions::Auto
3121                };
3122                self.tool_choice = Some(ResponsesToolChoice::Options(choice_value));
3123            }
3124            // If tools is None, leave tool_choice as None (don't set it)
3125        }
3126
3127        // 2. Apply default for parallel_tool_calls if tools are present
3128        if self.parallel_tool_calls.is_none() && self.tools.is_some() {
3129            self.parallel_tool_calls = Some(true);
3130        }
3131
3132        // 3. Ensure store defaults to true if not specified
3133        if self.store.is_none() {
3134            self.store = Some(true);
3135        }
3136    }
3137}
3138
3139impl GenerationRequest for ResponsesRequest {
3140    fn is_stream(&self) -> bool {
3141        self.stream.unwrap_or(false)
3142    }
3143
3144    fn get_model(&self) -> Option<&str> {
3145        Some(self.model.as_str())
3146    }
3147
3148    fn extract_text_for_routing(&self) -> String {
3149        match &self.input {
3150            ResponseInput::Text(text) => text.clone(),
3151            ResponseInput::Items(items) => {
3152                let mut result = String::with_capacity(256);
3153                let mut has_parts = false;
3154
3155                let mut append_text = |text: &str| {
3156                    if has_parts {
3157                        result.push(' ');
3158                    }
3159                    has_parts = true;
3160                    result.push_str(text);
3161                };
3162
3163                for item in items {
3164                    match item {
3165                        ResponseInputOutputItem::Message { content, .. } => {
3166                            for part in content {
3167                                let text = match part {
3168                                    ResponseContentPart::OutputText { text, .. } => {
3169                                        Some(text.as_str())
3170                                    }
3171                                    ResponseContentPart::InputText { text } => Some(text.as_str()),
3172                                    // Non-text parts (images, files, refusals) contribute no
3173                                    // prompt text; skip without appending.
3174                                    ResponseContentPart::InputImage { .. }
3175                                    | ResponseContentPart::InputFile { .. }
3176                                    | ResponseContentPart::Refusal { .. } => None,
3177                                };
3178                                if let Some(t) = text {
3179                                    append_text(t);
3180                                }
3181                            }
3182                        }
3183                        ResponseInputOutputItem::SimpleInputMessage { content, .. } => {
3184                            match content {
3185                                StringOrContentParts::String(s) => {
3186                                    append_text(s.as_str());
3187                                }
3188                                StringOrContentParts::Array(parts) => {
3189                                    for part in parts {
3190                                        let text = match part {
3191                                            ResponseContentPart::OutputText { text, .. } => {
3192                                                Some(text.as_str())
3193                                            }
3194                                            ResponseContentPart::InputText { text } => {
3195                                                Some(text.as_str())
3196                                            }
3197                                            ResponseContentPart::InputImage { .. }
3198                                            | ResponseContentPart::InputFile { .. }
3199                                            | ResponseContentPart::Refusal { .. } => None,
3200                                        };
3201                                        if let Some(t) = text {
3202                                            append_text(t);
3203                                        }
3204                                    }
3205                                }
3206                            }
3207                        }
3208                        ResponseInputOutputItem::Reasoning { content, .. } => {
3209                            for part in content {
3210                                match part {
3211                                    ResponseReasoningContent::ReasoningText { text } => {
3212                                        append_text(text.as_str());
3213                                    }
3214                                }
3215                            }
3216                        }
3217                        ResponseInputOutputItem::FunctionToolCall { .. }
3218                        | ResponseInputOutputItem::FunctionCallOutput { .. }
3219                        | ResponseInputOutputItem::McpApprovalRequest { .. }
3220                        | ResponseInputOutputItem::McpApprovalResponse { .. }
3221                        | ResponseInputOutputItem::ImageGenerationCall { .. }
3222                        | ResponseInputOutputItem::Compaction { .. }
3223                        | ResponseInputOutputItem::ComputerCall { .. }
3224                        | ResponseInputOutputItem::ComputerCallOutput { .. }
3225                        | ResponseInputOutputItem::CustomToolCall { .. }
3226                        | ResponseInputOutputItem::CustomToolCallOutput { .. }
3227                        | ResponseInputOutputItem::ShellCall { .. }
3228                        | ResponseInputOutputItem::ShellCallOutput { .. }
3229                        | ResponseInputOutputItem::ItemReference { .. }
3230                        | ResponseInputOutputItem::ApplyPatchCall { .. }
3231                        | ResponseInputOutputItem::ApplyPatchCallOutput { .. }
3232                        | ResponseInputOutputItem::LocalShellCall { .. }
3233                        | ResponseInputOutputItem::LocalShellCallOutput { .. }
3234                        | ResponseInputOutputItem::McpCall { .. }
3235                        | ResponseInputOutputItem::McpListTools { .. } => {}
3236                    }
3237                }
3238
3239                result
3240            }
3241        }
3242    }
3243}
3244
3245/// Validate the conversation reference's ID format.
3246///
3247/// The validator crate auto-unwraps `Option<ConversationRef>` for the
3248/// `#[validate(custom(...))]` attribute, so this function only runs when
3249/// the field is present. Both wire shapes (bare string or `{ id }` object)
3250/// are validated against the same rule by extracting the underlying id via
3251/// [`ConversationRef::as_id`].
3252pub fn validate_conversation_id(conv: &ConversationRef) -> Result<(), ValidationError> {
3253    let conv_id = conv.as_id();
3254    if !conv_id.starts_with("conv_") {
3255        let mut error = ValidationError::new("invalid_conversation_id");
3256        error.message = Some(std::borrow::Cow::Owned(format!(
3257            "Invalid 'conversation': '{conv_id}'. Expected an ID that begins with 'conv_'."
3258        )));
3259        return Err(error);
3260    }
3261
3262    // Check if the conversation ID contains only valid characters
3263    let is_valid = conv_id
3264        .chars()
3265        .all(|c| c.is_alphanumeric() || c == '_' || c == '-');
3266
3267    if !is_valid {
3268        let mut error = ValidationError::new("invalid_conversation_id");
3269        error.message = Some(std::borrow::Cow::Owned(format!(
3270            "Invalid 'conversation': '{conv_id}'. Expected an ID that contains letters, numbers, underscores, or dashes, but this value contained additional characters."
3271        )));
3272        return Err(error);
3273    }
3274    Ok(())
3275}
3276
3277/// Validates tool_choice requires tools and references exist
3278fn validate_tool_choice_with_tools(request: &ResponsesRequest) -> Result<(), ValidationError> {
3279    let Some(tool_choice) = &request.tool_choice else {
3280        return Ok(());
3281    };
3282
3283    let has_tools = request.tools.as_ref().is_some_and(|t| !t.is_empty());
3284    let is_some_choice = !matches!(
3285        tool_choice,
3286        ResponsesToolChoice::Options(ToolChoiceOptions::None)
3287    );
3288
3289    // Check if tool_choice requires tools but none are provided
3290    if is_some_choice && !has_tools {
3291        let mut e = ValidationError::new("tool_choice_requires_tools");
3292        e.message = Some("Invalid value for 'tool_choice': 'tool_choice' is only allowed when 'tools' are specified.".into());
3293        return Err(e);
3294    }
3295
3296    // Validate tool references exist when tools are present
3297    if !has_tools {
3298        return Ok(());
3299    }
3300
3301    // Extract function tool names from ResponseTools
3302    // INVARIANT: has_tools is true here, so tools is Some and non-empty
3303    let Some(tools) = request.tools.as_ref() else {
3304        return Ok(());
3305    };
3306    let function_tool_names: Vec<&str> = tools
3307        .iter()
3308        .filter_map(|t| match t {
3309            ResponseTool::Function(ft) => Some(ft.function.name.as_str()),
3310            _ => None,
3311        })
3312        .collect();
3313
3314    // Validate tool references exist
3315    match tool_choice {
3316        ResponsesToolChoice::Function(_) => {
3317            // Accessor goes through `function_name()` so we stay agnostic to
3318            // the underlying wire shape (flat vs. legacy nested) — both are
3319            // normalized at deserialize time.
3320            if let Some(name) = tool_choice.function_name() {
3321                if !function_tool_names.contains(&name) {
3322                    let mut e = ValidationError::new("tool_choice_function_not_found");
3323                    e.message = Some(
3324                        format!(
3325                            "Invalid value for 'tool_choice': function '{name}' not found in 'tools'.",
3326                        )
3327                        .into(),
3328                    );
3329                    return Err(e);
3330                }
3331            }
3332        }
3333        ResponsesToolChoice::AllowedTools {
3334            mode,
3335            tools: allowed_tools,
3336            ..
3337        } => {
3338            // Validate mode is "auto" or "required"
3339            if mode != "auto" && mode != "required" {
3340                let mut e = ValidationError::new("tool_choice_invalid_mode");
3341                e.message = Some(
3342                    format!(
3343                        "Invalid value for 'tool_choice.mode': must be 'auto' or 'required', got '{mode}'."
3344                    )
3345                    .into(),
3346                );
3347                return Err(e);
3348            }
3349
3350            // Validate that all function tool references exist
3351            for tool_ref in allowed_tools {
3352                if let ToolReference::Function { name } = tool_ref {
3353                    if !function_tool_names.contains(&name.as_str()) {
3354                        let mut e = ValidationError::new("tool_choice_tool_not_found");
3355                        e.message = Some(
3356                            format!(
3357                                "Invalid value for 'tool_choice.tools': tool '{name}' not found in 'tools'."
3358                            )
3359                            .into(),
3360                        );
3361                        return Err(e);
3362                    }
3363                }
3364                // Note: MCP and hosted tools don't need existence validation here
3365                // as they are resolved dynamically at runtime
3366            }
3367        }
3368        // Remaining variants have no cross-field existence constraints —
3369        // hosted built-ins, MCP server selection, custom tool names, and
3370        // `apply_patch` / `shell` are resolved at routing time.
3371        ResponsesToolChoice::Options(_)
3372        | ResponsesToolChoice::Types { .. }
3373        | ResponsesToolChoice::Mcp { .. }
3374        | ResponsesToolChoice::Custom { .. }
3375        | ResponsesToolChoice::ApplyPatch { .. }
3376        | ResponsesToolChoice::Shell { .. } => {}
3377    }
3378
3379    Ok(())
3380}
3381
3382/// Schema-level validation for cross-field dependencies
3383fn validate_responses_cross_parameters(request: &ResponsesRequest) -> Result<(), ValidationError> {
3384    // 1. Validate tool_choice requires tools (enhanced)
3385    validate_tool_choice_with_tools(request)?;
3386
3387    // 2. Validate top_logprobs requires include field
3388    if request.top_logprobs.is_some() {
3389        let has_logprobs_include = request
3390            .include
3391            .as_ref()
3392            .is_some_and(|inc| inc.contains(&IncludeField::MessageOutputTextLogprobs));
3393
3394        if !has_logprobs_include {
3395            let mut e = ValidationError::new("top_logprobs_requires_include");
3396            e.message = Some(
3397                "top_logprobs requires include field with 'message.output_text.logprobs'".into(),
3398            );
3399            return Err(e);
3400        }
3401    }
3402
3403    // 3. Validate conversation and previous_response_id are mutually exclusive
3404    if request.conversation.is_some() && request.previous_response_id.is_some() {
3405        let mut e = ValidationError::new("mutually_exclusive_parameters");
3406        e.message = Some("Mutually exclusive parameters. Ensure you are only providing one of: 'previous_response_id' or 'conversation'.".into());
3407        return Err(e);
3408    }
3409
3410    // 4. Validate input items structure
3411    if let ResponseInput::Items(items) = &request.input {
3412        // Check for at least one valid input message
3413        let has_valid_input = items.iter().any(|item| {
3414            matches!(
3415                item,
3416                ResponseInputOutputItem::Message { .. }
3417                    | ResponseInputOutputItem::SimpleInputMessage { .. }
3418            )
3419        });
3420
3421        if !has_valid_input {
3422            let mut e = ValidationError::new("input_missing_user_message");
3423            e.message = Some("Input items must contain at least one message".into());
3424            return Err(e);
3425        }
3426    }
3427
3428    // 5. Validate text format conflicts (for future structured output constraints)
3429    // Currently, Responses API doesn't have regex/ebnf like Chat API,
3430    // but this is here for completeness and future-proofing
3431
3432    Ok(())
3433}
3434
3435// ============================================================================
3436// Field-Level Validation Functions
3437// ============================================================================
3438
3439/// Validates response input is not empty and has valid content
3440fn validate_response_input(input: &ResponseInput) -> Result<(), ValidationError> {
3441    match input {
3442        ResponseInput::Text(text) => {
3443            if text.is_empty() {
3444                let mut e = ValidationError::new("input_text_empty");
3445                e.message = Some("Input text cannot be empty".into());
3446                return Err(e);
3447            }
3448        }
3449        ResponseInput::Items(items) => {
3450            if items.is_empty() {
3451                let mut e = ValidationError::new("input_items_empty");
3452                e.message = Some("Input items cannot be empty".into());
3453                return Err(e);
3454            }
3455            // Validate each item has valid content
3456            for item in items {
3457                validate_input_item(item)?;
3458            }
3459        }
3460    }
3461    Ok(())
3462}
3463
3464/// Validates individual input items have valid content
3465fn validate_input_item(item: &ResponseInputOutputItem) -> Result<(), ValidationError> {
3466    match item {
3467        ResponseInputOutputItem::Message { content, .. } => {
3468            if content.is_empty() {
3469                let mut e = ValidationError::new("message_content_empty");
3470                e.message = Some("Message content cannot be empty".into());
3471                return Err(e);
3472            }
3473        }
3474        ResponseInputOutputItem::SimpleInputMessage { content, .. } => match content {
3475            StringOrContentParts::String(s) if s.is_empty() => {
3476                let mut e = ValidationError::new("message_content_empty");
3477                e.message = Some("Message content cannot be empty".into());
3478                return Err(e);
3479            }
3480            StringOrContentParts::Array(parts) if parts.is_empty() => {
3481                let mut e = ValidationError::new("message_content_empty");
3482                e.message = Some("Message content parts cannot be empty".into());
3483                return Err(e);
3484            }
3485            _ => {}
3486        },
3487        ResponseInputOutputItem::Reasoning { .. } => {
3488            // Reasoning content can be empty - no validation needed
3489        }
3490        ResponseInputOutputItem::FunctionCallOutput { output, .. } => {
3491            if output.is_empty() {
3492                let mut e = ValidationError::new("function_output_empty");
3493                e.message = Some("Function call output cannot be empty".into());
3494                return Err(e);
3495            }
3496        }
3497        ResponseInputOutputItem::FunctionToolCall { .. } => {}
3498        ResponseInputOutputItem::McpApprovalRequest { .. } => {}
3499        ResponseInputOutputItem::McpApprovalResponse { .. } => {}
3500        ResponseInputOutputItem::ImageGenerationCall { .. } => {}
3501        ResponseInputOutputItem::Compaction { .. } => {}
3502        ResponseInputOutputItem::ComputerCall { .. } => {}
3503        ResponseInputOutputItem::ComputerCallOutput { .. } => {}
3504        // CustomToolCall is model-generated and echoed back on multi-turn
3505        // replay; matches the FunctionToolCall arm above with no content
3506        // validation so a parameterless custom tool with empty input can
3507        // round-trip cleanly.
3508        ResponseInputOutputItem::CustomToolCall { .. } => {}
3509        ResponseInputOutputItem::CustomToolCallOutput { output, .. } => match output {
3510            CustomToolCallOutputContent::Text(s) if s.is_empty() => {
3511                let mut e = ValidationError::new("custom_tool_call_output_empty");
3512                e.message = Some("Custom tool call output cannot be empty".into());
3513                return Err(e);
3514            }
3515            CustomToolCallOutputContent::Parts(parts) if parts.is_empty() => {
3516                let mut e = ValidationError::new("custom_tool_call_output_empty");
3517                e.message = Some("Custom tool call output parts cannot be empty".into());
3518                return Err(e);
3519            }
3520            _ => {}
3521        },
3522        // ShellCall is model-generated and echoed back on multi-turn replay;
3523        // mirrors FunctionToolCall above with no content validation so a
3524        // parameterless shell call can round-trip cleanly.
3525        ResponseInputOutputItem::ShellCall { .. } => {}
3526        ResponseInputOutputItem::ShellCallOutput { .. } => {
3527            // The router returns 501 for shell calls, so SMG never synthesises
3528            // a ShellCallOutput itself. Skip content validation here so
3529            // round-tripping a previously-recorded response (even with an
3530            // empty chunk list) stays lossless — the cross-turn replay
3531            // contract is the motivating use case for keeping this arm
3532            // content-agnostic.
3533        }
3534        // A bare reference to a prior item; no content to validate.
3535        ResponseInputOutputItem::ItemReference { .. } => {}
3536        // ApplyPatchCall is model-generated and echoed back on multi-turn
3537        // replay; matches the FunctionToolCall / CustomToolCall arms with no
3538        // diff/path validation — the operation payload is structurally
3539        // enforced by the `ApplyPatchOperation` enum, and accepting empty
3540        // diffs for `create_file` / `update_file` preserves round-trip
3541        // fidelity with items emitted by upstream providers.
3542        ResponseInputOutputItem::ApplyPatchCall { .. } => {}
3543        // ApplyPatchCallOutput.output is optional log text per spec
3544        // (openai-responses-api-spec.md §ApplyPatchCallOutput L251); an
3545        // absent or empty log is spec-legal (a clean `completed` with no
3546        // output, or a `failed` where the executor had nothing to log) so
3547        // no emptiness check applies here.
3548        ResponseInputOutputItem::ApplyPatchCallOutput { .. } => {}
3549        // Validation mirrors `ComputerCall` / `ImageGenerationCall` above
3550        // (no payload-level content checks).
3551        ResponseInputOutputItem::LocalShellCall { .. } => {}
3552        ResponseInputOutputItem::LocalShellCallOutput { .. } => {}
3553        // MCP call/list-tools input items replayed for stateless multi-turn.
3554        // Matches `McpApprovalRequest` above with no content validation so an
3555        // abridged or in-flight call (output / error absent) can round-trip
3556        // cleanly.
3557        ResponseInputOutputItem::McpCall { .. } => {}
3558        ResponseInputOutputItem::McpListTools { .. } => {}
3559    }
3560    Ok(())
3561}
3562
3563/// Validates ResponseTool structure based on tool type
3564fn validate_response_tools(tools: &[ResponseTool]) -> Result<(), ValidationError> {
3565    // MCP server_label must be present and unique (case-insensitive).
3566    let mut seen_mcp_labels: HashSet<String> = HashSet::new();
3567
3568    for (idx, tool) in tools.iter().enumerate() {
3569        if let ResponseTool::Mcp(mcp) = tool {
3570            let raw_label = mcp.server_label.as_str();
3571            if raw_label.is_empty() {
3572                let mut e = ValidationError::new("missing_required_parameter");
3573                e.message = Some(
3574                    format!("Missing required parameter: 'tools[{idx}].server_label'.").into(),
3575                );
3576                return Err(e);
3577            }
3578
3579            // OpenAI spec-compatible validation: require a non-empty label that starts with a
3580            // letter and contains only letters, digits, '-' and '_'.
3581            let valid = raw_label.starts_with(|c: char| c.is_ascii_alphabetic())
3582                && raw_label
3583                    .chars()
3584                    .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_');
3585            if !valid {
3586                let mut e = ValidationError::new("invalid_server_label");
3587                e.message = Some(
3588                    format!(
3589                        "Invalid input {raw_label}: 'server_label' must start with a letter and consist of only letters, digits, '-' and '_'"
3590                    )
3591                    .into(),
3592                );
3593                return Err(e);
3594            }
3595
3596            let normalized = raw_label.to_lowercase();
3597            if !seen_mcp_labels.insert(normalized) {
3598                let mut e = ValidationError::new("mcp_tool_duplicate_server_label");
3599                e.message = Some(
3600                    format!("Duplicate MCP server_label '{raw_label}' found in 'tools' parameter.")
3601                        .into(),
3602                );
3603                return Err(e);
3604            }
3605
3606            // One of `server_url` or `connector_id` is required, and the two
3607            // are mutually exclusive. Reject payloads that set both so
3608            // downstream target resolution is unambiguous.
3609            if mcp.server_url.is_some() && mcp.connector_id.is_some() {
3610                let mut e = ValidationError::new("mcp_tool_conflicting_targets");
3611                e.message = Some(
3612                    format!(
3613                        "MCP tool with server_label '{raw_label}' sets both 'server_url' and 'connector_id'; exactly one is required."
3614                    )
3615                    .into(),
3616                );
3617                return Err(e);
3618            }
3619        }
3620    }
3621    Ok(())
3622}
3623
3624/// Validates text format configuration (JSON schema name cannot be empty)
3625fn validate_text_format(text: &TextConfig) -> Result<(), ValidationError> {
3626    if let Some(TextFormat::JsonSchema { name, .. }) = &text.format {
3627        if name.is_empty() {
3628            let mut e = ValidationError::new("json_schema_name_empty");
3629            e.message = Some("JSON schema name cannot be empty".into());
3630            return Err(e);
3631        }
3632    }
3633    Ok(())
3634}
3635
3636/// Normalize a SimpleInputMessage to a proper Message item
3637///
3638/// This helper converts SimpleInputMessage (which can have flexible content)
3639/// into a fully-structured Message item with a generated ID, role, and content array.
3640///
3641/// SimpleInputMessage items are converted to Message items with IDs generated using
3642/// the centralized ID generation pattern with "msg_" prefix for consistency.
3643///
3644/// # Arguments
3645/// * `item` - The input item to normalize
3646///
3647/// # Returns
3648/// A normalized ResponseInputOutputItem (either Message if converted, or original if not SimpleInputMessage)
3649pub fn normalize_input_item(item: &ResponseInputOutputItem) -> ResponseInputOutputItem {
3650    match item {
3651        ResponseInputOutputItem::SimpleInputMessage {
3652            content,
3653            role,
3654            phase,
3655            ..
3656        } => {
3657            let content_vec = match content {
3658                StringOrContentParts::String(s) => {
3659                    vec![ResponseContentPart::InputText { text: s.clone() }]
3660                }
3661                StringOrContentParts::Array(parts) => parts.clone(),
3662            };
3663
3664            ResponseInputOutputItem::Message {
3665                id: generate_id("msg"),
3666                role: role.clone(),
3667                content: content_vec,
3668                status: Some("completed".to_string()),
3669                phase: *phase,
3670            }
3671        }
3672        _ => item.clone(),
3673    }
3674}
3675
3676pub fn generate_id(prefix: &str) -> String {
3677    use rand::Rng;
3678    let mut rng = rand::rng();
3679    // Generate exactly 50 hex characters (25 bytes) for the part after the underscore
3680    let mut bytes = [0u8; 25];
3681    rng.fill_bytes(&mut bytes);
3682    let hex_string: String = bytes.iter().map(|b| format!("{b:02x}")).collect();
3683    format!("{prefix}_{hex_string}")
3684}
3685
3686#[serde_with::skip_serializing_none]
3687#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
3688#[non_exhaustive]
3689pub struct ResponsesResponse {
3690    /// Response ID
3691    pub id: String,
3692
3693    /// Object type
3694    #[serde(default = "default_object_type")]
3695    pub object: String,
3696
3697    /// Creation timestamp (unix seconds)
3698    pub created_at: i64,
3699
3700    /// Completion timestamp (unix seconds). `None` until the response reaches
3701    /// a terminal state (`completed`, `incomplete`, `failed`, `cancelled`).
3702    #[serde(default)]
3703    pub completed_at: Option<i64>,
3704
3705    /// Whether the response was created in background mode.
3706    #[serde(default)]
3707    pub background: Option<bool>,
3708
3709    /// Conversation this response is linked to, if any.
3710    #[serde(default)]
3711    pub conversation: Option<String>,
3712
3713    /// Response status
3714    pub status: ResponseStatus,
3715
3716    /// Error information if status is failed
3717    pub error: Option<Value>,
3718
3719    /// Incomplete details if the response was truncated (`incomplete` status).
3720    pub incomplete_details: Option<IncompleteDetails>,
3721
3722    /// System instructions used
3723    pub instructions: Option<String>,
3724
3725    /// Max output tokens setting
3726    pub max_output_tokens: Option<u32>,
3727
3728    /// Model name
3729    pub model: String,
3730
3731    /// Output items
3732    #[serde(default)]
3733    pub output: Vec<ResponseOutputItem>,
3734
3735    /// Whether parallel tool calls are enabled
3736    #[serde(default = "default_true")]
3737    pub parallel_tool_calls: bool,
3738
3739    /// Previous response ID if this is a continuation
3740    pub previous_response_id: Option<String>,
3741
3742    /// Reasoning information
3743    pub reasoning: Option<ReasoningInfo>,
3744
3745    /// Whether the response is stored
3746    #[serde(default = "default_true")]
3747    pub store: bool,
3748
3749    /// Temperature setting used
3750    pub temperature: Option<f32>,
3751
3752    /// Text format settings
3753    pub text: Option<TextConfig>,
3754
3755    /// Tool choice setting
3756    #[serde(default = "default_tool_choice")]
3757    pub tool_choice: String,
3758
3759    /// Available tools
3760    #[serde(default)]
3761    pub tools: Vec<ResponseTool>,
3762
3763    /// Top-p setting used
3764    pub top_p: Option<f32>,
3765
3766    /// Truncation strategy used
3767    pub truncation: Option<String>,
3768
3769    /// Usage information
3770    pub usage: Option<ResponsesUsage>,
3771
3772    /// User identifier
3773    pub user: Option<String>,
3774
3775    /// Safety identifier for content moderation
3776    pub safety_identifier: Option<String>,
3777
3778    /// Additional metadata
3779    #[serde(default)]
3780    pub metadata: HashMap<String, Value>,
3781}
3782
3783fn default_object_type() -> String {
3784    "response".to_string()
3785}
3786
3787fn default_tool_choice() -> String {
3788    "auto".to_string()
3789}
3790
3791impl ResponsesResponse {
3792    /// Create a builder for constructing a ResponsesResponse
3793    pub fn builder(id: impl Into<String>, model: impl Into<String>) -> ResponsesResponseBuilder {
3794        ResponsesResponseBuilder::new(id, model)
3795    }
3796
3797    /// Check if the response is complete
3798    pub fn is_complete(&self) -> bool {
3799        matches!(self.status, ResponseStatus::Completed)
3800    }
3801
3802    /// Check if the response is in progress
3803    pub fn is_in_progress(&self) -> bool {
3804        matches!(self.status, ResponseStatus::InProgress)
3805    }
3806
3807    /// Check if the response failed
3808    pub fn is_failed(&self) -> bool {
3809        matches!(self.status, ResponseStatus::Failed)
3810    }
3811
3812    /// Check if the response terminated as incomplete (max_output_tokens / content_filter)
3813    pub fn is_incomplete(&self) -> bool {
3814        matches!(self.status, ResponseStatus::Incomplete)
3815    }
3816}
3817
3818impl ResponseInputOutputItem {
3819    /// Create a new reasoning input/output item.
3820    ///
3821    /// `encrypted_content` defaults to `None`; use
3822    /// [`Self::new_reasoning_encrypted`] when round-tripping gpt-5 /
3823    /// o-series encrypted reasoning.
3824    pub fn new_reasoning(
3825        id: String,
3826        summary: Vec<SummaryTextContent>,
3827        content: Vec<ResponseReasoningContent>,
3828        status: Option<String>,
3829    ) -> Self {
3830        Self::Reasoning {
3831            id,
3832            summary,
3833            content,
3834            encrypted_content: None,
3835            status,
3836        }
3837    }
3838
3839    /// Create a new reasoning input/output item carrying an encrypted
3840    /// reasoning payload. The `encrypted_content` must be the opaque
3841    /// ciphertext.
3842    pub fn new_reasoning_encrypted(
3843        id: String,
3844        summary: Vec<SummaryTextContent>,
3845        content: Vec<ResponseReasoningContent>,
3846        encrypted_content: String,
3847        status: Option<String>,
3848    ) -> Self {
3849        Self::Reasoning {
3850            id,
3851            summary,
3852            content,
3853            encrypted_content: Some(encrypted_content),
3854            status,
3855        }
3856    }
3857}
3858
3859impl ResponseOutputItem {
3860    /// Create a new message output item (no phase).
3861    pub fn new_message(
3862        id: String,
3863        role: String,
3864        content: Vec<ResponseContentPart>,
3865        status: String,
3866    ) -> Self {
3867        Self::Message {
3868            id,
3869            role,
3870            content,
3871            status,
3872            phase: None,
3873        }
3874    }
3875
3876    /// Create a new reasoning output item.
3877    ///
3878    /// `encrypted_content` defaults to `None`; use
3879    /// [`Self::new_reasoning_encrypted`] when carrying gpt-5 / o-series
3880    /// encrypted reasoning.
3881    pub fn new_reasoning(
3882        id: String,
3883        summary: Vec<SummaryTextContent>,
3884        content: Vec<ResponseReasoningContent>,
3885        status: Option<String>,
3886    ) -> Self {
3887        Self::Reasoning {
3888            id,
3889            summary,
3890            content,
3891            encrypted_content: None,
3892            status,
3893        }
3894    }
3895
3896    /// Create a new reasoning output item carrying an encrypted reasoning payload.
3897    ///
3898    /// The `encrypted_content` must be the opaque ciphertext; a `None` value
3899    /// would defeat the purpose of the `_encrypted` constructor — callers
3900    /// without ciphertext should use [`Self::new_reasoning`] instead.
3901    pub fn new_reasoning_encrypted(
3902        id: String,
3903        summary: Vec<SummaryTextContent>,
3904        content: Vec<ResponseReasoningContent>,
3905        encrypted_content: String,
3906        status: Option<String>,
3907    ) -> Self {
3908        Self::Reasoning {
3909            id,
3910            summary,
3911            content,
3912            encrypted_content: Some(encrypted_content),
3913            status,
3914        }
3915    }
3916
3917    /// Create a new function tool call output item
3918    pub fn new_function_tool_call(
3919        id: String,
3920        call_id: String,
3921        name: String,
3922        arguments: String,
3923        output: Option<String>,
3924        status: String,
3925    ) -> Self {
3926        Self::FunctionToolCall {
3927            id: Some(id),
3928            call_id,
3929            name,
3930            arguments,
3931            output,
3932            status,
3933        }
3934    }
3935}
3936
3937impl ResponseContentPart {
3938    /// Create a new `output_text` content part.
3939    pub fn new_text(
3940        text: String,
3941        annotations: Vec<Annotation>,
3942        logprobs: Option<ChatLogProbs>,
3943    ) -> Self {
3944        Self::OutputText {
3945            text,
3946            annotations,
3947            logprobs,
3948        }
3949    }
3950}
3951
3952impl ResponseReasoningContent {
3953    /// Create a new reasoning text content
3954    pub fn new_reasoning_text(text: String) -> Self {
3955        Self::ReasoningText { text }
3956    }
3957}
3958
3959#[cfg(test)]
3960mod tests {
3961    use super::*;
3962
3963    /// Lock `as_str()` to the canonical serde tag for the unit variants
3964    /// — drift between the two would produce inconsistent wire labels
3965    /// across dispatch paths and serializers.
3966    #[test]
3967    fn response_tool_as_str_matches_serde_tag_for_unit_variants() {
3968        for tool in [
3969            ResponseTool::Computer,
3970            ResponseTool::ApplyPatch,
3971            ResponseTool::LocalShell,
3972        ] {
3973            let serialized = serde_json::to_value(&tool).unwrap();
3974            let serde_tag = serialized.get("type").and_then(|v| v.as_str()).unwrap();
3975            assert_eq!(tool.as_str(), serde_tag);
3976        }
3977    }
3978}