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