locode_provider/openai/responses/wire.rs
1//! Serde DTOs for `POST /v1/responses` (Task-18 plan §3.3).
2//!
3//! Deliberately **`Value`-forward**: `input`/`tools`/`output` are raw JSON
4//! values built/peeked by `build`/`parse`. This is the plan's opaque-first
5//! philosophy taken one step further than its enum sketch — reasoning items
6//! must round-trip fields we have never heard of (probes found
7//! `format: "openai-responses-v1"` / `"xai-responses-v1"`), and gateways add
8//! response fields freely, so the typed surface is kept to the envelope where
9//! it pays (top-level request knobs, usage, status) and stays out of the way
10//! where it would rot (item internals). Hand-rolled, not `async_openai` — grok
11//! has to monkey-patch that crate's serialization (plan §5.2's cautionary tale).
12
13use serde::{Deserialize, Serialize};
14use serde_json::Value;
15
16/// The request body (stateless always: `store:false`, no `previous_response_id`).
17#[derive(Debug, Clone, Serialize)]
18pub struct ResponsesRequest {
19 /// The model id.
20 pub model: String,
21 /// The hoisted System prompt (codex's placement; `None` under
22 /// `SystemPlacement::InputMessage`).
23 #[serde(skip_serializing_if = "Option::is_none")]
24 pub instructions: Option<String>,
25 /// The input item list (messages, calls, outputs, replayed reasoning).
26 pub input: Vec<Value>,
27 /// Tool definitions (flat `function` shape / `custom` with grammar).
28 #[serde(skip_serializing_if = "Option::is_none")]
29 pub tools: Option<Vec<Value>>,
30 /// Reasoning controls (`{effort, summary}`).
31 #[serde(skip_serializing_if = "Option::is_none")]
32 pub reasoning: Option<Value>,
33 /// ALWAYS `false` — the stateless non-negotiable (plan §4.1): grok forces
34 /// it for ZDR, codex sends false for OpenAI, OpenRouter 400s `true`.
35 pub store: bool,
36 /// ALWAYS `false` in v0 (non-streaming).
37 pub stream: bool,
38 /// ALWAYS `["reasoning.encrypted_content"]` — returned by default since
39 /// the 2026 API change, sent anyway to guard older gateways.
40 pub include: Vec<String>,
41 /// Output-token cap (includes reasoning tokens; spec minimum 16).
42 #[serde(skip_serializing_if = "Option::is_none")]
43 pub max_output_tokens: Option<u32>,
44 /// Sampling temperature (no thinking-omit rule on this wire).
45 #[serde(skip_serializing_if = "Option::is_none")]
46 pub temperature: Option<f32>,
47 /// Nucleus-sampling probability mass.
48 #[serde(skip_serializing_if = "Option::is_none")]
49 pub top_p: Option<f32>,
50 /// Cache-routing hint — the facade passes the session id (codex's rule).
51 #[serde(skip_serializing_if = "Option::is_none")]
52 pub prompt_cache_key: Option<String>,
53 /// OpenRouter routing preferences (never sent to other backends).
54 #[serde(skip_serializing_if = "Option::is_none")]
55 pub provider: Option<Value>,
56}
57
58/// The non-streaming response envelope. Unknown fields tolerated everywhere
59/// (OpenRouter appends `cost`/`is_byok`/…; the spec adds fields freely).
60#[derive(Debug, Clone, Deserialize)]
61pub struct ResponsesResponse {
62 /// `completed` | `incomplete` | `failed` | …
63 #[serde(default)]
64 pub status: Option<String>,
65 /// The output items, kept raw — `parse` peeks each item's `type`.
66 #[serde(default)]
67 pub output: Vec<Value>,
68 /// Why an `incomplete` response stopped (`{reason}`).
69 #[serde(default)]
70 pub incomplete_details: Option<IncompleteDetails>,
71 /// The error payload of a `failed` response.
72 #[serde(default)]
73 pub error: Option<ResponseError>,
74 /// Token accounting (Option everything: gateway variance).
75 #[serde(default)]
76 pub usage: Option<ResponsesUsage>,
77}
78
79/// `incomplete_details` of an `incomplete` response.
80#[derive(Debug, Clone, Deserialize)]
81pub struct IncompleteDetails {
82 /// e.g. `"max_output_tokens"`, `"content_filter"`.
83 #[serde(default)]
84 pub reason: Option<String>,
85}
86
87/// The `error` object of a `failed` response (HTTP 200).
88#[derive(Debug, Clone, Deserialize)]
89pub struct ResponseError {
90 /// e.g. `"rate_limit_exceeded"`, `"server_error"`.
91 #[serde(default)]
92 pub code: Option<String>,
93 /// The human-readable message.
94 #[serde(default)]
95 pub message: String,
96}
97
98/// Usage block: every counter `Option` (the Task-12 null-usage lesson).
99#[derive(Debug, Clone, Default, Deserialize)]
100pub struct ResponsesUsage {
101 /// Input (prompt) tokens.
102 #[serde(default)]
103 pub input_tokens: Option<u64>,
104 /// Input-side detail (`cached_tokens`, `cache_write_tokens`).
105 #[serde(default)]
106 pub input_tokens_details: Option<InputTokensDetails>,
107 /// Output (completion) tokens, reasoning included.
108 #[serde(default)]
109 pub output_tokens: Option<u64>,
110 /// Output-side detail (`reasoning_tokens`).
111 #[serde(default)]
112 pub output_tokens_details: Option<OutputTokensDetails>,
113}
114
115/// `usage.input_tokens_details`.
116#[derive(Debug, Clone, Default, Deserialize)]
117pub struct InputTokensDetails {
118 /// Prompt-cache read tokens.
119 #[serde(default)]
120 pub cached_tokens: Option<u64>,
121 /// Prompt-cache write tokens (2026 spec addition; often absent).
122 #[serde(default)]
123 pub cache_write_tokens: Option<u64>,
124}
125
126/// `usage.output_tokens_details`.
127#[derive(Debug, Clone, Default, Deserialize)]
128pub struct OutputTokensDetails {
129 /// Reasoning/thinking tokens.
130 #[serde(default)]
131 pub reasoning_tokens: Option<u64>,
132}