Skip to main content

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}