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    /// Optional system prompt (string or array of `{"type":"text","text":"..."}` blocks).
119    #[serde(
120        default,
121        skip_serializing_if = "Option::is_none",
122        deserialize_with = "deserialize_system_prompt"
123    )]
124    pub system: Option<SystemContent>,
125
126    /// Sampling temperature (0.0 - 1.0).
127    #[serde(skip_serializing_if = "Option::is_none")]
128    pub temperature: Option<f32>,
129
130    /// Nucleus sampling parameter.
131    #[serde(skip_serializing_if = "Option::is_none")]
132    pub top_p: Option<f32>,
133
134    /// Top-K sampling parameter.
135    #[serde(skip_serializing_if = "Option::is_none")]
136    pub top_k: Option<u32>,
137
138    /// Custom stop sequences.
139    #[serde(skip_serializing_if = "Option::is_none")]
140    pub stop_sequences: Option<Vec<String>>,
141
142    /// Whether to stream the response.
143    #[serde(default)]
144    pub stream: bool,
145
146    /// Optional metadata (e.g. user_id).
147    #[serde(skip_serializing_if = "Option::is_none")]
148    pub metadata: Option<serde_json::Value>,
149
150    /// Tools the model may call.
151    #[serde(skip_serializing_if = "Option::is_none")]
152    pub tools: Option<Vec<AnthropicTool>>,
153
154    /// How the model should choose which tool to call.
155    #[serde(skip_serializing_if = "Option::is_none")]
156    pub tool_choice: Option<AnthropicToolChoice>,
157
158    /// Top-level cache control for automatic prompt prefix caching.
159    /// When present, the system caches all content up to the last cacheable block.
160    /// Matches the Anthropic Messages API automatic caching mode.
161    /// See: https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching#automatic-caching
162    #[serde(default, skip_serializing_if = "Option::is_none")]
163    pub cache_control: Option<CacheControl>,
164
165    /// Extended thinking configuration. When enabled, the model produces
166    /// `thinking` content blocks containing its internal reasoning before
167    /// the final response. The `budget_tokens` field controls how many tokens
168    /// the model may use for thinking (must be >= 1024 and < max_tokens).
169    #[serde(default, skip_serializing_if = "Option::is_none")]
170    pub thinking: Option<ThinkingConfig>,
171
172    /// Service tier selection: `"auto"` or `"standard_only"`.
173    #[serde(default, skip_serializing_if = "Option::is_none")]
174    pub service_tier: Option<String>,
175
176    /// Container identifier for stateful sandbox sessions.
177    #[serde(default, skip_serializing_if = "Option::is_none")]
178    pub container: Option<String>,
179
180    /// Output configuration: effort level and optional JSON schema format.
181    /// `effort` can be `"low"`, `"medium"`, `"high"`, or `"max"`.
182    /// `format` specifies structured JSON output constraints.
183    #[serde(default, skip_serializing_if = "Option::is_none")]
184    pub output_config: Option<serde_json::Value>,
185}
186
187/// Extended thinking configuration for the request.
188///
189/// When `type` is `"enabled"`, the model will produce `thinking` content blocks
190/// with its internal reasoning. `budget_tokens` controls the maximum tokens
191/// available for thinking (minimum 1024, must be less than `max_tokens`).
192/// When `type` is `"disabled"`, no thinking blocks are produced.
193#[derive(Debug, Clone, Serialize, Deserialize)]
194pub struct ThinkingConfig {
195    /// Either `"enabled"` or `"disabled"`.
196    #[serde(rename = "type")]
197    pub thinking_type: String,
198    /// Maximum tokens for internal reasoning. Only relevant when type is "enabled".
199    #[serde(skip_serializing_if = "Option::is_none")]
200    pub budget_tokens: Option<u32>,
201}
202
203/// A single message in the conversation.
204#[derive(Debug, Clone, Serialize, Deserialize)]
205pub struct AnthropicMessage {
206    pub role: AnthropicRole,
207    #[serde(flatten)]
208    pub content: AnthropicMessageContent,
209}
210
211/// The role of a message sender.
212#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
213#[serde(rename_all = "lowercase")]
214pub enum AnthropicRole {
215    User,
216    Assistant,
217    /// Compatibility for clients that place system instructions in `messages[]`
218    /// instead of the top-level `system` field.
219    System,
220}
221
222/// Message content -- either a plain string or an array of content blocks.
223#[derive(Debug, Clone, Serialize, Deserialize)]
224#[serde(untagged)]
225pub enum AnthropicMessageContent {
226    /// Plain text content.
227    Text { content: String },
228    /// Array of structured content blocks.
229    Blocks { content: Vec<AnthropicContentBlock> },
230}
231
232/// A single content block within a message.
233///
234/// Uses a custom deserializer so that unknown block types (e.g. `citations`,
235/// `server_tool_use`, `redacted_thinking`) are captured as `Other(Value)` instead
236/// of causing a hard deserialization failure. This is important because Claude
237/// Code may send block types that we don't yet handle.
238#[derive(Debug, Clone, Serialize)]
239#[serde(tag = "type")]
240pub enum AnthropicContentBlock {
241    /// Text content block. May optionally include `citations` -- references to
242    /// source documents that support the text content. Citations are generated
243    /// by the model when document/PDF content is provided and citation mode is enabled.
244    #[serde(rename = "text")]
245    Text {
246        text: String,
247        #[serde(default, skip_serializing_if = "Option::is_none")]
248        citations: Option<Vec<serde_json::Value>>,
249        #[serde(default, skip_serializing_if = "Option::is_none")]
250        cache_control: Option<CacheControl>,
251    },
252    /// Image content block.
253    #[serde(rename = "image")]
254    Image { source: AnthropicImageSource },
255    /// Tool use request from assistant.
256    #[serde(rename = "tool_use")]
257    ToolUse {
258        id: String,
259        name: String,
260        input: serde_json::Value,
261        #[serde(default, skip_serializing_if = "Option::is_none")]
262        cache_control: Option<CacheControl>,
263    },
264    /// Tool result from user.
265    #[serde(rename = "tool_result")]
266    ToolResult {
267        tool_use_id: String,
268        #[serde(default, skip_serializing_if = "Option::is_none")]
269        content: Option<ToolResultContent>,
270        #[serde(skip_serializing_if = "Option::is_none")]
271        is_error: Option<bool>,
272        #[serde(default, skip_serializing_if = "Option::is_none")]
273        cache_control: Option<CacheControl>,
274    },
275    /// Thinking content block from assistant (extended thinking / reasoning).
276    #[serde(rename = "thinking")]
277    Thinking {
278        thinking: String,
279        signature: String,
280        #[serde(default, skip_serializing_if = "Option::is_none")]
281        cache_control: Option<CacheControl>,
282    },
283    /// Redacted thinking block from assistant. Contains encrypted reasoning data
284    /// that is opaque to the client but must be passed back verbatim in multi-turn
285    /// conversations so the model can maintain its chain of thought.
286    #[serde(rename = "redacted_thinking")]
287    RedactedThinking { data: String },
288    /// Server-initiated tool use block. Represents a tool call that the API
289    /// executes server-side (e.g., web search). The client receives the result
290    /// via a corresponding `web_search_tool_result` or similar block.
291    #[serde(rename = "server_tool_use")]
292    ServerToolUse {
293        id: String,
294        name: String,
295        #[serde(default)]
296        input: serde_json::Value,
297    },
298    /// Result from a server-initiated tool (e.g., web search results).
299    /// Contains structured content returned by the server-side tool execution.
300    #[serde(rename = "web_search_tool_result")]
301    WebSearchToolResult {
302        tool_use_id: String,
303        #[serde(default)]
304        content: serde_json::Value,
305    },
306    /// Catch-all for unrecognized block types. Preserves the full JSON value
307    /// so that new Anthropic features don't break the endpoint and can be
308    /// round-tripped or inspected.
309    #[serde(untagged)]
310    Other(serde_json::Value),
311}
312
313/// Content of a `tool_result` block -- either a plain string or an array of
314/// content blocks (the Anthropic API accepts both).
315#[derive(Debug, Clone, Serialize, Deserialize)]
316#[serde(untagged)]
317pub enum ToolResultContent {
318    Text(String),
319    Blocks(Vec<ToolResultContentBlock>),
320}
321
322impl ToolResultContent {
323    /// Extract the text content, concatenating array blocks if needed.
324    pub fn into_text(self) -> String {
325        match self {
326            ToolResultContent::Text(s) => s,
327            ToolResultContent::Blocks(blocks) => blocks
328                .into_iter()
329                .filter_map(|b| match b {
330                    ToolResultContentBlock::Text { text } => Some(text),
331                    ToolResultContentBlock::Other(_) => None,
332                })
333                .collect::<Vec<_>>()
334                .join(""),
335        }
336    }
337}
338
339/// A content block within a `tool_result.content` array.
340#[derive(Debug, Clone, Serialize, Deserialize)]
341#[serde(untagged)]
342pub enum ToolResultContentBlock {
343    Text {
344        text: String,
345    },
346    /// Catch-all for non-text blocks (images, etc.) in tool results.
347    Other(serde_json::Value),
348}
349
350/// Custom deserializer for `AnthropicContentBlock` that handles unknown types
351/// gracefully. Since serde's `#[serde(other)]` is not supported on internally
352/// tagged enums, we deserialize as `Value` first and dispatch manually.
353impl<'de> Deserialize<'de> for AnthropicContentBlock {
354    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
355    where
356        D: serde::Deserializer<'de>,
357    {
358        let value = serde_json::Value::deserialize(deserializer)?;
359        let block_type = value
360            .get("type")
361            .and_then(|t| t.as_str())
362            .unwrap_or("")
363            .to_string();
364
365        match block_type.as_str() {
366            "text" => {
367                let text = value
368                    .get("text")
369                    .and_then(|t| t.as_str())
370                    .ok_or_else(|| serde::de::Error::missing_field("text"))?
371                    .to_string();
372                let citations: Option<Vec<serde_json::Value>> = value
373                    .get("citations")
374                    .cloned()
375                    .and_then(|v| serde_json::from_value(v).ok());
376                let cache_control: Option<CacheControl> = value
377                    .get("cache_control")
378                    .cloned()
379                    .and_then(|v| serde_json::from_value(v).ok());
380                Ok(AnthropicContentBlock::Text {
381                    text,
382                    citations,
383                    cache_control,
384                })
385            }
386            "image" => {
387                let source: AnthropicImageSource =
388                    serde_json::from_value(value.get("source").cloned().unwrap_or_default())
389                        .map_err(serde::de::Error::custom)?;
390                Ok(AnthropicContentBlock::Image { source })
391            }
392            "tool_use" => {
393                let id = value
394                    .get("id")
395                    .and_then(|v| v.as_str())
396                    .ok_or_else(|| serde::de::Error::missing_field("id"))?
397                    .to_string();
398                let name = value
399                    .get("name")
400                    .and_then(|v| v.as_str())
401                    .ok_or_else(|| serde::de::Error::missing_field("name"))?
402                    .to_string();
403                let input = value.get("input").cloned().unwrap_or(serde_json::json!({}));
404                let cache_control: Option<CacheControl> = value
405                    .get("cache_control")
406                    .cloned()
407                    .and_then(|v| serde_json::from_value(v).ok());
408                Ok(AnthropicContentBlock::ToolUse {
409                    id,
410                    name,
411                    input,
412                    cache_control,
413                })
414            }
415            "tool_result" => {
416                let tool_use_id = value
417                    .get("tool_use_id")
418                    .and_then(|v| v.as_str())
419                    .ok_or_else(|| serde::de::Error::missing_field("tool_use_id"))?
420                    .to_string();
421                let content: Option<ToolResultContent> = value
422                    .get("content")
423                    .cloned()
424                    .and_then(|v| serde_json::from_value(v).ok());
425                let is_error = value.get("is_error").and_then(|v| v.as_bool());
426                let cache_control: Option<CacheControl> = value
427                    .get("cache_control")
428                    .cloned()
429                    .and_then(|v| serde_json::from_value(v).ok());
430                Ok(AnthropicContentBlock::ToolResult {
431                    tool_use_id,
432                    content,
433                    is_error,
434                    cache_control,
435                })
436            }
437            "thinking" => {
438                let thinking = value
439                    .get("thinking")
440                    .and_then(|v| v.as_str())
441                    .ok_or_else(|| serde::de::Error::missing_field("thinking"))?
442                    .to_string();
443                let signature = value
444                    .get("signature")
445                    .and_then(|v| v.as_str())
446                    .ok_or_else(|| serde::de::Error::missing_field("signature"))?
447                    .to_string();
448                let cache_control: Option<CacheControl> = value
449                    .get("cache_control")
450                    .cloned()
451                    .and_then(|v| serde_json::from_value(v).ok());
452                Ok(AnthropicContentBlock::Thinking {
453                    thinking,
454                    signature,
455                    cache_control,
456                })
457            }
458            "redacted_thinking" => {
459                let data = value
460                    .get("data")
461                    .and_then(|v| v.as_str())
462                    .ok_or_else(|| serde::de::Error::missing_field("data"))?
463                    .to_string();
464                Ok(AnthropicContentBlock::RedactedThinking { data })
465            }
466            "server_tool_use" => {
467                let id = value
468                    .get("id")
469                    .and_then(|v| v.as_str())
470                    .ok_or_else(|| serde::de::Error::missing_field("id"))?
471                    .to_string();
472                let name = value
473                    .get("name")
474                    .and_then(|v| v.as_str())
475                    .ok_or_else(|| serde::de::Error::missing_field("name"))?
476                    .to_string();
477                let input = value.get("input").cloned().unwrap_or(serde_json::json!({}));
478                Ok(AnthropicContentBlock::ServerToolUse { id, name, input })
479            }
480            "web_search_tool_result" => {
481                let tool_use_id = value
482                    .get("tool_use_id")
483                    .and_then(|v| v.as_str())
484                    .ok_or_else(|| serde::de::Error::missing_field("tool_use_id"))?
485                    .to_string();
486                let content = value
487                    .get("content")
488                    .cloned()
489                    .unwrap_or(serde_json::json!([]));
490                Ok(AnthropicContentBlock::WebSearchToolResult {
491                    tool_use_id,
492                    content,
493                })
494            }
495            other => {
496                tracing::debug!(
497                    "Unrecognized Anthropic content block type '{}', preserving as Other",
498                    other
499                );
500                Ok(AnthropicContentBlock::Other(value))
501            }
502        }
503    }
504}
505
506/// Image source for image content blocks.
507#[derive(Debug, Clone, Serialize, Deserialize)]
508pub struct AnthropicImageSource {
509    #[serde(rename = "type")]
510    pub source_type: String,
511    pub media_type: String,
512    pub data: String,
513}
514
515/// A tool definition.
516///
517/// Client tools (custom) require `name` + `input_schema`. Server tools
518/// (web_search, bash, text_editor, code_execution, etc.) are discriminated
519/// by their `type` field (e.g. `"web_search_20260209"`) and may not have
520/// `input_schema`. We keep all fields optional beyond `name` so both
521/// kinds deserialize successfully and pass through to the backend.
522#[derive(Debug, Clone, Serialize, Deserialize)]
523pub struct AnthropicTool {
524    /// Tool name (required for client tools, present on server tools too).
525    pub name: String,
526    /// Tool type discriminator. Client tools use `"custom"` (or omit).
527    /// Server tools use versioned types like `"web_search_20260209"`.
528    #[serde(default, rename = "type", skip_serializing_if = "Option::is_none")]
529    pub tool_type: Option<String>,
530    #[serde(skip_serializing_if = "Option::is_none")]
531    pub description: Option<String>,
532    /// JSON Schema for the tool input. Required for client tools, absent on
533    /// server tools (which define their own input shape server-side).
534    #[serde(default, skip_serializing_if = "Option::is_none")]
535    pub input_schema: Option<serde_json::Value>,
536    /// Cache control breakpoint on this tool definition.
537    #[serde(default, skip_serializing_if = "Option::is_none")]
538    pub cache_control: Option<CacheControl>,
539}
540
541/// Tool choice specification.
542#[derive(Debug, Clone, Serialize, Deserialize)]
543#[serde(untagged)]
544pub enum AnthropicToolChoice {
545    /// Named tool: `{type: "tool", name: "..."}`
546    /// Must be listed before Simple so serde tries the stricter shape first.
547    Named(AnthropicToolChoiceNamed),
548    /// Simple mode: "auto", "any", or "none".
549    Simple(AnthropicToolChoiceSimple),
550}
551
552/// Simple tool choice modes.
553#[derive(Debug, Clone, Serialize, Deserialize)]
554pub struct AnthropicToolChoiceSimple {
555    #[serde(rename = "type")]
556    pub choice_type: AnthropicToolChoiceMode,
557    /// When true, the model will call tools one at a time instead of
558    /// potentially issuing multiple tool calls in a single response.
559    #[serde(default, skip_serializing_if = "Option::is_none")]
560    pub disable_parallel_tool_use: Option<bool>,
561}
562
563#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
564#[serde(rename_all = "lowercase")]
565pub enum AnthropicToolChoiceMode {
566    Auto,
567    Any,
568    None,
569    Tool,
570}
571
572/// Named tool choice.
573#[derive(Debug, Clone, Serialize, Deserialize)]
574pub struct AnthropicToolChoiceNamed {
575    #[serde(rename = "type")]
576    pub choice_type: AnthropicToolChoiceMode,
577    pub name: String,
578    /// When true, the model will call tools one at a time instead of
579    /// potentially issuing multiple tool calls in a single response.
580    #[serde(default, skip_serializing_if = "Option::is_none")]
581    pub disable_parallel_tool_use: Option<bool>,
582}
583/// Response body for `POST /v1/messages` (non-streaming).
584#[derive(Debug, Clone, Serialize, Deserialize)]
585pub struct AnthropicMessageResponse {
586    pub id: String,
587    #[serde(rename = "type")]
588    pub object_type: String,
589    pub role: String,
590    pub content: Vec<AnthropicResponseContentBlock>,
591    pub model: String,
592    pub stop_reason: Option<AnthropicStopReason>,
593    pub stop_sequence: Option<String>,
594    pub usage: AnthropicUsage,
595}
596
597/// A content block in the response.
598///
599/// The Anthropic API returns up to 12 different block types. We model the
600/// common ones explicitly and catch the rest as `Other` so the proxy can
601/// forward them without losing data.
602#[derive(Debug, Clone, Serialize, Deserialize)]
603#[serde(tag = "type")]
604pub enum AnthropicResponseContentBlock {
605    #[serde(rename = "thinking")]
606    Thinking { thinking: String, signature: String },
607    #[serde(rename = "text")]
608    Text {
609        text: String,
610        #[serde(default, skip_serializing_if = "Option::is_none")]
611        citations: Option<Vec<serde_json::Value>>,
612    },
613    #[serde(rename = "tool_use")]
614    ToolUse {
615        id: String,
616        name: String,
617        input: serde_json::Value,
618    },
619    #[serde(rename = "redacted_thinking")]
620    RedactedThinking { data: String },
621    #[serde(rename = "server_tool_use")]
622    ServerToolUse {
623        id: String,
624        name: String,
625        #[serde(default)]
626        input: serde_json::Value,
627    },
628    #[serde(rename = "web_search_tool_result")]
629    WebSearchToolResult {
630        tool_use_id: String,
631        #[serde(default)]
632        content: serde_json::Value,
633    },
634    /// Catch-all for new/uncommon block types (web_fetch_tool_result,
635    /// code_execution_tool_result, container_upload, etc.) so the proxy
636    /// can serialize them back without data loss.
637    #[serde(untagged)]
638    Other(serde_json::Value),
639}
640
641/// Token usage information.
642#[derive(Debug, Clone, Serialize, Deserialize, Default)]
643pub struct AnthropicUsage {
644    pub input_tokens: u32,
645    pub output_tokens: u32,
646    /// Number of input tokens used to create a new cache entry.
647    #[serde(default, skip_serializing_if = "Option::is_none")]
648    pub cache_creation_input_tokens: Option<u32>,
649    /// Number of input tokens read from the prompt cache (prefix cache hits).
650    #[serde(default, skip_serializing_if = "Option::is_none")]
651    pub cache_read_input_tokens: Option<u32>,
652}
653
654/// Reason the model stopped generating.
655#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
656#[serde(rename_all = "snake_case")]
657pub enum AnthropicStopReason {
658    EndTurn,
659    MaxTokens,
660    StopSequence,
661    ToolUse,
662    /// The model paused to yield control in an agentic loop, intending to
663    /// continue in a subsequent turn. Used with extended thinking / tool use.
664    PauseTurn,
665    /// The model refused to generate content (safety refusal).
666    Refusal,
667}
668/// SSE event types for the Anthropic streaming API.
669#[derive(Debug, Clone, Serialize, Deserialize)]
670#[serde(tag = "type")]
671pub enum AnthropicStreamEvent {
672    #[serde(rename = "message_start")]
673    MessageStart { message: AnthropicMessageResponse },
674
675    #[serde(rename = "content_block_start")]
676    ContentBlockStart {
677        index: u32,
678        content_block: AnthropicResponseContentBlock,
679    },
680
681    #[serde(rename = "content_block_delta")]
682    ContentBlockDelta { index: u32, delta: AnthropicDelta },
683
684    #[serde(rename = "content_block_stop")]
685    ContentBlockStop { index: u32 },
686
687    #[serde(rename = "message_delta")]
688    MessageDelta {
689        delta: AnthropicMessageDeltaBody,
690        usage: AnthropicUsage,
691    },
692
693    #[serde(rename = "message_stop")]
694    MessageStop {},
695
696    #[serde(rename = "ping")]
697    Ping {},
698
699    #[serde(rename = "error")]
700    Error { error: AnthropicErrorBody },
701}
702
703/// Delta content in a streaming content_block_delta event.
704#[derive(Debug, Clone, Serialize, Deserialize)]
705#[serde(tag = "type")]
706pub enum AnthropicDelta {
707    #[serde(rename = "thinking_delta")]
708    ThinkingDelta { thinking: String },
709    #[serde(rename = "text_delta")]
710    TextDelta { text: String },
711    #[serde(rename = "input_json_delta")]
712    InputJsonDelta { partial_json: String },
713    /// Incremental signature for a thinking block (sent at the end).
714    #[serde(rename = "signature_delta")]
715    SignatureDelta { signature: String },
716    /// Incremental citation attached to a text block.
717    #[serde(rename = "citations_delta")]
718    CitationsDelta { citation: serde_json::Value },
719}
720
721/// The delta body in a message_delta event.
722#[derive(Debug, Clone, Serialize, Deserialize)]
723pub struct AnthropicMessageDeltaBody {
724    pub stop_reason: Option<AnthropicStopReason>,
725    #[serde(skip_serializing_if = "Option::is_none")]
726    pub stop_sequence: Option<String>,
727}
728/// Anthropic API error response wrapper.
729#[derive(Debug, Clone, Serialize, Deserialize)]
730pub struct AnthropicErrorResponse {
731    #[serde(rename = "type")]
732    pub object_type: String,
733    pub error: AnthropicErrorBody,
734}
735
736/// Error body within an error response.
737#[derive(Debug, Clone, Serialize, Deserialize)]
738pub struct AnthropicErrorBody {
739    #[serde(rename = "type")]
740    pub error_type: String,
741    pub message: String,
742}
743
744impl AnthropicErrorResponse {
745    /// Create an `invalid_request_error` response.
746    pub fn invalid_request(message: impl Into<String>) -> Self {
747        Self {
748            object_type: "error".to_string(),
749            error: AnthropicErrorBody {
750                error_type: "invalid_request_error".to_string(),
751                message: message.into(),
752            },
753        }
754    }
755
756    /// Create an `api_error` (internal server error) response.
757    pub fn api_error(message: impl Into<String>) -> Self {
758        Self {
759            object_type: "error".to_string(),
760            error: AnthropicErrorBody {
761                error_type: "api_error".to_string(),
762                message: message.into(),
763            },
764        }
765    }
766
767    /// Create a `not_found_error` response.
768    pub fn not_found(message: impl Into<String>) -> Self {
769        Self {
770            object_type: "error".to_string(),
771            error: AnthropicErrorBody {
772                error_type: "not_found_error".to_string(),
773                message: message.into(),
774            },
775        }
776    }
777}
778/// Request body for `POST /v1/messages/count_tokens`.
779#[derive(Debug, Clone, Deserialize)]
780pub struct AnthropicCountTokensRequest {
781    pub model: String,
782    pub messages: Vec<AnthropicMessage>,
783    #[serde(
784        default,
785        skip_serializing_if = "Option::is_none",
786        deserialize_with = "deserialize_system_prompt"
787    )]
788    pub system: Option<SystemContent>,
789    #[serde(default)]
790    pub tools: Option<Vec<AnthropicTool>>,
791}
792
793/// Response body for `POST /v1/messages/count_tokens`.
794#[derive(Debug, Clone, Serialize)]
795pub struct AnthropicCountTokensResponse {
796    pub input_tokens: u32,
797}
798
799impl AnthropicCountTokensRequest {
800    /// Estimate input token count using a `len/3` heuristic.
801    pub fn estimate_tokens(&self) -> u32 {
802        let mut total_len: usize = 0;
803
804        if let Some(system) = &self.system {
805            total_len += system.text.len();
806        }
807
808        for msg in &self.messages {
809            // Count role
810            total_len += match msg.role {
811                AnthropicRole::User => 4,
812                AnthropicRole::Assistant => 9,
813                AnthropicRole::System => 6,
814            };
815            // Count content
816            match &msg.content {
817                AnthropicMessageContent::Text { content } => total_len += content.len(),
818                AnthropicMessageContent::Blocks { content } => {
819                    for block in content {
820                        total_len += estimate_block_len(block);
821                    }
822                }
823            }
824        }
825
826        if let Some(tools) = &self.tools {
827            for tool in tools {
828                total_len += tool.name.len();
829                if let Some(desc) = &tool.description {
830                    total_len += desc.len();
831                }
832                if let Some(schema) = &tool.input_schema {
833                    total_len += schema.to_string().len();
834                }
835            }
836        }
837
838        let tokens = total_len / 3;
839        if tokens == 0 && total_len > 0 {
840            1
841        } else {
842            tokens as u32
843        }
844    }
845}
846
847fn estimate_block_len(block: &AnthropicContentBlock) -> usize {
848    match block {
849        AnthropicContentBlock::Text { text, .. } => text.len(),
850        AnthropicContentBlock::ToolUse { name, input, .. } => name.len() + input.to_string().len(),
851        AnthropicContentBlock::ToolResult { content, .. } => content
852            .as_ref()
853            .map(|c| match c {
854                ToolResultContent::Text(s) => s.len(),
855                ToolResultContent::Blocks(blocks) => blocks
856                    .iter()
857                    .map(|b| match b {
858                        ToolResultContentBlock::Text { text } => text.len(),
859                        ToolResultContentBlock::Other(v) => v.to_string().len(),
860                    })
861                    .sum(),
862            })
863            .unwrap_or(0),
864        AnthropicContentBlock::Thinking { thinking, .. } => thinking.len(),
865        AnthropicContentBlock::RedactedThinking { data, .. } => data.len(),
866        AnthropicContentBlock::ServerToolUse { name, input, .. } => {
867            name.len() + input.to_string().len()
868        }
869        AnthropicContentBlock::WebSearchToolResult { content, .. } => content.to_string().len(),
870        AnthropicContentBlock::Image { .. } => 256, // rough estimate for image metadata
871        AnthropicContentBlock::Other(v) => v.to_string().len(),
872    }
873}