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