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, 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}