Skip to main content

vtcode_llm/providers/
anthropic_types.rs

1use std::borrow::Cow;
2
3use serde::{Deserialize, Deserializer, Serialize};
4use serde_json::{Map, Value};
5
6use crate::provider::MessageClearAt;
7
8#[derive(Debug, Serialize, Deserialize, Clone)]
9#[serde(tag = "type", rename_all = "lowercase")]
10pub enum AnthropicFallback {
11    #[serde(rename = "fallback")]
12    Fallback {
13        from: AnthropicFallbackModel,
14        to: AnthropicFallbackModel,
15    },
16}
17
18#[derive(Debug, Serialize, Deserialize, Clone)]
19pub struct AnthropicFallbackModel {
20    model: String,
21}
22
23#[derive(Debug, Serialize, Deserialize)]
24pub struct AnthropicRequest {
25    pub(crate) model: String,
26    pub(crate) messages: Vec<AnthropicMessage>,
27    pub(crate) max_tokens: u32,
28    #[serde(skip_serializing_if = "Option::is_none")]
29    pub(crate) cache_control: Option<CacheControl>,
30    #[serde(skip_serializing_if = "Option::is_none")]
31    pub(crate) system: Option<Value>, // Can be string or array of blocks
32    #[serde(skip_serializing_if = "Option::is_none")]
33    pub(crate) temperature: Option<f32>,
34    #[serde(skip_serializing_if = "Option::is_none")]
35    pub(crate) tools: Option<Vec<AnthropicTool>>,
36    #[serde(skip_serializing_if = "Option::is_none")]
37    pub(crate) tool_choice: Option<Value>,
38    #[serde(skip_serializing_if = "Option::is_none")]
39    pub(crate) thinking: Option<ThinkingConfig>,
40    #[serde(skip_serializing_if = "Option::is_none")]
41    pub(crate) reasoning: Option<Value>, // Deprecated in favor of thinking, but kept for backward compat or direct effort passing
42    #[serde(
43        default,
44        deserialize_with = "deserialize_boxed_output_config_opt",
45        skip_serializing_if = "Option::is_none"
46    )]
47    pub(crate) output_config: Option<Box<AnthropicOutputConfig>>,
48    #[serde(skip_serializing_if = "Option::is_none")]
49    pub(crate) context_management: Option<Value>,
50    #[serde(skip_serializing_if = "Option::is_none")]
51    pub(crate) fallbacks: Option<AnthropicFallbacksParam>,
52    /// Opaque credit token returned by a refused request's `stop_details.fallback_credit_token`.
53    /// Echoed on the retry to avoid paying the prompt-cache cost twice.
54    #[serde(skip_serializing_if = "Option::is_none")]
55    pub(crate) fallback_credit_token: Option<String>,
56    pub(crate) stream: bool,
57}
58
59/// The `fallbacks` request parameter: the `"default"` keyword (Anthropic picks
60/// the fallback by refusal category) or an explicit list of entries. Each form
61/// needs its own beta header; see `headers::ServerSideFallbackForm`.
62#[derive(Debug, Serialize, Deserialize, Clone)]
63#[serde(untagged)]
64pub enum AnthropicFallbacksParam {
65    Mode(AnthropicFallbacksKeyword),
66    Models(Vec<AnthropicFallbackParam>),
67}
68
69#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]
70#[serde(rename_all = "lowercase")]
71pub enum AnthropicFallbacksKeyword {
72    Default,
73}
74
75impl AnthropicFallbacksParam {
76    /// Explicit entries, or an empty slice for the `"default"` form.
77    pub(crate) fn models(&self) -> &[AnthropicFallbackParam] {
78        match self {
79            Self::Mode(_) => &[],
80            Self::Models(models) => models,
81        }
82    }
83}
84
85#[derive(Debug, Serialize, Deserialize, Clone)]
86pub struct AnthropicFallbackParam {
87    pub(crate) model: String,
88    #[serde(skip_serializing_if = "Option::is_none")]
89    pub(crate) max_tokens: Option<u32>,
90    #[serde(skip_serializing_if = "Option::is_none")]
91    pub(crate) thinking: Option<ThinkingConfig>,
92}
93
94#[derive(Debug, Serialize, Deserialize, Clone)]
95#[serde(tag = "type", rename_all = "lowercase")]
96pub enum ThinkingConfig {
97    Enabled {
98        budget_tokens: u32,
99        #[serde(skip_serializing_if = "Option::is_none")]
100        display: Option<ThinkingDisplay>,
101    },
102    Adaptive {
103        #[serde(skip_serializing_if = "Option::is_none")]
104        display: Option<ThinkingDisplay>,
105    },
106    Disabled,
107    /// Claude Sonnet 5.5's lowest thinking setting: up-front thinking is off
108    /// while the short progress notes written between tool calls still come
109    /// back as `thinking` blocks. `disabled` is rejected with a 400 on that
110    /// model, and `between_tools` is itself rejected at `xhigh`/`max` effort.
111    /// Takes no other field.
112    #[serde(rename = "between_tools")]
113    BetweenTools,
114    /// Catch-all for unknown thinking config types added by the Anthropic API.
115    #[serde(other)]
116    Unknown,
117}
118
119/// Controls how thinking content is returned in API responses.
120#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]
121#[serde(rename_all = "lowercase")]
122pub enum ThinkingDisplay {
123    /// Thinking blocks contain summarized thinking text (default on Claude 4 models).
124    Summarized,
125    /// Thinking blocks are returned with an empty `thinking` field; the `signature`
126    /// still carries encrypted full thinking for multi-turn continuity
127    /// (default on Claude Opus 4.7).
128    Omitted,
129    /// Progress-update thinking blocks (the short notes written between tool
130    /// calls) carry their text; reasoning blocks stay empty. Requires the
131    /// `thinking-display-updates-2026-08-18` beta; supported on Claude Sonnet
132    /// 5.5, Claude Opus 5.5 and Claude Fable 5.x.
133    Updates,
134    /// Catch-all for unknown display modes added by the Anthropic API.
135    #[serde(other)]
136    Unknown,
137}
138
139#[derive(Debug, Serialize, Deserialize)]
140pub struct AnthropicMessage {
141    pub(crate) role: String,
142    pub(crate) content: Vec<AnthropicContentBlock>,
143    /// Anthropic's turn-scoped system-message lifecycle field.
144    #[serde(default, skip_serializing_if = "Option::is_none")]
145    pub(crate) clear_at: Option<MessageClearAt>,
146}
147
148#[derive(Debug, Serialize, Deserialize, Clone)]
149#[serde(tag = "type")]
150pub enum AnthropicContentBlock {
151    #[serde(rename = "text")]
152    Text {
153        text: String,
154        #[serde(skip_serializing_if = "Option::is_none")]
155        citations: Option<Vec<TextCitation>>,
156        #[serde(skip_serializing_if = "Option::is_none")]
157        cache_control: Option<CacheControl>,
158    },
159    #[serde(rename = "image")]
160    Image {
161        source: ImageSource,
162        #[serde(skip_serializing_if = "Option::is_none")]
163        cache_control: Option<CacheControl>,
164    },
165    #[serde(rename = "tool_use")]
166    ToolUse(Box<AnthropicToolUseBlock>),
167    #[serde(rename = "tool_result")]
168    ToolResult(Box<AnthropicToolResultBlock>),
169    #[serde(rename = "thinking")]
170    Thinking {
171        thinking: String,
172        #[serde(skip_serializing_if = "Option::is_none")]
173        signature: Option<String>,
174        #[serde(skip_serializing_if = "Option::is_none")]
175        cache_control: Option<CacheControl>,
176    },
177    #[serde(rename = "redacted_thinking")]
178    RedactedThinking {
179        data: String,
180        #[serde(skip_serializing_if = "Option::is_none")]
181        cache_control: Option<CacheControl>,
182    },
183    #[serde(rename = "compaction")]
184    Compaction {
185        /// Threshold compaction streams may start with `null` and provide the
186        /// summary in a single delta. On-demand compaction returns the full
187        /// signed block in the start event.
188        content: Option<String>,
189        #[serde(skip_serializing_if = "Option::is_none")]
190        signature: Option<String>,
191        #[serde(skip_serializing_if = "Option::is_none")]
192        cache_control: Option<CacheControl>,
193        /// Preserve provider fields that are not yet modeled so an opaque
194        /// compaction block can be replayed without alteration.
195        #[serde(flatten, default, skip_serializing_if = "Map::is_empty")]
196        extra: Map<String, Value>,
197    },
198    /// Server-side tool use (e.g., tool search execution) - advanced-tool-use beta
199    #[serde(rename = "server_tool_use")]
200    ServerToolUse { id: String, name: String, input: Value },
201    /// Tool search result containing discovered tool references - advanced-tool-use beta
202    #[serde(rename = "tool_search_tool_result")]
203    ToolSearchToolResult {
204        tool_use_id: String,
205        content: ToolSearchResultContent,
206    },
207    /// Files API upload reference used by Anthropic code execution.
208    #[serde(rename = "container_upload")]
209    ContainerUpload { file_id: String },
210    /// Generic code execution result block used in some Anthropic responses.
211    #[serde(rename = "code_execution_tool_result")]
212    CodeExecutionToolResult { tool_use_id: String, content: Value },
213    /// Bash code execution result block returned by Anthropic code execution.
214    #[serde(rename = "bash_code_execution_tool_result")]
215    BashCodeExecutionToolResult { tool_use_id: String, content: Value },
216    /// Text editor code execution result block returned by Anthropic code execution.
217    #[serde(rename = "text_editor_code_execution_tool_result")]
218    TextEditorCodeExecutionToolResult { tool_use_id: String, content: Value },
219    /// Native web search result blocks returned by Anthropic web search tools.
220    #[serde(rename = "web_search_tool_result")]
221    WebSearchToolResult { tool_use_id: String, content: Value },
222    /// Advisor tool result block returned by the server-side advisor sub-inference.
223    /// `content` is the verbatim result union (`advisor_result`,
224    /// `advisor_redacted_result`, or `advisor_tool_result_error`).
225    #[serde(rename = "advisor_tool_result")]
226    AdvisorToolResult { tool_use_id: String, content: Value },
227    /// Fallback content block marking model boundary in server-side fallback
228    #[serde(rename = "fallback")]
229    Fallback {
230        from: AnthropicFallbackModel,
231        to: AnthropicFallbackModel,
232    },
233    /// Catch-all for unknown content block types added by the Anthropic API.
234    #[serde(other)]
235    Unknown,
236}
237
238/// Extracted struct for `AnthropicContentBlock::ToolUse` (boxed to reduce enum size).
239#[derive(Debug, Serialize, Deserialize, Clone)]
240pub struct AnthropicToolUseBlock {
241    pub(crate) id: String,
242    pub(crate) name: String,
243    pub(crate) input: Value,
244    #[serde(skip_serializing_if = "Option::is_none")]
245    pub(crate) cache_control: Option<CacheControl>,
246}
247
248/// Extracted struct for `AnthropicContentBlock::ToolResult` (boxed to reduce enum size).
249#[derive(Debug, Serialize, Deserialize, Clone)]
250pub struct AnthropicToolResultBlock {
251    pub(crate) tool_use_id: String,
252    pub(crate) content: Value, // string or array of blocks
253    #[serde(skip_serializing_if = "Option::is_none")]
254    pub(crate) is_error: Option<bool>,
255    #[serde(skip_serializing_if = "Option::is_none")]
256    pub(crate) cache_control: Option<CacheControl>,
257}
258
259/// Content of a tool search result
260#[derive(Debug, Serialize, Deserialize, Clone)]
261#[serde(tag = "type")]
262pub enum ToolSearchResultContent {
263    #[serde(rename = "tool_search_tool_search_result")]
264    SearchResult { tool_references: Vec<ToolReference> },
265    #[serde(rename = "tool_search_tool_result_error")]
266    Error { error_code: String },
267    /// Catch-all for unknown tool search result types.
268    #[serde(other)]
269    Unknown,
270}
271
272/// A reference to a discovered tool from tool search
273#[derive(Debug, Serialize, Deserialize, Clone)]
274pub struct ToolReference {
275    #[serde(rename = "type")]
276    ref_type: Option<String>, // "tool_reference"
277    tool_name: String,
278}
279
280#[derive(Debug, Serialize, Deserialize, Clone)]
281#[serde(tag = "type")]
282pub enum TextCitation {
283    #[serde(rename = "char_location")]
284    CharLocation {
285        cited_text: String,
286        document_index: usize,
287        document_title: Option<String>,
288        start_char_index: usize,
289        end_char_index: usize,
290    },
291    #[serde(rename = "page_location")]
292    PageLocation {
293        cited_text: String,
294        document_index: usize,
295        document_title: Option<String>,
296        start_page_number: usize,
297        end_page_number: usize,
298    },
299    #[serde(rename = "content_block_location")]
300    ContentBlockLocation {
301        cited_text: String,
302        document_index: usize,
303        document_title: Option<String>,
304        start_block_index: usize,
305        end_block_index: usize,
306    },
307    #[serde(rename = "web_search_result_location")]
308    WebSearchResultLocation {
309        url: Option<String>,
310        title: Option<String>,
311        encrypted_index: Option<String>,
312        cited_text: Option<String>,
313    },
314    /// Catch-all for unknown citation types.
315    #[serde(other)]
316    Unknown,
317}
318
319#[derive(Debug, Serialize, Deserialize, Clone)]
320pub struct ImageSource {
321    #[serde(rename = "type")]
322    pub(crate) source_type: String, // "base64"
323    pub(crate) media_type: String,
324    pub(crate) data: String,
325}
326
327#[derive(Debug, Serialize, Deserialize, Clone)]
328pub struct CacheControl {
329    #[serde(rename = "type")]
330    pub(crate) control_type: Cow<'static, str>, // "ephemeral"
331    #[serde(skip_serializing_if = "Option::is_none")]
332    pub(crate) ttl: Option<Cow<'static, str>>, // "5m" or "1h"
333}
334
335/// Anthropic tool definition
336/// Supports both regular function tools and tool search tools (advanced-tool-use beta)
337#[derive(Debug, Serialize, Deserialize, Clone)]
338#[serde(untagged)]
339pub enum AnthropicTool {
340    /// Tool search tool (regex or bm25)
341    ToolSearch(AnthropicToolSearchTool),
342    /// Native Anthropic code execution tool revision
343    CodeExecution(AnthropicCodeExecutionTool),
344    /// Native Anthropic memory tool revision
345    Memory(AnthropicMemoryTool),
346    /// Native Anthropic web search tool revision
347    WebSearch(AnthropicWebSearchTool),
348    /// Anthropic server-side advisor tool (beta `advisor-tool-2026-03-01`).
349    Advisor(AnthropicAdvisorTool),
350    /// Regular function tool
351    Function(AnthropicFunctionTool),
352}
353
354/// Regular function tool definition for Anthropic API
355#[derive(Debug, Serialize, Deserialize, Clone)]
356pub struct AnthropicFunctionTool {
357    pub(crate) name: String,
358    pub(crate) description: String,
359    pub(crate) input_schema: Value,
360    #[serde(skip_serializing_if = "Option::is_none")]
361    pub(crate) input_examples: Option<Vec<Value>>,
362    #[serde(skip_serializing_if = "Option::is_none")]
363    pub(crate) strict: Option<bool>,
364    #[serde(skip_serializing_if = "Option::is_none")]
365    pub(crate) allowed_callers: Option<Vec<String>>,
366    #[serde(skip_serializing_if = "Option::is_none")]
367    pub(crate) cache_control: Option<CacheControl>,
368    /// When true, the tool is deferred and only loaded when discovered via tool search
369    #[serde(skip_serializing_if = "Option::is_none")]
370    pub(crate) defer_loading: Option<bool>,
371}
372
373/// Native code execution tool definition for Anthropic API.
374#[derive(Debug, Serialize, Deserialize, Clone)]
375pub struct AnthropicCodeExecutionTool {
376    /// Versioned code execution type (e.g. "code_execution_20250825")
377    #[serde(rename = "type")]
378    pub(crate) tool_type: String,
379    /// Tool name (typically "code_execution")
380    pub(crate) name: String,
381}
382
383/// Native memory tool definition for Anthropic API.
384#[derive(Debug, Serialize, Deserialize, Clone)]
385pub struct AnthropicMemoryTool {
386    /// Versioned memory type (e.g. "memory_20250818")
387    #[serde(rename = "type")]
388    pub(crate) tool_type: String,
389    /// Tool name (typically "memory")
390    pub(crate) name: String,
391}
392
393/// Tool search tool definition for Anthropic's advanced-tool-use beta
394#[derive(Debug, Serialize, Deserialize, Clone)]
395pub struct AnthropicToolSearchTool {
396    /// The type of tool search: "tool_search_tool_regex_20251119" or "tool_search_tool_bm25_20251119"
397    #[serde(rename = "type")]
398    pub(crate) tool_type: String,
399    /// Tool name (e.g., "tool_search_tool_regex" or "tool_search_tool_bm25")
400    pub(crate) name: String,
401}
402
403/// Native web search tool definition for Anthropic API (PTC-enabled search revisions)
404#[derive(Debug, Serialize, Deserialize, Clone)]
405pub struct AnthropicWebSearchTool {
406    /// Versioned web search type (e.g., "web_search_20260209")
407    #[serde(rename = "type")]
408    pub(crate) tool_type: String,
409    /// Tool name (typically "web_search")
410    pub(crate) name: String,
411    /// Optional Anthropic-native web search configuration.
412    #[serde(flatten, default, skip_serializing_if = "Map::is_empty")]
413    pub(crate) options: Map<String, Value>,
414}
415
416/// Anthropic server-side advisor tool definition (beta `advisor-tool-2026-03-01`).
417///
418/// The executor model consults the advisor model for strategic guidance
419/// mid-generation. The tool takes no client-supplied input; the server builds
420/// the advisor's view from the full transcript.
421#[derive(Debug, Serialize, Deserialize, Clone)]
422pub struct AnthropicAdvisorTool {
423    /// Fixed advisor tool type.
424    #[serde(rename = "type")]
425    pub(crate) tool_type: String, // "advisor_20260301"
426    /// Fixed advisor tool name.
427    pub(crate) name: String, // "advisor"
428    /// Advisor model id (e.g. "claude-opus-5").
429    pub(crate) model: String,
430    /// Maximum number of advisor invocations per request.
431    #[serde(skip_serializing_if = "Option::is_none")]
432    pub(crate) max_uses: Option<u32>,
433    /// Caps the advisor's total output (thinking plus text) per call (min 1024).
434    #[serde(skip_serializing_if = "Option::is_none")]
435    pub(crate) max_tokens: Option<u32>,
436    /// Enables prompt caching for the advisor's own transcript across calls.
437    #[serde(skip_serializing_if = "Option::is_none")]
438    pub(crate) caching: Option<AnthropicAdvisorCaching>,
439}
440
441/// Prompt-caching configuration for the advisor tool.
442#[derive(Debug, Serialize, Deserialize, Clone)]
443pub struct AnthropicAdvisorCaching {
444    /// Fixed cache type.
445    #[serde(rename = "type")]
446    pub(crate) cache_type: String, // "ephemeral"
447    /// Cache lifetime ("5m" or "1h").
448    pub(crate) ttl: String,
449}
450
451#[derive(Debug, Deserialize)]
452#[serde(tag = "type")]
453pub enum AnthropicStreamEvent {
454    #[serde(rename = "message_start")]
455    MessageStart { message: AnthropicMessageResponse },
456    #[serde(rename = "content_block_start")]
457    ContentBlockStart {
458        index: usize,
459        content_block: AnthropicContentBlock,
460    },
461    #[serde(rename = "ping")]
462    Ping,
463    #[serde(rename = "content_block_delta")]
464    ContentBlockDelta { index: usize, delta: AnthropicStreamDelta },
465    #[serde(rename = "content_block_stop")]
466    ContentBlockStop { index: usize },
467    #[serde(rename = "message_delta")]
468    MessageDelta {
469        delta: AnthropicMessageDelta,
470        usage: Option<AnthropicUsage>,
471    },
472    #[serde(rename = "message_stop")]
473    MessageStop,
474    #[serde(rename = "error")]
475    Error { error: AnthropicErrorBody },
476    /// Catch-all for unknown streaming event types added by the Anthropic API.
477    #[serde(other)]
478    Unknown,
479}
480
481#[derive(Debug, Deserialize)]
482#[serde(tag = "type")]
483pub enum AnthropicStreamDelta {
484    #[serde(rename = "text_delta")]
485    TextDelta { text: String },
486    #[serde(rename = "input_json_delta")]
487    InputJsonDelta { partial_json: String },
488    #[serde(rename = "thinking_delta")]
489    ThinkingDelta { thinking: String },
490    #[serde(rename = "signature_delta")]
491    SignatureDelta { signature: String },
492    #[serde(rename = "compaction_delta")]
493    CompactionDelta {
494        content: Option<String>,
495        /// Preserve opaque fields such as `encrypted_content` from a delta.
496        #[serde(flatten, default)]
497        extra: Map<String, Value>,
498    },
499    /// Catch-all for unknown delta types added by the Anthropic API.
500    #[serde(other)]
501    Unknown,
502}
503
504#[derive(Debug, Deserialize)]
505pub struct AnthropicMessageDelta {
506    pub(crate) stop_reason: Option<String>,
507    stop_sequence: Option<String>,
508    /// Refusal category, explanation and fallback-credit fields. Streaming
509    /// responses deliver `stop_details` here rather than on `message_start`.
510    #[serde(default)]
511    pub(crate) stop_details: Option<Value>,
512}
513
514#[derive(Debug, Deserialize, Clone)]
515pub struct AnthropicStopDetails {
516    #[serde(rename = "type")]
517    stop_details_type: Option<String>,
518    category: Option<String>,
519    explanation: Option<String>,
520    /// Opaque token that represents fallback credit when retrying on another model.
521    /// Present when the refusal qualifies for fallback credit.
522    fallback_credit_token: Option<String>,
523    /// Whether the retry can append an assistant message continuing the refused model's
524    /// partial output (`true`) or must use the unchanged request body (`false`).
525    fallback_has_prefill_claim: Option<bool>,
526}
527
528#[derive(Debug, Deserialize)]
529pub struct AnthropicMessageResponse {
530    id: String,
531    role: String,
532    content: Vec<AnthropicContentBlock>,
533    model: String,
534    stop_reason: Option<String>,
535    stop_sequence: Option<String>,
536    stop_details: Option<AnthropicStopDetails>,
537    pub(crate) usage: AnthropicUsage,
538}
539
540#[derive(Debug, Serialize, Deserialize)]
541pub struct AnthropicUsage {
542    #[serde(default, skip_serializing_if = "Option::is_none")]
543    pub(crate) speed: Option<String>,
544    #[serde(default, deserialize_with = "deserialize_nullable_u32")]
545    pub(crate) input_tokens: u32,
546    #[serde(default, deserialize_with = "deserialize_nullable_u32")]
547    pub(crate) output_tokens: u32,
548    pub(crate) cache_creation_input_tokens: Option<u32>,
549    pub(crate) cache_read_input_tokens: Option<u32>,
550    /// Per-iteration token usage, populated when compaction, advisor, or
551    /// server-side fallback runs. Each entry represents one sampling pass.
552    #[serde(default, skip_serializing_if = "Option::is_none")]
553    iterations: Option<Vec<AnthropicUsageIteration>>,
554}
555
556#[derive(Debug, Serialize, Deserialize)]
557#[serde(tag = "type", rename_all = "snake_case")]
558pub enum AnthropicUsageIteration {
559    Message {
560        model: Option<String>,
561        #[serde(default, deserialize_with = "deserialize_nullable_u32")]
562        input_tokens: u32,
563        #[serde(default, deserialize_with = "deserialize_nullable_u32")]
564        output_tokens: u32,
565        cache_creation_input_tokens: Option<u32>,
566        cache_read_input_tokens: Option<u32>,
567    },
568    FallbackMessage {
569        model: Option<String>,
570        #[serde(default, deserialize_with = "deserialize_nullable_u32")]
571        input_tokens: u32,
572        #[serde(default, deserialize_with = "deserialize_nullable_u32")]
573        output_tokens: u32,
574        cache_creation_input_tokens: Option<u32>,
575        cache_read_input_tokens: Option<u32>,
576    },
577    AdvisorMessage {
578        model: Option<String>,
579        #[serde(default, deserialize_with = "deserialize_nullable_u32")]
580        input_tokens: u32,
581        #[serde(default, deserialize_with = "deserialize_nullable_u32")]
582        output_tokens: u32,
583        cache_creation_input_tokens: Option<u32>,
584        cache_read_input_tokens: Option<u32>,
585    },
586    Compaction {
587        #[serde(default, deserialize_with = "deserialize_nullable_u32")]
588        input_tokens: u32,
589        #[serde(default, deserialize_with = "deserialize_nullable_u32")]
590        output_tokens: u32,
591        cache_creation_input_tokens: Option<u32>,
592        cache_read_input_tokens: Option<u32>,
593    },
594    /// Catch-all for unknown iteration types added by the Anthropic API.
595    #[serde(other)]
596    Unknown,
597}
598
599fn deserialize_nullable_u32<'de, D>(deserializer: D) -> Result<u32, D::Error>
600where
601    D: Deserializer<'de>,
602{
603    Option::<u32>::deserialize(deserializer).map(Option::unwrap_or_default)
604}
605
606#[derive(Debug, Deserialize)]
607pub struct AnthropicErrorBody {
608    #[serde(rename = "type")]
609    error_type: String,
610    pub(crate) message: String,
611}
612
613/// Output configuration for Anthropic API (e.g., effort parameter)
614#[derive(Debug, Serialize, Deserialize, Clone)]
615pub struct AnthropicOutputConfig {
616    #[serde(skip_serializing_if = "Option::is_none")]
617    pub(crate) effort: Option<String>,
618    #[serde(skip_serializing_if = "Option::is_none")]
619    pub(crate) task_budget: Option<AnthropicTaskBudget>,
620    #[serde(skip_serializing_if = "Option::is_none")]
621    pub(crate) format: Option<AnthropicOutputFormat>,
622}
623
624impl AnthropicOutputConfig {
625    fn is_empty(&self) -> bool {
626        self.effort.is_none() && self.task_budget.is_none() && self.format.is_none()
627    }
628
629    fn into_boxed_if_non_empty(self) -> Option<Box<Self>> {
630        (!self.is_empty()).then_some(Box::new(self))
631    }
632}
633
634#[derive(Debug, Serialize, Deserialize, Clone)]
635pub struct AnthropicTaskBudget {
636    #[serde(rename = "type")]
637    pub(crate) budget_type: String,
638    pub(crate) total: u32,
639}
640
641/// Native structured output format for Anthropic responses.
642#[derive(Debug, Serialize, Deserialize, Clone)]
643#[serde(tag = "type", rename_all = "snake_case")]
644pub enum AnthropicOutputFormat {
645    JsonSchema {
646        schema: Value,
647    },
648    /// Catch-all for unknown output format types added by the Anthropic API.
649    #[serde(other)]
650    Unknown,
651}
652
653/// Request body for Anthropic's count_tokens endpoint
654/// <https://docs.anthropic.com/en/api/messages-count-tokens>
655#[derive(Debug, Serialize, Deserialize)]
656pub struct CountTokensRequest {
657    model: String,
658    #[serde(skip_serializing_if = "Option::is_none")]
659    system: Option<Value>,
660    messages: Vec<AnthropicMessage>,
661    #[serde(skip_serializing_if = "Option::is_none")]
662    tools: Option<Vec<AnthropicTool>>,
663    #[serde(skip_serializing_if = "Option::is_none")]
664    thinking: Option<ThinkingConfig>,
665}
666
667fn deserialize_boxed_output_config_opt<'de, D>(deserializer: D) -> Result<Option<Box<AnthropicOutputConfig>>, D::Error>
668where
669    D: Deserializer<'de>,
670{
671    Option::<AnthropicOutputConfig>::deserialize(deserializer)
672        .map(|value| value.and_then(AnthropicOutputConfig::into_boxed_if_non_empty))
673}
674
675/// Response from Anthropic's count_tokens endpoint
676#[derive(Debug, Deserialize)]
677pub struct CountTokensResponse {
678    input_tokens: u32,
679}
680
681#[cfg(test)]
682mod tests {
683    use super::{AnthropicContentBlock, AnthropicOutputConfig, AnthropicRequest, AnthropicStreamEvent, TextCitation};
684
685    #[test]
686    fn stream_content_block_start_thinking_allows_missing_signature() {
687        let payload = r#"{
688            "type": "content_block_start",
689            "index": 0,
690            "content_block": {
691                "type": "thinking",
692                "thinking": "Drafting plan"
693            }
694        }"#;
695
696        let event: AnthropicStreamEvent = serde_json::from_str(payload).expect("should deserialize thinking block");
697        match event {
698            AnthropicStreamEvent::ContentBlockStart {
699                content_block: AnthropicContentBlock::Thinking { thinking, signature, .. },
700                ..
701            } => {
702                assert_eq!(thinking, "Drafting plan");
703                assert!(signature.is_none());
704            }
705            other => panic!("expected thinking content_block_start, got {other:?}"),
706        }
707    }
708
709    #[test]
710    fn stream_content_block_start_accepts_web_search_tool_result() {
711        let payload = r#"{
712            "type": "content_block_start",
713            "index": 1,
714            "content_block": {
715                "type": "web_search_tool_result",
716                "tool_use_id": "srvtoolu_123",
717                "content": [{
718                    "type": "web_search_result",
719                    "title": "Rust Releases",
720                    "url": "https://blog.rust-lang.org"
721                }]
722            }
723        }"#;
724
725        let event: AnthropicStreamEvent =
726            serde_json::from_str(payload).expect("should deserialize web search tool result");
727        match event {
728            AnthropicStreamEvent::ContentBlockStart {
729                content_block: AnthropicContentBlock::WebSearchToolResult { tool_use_id, .. },
730                ..
731            } => assert_eq!(tool_use_id, "srvtoolu_123"),
732            other => panic!("expected web_search_tool_result content_block_start, got {other:?}"),
733        }
734    }
735
736    #[test]
737    fn stream_content_block_start_accepts_bash_code_execution_result() {
738        let payload = r#"{
739            "type": "content_block_start",
740            "index": 1,
741            "content_block": {
742                "type": "bash_code_execution_tool_result",
743                "tool_use_id": "srvtoolu_456",
744                "content": {
745                    "type": "bash_code_execution_result",
746                    "stdout": "Python 3.11.12",
747                    "stderr": "",
748                    "return_code": 0
749                }
750            }
751        }"#;
752
753        let event: AnthropicStreamEvent =
754            serde_json::from_str(payload).expect("should deserialize bash code execution result");
755        match event {
756            AnthropicStreamEvent::ContentBlockStart {
757                content_block: AnthropicContentBlock::BashCodeExecutionToolResult { tool_use_id, .. },
758                ..
759            } => assert_eq!(tool_use_id, "srvtoolu_456"),
760            other => panic!("expected bash_code_execution_tool_result content_block_start, got {other:?}"),
761        }
762    }
763
764    #[test]
765    fn stream_content_block_start_accepts_text_editor_code_execution_result() {
766        let payload = r#"{
767            "type": "content_block_start",
768            "index": 2,
769            "content_block": {
770                "type": "text_editor_code_execution_tool_result",
771                "tool_use_id": "srvtoolu_789",
772                "content": {
773                    "type": "text_editor_code_execution_result",
774                    "is_file_update": false
775                }
776            }
777        }"#;
778
779        let event: AnthropicStreamEvent =
780            serde_json::from_str(payload).expect("should deserialize text editor code execution result");
781        match event {
782            AnthropicStreamEvent::ContentBlockStart {
783                content_block: AnthropicContentBlock::TextEditorCodeExecutionToolResult { tool_use_id, .. },
784                ..
785            } => assert_eq!(tool_use_id, "srvtoolu_789"),
786            other => panic!("expected text_editor_code_execution_tool_result content_block_start, got {other:?}"),
787        }
788    }
789
790    #[test]
791    fn text_block_accepts_web_search_result_citation() {
792        let payload = r#"{
793            "type": "text",
794            "text": "Rust 1.82 shipped",
795            "citations": [{
796                "type": "web_search_result_location",
797                "url": "https://blog.rust-lang.org",
798                "title": "Rust Blog",
799                "encrypted_index": "enc_123",
800                "cited_text": "Rust 1.82 shipped"
801            }]
802        }"#;
803
804        let block: AnthropicContentBlock = serde_json::from_str(payload).expect("should deserialize cited text block");
805        match block {
806            AnthropicContentBlock::Text { citations: Some(citations), .. } => {
807                assert!(matches!(
808                    &citations[0],
809                    TextCitation::WebSearchResultLocation {
810                        url: Some(url),
811                        title: Some(title),
812                        encrypted_index: Some(index),
813                        cited_text: Some(cited_text),
814                    } if url == "https://blog.rust-lang.org"
815                        && title == "Rust Blog"
816                        && index == "enc_123"
817                        && cited_text == "Rust 1.82 shipped"
818                ));
819            }
820            other => panic!("expected text block with citations, got {other:?}"),
821        }
822    }
823
824    #[test]
825    fn empty_output_config_deserializes_to_none() {
826        let request: AnthropicRequest = serde_json::from_str(
827            r#"{
828                "model": "claude-sonnet",
829                "messages": [],
830                "max_tokens": 128,
831                "output_config": {},
832                "stream": false
833            }"#,
834        )
835        .expect("should deserialize request");
836
837        assert!(request.output_config.is_none());
838    }
839
840    #[test]
841    fn boxed_output_config_is_smaller_than_inline_option() {
842        use std::mem::size_of;
843
844        assert!(size_of::<Option<Box<AnthropicOutputConfig>>>() < size_of::<Option<AnthropicOutputConfig>>());
845    }
846}