Skip to main content

dynamo_protocols/types/
anthropic.rs

1// SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Anthropic Messages API types.
5//!
6//! Pure protocol types for the `/v1/messages` endpoint -- request, response,
7//! streaming events, error shapes, and count-tokens types.
8
9use serde::{Deserialize, Serialize};
10
11/// Anthropic-style cache control hint for prefix pinning with TTL.
12#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)]
13pub struct CacheControl {
14    #[serde(rename = "type")]
15    pub control_type: CacheControlType,
16    /// TTL as seconds (integer) or shorthand ("5m" = 300s, "1h" = 3600s). Clamped to [300, 3600].
17    #[serde(default, skip_serializing_if = "Option::is_none")]
18    pub ttl: Option<String>,
19}
20
21#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)]
22#[serde(rename_all = "lowercase")]
23pub enum CacheControlType {
24    #[default]
25    Ephemeral,
26    #[serde(other)]
27    Unknown,
28}
29
30const MIN_TTL_SECONDS: u64 = 300;
31const MAX_TTL_SECONDS: u64 = 3600;
32
33impl CacheControl {
34    /// Parse TTL string to seconds, clamped to [300, 3600].
35    ///
36    /// Accepts integer seconds ("120", "600") or shorthand ("5m", "1h").
37    /// Values below 300 are clamped to 300; values above 3600 are clamped to 3600.
38    /// Unrecognized strings default to 300s.
39    pub fn ttl_seconds(&self) -> u64 {
40        let raw = match self.ttl.as_deref() {
41            None => return MIN_TTL_SECONDS,
42            Some("5m") => 300,
43            Some("1h") => 3600,
44            Some(other) => match other.parse::<u64>() {
45                Ok(secs) => secs,
46                Err(_) => {
47                    tracing::warn!("Unrecognized TTL '{}', defaulting to 300s", other);
48                    return MIN_TTL_SECONDS;
49                }
50            },
51        };
52        raw.clamp(MIN_TTL_SECONDS, MAX_TTL_SECONDS)
53    }
54}
55/// Parsed system prompt content, preserving cache_control from block arrays.
56#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct SystemContent {
58    /// The concatenated text from all system blocks (or the plain string).
59    pub text: String,
60    /// Cache control from the last system block that had one.
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub cache_control: Option<CacheControl>,
63}
64
65/// Deserialize `system` from either a plain string or an array of text blocks.
66/// The Anthropic API accepts both `"system": "text"` and
67/// `"system": [{"type": "text", "text": "...", "cache_control": {...}}]`.
68fn deserialize_system_prompt<'de, D>(deserializer: D) -> Result<Option<SystemContent>, D::Error>
69where
70    D: serde::Deserializer<'de>,
71{
72    #[derive(Deserialize)]
73    #[serde(untagged)]
74    enum SystemPrompt {
75        Text(String),
76        Blocks(Vec<SystemBlock>),
77    }
78
79    #[derive(Deserialize)]
80    struct SystemBlock {
81        text: String,
82        #[serde(default)]
83        cache_control: Option<CacheControl>,
84    }
85
86    let maybe: Option<SystemPrompt> = Option::deserialize(deserializer)?;
87    Ok(maybe.map(|sp| match sp {
88        SystemPrompt::Text(s) => SystemContent {
89            text: s,
90            cache_control: None,
91        },
92        SystemPrompt::Blocks(blocks) => {
93            let cache_control = blocks.iter().rev().find_map(|b| b.cache_control.clone());
94            let text = blocks
95                .into_iter()
96                .map(|b| b.text)
97                .collect::<Vec<_>>()
98                .join("\n");
99            SystemContent {
100                text,
101                cache_control,
102            }
103        }
104    }))
105}
106/// Top-level request body for `POST /v1/messages`.
107#[derive(Debug, Clone, Serialize, Deserialize)]
108pub struct AnthropicCreateMessageRequest {
109    /// The model to use (e.g. "claude-sonnet-4-20250514").
110    pub model: String,
111
112    /// The maximum number of tokens to generate.
113    pub max_tokens: u32,
114
115    /// The conversation messages.
116    pub messages: Vec<AnthropicMessage>,
117
118    /// Dynamo protocol extension envelope. Protocol parsing keeps this opaque;
119    /// LLM request handling owns extension validation and normalization.
120    #[serde(default, skip_serializing_if = "Option::is_none")]
121    pub nvext: Option<serde_json::Value>,
122
123    /// Optional system prompt (string or array of `{"type":"text","text":"..."}` blocks).
124    #[serde(
125        default,
126        skip_serializing_if = "Option::is_none",
127        deserialize_with = "deserialize_system_prompt"
128    )]
129    pub system: Option<SystemContent>,
130
131    /// Sampling temperature (0.0 - 1.0).
132    #[serde(skip_serializing_if = "Option::is_none")]
133    pub temperature: Option<f32>,
134
135    /// Nucleus sampling parameter.
136    #[serde(skip_serializing_if = "Option::is_none")]
137    pub top_p: Option<f32>,
138
139    /// Top-K sampling parameter.
140    #[serde(skip_serializing_if = "Option::is_none")]
141    pub top_k: Option<u32>,
142
143    /// Custom stop sequences.
144    #[serde(skip_serializing_if = "Option::is_none")]
145    pub stop_sequences: Option<Vec<String>>,
146
147    /// Whether to stream the response.
148    #[serde(default)]
149    pub stream: bool,
150
151    /// Optional metadata (e.g. user_id).
152    #[serde(skip_serializing_if = "Option::is_none")]
153    pub metadata: Option<serde_json::Value>,
154
155    /// Tools the model may call.
156    #[serde(skip_serializing_if = "Option::is_none")]
157    pub tools: Option<Vec<AnthropicTool>>,
158
159    /// How the model should choose which tool to call.
160    #[serde(skip_serializing_if = "Option::is_none")]
161    pub tool_choice: Option<AnthropicToolChoice>,
162
163    /// Top-level cache control for automatic prompt prefix caching.
164    /// When present, the system caches all content up to the last cacheable block.
165    /// Matches the Anthropic Messages API automatic caching mode.
166    /// See: https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching#automatic-caching
167    #[serde(default, skip_serializing_if = "Option::is_none")]
168    pub cache_control: Option<CacheControl>,
169
170    /// Extended thinking configuration. When enabled, the model produces
171    /// `thinking` content blocks containing its internal reasoning before
172    /// the final response. The `budget_tokens` field controls how many tokens
173    /// the model may use for thinking (must be >= 1024 and < max_tokens).
174    #[serde(default, skip_serializing_if = "Option::is_none")]
175    pub thinking: Option<ThinkingConfig>,
176
177    /// Service tier selection: `"auto"` or `"standard_only"`.
178    #[serde(default, skip_serializing_if = "Option::is_none")]
179    pub service_tier: Option<String>,
180
181    /// Container identifier for stateful sandbox sessions.
182    #[serde(default, skip_serializing_if = "Option::is_none")]
183    pub container: Option<String>,
184
185    /// Output configuration: effort level and optional JSON schema format.
186    /// `effort` can be `"low"`, `"medium"`, `"high"`, or `"max"`.
187    /// `format` specifies structured JSON output constraints.
188    #[serde(default, skip_serializing_if = "Option::is_none")]
189    pub output_config: Option<serde_json::Value>,
190}
191
192/// Extended thinking configuration for the request.
193///
194/// When `type` is `"enabled"`, the model will produce `thinking` content blocks
195/// with its internal reasoning. `budget_tokens` controls the maximum tokens
196/// available for thinking (minimum 1024, must be less than `max_tokens`).
197/// When `type` is `"disabled"`, no thinking blocks are produced.
198#[derive(Debug, Clone, Serialize, Deserialize)]
199pub struct ThinkingConfig {
200    /// Either `"enabled"` or `"disabled"`.
201    #[serde(rename = "type")]
202    pub thinking_type: String,
203    /// Maximum tokens for internal reasoning. Only relevant when type is "enabled".
204    #[serde(skip_serializing_if = "Option::is_none")]
205    pub budget_tokens: Option<u32>,
206}
207
208/// A single message in the conversation.
209#[derive(Debug, Clone, Serialize, Deserialize)]
210pub struct AnthropicMessage {
211    pub role: AnthropicRole,
212    #[serde(flatten)]
213    pub content: AnthropicMessageContent,
214}
215
216/// The role of a message sender.
217#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
218#[serde(rename_all = "lowercase")]
219pub enum AnthropicRole {
220    User,
221    Assistant,
222    /// Compatibility for clients that place system instructions in `messages[]`
223    /// instead of the top-level `system` field.
224    System,
225}
226
227/// Message content -- either a plain string or an array of content blocks.
228#[derive(Debug, Clone, Serialize, Deserialize)]
229#[serde(untagged)]
230pub enum AnthropicMessageContent {
231    /// Plain text content.
232    Text { content: String },
233    /// Array of structured content blocks.
234    Blocks { content: Vec<AnthropicContentBlock> },
235}
236
237/// A single content block within a message.
238///
239/// Uses a custom deserializer so that unknown block types (e.g. `citations`,
240/// `server_tool_use`, `redacted_thinking`) are captured as `Other(Value)` instead
241/// of causing a hard deserialization failure. This is important because Claude
242/// Code may send block types that we don't yet handle.
243#[derive(Debug, Clone, Serialize)]
244#[serde(tag = "type")]
245pub enum AnthropicContentBlock {
246    /// Text content block. May optionally include `citations` -- references to
247    /// source documents that support the text content. Citations are generated
248    /// by the model when document/PDF content is provided and citation mode is enabled.
249    #[serde(rename = "text")]
250    Text {
251        text: String,
252        #[serde(default, skip_serializing_if = "Option::is_none")]
253        citations: Option<Vec<serde_json::Value>>,
254        #[serde(default, skip_serializing_if = "Option::is_none")]
255        cache_control: Option<CacheControl>,
256    },
257    /// Image content block.
258    #[serde(rename = "image")]
259    Image { source: AnthropicImageSource },
260    /// Tool use request from assistant.
261    #[serde(rename = "tool_use")]
262    ToolUse {
263        id: String,
264        name: String,
265        input: serde_json::Value,
266        #[serde(default, skip_serializing_if = "Option::is_none")]
267        cache_control: Option<CacheControl>,
268    },
269    /// Tool result from user.
270    #[serde(rename = "tool_result")]
271    ToolResult {
272        tool_use_id: String,
273        #[serde(default, skip_serializing_if = "Option::is_none")]
274        content: Option<ToolResultContent>,
275        #[serde(skip_serializing_if = "Option::is_none")]
276        is_error: Option<bool>,
277        #[serde(default, skip_serializing_if = "Option::is_none")]
278        cache_control: Option<CacheControl>,
279    },
280    /// Thinking content block from assistant (extended thinking / reasoning).
281    #[serde(rename = "thinking")]
282    Thinking {
283        thinking: String,
284        signature: String,
285        #[serde(default, skip_serializing_if = "Option::is_none")]
286        cache_control: Option<CacheControl>,
287    },
288    /// Redacted thinking block from assistant. Contains encrypted reasoning data
289    /// that is opaque to the client but must be passed back verbatim in multi-turn
290    /// conversations so the model can maintain its chain of thought.
291    #[serde(rename = "redacted_thinking")]
292    RedactedThinking { data: String },
293    /// Server-initiated tool use block. Represents a tool call that the API
294    /// executes server-side (e.g., web search). The client receives the result
295    /// via a corresponding `web_search_tool_result` or similar block.
296    #[serde(rename = "server_tool_use")]
297    ServerToolUse {
298        id: String,
299        name: String,
300        #[serde(default)]
301        input: serde_json::Value,
302    },
303    /// Result from a server-initiated tool (e.g., web search results).
304    /// Contains structured content returned by the server-side tool execution.
305    #[serde(rename = "web_search_tool_result")]
306    WebSearchToolResult {
307        tool_use_id: String,
308        #[serde(default)]
309        content: serde_json::Value,
310    },
311    /// Catch-all for unrecognized block types. Preserves the full JSON value
312    /// so that new Anthropic features don't break the endpoint and can be
313    /// round-tripped or inspected.
314    #[serde(untagged)]
315    Other(serde_json::Value),
316}
317
318/// Content of a `tool_result` block -- either a plain string or an array of
319/// content blocks (the Anthropic API accepts both).
320#[derive(Debug, Clone, Serialize, Deserialize)]
321#[serde(untagged)]
322pub enum ToolResultContent {
323    Text(String),
324    Blocks(Vec<ToolResultContentBlock>),
325}
326
327impl ToolResultContent {
328    /// Extract the text content, concatenating array blocks if needed.
329    pub fn into_text(self) -> String {
330        match self {
331            ToolResultContent::Text(s) => s,
332            ToolResultContent::Blocks(blocks) => blocks
333                .into_iter()
334                .filter_map(|b| match b {
335                    ToolResultContentBlock::Text { text } => Some(text),
336                    ToolResultContentBlock::Image { .. } | ToolResultContentBlock::Other(_) => None,
337                })
338                .collect::<Vec<_>>()
339                .join(""),
340        }
341    }
342}
343
344/// A content block within a `tool_result.content` array.
345#[derive(Debug, Clone)]
346pub enum ToolResultContentBlock {
347    Text {
348        text: String,
349    },
350    /// Image returned by a tool.
351    Image {
352        source: AnthropicImageSource,
353    },
354    /// Catch-all for other non-text blocks in tool results.
355    Other(serde_json::Value),
356}
357
358impl Serialize for ToolResultContentBlock {
359    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
360    where
361        S: serde::Serializer,
362    {
363        match self {
364            Self::Text { text } => serde_json::json!({
365                "type": "text",
366                "text": text,
367            })
368            .serialize(serializer),
369            Self::Image { source } => serde_json::json!({
370                "type": "image",
371                "source": source,
372            })
373            .serialize(serializer),
374            Self::Other(value) => value.serialize(serializer),
375        }
376    }
377}
378
379fn take_field(value: &mut serde_json::Value, field: &str) -> Option<serde_json::Value> {
380    value.as_object_mut()?.remove(field)
381}
382
383fn take_required_string<E: serde::de::Error>(
384    value: &mut serde_json::Value,
385    field: &'static str,
386) -> Result<String, E> {
387    match take_field(value, field) {
388        Some(serde_json::Value::String(text)) => Ok(text),
389        _ => Err(E::missing_field(field)),
390    }
391}
392
393impl<'de> Deserialize<'de> for ToolResultContentBlock {
394    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
395    where
396        D: serde::Deserializer<'de>,
397    {
398        let mut value = serde_json::Value::deserialize(deserializer)?;
399        match value.get("type").and_then(|value| value.as_str()) {
400            Some("text") => {
401                let text = take_required_string::<D::Error>(&mut value, "text")?;
402                Ok(Self::Text { text })
403            }
404            Some("image") => {
405                let source = take_field(&mut value, "source")
406                    .ok_or_else(|| serde::de::Error::missing_field("source"))
407                    .and_then(|value| {
408                        serde_json::from_value(value).map_err(serde::de::Error::custom)
409                    })?;
410                Ok(Self::Image { source })
411            }
412            None if value.get("text").is_some_and(serde_json::Value::is_string) => Ok(Self::Text {
413                text: take_required_string::<D::Error>(&mut value, "text")?,
414            }),
415            _ => Ok(Self::Other(value)),
416        }
417    }
418}
419
420/// Custom deserializer for `AnthropicContentBlock` that handles unknown types
421/// gracefully. Since serde's `#[serde(other)]` is not supported on internally
422/// tagged enums, we deserialize as `Value` first and dispatch manually.
423impl<'de> Deserialize<'de> for AnthropicContentBlock {
424    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
425    where
426        D: serde::Deserializer<'de>,
427    {
428        let mut value = serde_json::Value::deserialize(deserializer)?;
429        let block_type = value.get("type").and_then(|t| t.as_str()).unwrap_or("");
430
431        match block_type {
432            "text" => {
433                let text = take_required_string::<D::Error>(&mut value, "text")?;
434                let citations: Option<Vec<serde_json::Value>> = take_field(&mut value, "citations")
435                    .and_then(|v| serde_json::from_value(v).ok());
436                let cache_control: Option<CacheControl> = take_field(&mut value, "cache_control")
437                    .and_then(|v| serde_json::from_value(v).ok());
438                Ok(AnthropicContentBlock::Text {
439                    text,
440                    citations,
441                    cache_control,
442                })
443            }
444            "image" => {
445                let source: AnthropicImageSource =
446                    serde_json::from_value(take_field(&mut value, "source").unwrap_or_default())
447                        .map_err(serde::de::Error::custom)?;
448                Ok(AnthropicContentBlock::Image { source })
449            }
450            "tool_use" => {
451                let id = take_required_string::<D::Error>(&mut value, "id")?;
452                let name = take_required_string::<D::Error>(&mut value, "name")?;
453                let input =
454                    take_field(&mut value, "input").unwrap_or_else(|| serde_json::json!({}));
455                let cache_control: Option<CacheControl> = take_field(&mut value, "cache_control")
456                    .and_then(|v| serde_json::from_value(v).ok());
457                Ok(AnthropicContentBlock::ToolUse {
458                    id,
459                    name,
460                    input,
461                    cache_control,
462                })
463            }
464            "tool_result" => {
465                let tool_use_id = take_required_string::<D::Error>(&mut value, "tool_use_id")?;
466                let content: Option<ToolResultContent> =
467                    take_field(&mut value, "content").and_then(|v| serde_json::from_value(v).ok());
468                let is_error = value.get("is_error").and_then(|v| v.as_bool());
469                let cache_control: Option<CacheControl> = take_field(&mut value, "cache_control")
470                    .and_then(|v| serde_json::from_value(v).ok());
471                Ok(AnthropicContentBlock::ToolResult {
472                    tool_use_id,
473                    content,
474                    is_error,
475                    cache_control,
476                })
477            }
478            "thinking" => {
479                let thinking = take_required_string::<D::Error>(&mut value, "thinking")?;
480                let signature = take_required_string::<D::Error>(&mut value, "signature")?;
481                let cache_control: Option<CacheControl> = take_field(&mut value, "cache_control")
482                    .and_then(|v| serde_json::from_value(v).ok());
483                Ok(AnthropicContentBlock::Thinking {
484                    thinking,
485                    signature,
486                    cache_control,
487                })
488            }
489            "redacted_thinking" => {
490                let data = take_required_string::<D::Error>(&mut value, "data")?;
491                Ok(AnthropicContentBlock::RedactedThinking { data })
492            }
493            "server_tool_use" => {
494                let id = take_required_string::<D::Error>(&mut value, "id")?;
495                let name = take_required_string::<D::Error>(&mut value, "name")?;
496                let input =
497                    take_field(&mut value, "input").unwrap_or_else(|| serde_json::json!({}));
498                Ok(AnthropicContentBlock::ServerToolUse { id, name, input })
499            }
500            "web_search_tool_result" => {
501                let tool_use_id = take_required_string::<D::Error>(&mut value, "tool_use_id")?;
502                let content =
503                    take_field(&mut value, "content").unwrap_or_else(|| serde_json::json!([]));
504                Ok(AnthropicContentBlock::WebSearchToolResult {
505                    tool_use_id,
506                    content,
507                })
508            }
509            other => {
510                tracing::debug!(
511                    "Unrecognized Anthropic content block type '{}', preserving as Other",
512                    other
513                );
514                Ok(AnthropicContentBlock::Other(value))
515            }
516        }
517    }
518}
519
520/// Image source for image content blocks.
521#[derive(Debug, Clone, Serialize, Deserialize)]
522pub struct AnthropicImageSource {
523    #[serde(rename = "type")]
524    pub source_type: String,
525    pub media_type: String,
526    pub data: String,
527}
528
529/// A tool definition.
530///
531/// Client tools (custom) require `name` + `input_schema`. Server tools
532/// (web_search, bash, text_editor, code_execution, etc.) are discriminated
533/// by their `type` field (e.g. `"web_search_20260209"`) and may not have
534/// `input_schema`. We keep all fields optional beyond `name` so both
535/// kinds deserialize successfully and pass through to the backend.
536#[derive(Debug, Clone, Serialize, Deserialize)]
537pub struct AnthropicTool {
538    /// Tool name (required for client tools, present on server tools too).
539    pub name: String,
540    /// Tool type discriminator. Client tools use `"custom"` (or omit).
541    /// Server tools use versioned types like `"web_search_20260209"`.
542    #[serde(default, rename = "type", skip_serializing_if = "Option::is_none")]
543    pub tool_type: Option<String>,
544    #[serde(skip_serializing_if = "Option::is_none")]
545    pub description: Option<String>,
546    /// JSON Schema for the tool input. Required for client tools, absent on
547    /// server tools (which define their own input shape server-side).
548    #[serde(default, skip_serializing_if = "Option::is_none")]
549    pub input_schema: Option<serde_json::Value>,
550    /// Whether tool arguments must conform to the input schema.
551    #[serde(default, skip_serializing_if = "Option::is_none")]
552    pub strict: Option<bool>,
553    /// Cache control breakpoint on this tool definition.
554    #[serde(default, skip_serializing_if = "Option::is_none")]
555    pub cache_control: Option<CacheControl>,
556}
557
558/// Tool choice specification.
559#[derive(Debug, Clone, Serialize, Deserialize)]
560#[serde(untagged)]
561pub enum AnthropicToolChoice {
562    /// Named tool: `{type: "tool", name: "..."}`
563    /// Must be listed before Simple so serde tries the stricter shape first.
564    Named(AnthropicToolChoiceNamed),
565    /// Simple mode: "auto", "any", or "none".
566    Simple(AnthropicToolChoiceSimple),
567}
568
569/// Simple tool choice modes.
570#[derive(Debug, Clone, Serialize, Deserialize)]
571pub struct AnthropicToolChoiceSimple {
572    #[serde(rename = "type")]
573    pub choice_type: AnthropicToolChoiceMode,
574    /// When true, the model will call tools one at a time instead of
575    /// potentially issuing multiple tool calls in a single response.
576    #[serde(default, skip_serializing_if = "Option::is_none")]
577    pub disable_parallel_tool_use: Option<bool>,
578}
579
580#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
581#[serde(rename_all = "lowercase")]
582pub enum AnthropicToolChoiceMode {
583    Auto,
584    Any,
585    None,
586    Tool,
587}
588
589/// Named tool choice.
590#[derive(Debug, Clone, Serialize, Deserialize)]
591pub struct AnthropicToolChoiceNamed {
592    #[serde(rename = "type")]
593    pub choice_type: AnthropicToolChoiceMode,
594    pub name: String,
595    /// When true, the model will call tools one at a time instead of
596    /// potentially issuing multiple tool calls in a single response.
597    #[serde(default, skip_serializing_if = "Option::is_none")]
598    pub disable_parallel_tool_use: Option<bool>,
599}
600/// Response body for `POST /v1/messages` (non-streaming).
601#[derive(Debug, Clone, Serialize, Deserialize)]
602pub struct AnthropicMessageResponse {
603    pub id: String,
604    #[serde(rename = "type")]
605    pub object_type: String,
606    pub role: String,
607    pub content: Vec<AnthropicResponseContentBlock>,
608    pub model: String,
609    pub stop_reason: Option<AnthropicStopReason>,
610    pub stop_sequence: Option<String>,
611    pub usage: AnthropicUsage,
612}
613
614/// A content block in the response.
615///
616/// The Anthropic API returns up to 12 different block types. We model the
617/// common ones explicitly and catch the rest as `Other` so the proxy can
618/// forward them without losing data.
619#[derive(Debug, Clone, Serialize, Deserialize)]
620#[serde(tag = "type")]
621pub enum AnthropicResponseContentBlock {
622    #[serde(rename = "thinking")]
623    Thinking { thinking: String, signature: String },
624    #[serde(rename = "text")]
625    Text {
626        text: String,
627        #[serde(default, skip_serializing_if = "Option::is_none")]
628        citations: Option<Vec<serde_json::Value>>,
629    },
630    #[serde(rename = "tool_use")]
631    ToolUse {
632        id: String,
633        name: String,
634        input: serde_json::Value,
635    },
636    #[serde(rename = "redacted_thinking")]
637    RedactedThinking { data: String },
638    #[serde(rename = "server_tool_use")]
639    ServerToolUse {
640        id: String,
641        name: String,
642        #[serde(default)]
643        input: serde_json::Value,
644    },
645    #[serde(rename = "web_search_tool_result")]
646    WebSearchToolResult {
647        tool_use_id: String,
648        #[serde(default)]
649        content: serde_json::Value,
650    },
651    /// Catch-all for new/uncommon block types (web_fetch_tool_result,
652    /// code_execution_tool_result, container_upload, etc.) so the proxy
653    /// can serialize them back without data loss.
654    #[serde(untagged)]
655    Other(serde_json::Value),
656}
657
658/// Token usage information.
659#[derive(Debug, Clone, Serialize, Deserialize, Default)]
660pub struct AnthropicUsage {
661    pub input_tokens: u32,
662    pub output_tokens: u32,
663    /// Number of input tokens used to create a new cache entry.
664    #[serde(default, skip_serializing_if = "Option::is_none")]
665    pub cache_creation_input_tokens: Option<u32>,
666    /// Number of input tokens read from the prompt cache (prefix cache hits).
667    #[serde(default, skip_serializing_if = "Option::is_none")]
668    pub cache_read_input_tokens: Option<u32>,
669}
670
671/// Reason the model stopped generating.
672#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
673#[serde(rename_all = "snake_case")]
674pub enum AnthropicStopReason {
675    EndTurn,
676    MaxTokens,
677    StopSequence,
678    ToolUse,
679    /// The model paused to yield control in an agentic loop, intending to
680    /// continue in a subsequent turn. Used with extended thinking / tool use.
681    PauseTurn,
682    /// The model refused to generate content (safety refusal).
683    Refusal,
684}
685/// SSE event types for the Anthropic streaming API.
686#[derive(Debug, Clone, Serialize, Deserialize)]
687#[serde(tag = "type")]
688pub enum AnthropicStreamEvent {
689    #[serde(rename = "message_start")]
690    MessageStart { message: AnthropicMessageResponse },
691
692    #[serde(rename = "content_block_start")]
693    ContentBlockStart {
694        index: u32,
695        content_block: AnthropicResponseContentBlock,
696    },
697
698    #[serde(rename = "content_block_delta")]
699    ContentBlockDelta { index: u32, delta: AnthropicDelta },
700
701    #[serde(rename = "content_block_stop")]
702    ContentBlockStop { index: u32 },
703
704    #[serde(rename = "message_delta")]
705    MessageDelta {
706        delta: AnthropicMessageDeltaBody,
707        usage: AnthropicUsage,
708    },
709
710    #[serde(rename = "message_stop")]
711    MessageStop {},
712
713    #[serde(rename = "ping")]
714    Ping {},
715
716    #[serde(rename = "error")]
717    Error { error: AnthropicErrorBody },
718}
719
720/// Delta content in a streaming content_block_delta event.
721#[derive(Debug, Clone, Serialize, Deserialize)]
722#[serde(tag = "type")]
723pub enum AnthropicDelta {
724    #[serde(rename = "thinking_delta")]
725    ThinkingDelta { thinking: String },
726    #[serde(rename = "text_delta")]
727    TextDelta { text: String },
728    #[serde(rename = "input_json_delta")]
729    InputJsonDelta { partial_json: String },
730    /// Incremental signature for a thinking block (sent at the end).
731    #[serde(rename = "signature_delta")]
732    SignatureDelta { signature: String },
733    /// Incremental citation attached to a text block.
734    #[serde(rename = "citations_delta")]
735    CitationsDelta { citation: serde_json::Value },
736}
737
738/// The delta body in a message_delta event.
739#[derive(Debug, Clone, Serialize, Deserialize)]
740pub struct AnthropicMessageDeltaBody {
741    pub stop_reason: Option<AnthropicStopReason>,
742    #[serde(skip_serializing_if = "Option::is_none")]
743    pub stop_sequence: Option<String>,
744}
745/// Anthropic API error response wrapper.
746#[derive(Debug, Clone, Serialize, Deserialize)]
747pub struct AnthropicErrorResponse {
748    #[serde(rename = "type")]
749    pub object_type: String,
750    pub error: AnthropicErrorBody,
751}
752
753/// Error body within an error response.
754#[derive(Debug, Clone, Serialize, Deserialize)]
755pub struct AnthropicErrorBody {
756    #[serde(rename = "type")]
757    pub error_type: String,
758    pub message: String,
759}
760
761impl AnthropicErrorResponse {
762    /// Create an `invalid_request_error` response.
763    pub fn invalid_request(message: impl Into<String>) -> Self {
764        Self {
765            object_type: "error".to_string(),
766            error: AnthropicErrorBody {
767                error_type: "invalid_request_error".to_string(),
768                message: message.into(),
769            },
770        }
771    }
772
773    /// Create an `api_error` (internal server error) response.
774    pub fn api_error(message: impl Into<String>) -> Self {
775        Self {
776            object_type: "error".to_string(),
777            error: AnthropicErrorBody {
778                error_type: "api_error".to_string(),
779                message: message.into(),
780            },
781        }
782    }
783
784    /// Create a `not_found_error` response.
785    pub fn not_found(message: impl Into<String>) -> Self {
786        Self {
787            object_type: "error".to_string(),
788            error: AnthropicErrorBody {
789                error_type: "not_found_error".to_string(),
790                message: message.into(),
791            },
792        }
793    }
794}
795/// Request body for `POST /v1/messages/count_tokens`.
796#[derive(Debug, Clone, Deserialize)]
797pub struct AnthropicCountTokensRequest {
798    pub model: String,
799    pub messages: Vec<AnthropicMessage>,
800    #[serde(
801        default,
802        skip_serializing_if = "Option::is_none",
803        deserialize_with = "deserialize_system_prompt"
804    )]
805    pub system: Option<SystemContent>,
806    #[serde(default)]
807    pub tools: Option<Vec<AnthropicTool>>,
808}
809
810/// Response body for `POST /v1/messages/count_tokens`.
811#[derive(Debug, Clone, Serialize)]
812pub struct AnthropicCountTokensResponse {
813    pub input_tokens: u32,
814}
815
816impl AnthropicCountTokensRequest {
817    /// Estimate input token count using a `len/3` heuristic.
818    pub fn estimate_tokens(&self) -> u32 {
819        estimate_input_tokens(&self.messages, self.system.as_ref(), self.tools.as_deref())
820    }
821}
822
823impl AnthropicCreateMessageRequest {
824    /// Estimate input token count using the same `len/3` heuristic as `/count_tokens`.
825    pub fn estimate_tokens(&self) -> u32 {
826        estimate_input_tokens(&self.messages, self.system.as_ref(), self.tools.as_deref())
827    }
828}
829
830fn estimate_input_tokens(
831    messages: &[AnthropicMessage],
832    system: Option<&SystemContent>,
833    tools: Option<&[AnthropicTool]>,
834) -> u32 {
835    let mut total_len: usize = 0;
836
837    if let Some(system) = system {
838        total_len += system.text.len();
839    }
840
841    for msg in messages {
842        // Count role
843        total_len += match msg.role {
844            AnthropicRole::User => 4,
845            AnthropicRole::Assistant => 9,
846            AnthropicRole::System => 6,
847        };
848        // Count content
849        match &msg.content {
850            AnthropicMessageContent::Text { content } => total_len += content.len(),
851            AnthropicMessageContent::Blocks { content } => {
852                for block in content {
853                    total_len += estimate_block_len(block);
854                }
855            }
856        }
857    }
858
859    if let Some(tools) = tools {
860        for tool in tools {
861            total_len += tool.name.len();
862            if let Some(desc) = &tool.description {
863                total_len += desc.len();
864            }
865            if let Some(schema) = &tool.input_schema {
866                total_len += schema.to_string().len();
867            }
868        }
869    }
870
871    let tokens = total_len / 3;
872    if tokens == 0 && total_len > 0 {
873        1
874    } else {
875        tokens as u32
876    }
877}
878
879fn estimate_block_len(block: &AnthropicContentBlock) -> usize {
880    match block {
881        AnthropicContentBlock::Text { text, .. } => text.len(),
882        AnthropicContentBlock::ToolUse { name, input, .. } => name.len() + input.to_string().len(),
883        AnthropicContentBlock::ToolResult { content, .. } => content
884            .as_ref()
885            .map(|c| match c {
886                ToolResultContent::Text(s) => s.len(),
887                ToolResultContent::Blocks(blocks) => blocks
888                    .iter()
889                    .map(|b| match b {
890                        ToolResultContentBlock::Text { text } => text.len(),
891                        ToolResultContentBlock::Image { .. } => 256,
892                        ToolResultContentBlock::Other(v) => v.to_string().len(),
893                    })
894                    .sum(),
895            })
896            .unwrap_or(0),
897        AnthropicContentBlock::Thinking { thinking, .. } => thinking.len(),
898        AnthropicContentBlock::RedactedThinking { data, .. } => data.len(),
899        AnthropicContentBlock::ServerToolUse { name, input, .. } => {
900            name.len() + input.to_string().len()
901        }
902        AnthropicContentBlock::WebSearchToolResult { content, .. } => content.to_string().len(),
903        AnthropicContentBlock::Image { .. } => 256, // rough estimate for image metadata
904        AnthropicContentBlock::Other(v) => v.to_string().len(),
905    }
906}
907
908#[cfg(test)]
909mod tests {
910    use super::*;
911
912    #[test]
913    fn tool_strict_round_trips() {
914        for strict in [Some(true), Some(false), None] {
915            let mut input = serde_json::json!({
916                "name": "set_status",
917                "input_schema": {"type": "object"}
918            });
919            if let Some(strict) = strict {
920                input["strict"] = strict.into();
921            }
922            let tool: AnthropicTool = serde_json::from_value(input.clone()).unwrap();
923            assert_eq!(serde_json::to_value(tool).unwrap(), input);
924        }
925    }
926
927    #[test]
928    fn messages_request_keeps_nvext_opaque() {
929        let request: AnthropicCreateMessageRequest = serde_json::from_value(serde_json::json!({
930            "model": "test-model",
931            "max_tokens": 16,
932            "messages": [{"role": "user", "content": "hi"}],
933            "nvext": {
934                "unknown_future_extension": {"nested": true},
935                "agent_context": {"trajectory_id": 7}
936            }
937        }))
938        .unwrap();
939
940        let nvext = request.nvext.expect("opaque nvext value");
941        assert_eq!(nvext["unknown_future_extension"]["nested"], true);
942        assert_eq!(nvext["agent_context"]["trajectory_id"], 7);
943    }
944
945    #[test]
946    fn tool_result_blocks_preserve_image_and_reject_document() {
947        let input = serde_json::json!([
948            {"type": "text", "text": "Screenshot captured"},
949            {
950                "type": "image",
951                "source": {
952                    "type": "base64",
953                    "media_type": "image/png",
954                    "data": "aGVsbG8="
955                }
956            },
957            {
958                "type": "document",
959                "source": {
960                    "type": "base64",
961                    "media_type": "application/pdf",
962                    "data": "aGVsbG8="
963                }
964            },
965            {"type": "", "text": "not legacy text"}
966        ]);
967        let content: ToolResultContent = serde_json::from_value(input.clone()).unwrap();
968
969        let ToolResultContent::Blocks(blocks) = &content else {
970            panic!("expected content blocks");
971        };
972        assert!(matches!(blocks[1], ToolResultContentBlock::Image { .. }));
973        assert!(matches!(blocks[2], ToolResultContentBlock::Other(_)));
974        assert!(matches!(blocks[3], ToolResultContentBlock::Other(_)));
975        assert_eq!(serde_json::to_value(content).unwrap(), input);
976
977        for input in [
978            serde_json::json!({"text": "legacy"}),
979            serde_json::json!({"type": null, "text": "legacy"}),
980            serde_json::json!({"type": false, "text": "legacy"}),
981        ] {
982            let legacy: ToolResultContentBlock = serde_json::from_value(input).unwrap();
983            assert!(matches!(legacy, ToolResultContentBlock::Text { .. }));
984        }
985    }
986}