locode_provider/anthropic/wire.rs
1//! Serde DTOs for the Anthropic Messages API (`POST /v1/messages`).
2//!
3//! Ported nearly verbatim from Grok Build's `xai-grok-sampling-types/src/messages.rs`
4//! (already correct and forward-compatible), with two deliberate deltas (plan §3.2):
5//!
6//! 1. [`ContentBlock::ToolResult`] carries **`is_error`** — our protocol has it
7//! (ADR-0013) and Anthropic honours it; grok's version dropped it.
8//! 2. [`MessagesRequest`] carries an optional OpenRouter **`provider`** preferences
9//! field (plan §9.2) — injected only when the backend is OpenRouter.
10//!
11//! These are pure wire shapes: no conversion logic lives here (that is
12//! [`build`](super::build) and `parse`). The streaming event types are
13//! ported present-but-unused for the deferred streaming path (plan §1).
14
15use serde::{Deserialize, Serialize};
16
17// ============================================================================
18// Request types
19// ============================================================================
20
21/// `POST /v1/messages` request body.
22#[derive(Debug, Clone, Default, Serialize, Deserialize)]
23pub struct MessagesRequest {
24 /// The model id (native `claude-…` or OpenRouter `anthropic/claude-…`).
25 pub model: String,
26 /// The conversation turns (user/assistant; system is top-level).
27 pub messages: Vec<Message>,
28 /// Hard output-token cap for this call.
29 pub max_tokens: u32,
30 /// Top-level system prompt (hoisted from `Role::System` messages).
31 #[serde(skip_serializing_if = "Option::is_none")]
32 pub system: Option<SystemParam>,
33 /// Tool definitions offered to the model.
34 #[serde(skip_serializing_if = "Option::is_none")]
35 pub tools: Option<Vec<ToolParam>>,
36 /// Tool-choice constraint (v0 leaves this to the default `auto`).
37 #[serde(skip_serializing_if = "Option::is_none")]
38 pub tool_choice: Option<ToolChoiceParam>,
39 /// Sampling temperature. **Omitted whenever thinking is enabled** (plan §4.3).
40 #[serde(skip_serializing_if = "Option::is_none")]
41 pub temperature: Option<f32>,
42 /// Nucleus-sampling probability mass.
43 #[serde(skip_serializing_if = "Option::is_none")]
44 pub top_p: Option<f32>,
45 /// Top-k sampling cutoff (wire-specific; unused by the neutral core).
46 #[serde(skip_serializing_if = "Option::is_none")]
47 pub top_k: Option<u32>,
48 /// Whether to stream the response (v0 always sends `false`).
49 #[serde(skip_serializing_if = "Option::is_none")]
50 pub stream: Option<bool>,
51 /// Custom stop sequences.
52 #[serde(skip_serializing_if = "Option::is_none")]
53 pub stop_sequences: Option<Vec<String>>,
54 /// Extended-thinking configuration.
55 #[serde(skip_serializing_if = "Option::is_none")]
56 pub thinking: Option<ThinkingConfig>,
57 /// Effort/format output configuration (effort-based reasoning encoding).
58 #[serde(skip_serializing_if = "Option::is_none")]
59 pub output_config: Option<OutputConfig>,
60 /// Request metadata.
61 #[serde(skip_serializing_if = "Option::is_none")]
62 pub metadata: Option<Metadata>,
63 /// OpenRouter provider-routing preferences (plan §9.2). **Not** part of the
64 /// Anthropic API — set only when [`ApiBackend::OpenRouter`](super::config::ApiBackend)
65 /// is active, where `require_parameters: true` prevents OpenRouter from routing
66 /// to a backend that silently drops `cache_control`/`thinking`.
67 #[serde(skip_serializing_if = "Option::is_none")]
68 pub provider: Option<serde_json::Value>,
69}
70
71/// Effort/format output configuration (grok `messages.rs`).
72#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct OutputConfig {
74 /// Reasoning effort string (`"low"`/`"medium"`/`"high"`), effort-beta gated.
75 #[serde(skip_serializing_if = "Option::is_none")]
76 pub effort: Option<String>,
77 /// Structured-output format (deferred with `--json-schema`).
78 #[serde(skip_serializing_if = "Option::is_none")]
79 pub format: Option<OutputFormat>,
80}
81
82/// Structured-output format selector.
83#[derive(Debug, Clone, Serialize, Deserialize)]
84#[serde(tag = "type", rename_all = "snake_case")]
85pub enum OutputFormat {
86 /// Constrain the final answer to a JSON schema.
87 JsonSchema {
88 /// The JSON schema to constrain to.
89 schema: serde_json::Value,
90 },
91}
92
93/// One conversation turn on the wire.
94#[derive(Debug, Clone, Serialize, Deserialize)]
95pub struct Message {
96 /// Who authored the turn.
97 pub role: MessageRole,
98 /// The turn's content.
99 pub content: MessageContent,
100}
101
102/// Wire message roles.
103///
104/// Grok's enum is `User | Assistant` only; we add `System` for the
105/// mid-conversation-system beta path (`DeveloperRendering::MidConversationSystemBeta`,
106/// plan §4.1) — never emitted under the portable default.
107#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
108#[serde(rename_all = "lowercase")]
109pub enum MessageRole {
110 /// A human / tool-result turn.
111 User,
112 /// A model turn.
113 Assistant,
114 /// A mid-conversation system message (beta-gated; see enum docs).
115 System,
116}
117
118/// Message content: a bare string or a block list.
119#[derive(Debug, Clone, Serialize, Deserialize)]
120#[serde(untagged)]
121pub enum MessageContent {
122 /// Plain-text shorthand.
123 Text(String),
124 /// Structured content blocks.
125 Blocks(Vec<ContentBlock>),
126}
127
128/// The top-level `system` parameter: a bare string or text blocks.
129///
130/// Grok's rule (ported): a single block *without* `cache_control` collapses to
131/// `Text`; otherwise use `Blocks` (plan §4.3).
132#[derive(Debug, Clone, Serialize, Deserialize)]
133#[serde(untagged)]
134pub enum SystemParam {
135 /// Plain-text shorthand.
136 Text(String),
137 /// Text blocks (each may carry a cache marker).
138 Blocks(Vec<TextBlock>),
139}
140
141/// A system text block.
142#[derive(Debug, Clone, Serialize, Deserialize)]
143pub struct TextBlock {
144 /// Always `"text"`.
145 #[serde(rename = "type")]
146 pub r#type: String,
147 /// The text content.
148 pub text: String,
149 /// Prompt-cache breakpoint marker.
150 #[serde(skip_serializing_if = "Option::is_none")]
151 pub cache_control: Option<CacheControl>,
152}
153
154/// A prompt-cache breakpoint (`{"type": "ephemeral"}`).
155#[derive(Debug, Clone, Serialize, Deserialize)]
156pub struct CacheControl {
157 /// Always `"ephemeral"`.
158 #[serde(rename = "type")]
159 pub r#type: String,
160}
161
162impl CacheControl {
163 /// The standard ephemeral marker.
164 #[must_use]
165 pub fn ephemeral() -> Self {
166 Self {
167 r#type: "ephemeral".to_string(),
168 }
169 }
170}
171
172/// Content blocks used in both requests and responses.
173#[derive(Debug, Clone, Serialize, Deserialize)]
174#[serde(tag = "type", rename_all = "snake_case")]
175pub enum ContentBlock {
176 /// Plain text.
177 Text {
178 /// The text content.
179 text: String,
180 /// Prompt-cache breakpoint marker (last-message placement, plan §4.3).
181 #[serde(skip_serializing_if = "Option::is_none")]
182 cache_control: Option<CacheControl>,
183 },
184 /// An image.
185 Image {
186 /// Where the image bytes come from.
187 source: ImageSource,
188 },
189 /// A tool call emitted by the assistant.
190 ToolUse {
191 /// Provider-assigned id — preserved **verbatim** (ADR-0007, plan §4.5).
192 id: String,
193 /// The tool's wire name.
194 name: String,
195 /// The tool arguments.
196 input: serde_json::Value,
197 },
198 /// A tool result, carried in a user turn.
199 ToolResult {
200 /// The id of the `tool_use` this answers.
201 tool_use_id: String,
202 /// The result payload.
203 content: ToolResultContent,
204 /// Whether the tool call failed. **Our delta over grok's structs** —
205 /// the protocol carries it (ADR-0013) and Anthropic honours it.
206 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
207 is_error: bool,
208 /// Prompt-cache breakpoint marker.
209 #[serde(skip_serializing_if = "Option::is_none")]
210 cache_control: Option<CacheControl>,
211 },
212 /// Assistant extended thinking. Replayed **verbatim with its signature** on
213 /// subsequent requests (plan §4.2) — Anthropic 400s a thinking + `tool_use`
214 /// turn whose signed thinking block is not echoed back.
215 Thinking {
216 /// The reasoning text.
217 thinking: String,
218 /// The opaque signature required for replay.
219 signature: String,
220 },
221 /// Encrypted assistant thinking, replayed verbatim like signed thinking.
222 /// **Delta over grok's structs**: observed live during the Task-12 smoke —
223 /// without this variant the whole response fails to deserialize.
224 RedactedThinking {
225 /// The encrypted payload.
226 data: String,
227 },
228}
229
230/// The source of an image block.
231#[derive(Debug, Clone, Serialize, Deserialize)]
232#[serde(tag = "type", rename_all = "snake_case")]
233pub enum ImageSource {
234 /// Inline base64-encoded bytes.
235 Base64 {
236 /// The MIME type, e.g. `image/png`.
237 media_type: String,
238 /// The base64-encoded image data.
239 data: String,
240 },
241 /// A URL the provider fetches.
242 Url {
243 /// The image URL.
244 url: String,
245 },
246}
247
248/// Tool-result payload: a bare string or nested blocks.
249#[derive(Debug, Clone, Serialize, Deserialize)]
250#[serde(untagged)]
251pub enum ToolResultContent {
252 /// Plain-text shorthand.
253 Text(String),
254 /// Structured content blocks (text/image).
255 Blocks(Vec<ContentBlock>),
256}
257
258/// A tool definition (Anthropic Messages format).
259#[derive(Debug, Clone, Serialize, Deserialize)]
260pub struct ToolParam {
261 /// The model-facing tool name.
262 pub name: String,
263 /// The tool description.
264 #[serde(skip_serializing_if = "Option::is_none")]
265 pub description: Option<String>,
266 /// The JSON schema for the tool's arguments (normalized, plan §9 spike).
267 pub input_schema: serde_json::Value,
268}
269
270/// Tool-choice constraint.
271#[derive(Debug, Clone, Serialize, Deserialize)]
272#[serde(tag = "type", rename_all = "snake_case")]
273pub enum ToolChoiceParam {
274 /// The model decides (the default).
275 Auto,
276 /// The model must call some tool.
277 Any,
278 /// The model must call the named tool.
279 Tool {
280 /// The required tool's name.
281 name: String,
282 },
283}
284
285/// How thinking content is displayed for adaptive-thinking models.
286#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
287#[serde(rename_all = "snake_case")]
288pub enum ThinkingDisplay {
289 /// Thinking content omitted from the response.
290 Omitted,
291 /// Thinking content returned as a summary.
292 Summarized,
293}
294
295/// Extended-thinking configuration (grok `messages.rs`: three modes).
296#[derive(Debug, Clone, Serialize, Deserialize)]
297#[serde(tag = "type", rename_all = "snake_case")]
298pub enum ThinkingConfig {
299 /// Explicit token budget (4.0–4.5 models; the v0 default encoding).
300 Enabled {
301 /// The thinking-token budget. May exceed `max_tokens` under the
302 /// interleaved-thinking beta (plan §9.3).
303 budget_tokens: u32,
304 },
305 /// The API decides the budget (4.6+ models, effort encoding).
306 Adaptive {
307 /// Thinking display mode; omitted when `None` for back-compat.
308 #[serde(skip_serializing_if = "Option::is_none")]
309 display: Option<ThinkingDisplay>,
310 },
311 /// Thinking off.
312 Disabled,
313}
314
315/// Request metadata.
316#[derive(Debug, Clone, Serialize, Deserialize)]
317pub struct Metadata {
318 /// An opaque end-user id.
319 #[serde(skip_serializing_if = "Option::is_none")]
320 pub user_id: Option<String>,
321}
322
323// ============================================================================
324// Response types
325// ============================================================================
326
327/// Non-streaming response from `POST /v1/messages`.
328#[derive(Debug, Clone, Serialize, Deserialize)]
329pub struct MessagesResponse {
330 /// The server-assigned message id.
331 pub id: String,
332 /// Always `"message"`.
333 #[serde(rename = "type")]
334 pub r#type: String,
335 /// Always `"assistant"`.
336 pub role: String,
337 /// The assistant turn's content blocks, in order.
338 pub content: Vec<ContentBlock>,
339 /// The model that produced the response.
340 pub model: String,
341 /// Why generation stopped.
342 pub stop_reason: Option<StopReason>,
343 /// Token accounting.
344 pub usage: MessagesUsage,
345}
346
347/// Wire stop reasons.
348///
349/// The `Unknown` catch-all (grok `messages.rs:219-226`) means a new server-side
350/// value can never fail the parse; it preserves the wire string and **must stay
351/// the last variant** (serde tries the tagged variants first).
352#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
353#[serde(rename_all = "snake_case")]
354pub enum StopReason {
355 /// Natural end of turn.
356 EndTurn,
357 /// Hit the `max_tokens` cap.
358 MaxTokens,
359 /// The model wants to call tools.
360 ToolUse,
361 /// A stop sequence fired.
362 StopSequence,
363 /// The model refused to continue.
364 Refusal,
365 /// The turn was paused (server-tool round-trip).
366 PauseTurn,
367 /// The request exceeded the model's context window.
368 ModelContextWindowExceeded,
369 /// A stop reason this client does not model, carried verbatim.
370 #[serde(untagged)]
371 Unknown(String),
372}
373
374/// Token accounting from a Messages response.
375///
376/// Every field is `Option` + `default`: gateways translating other providers
377/// into the Messages shape return `null` (observed live: OpenRouter serving
378/// `openai/*` models sends `"cache_creation_input_tokens": null`), and
379/// `#[serde(default)]` alone covers *missing*, not `null` — a bare `u64`
380/// would fail the whole response parse.
381#[derive(Debug, Clone, Default, Serialize, Deserialize)]
382pub struct MessagesUsage {
383 /// Input (prompt) tokens.
384 #[serde(default)]
385 pub input_tokens: Option<u64>,
386 /// Output (completion) tokens.
387 #[serde(default)]
388 pub output_tokens: Option<u64>,
389 /// Tokens written to the prompt cache.
390 #[serde(default)]
391 pub cache_creation_input_tokens: Option<u64>,
392 /// Tokens served from the prompt cache.
393 #[serde(default)]
394 pub cache_read_input_tokens: Option<u64>,
395 /// Output-token breakdown, when the endpoint reports one.
396 #[serde(default)]
397 pub output_tokens_details: Option<OutputTokensDetails>,
398}
399
400/// The `output_tokens` breakdown (`usage.output_tokens_details`).
401///
402/// Optional throughout: not every endpoint sends it, and the field is absent on
403/// older responses — a missing breakdown must never fail the parse.
404#[derive(Debug, Clone, Default, Serialize, Deserialize)]
405pub struct OutputTokensDetails {
406 /// Tokens the model spent thinking.
407 #[serde(default)]
408 pub thinking_tokens: Option<u64>,
409}
410
411/// A structured error body (`{"type":"error","error":{"type":…,"message":…}}`).
412#[derive(Debug, Clone, Default, Serialize, Deserialize)]
413pub struct ErrorBody {
414 /// The inner error object.
415 #[serde(default)]
416 pub error: ErrorDetail,
417}
418
419/// The inner error object of an [`ErrorBody`].
420#[derive(Debug, Clone, Default, Serialize, Deserialize)]
421pub struct ErrorDetail {
422 /// The error type slug (e.g. `overloaded_error`, `invalid_request_error`).
423 #[serde(rename = "type", default)]
424 pub r#type: String,
425 /// The human-readable error message.
426 #[serde(default)]
427 pub message: String,
428}
429
430// ============================================================================
431// Streaming event types (present-but-unused: the deferred streaming path)
432// ============================================================================
433
434/// Top-level SSE streaming event.
435#[derive(Debug, Clone, Serialize, Deserialize)]
436#[serde(tag = "type", rename_all = "snake_case")]
437pub enum MessageStreamEvent {
438 /// Stream opened with the initial message skeleton.
439 MessageStart {
440 /// The initial (mostly empty) response shell.
441 message: MessagesResponse,
442 },
443 /// Terminal delta carrying the stop reason and final usage.
444 MessageDelta {
445 /// Stop reason + details.
446 delta: MessageDeltaBody,
447 /// Final usage counts.
448 usage: MessageDeltaUsage,
449 },
450 /// Stream closed.
451 MessageStop,
452 /// A content block opened.
453 ContentBlockStart {
454 /// The block's index in the content array.
455 index: u32,
456 /// The opening block shell.
457 content_block: ContentBlock,
458 },
459 /// A content block grew.
460 ContentBlockDelta {
461 /// The block's index in the content array.
462 index: u32,
463 /// The delta payload.
464 delta: StreamDelta,
465 },
466 /// A content block closed.
467 ContentBlockStop {
468 /// The block's index in the content array.
469 index: u32,
470 },
471 /// Keep-alive.
472 Ping,
473 /// A mid-stream error.
474 Error {
475 /// The error payload.
476 error: StreamError,
477 },
478}
479
480/// Body of a terminal `message_delta`.
481#[derive(Debug, Clone, Serialize, Deserialize)]
482pub struct MessageDeltaBody {
483 /// Why generation stopped.
484 pub stop_reason: Option<StopReason>,
485 /// Provider detail for the stop (e.g. a refusal explanation).
486 #[serde(default, skip_serializing_if = "Option::is_none")]
487 pub stop_details: Option<StopDetails>,
488}
489
490/// Detail for a terminal `message_delta`; all fields optional so an unknown
491/// shape never fails the terminal parse (grok `messages.rs:283-291`).
492#[derive(Debug, Clone, Default, Serialize, Deserialize)]
493pub struct StopDetails {
494 /// The detail type (e.g. `"refusal"`).
495 #[serde(rename = "type", default)]
496 pub r#type: Option<String>,
497 /// The refusal category.
498 #[serde(default)]
499 pub category: Option<String>,
500 /// The human-readable explanation.
501 #[serde(default)]
502 pub explanation: Option<String>,
503}
504
505/// Usage payload of a terminal `message_delta`.
506#[derive(Debug, Clone, Default, Serialize, Deserialize)]
507pub struct MessageDeltaUsage {
508 /// Output tokens so far.
509 pub output_tokens: u64,
510 /// Input tokens (present on the terminal delta).
511 #[serde(default)]
512 pub input_tokens: Option<u64>,
513 /// Cache-read tokens.
514 #[serde(default)]
515 pub cache_read_input_tokens: Option<u64>,
516 /// Cache-write tokens.
517 #[serde(default)]
518 pub cache_creation_input_tokens: Option<u64>,
519}
520
521/// Content delta within a `content_block_delta` event.
522#[derive(Debug, Clone, Serialize, Deserialize)]
523#[serde(tag = "type", rename_all = "snake_case")]
524pub enum StreamDelta {
525 /// Text grew.
526 TextDelta {
527 /// The appended text.
528 text: String,
529 },
530 /// Tool-call arguments grew (partial JSON; see `ToolCallAssembler`).
531 InputJsonDelta {
532 /// The appended raw JSON fragment.
533 partial_json: String,
534 },
535 /// Thinking text grew.
536 ThinkingDelta {
537 /// The appended thinking text.
538 thinking: String,
539 },
540 /// The thinking signature arrived.
541 SignatureDelta {
542 /// The signature fragment.
543 signature: String,
544 },
545}
546
547/// A mid-stream error payload.
548#[derive(Debug, Clone, Serialize, Deserialize)]
549pub struct StreamError {
550 /// The error type slug.
551 #[serde(rename = "type")]
552 pub r#type: String,
553 /// The human-readable message.
554 pub message: String,
555}