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