Skip to main content

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}