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