1use std::collections::BTreeMap;
4use std::collections::BTreeSet;
5use std::sync::Arc;
6use std::time::SystemTime;
7use std::time::UNIX_EPOCH;
8
9use reqwest::Client;
10use serde::Deserialize;
11use serde_json::Value;
12
13use super::Model;
14use super::ModelEventSink;
15use super::ModelOutput;
16use super::ModelPricing;
17use super::ModelRequest;
18use super::PROMPT_CACHE_BREAKPOINT_FIELD;
19use super::PromptCacheMode;
20use super::REPLAY_REASONING_FIELD;
21use super::StreamingToolCalls;
22use super::TOOL_ERROR_FIELD;
23use super::TOOLS_SEARCH_NAME;
24use super::ToolDefinition;
25use super::image_input;
26use super::provider::HostedWebSearch;
27use super::provider::ProviderAuth;
28use super::provider::ProviderBuildConfig;
29use super::provider::ProviderDefinition;
30use super::provider::validate_base_url;
31use super::transport::SseDecoder;
32use super::transport::frame_data;
33use super::transport::status_error;
34use super::transport::streaming_client;
35use super::usage_i64;
36use crate::BoxFuture;
37use crate::Error;
38use crate::Result;
39use crate::protocol::ModelEvent;
40use crate::protocol::ModelInfo;
41use crate::protocol::ModelStepAnnotation;
42use crate::protocol::ModelStepContent;
43use crate::protocol::ModelStepContentPhase;
44use crate::protocol::TokenUsage;
45use crate::protocol::ToolDiscoveryMode;
46use crate::protocol::ToolLoad;
47use crate::protocol::WebSearchAction;
48
49mod manifest {
50 use crate::backend::model::provider::{HostedWebSearch, ModelPreset, ReasoningPreset};
51 use crate::protocol::ToolDiscoveryMode;
52 pub const PROVIDER_LABEL: &str = "Anthropic";
53 pub const PROVIDER_DESCRIPTION: &str = "Native Messages API with adaptive thinking";
54 pub const TOOL_DISCOVERY: ToolDiscoveryMode = ToolDiscoveryMode::Rebuild;
55 pub const CUSTOM_ENDPOINT_TOOL_DISCOVERY: Option<ToolDiscoveryMode> =
56 Some(ToolDiscoveryMode::Rebuild);
57 pub const DEFAULT_MODEL: Option<&str> = Some("claude-sonnet-5");
58 const REASONING: &[ReasoningPreset] = &[
59 ReasoningPreset {
60 id: "low",
61 label: "Low",
62 description: "Prefer speed and lower cost",
63 },
64 ReasoningPreset {
65 id: "medium",
66 label: "Medium",
67 description: "Balance reasoning and latency",
68 },
69 ReasoningPreset {
70 id: "high",
71 label: "High",
72 description: "Anthropic's default reasoning effort",
73 },
74 ReasoningPreset {
75 id: "xhigh",
76 label: "Extra high",
77 description: "Extended effort for long-horizon work",
78 },
79 ReasoningPreset {
80 id: "max",
81 label: "Maximum",
82 description: "Use maximum available reasoning",
83 },
84 ];
85 pub const MODELS: &[ModelPreset] = &[
86 ModelPreset {
87 id: "claude-sonnet-5",
88 label: "Claude Sonnet 5",
89 description: "Fast frontier model for coding and agents",
90 context_window: 1000000,
91 reasoning: REASONING,
92 default_reasoning: Some("high"),
93 tool_discovery: ToolDiscoveryMode::Rebuild,
94 },
95 ModelPreset {
96 id: "claude-opus-4-8",
97 label: "Claude Opus 4.8",
98 description: "Highest-capability Anthropic model",
99 context_window: 1000000,
100 reasoning: REASONING,
101 default_reasoning: Some("high"),
102 tool_discovery: ToolDiscoveryMode::Native,
103 },
104 ModelPreset {
105 id: "claude-haiku-4-5",
106 label: "Claude Haiku 4.5",
107 description: "Fast, economical Anthropic model",
108 context_window: 200000,
109 reasoning: &[],
110 default_reasoning: None,
111 tool_discovery: ToolDiscoveryMode::Native,
112 },
113 ];
114 pub const SEARCH: &[HostedWebSearch] = &[HostedWebSearch::Off, HostedWebSearch::Live];
115}
116const DEFAULT_BASE_URL: &str = "https://api.anthropic.com/v1";
117const API_VERSION: &str = "2023-06-01";
118const MAX_OUTPUT_TOKENS: u64 = 64_000;
119const MAX_CONTENT_BLOCKS: usize = 1_024;
120const RAW_CONTENT: &str = "_anthropic_content";
121const SONNET_5_STANDARD_PRICING_START_UNIX_SECONDS: u64 = 1_788_220_800;
122
123fn anthropic_model_pricing(model: &str) -> Option<ModelPricing> {
124 let now = SystemTime::now()
125 .duration_since(UNIX_EPOCH)
126 .map_or(0, |duration| duration.as_secs());
127 anthropic_model_pricing_at(model, now)
128}
129
130pub(super) fn anthropic_model_pricing_at(model: &str, unix_seconds: u64) -> Option<ModelPricing> {
131 match model {
132 "claude-sonnet-5" if unix_seconds < SONNET_5_STANDARD_PRICING_START_UNIX_SECONDS => {
133 Some(ModelPricing::new(2_000_000, 200_000, 2_500_000, 10_000_000))
134 }
135 "claude-sonnet-5" => Some(ModelPricing::new(3_000_000, 300_000, 3_750_000, 15_000_000)),
136 "claude-opus-4-8" => Some(ModelPricing::new(5_000_000, 500_000, 6_250_000, 25_000_000)),
137 "claude-haiku-4-5" => Some(ModelPricing::new(1_000_000, 100_000, 1_250_000, 5_000_000)),
138 _ => None,
139 }
140}
141
142pub struct Anthropic {
144 client: Client,
145 api_key: Option<String>,
146 base_url: String,
147 model: String,
148 tool_discovery: ToolDiscoveryMode,
149 reasoning_effort: Option<String>,
150 web_search: bool,
151}
152
153impl Anthropic {
154 pub fn new(
159 api_key: impl Into<String>,
160 base_url: impl Into<String>,
161 model: impl Into<String>,
162 ) -> Result<Self> {
163 Self::with_client(Some(api_key.into()), base_url, model, streaming_client()?)
164 }
165
166 fn with_client(
167 api_key: Option<String>,
168 base_url: impl Into<String>,
169 model: impl Into<String>,
170 client: Client,
171 ) -> Result<Self> {
172 if api_key.as_deref().is_some_and(|key| key.trim().is_empty()) {
173 return Err(Error::Config("ANTHROPIC_API_KEY is empty".into()));
174 }
175 let base_url = base_url.into().trim_end_matches('/').to_string();
176 validate_base_url(&base_url)?;
177 let model = model.into();
178 if model.trim().is_empty() {
179 return Err(Error::Config("Anthropic model is empty".into()));
180 }
181 let tool_discovery = provider().tool_discovery(&model, Some(&base_url));
182 Ok(Self {
183 client,
184 api_key,
185 base_url,
186 model,
187 tool_discovery,
188 reasoning_effort: None,
189 web_search: false,
190 })
191 }
192
193 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Result<Self> {
198 let effort = effort.into();
199 let supported = manifest::MODELS
200 .iter()
201 .find(|model| model.id == self.model)
202 .is_some_and(|model| model.reasoning.iter().any(|preset| preset.id == effort));
203 if !supported {
204 return Err(Error::Config(format!(
205 "model `{}` does not support reasoning effort `{effort}`",
206 self.model
207 )));
208 }
209 self.reasoning_effort = Some(effort);
210 Ok(self)
211 }
212
213 #[must_use]
215 pub fn with_web_search(mut self) -> Self {
216 self.web_search = true;
217 self
218 }
219
220 async fn send_response(
221 &self,
222 request: ModelRequest<'_>,
223 events: ModelEventSink,
224 ) -> Result<ModelOutput> {
225 let body = self.request_body(
226 request.instructions,
227 request.input,
228 request.catalog_revision,
229 request.tools,
230 request.deferred_tools,
231 request.allow_hosted_tools,
232 )?;
233 let mut response = self.post(&body).await?;
234 let mut sse = SseDecoder::default();
235 let mut stream = StreamState::default();
236 while let Some(chunk) = response.chunk().await? {
237 sse.push(&chunk, "Anthropic")?;
238 while let Some(frame) = sse.next_frame()? {
239 let Some(data) = frame_data(frame) else {
240 continue;
241 };
242 stream.apply(serde_json::from_str(&data)?, &events).await?;
243 }
244 }
245 if !stream.stopped {
246 return Err(Error::Provider(
247 "Anthropic stream ended before message_stop".into(),
248 ));
249 }
250 stream.finish()
251 }
252
253 fn request_body(
254 &self,
255 instructions: &str,
256 input: &[Value],
257 catalog_revision: &str,
258 tools: &[ToolDefinition],
259 deferred_tools: &[ToolDefinition],
260 allow_hosted_tools: bool,
261 ) -> Result<Value> {
262 let discovery = self.tool_discovery();
263 let mut body = serde_json::json!({
264 "model": self.model,
265 "max_tokens": MAX_OUTPUT_TOKENS,
266 "system": instructions,
267 "messages": translate_messages(
268 input,
269 discovery,
270 catalog_revision,
271 deferred_tools,
272 )?,
273 "tools": wire_tools(
274 tools,
275 if discovery == ToolDiscoveryMode::Native { deferred_tools } else { &[] },
276 self.web_search && allow_hosted_tools,
277 ),
278 "stream": true
279 });
280 self.apply_reasoning(&mut body);
281 Ok(body)
282 }
283
284 fn apply_reasoning(&self, body: &mut Value) {
285 if let Some(effort) = &self.reasoning_effort {
286 body["thinking"] = serde_json::json!({"type": "adaptive"});
287 body["output_config"] = serde_json::json!({"effort": effort});
288 }
289 }
290
291 async fn post(&self, body: &Value) -> Result<reqwest::Response> {
292 let mut request = self
293 .client
294 .post(format!("{}/messages", self.base_url))
295 .header("anthropic-version", API_VERSION);
296 if let Some(api_key) = &self.api_key {
297 request = request.header("x-api-key", api_key);
298 }
299 let response = request.json(body).send().await?;
300 if response.status().is_success() {
301 Ok(response)
302 } else {
303 Err(status_error(response, "Anthropic").await)
304 }
305 }
306}
307
308impl Model for Anthropic {
309 fn info(&self) -> ModelInfo {
310 ModelInfo {
311 model: self.model.clone(),
312 reasoning_effort: self.reasoning_effort.clone(),
313 }
314 }
315
316 fn supports_tool_image_input(&self) -> bool {
317 self.supports_image_input()
318 }
319
320 fn supports_image_input(&self) -> bool {
321 true
322 }
323
324 fn prompt_cache_capability(&self) -> PromptCacheMode {
325 PromptCacheMode::Explicit
326 }
327
328 fn tool_discovery(&self) -> ToolDiscoveryMode {
329 self.tool_discovery
330 }
331
332 fn pricing(&self) -> Option<ModelPricing> {
333 anthropic_model_pricing(&self.model)
334 }
335
336 fn respond<'a>(
337 &'a self,
338 request: ModelRequest<'a>,
339 events: ModelEventSink,
340 ) -> BoxFuture<'a, Result<ModelOutput>> {
341 Box::pin(self.send_response(request, events))
342 }
343}
344
345#[derive(Default)]
346struct StreamState {
347 blocks: BTreeMap<usize, Value>,
348 partial_json: BTreeMap<usize, String>,
349 completed_blocks: BTreeSet<usize>,
350 next_completed_block: usize,
351 streamed_tool_calls: StreamingToolCalls,
352 web_queries: BTreeMap<String, Option<String>>,
353 usage: Usage,
354 stop_reason: Option<String>,
355 stopped: bool,
356}
357
358impl StreamState {
359 async fn apply(&mut self, event: Value, events: &ModelEventSink) -> Result<()> {
360 match event.get("type").and_then(Value::as_str) {
361 Some("message_start") => self.usage.update(event.pointer("/message/usage"))?,
362 Some("content_block_start") => self.start_block(&event, events).await?,
363 Some("content_block_delta") => self.delta_block(&event, events).await?,
364 Some("content_block_stop") => self.stop_block(&event, events).await?,
365 Some("message_delta") => {
366 self.usage.update(event.get("usage"))?;
367 self.stop_reason = event
368 .pointer("/delta/stop_reason")
369 .and_then(Value::as_str)
370 .map(ToString::to_string);
371 }
372 Some("message_stop") => self.stopped = true,
373 Some("error") => {
374 let message = event
375 .pointer("/error/message")
376 .and_then(Value::as_str)
377 .unwrap_or("Anthropic stream error");
378 return Err(Error::Provider(message.to_string().into()));
379 }
380 Some("ping") | None | Some(_) => {}
381 }
382 Ok(())
383 }
384
385 async fn start_block(&mut self, event: &Value, events: &ModelEventSink) -> Result<()> {
386 let index = event_index(event)?;
387 if self.blocks.contains_key(&index) {
388 return Err(Error::Provider(
389 format!("Anthropic repeated content block index {index}").into(),
390 ));
391 }
392 if self.blocks.len() >= MAX_CONTENT_BLOCKS {
393 return Err(Error::Provider(
394 format!("Anthropic returned more than {MAX_CONTENT_BLOCKS} content blocks").into(),
395 ));
396 }
397 let block = event
398 .get("content_block")
399 .cloned()
400 .ok_or_else(|| Error::Provider("Anthropic content block omitted value".into()))?;
401 if block.get("type").and_then(Value::as_str) == Some("server_tool_use")
402 && block.get("name").and_then(Value::as_str) == Some("web_search")
403 {
404 let id = required_string(&block, "id")?.to_string();
405 let query = block
406 .pointer("/input/query")
407 .and_then(Value::as_str)
408 .map(ToString::to_string);
409 self.web_queries.insert(id.clone(), query);
410 events(ModelEvent::WebSearchStarted { call_id: id }).await?;
411 }
412 if block.get("type").and_then(Value::as_str) == Some("web_search_tool_result") {
413 let call_id = required_string(&block, "tool_use_id")?.to_string();
414 let action = self
415 .web_queries
416 .get(&call_id)
417 .cloned()
418 .flatten()
419 .filter(|query| !query.is_empty())
420 .map_or(WebSearchAction::Other, |query| WebSearchAction::Search {
421 queries: vec![query],
422 });
423 events(ModelEvent::WebSearchCompleted { call_id, action }).await?;
424 }
425 self.blocks.insert(index, block);
426 Ok(())
427 }
428
429 async fn delta_block(&mut self, event: &Value, events: &ModelEventSink) -> Result<()> {
430 let index = event_index(event)?;
431 if self.completed_blocks.contains(&index) {
432 return Err(Error::Provider(
433 format!("Anthropic delta followed completed content block index {index}").into(),
434 ));
435 }
436 let delta = event
437 .get("delta")
438 .ok_or_else(|| Error::Provider("Anthropic content delta omitted value".into()))?;
439 let block = self
440 .blocks
441 .get_mut(&index)
442 .ok_or_else(|| Error::Provider("Anthropic delta referenced unknown block".into()))?;
443 match delta.get("type").and_then(Value::as_str) {
444 Some("text_delta") => {
445 let text = required_string(delta, "text")?;
446 append_string(block, "text", text);
447 events(ModelEvent::TextDelta(text.to_string())).await?;
448 }
449 Some("thinking_delta") => {
450 let thinking = required_string(delta, "thinking")?;
451 append_string(block, "thinking", thinking);
452 events(ModelEvent::ReasoningDelta(thinking.to_string())).await?;
453 }
454 Some("signature_delta") => {
455 block["signature"] =
456 Value::String(required_string(delta, "signature")?.to_string());
457 }
458 Some("input_json_delta") => self
459 .partial_json
460 .entry(index)
461 .or_default()
462 .push_str(required_string(delta, "partial_json")?),
463 Some("citations_delta") => {
464 if let Some(citation) = delta.get("citation") {
465 let citations = block
466 .as_object_mut()
467 .ok_or_else(|| {
468 Error::Provider("Anthropic content block was not an object".into())
469 })?
470 .entry("citations")
471 .or_insert_with(|| Value::Array(Vec::new()));
472 citations
473 .as_array_mut()
474 .ok_or_else(|| {
475 Error::Provider("Anthropic citations were not an array".into())
476 })?
477 .push(citation.clone());
478 }
479 }
480 None | Some(_) => {}
481 }
482 Ok(())
483 }
484
485 async fn stop_block(&mut self, event: &Value, events: &ModelEventSink) -> Result<()> {
486 let index = event_index(event)?;
487 if self.completed_blocks.contains(&index) {
488 return Err(Error::Provider(
489 format!("Anthropic repeated content block stop index {index}").into(),
490 ));
491 }
492 if let Some(partial) = self.partial_json.remove(&index) {
493 let input: Value = serde_json::from_str(&partial)?;
494 let block = self
495 .blocks
496 .get_mut(&index)
497 .ok_or_else(|| Error::Provider("Anthropic stop referenced unknown block".into()))?;
498 block["input"] = input;
499 if block.get("type").and_then(Value::as_str) == Some("server_tool_use")
500 && block.get("name").and_then(Value::as_str) == Some("web_search")
501 {
502 let id = required_string(block, "id")?.to_string();
503 let query = block
504 .pointer("/input/query")
505 .and_then(Value::as_str)
506 .map(ToString::to_string);
507 self.web_queries.insert(id, query);
508 }
509 }
510 self.completed_blocks.insert(index);
511 self.emit_ready_tool_calls(events).await
512 }
513
514 async fn emit_ready_tool_calls(&mut self, events: &ModelEventSink) -> Result<()> {
515 while self.completed_blocks.contains(&self.next_completed_block) {
516 let index = self.next_completed_block;
517 self.next_completed_block = self
518 .next_completed_block
519 .checked_add(1)
520 .ok_or_else(|| Error::Provider("Anthropic block index overflowed".into()))?;
521 let Some(block) = self.blocks.get(&index) else {
522 return Err(Error::Provider(
523 "Anthropic completed block was not started".into(),
524 ));
525 };
526 if block.get("type").and_then(Value::as_str) != Some("tool_use") {
527 continue;
528 }
529 let input = block
530 .get("input")
531 .cloned()
532 .unwrap_or_else(|| serde_json::json!({}));
533 let item = serde_json::json!({
534 "type": "function_call",
535 "call_id": required_string(block, "id")?,
536 "name": required_string(block, "name")?,
537 "arguments": serde_json::to_string(&input)?
538 });
539 let call = super::decode_tool_call(&item)?;
540 self.streamed_tool_calls.accept(&call)?;
541 events(ModelEvent::ToolCallReady(call)).await?;
542 }
543 Ok(())
544 }
545
546 fn finish(self) -> Result<ModelOutput> {
547 let step_content = normalized_step_content(&self.blocks)?;
548 let content = self.blocks.into_values().collect::<Vec<_>>();
549 let calls = content
550 .iter()
551 .filter(|block| block.get("type").and_then(Value::as_str) == Some("tool_use"))
552 .map(|block| {
553 let arguments = block
554 .get("input")
555 .cloned()
556 .unwrap_or_else(|| serde_json::json!({}));
557 Ok(serde_json::json!({
558 "type": "function_call",
559 "call_id": required_string(block, "id")?,
560 "name": required_string(block, "name")?,
561 "arguments": serde_json::to_string(&arguments)?
562 }))
563 })
564 .collect::<Result<Vec<_>>>()?;
565 let visible = content
566 .iter()
567 .filter(|block| block.get("type").and_then(Value::as_str) == Some("text"))
568 .map(|block| {
569 serde_json::json!({
570 "type": "output_text",
571 "text": block.get("text").and_then(Value::as_str).unwrap_or_default()
572 })
573 })
574 .collect::<Vec<_>>();
575 let mut message = serde_json::json!({
576 "type": "message",
577 "role": "assistant",
578 "content": visible
579 });
580 let reasoning = content
581 .iter()
582 .filter(|block| block.get("type").and_then(Value::as_str) == Some("thinking"))
583 .filter_map(|block| block.get("thinking").and_then(Value::as_str))
584 .collect::<Vec<_>>()
585 .join("\n");
586 if !reasoning.is_empty() {
587 message[REPLAY_REASONING_FIELD] = Value::String(reasoning);
588 }
589 message[RAW_CONTENT] = Value::Array(content);
590 let mut output = vec![message];
591 output.extend(calls);
592 ModelOutput::from_output_with_content(
593 output,
594 self.stop_reason.as_deref() != Some("pause_turn"),
595 self.usage.finish()?,
596 step_content,
597 )
598 }
599}
600
601fn normalized_step_content(blocks: &BTreeMap<usize, Value>) -> Result<Vec<ModelStepContent>> {
602 let mut content = Vec::new();
603 for (&part_index, block) in blocks {
604 let (phase, text, annotations) = match block.get("type").and_then(Value::as_str) {
605 Some("thinking") => (
606 ModelStepContentPhase::Reasoning,
607 block.get("thinking").and_then(Value::as_str),
608 Vec::new(),
609 ),
610 Some("text") => (
611 ModelStepContentPhase::FinalAnswer,
612 block.get("text").and_then(Value::as_str),
613 normalize_citations(block)?,
614 ),
615 None | Some(_) => continue,
616 };
617 let Some(text) = text.filter(|text| !text.is_empty()) else {
618 continue;
619 };
620 content.push(ModelStepContent {
621 output_index: 0,
622 part_index,
623 phase,
624 text: text.into(),
625 annotations,
626 });
627 }
628 Ok(content)
629}
630
631fn normalize_citations(block: &Value) -> Result<Vec<ModelStepAnnotation>> {
632 let Some(citations) = block.get("citations") else {
633 return Ok(Vec::new());
634 };
635 if citations.is_null() {
636 return Ok(Vec::new());
637 }
638 let citations: Vec<AnthropicCitation> = serde_json::from_value(citations.clone())
639 .map_err(|error| Error::Provider(format!("invalid Anthropic citation: {error}").into()))?;
640 Ok(citations.into_iter().map(Into::into).collect())
641}
642
643#[derive(Deserialize)]
644#[serde(tag = "type", deny_unknown_fields)]
645enum AnthropicCitation {
646 #[serde(rename = "char_location")]
647 Character {
648 cited_text: String,
649 document_index: usize,
650 document_title: Option<String>,
651 file_id: Option<String>,
652 start_char_index: usize,
653 end_char_index: usize,
654 },
655 #[serde(rename = "page_location")]
656 Page {
657 cited_text: String,
658 document_index: usize,
659 document_title: Option<String>,
660 file_id: Option<String>,
661 start_page_number: usize,
662 end_page_number: usize,
663 },
664 #[serde(rename = "content_block_location")]
665 ContentBlock {
666 cited_text: String,
667 document_index: usize,
668 document_title: Option<String>,
669 file_id: Option<String>,
670 start_block_index: usize,
671 end_block_index: usize,
672 },
673 #[serde(rename = "search_result_location")]
674 SearchResult {
675 cited_text: String,
676 search_result_index: usize,
677 source: String,
678 title: Option<String>,
679 start_block_index: usize,
680 end_block_index: usize,
681 },
682 #[serde(rename = "web_search_result_location")]
683 WebSearchResult {
684 cited_text: String,
685 encrypted_index: String,
686 title: Option<String>,
687 url: String,
688 },
689}
690
691impl From<AnthropicCitation> for ModelStepAnnotation {
692 fn from(citation: AnthropicCitation) -> Self {
693 match citation {
694 AnthropicCitation::Character {
695 cited_text,
696 document_index,
697 document_title,
698 file_id,
699 start_char_index,
700 end_char_index,
701 } => Self::DocumentCharacterCitation {
702 cited_text,
703 document_index,
704 document_title,
705 file_id,
706 start_char_index,
707 end_char_index,
708 },
709 AnthropicCitation::Page {
710 cited_text,
711 document_index,
712 document_title,
713 file_id,
714 start_page_number,
715 end_page_number,
716 } => Self::DocumentPageCitation {
717 cited_text,
718 document_index,
719 document_title,
720 file_id,
721 start_page_number,
722 end_page_number,
723 },
724 AnthropicCitation::ContentBlock {
725 cited_text,
726 document_index,
727 document_title,
728 file_id,
729 start_block_index,
730 end_block_index,
731 } => Self::DocumentContentBlockCitation {
732 cited_text,
733 document_index,
734 document_title,
735 file_id,
736 start_block_index,
737 end_block_index,
738 },
739 AnthropicCitation::SearchResult {
740 cited_text,
741 search_result_index,
742 source,
743 title,
744 start_block_index,
745 end_block_index,
746 } => Self::SearchResultCitation {
747 cited_text,
748 search_result_index,
749 source,
750 title,
751 start_block_index,
752 end_block_index,
753 },
754 AnthropicCitation::WebSearchResult {
755 cited_text,
756 encrypted_index,
757 title,
758 url,
759 } => Self::WebSearchResultCitation {
760 cited_text,
761 encrypted_index,
762 title,
763 url,
764 },
765 }
766 }
767}
768
769#[derive(Default)]
770struct Usage {
771 input: i64,
772 cache_read: i64,
773 cache_write: i64,
774 output: i64,
775 thinking: i64,
776}
777
778impl Usage {
779 fn update(&mut self, usage: Option<&Value>) -> Result<()> {
780 let Some(usage) = usage else {
781 return Ok(());
782 };
783 update_i64(&mut self.input, usage, "/input_tokens")?;
784 update_i64(&mut self.cache_read, usage, "/cache_read_input_tokens")?;
785 update_i64(&mut self.cache_write, usage, "/cache_creation_input_tokens")?;
786 update_i64(&mut self.output, usage, "/output_tokens")?;
787 update_i64(
788 &mut self.thinking,
789 usage,
790 "/output_tokens_details/thinking_tokens",
791 )?;
792 Ok(())
793 }
794
795 fn finish(self) -> Result<TokenUsage> {
796 let input_tokens = self
797 .input
798 .checked_add(self.cache_read)
799 .and_then(|tokens| tokens.checked_add(self.cache_write))
800 .ok_or_else(|| Error::Provider("Anthropic token usage overflowed".into()))?;
801 let total_tokens = input_tokens
802 .checked_add(self.output)
803 .ok_or_else(|| Error::Provider("Anthropic token usage overflowed".into()))?;
804 Ok(TokenUsage {
805 input_tokens,
806 cached_input_tokens: self.cache_read,
807 cache_write_input_tokens: self.cache_write,
808 output_tokens: self.output,
809 reasoning_output_tokens: self.thinking,
810 total_tokens,
811 })
812 }
813}
814
815fn translate_messages(
816 input: &[Value],
817 discovery: ToolDiscoveryMode,
818 catalog_revision: &str,
819 deferred_tools: &[ToolDefinition],
820) -> Result<Vec<Value>> {
821 let mut messages = Vec::new();
822 let mut preserved_tools = BTreeSet::new();
823 let mut search_calls = BTreeSet::new();
824 let deferred_tool_names = deferred_tools
825 .iter()
826 .map(|tool| tool.name.as_str())
827 .collect::<BTreeSet<_>>();
828 for (index, item) in input.iter().enumerate() {
829 let kind = item
830 .get("type")
831 .and_then(Value::as_str)
832 .or_else(|| item.get("role").is_some().then_some("message"));
833 match kind {
834 Some("message") => {
835 let role = item
836 .get("role")
837 .and_then(Value::as_str)
838 .ok_or_else(|| Error::Provider("history message omitted role".into()))?;
839 if let Some(content) = item.get(RAW_CONTENT).and_then(Value::as_array) {
840 preserved_tools.extend(
841 content
842 .iter()
843 .filter(|block| {
844 block.get("type").and_then(Value::as_str) == Some("tool_use")
845 })
846 .filter_map(|block| block.get("id").and_then(Value::as_str))
847 .map(ToString::to_string),
848 );
849 push_message(&mut messages, role, content.clone());
850 } else {
851 let blocks = item
852 .get("content")
853 .and_then(Value::as_array)
854 .into_iter()
855 .flatten()
856 .map(content_block)
857 .filter_map(Result::transpose)
858 .collect::<Result<Vec<_>>>()?;
859 push_message(&mut messages, role, blocks);
860 }
861 }
862 Some("function_call") => {
863 let call_id = required_string(item, "call_id")?;
864 let name = required_string(item, "name")?;
865 remember_search_call(&mut search_calls, call_id, name);
866 if !preserved_tools.contains(call_id) {
867 push_message(
868 &mut messages,
869 "assistant",
870 vec![serde_json::json!({
871 "type": "tool_use",
872 "id": call_id,
873 "name": name,
874 "input": serde_json::from_str::<Value>(required_string(item, "arguments")?)?
875 })],
876 );
877 }
878 }
879 Some("function_call_output") => push_message(
880 &mut messages,
881 "user",
882 vec![tool_result_block(
883 item,
884 input.get(index + 1),
885 discovery,
886 catalog_revision,
887 &search_calls,
888 &deferred_tool_names,
889 )?],
890 ),
891 Some("tool_load") => replay_standalone_tool_load(
892 &mut messages,
893 ToolLoad::from_input(item)?,
894 follows_search_result(input.get(index.saturating_sub(1)), &search_calls),
895 index,
896 discovery,
897 catalog_revision,
898 &deferred_tool_names,
899 ),
900 None | Some(_) => {}
901 }
902 }
903 if messages.is_empty() {
904 return Err(Error::Provider("Anthropic request has no messages".into()));
905 }
906 Ok(messages)
907}
908
909fn remember_search_call(search_calls: &mut BTreeSet<String>, call_id: &str, name: &str) {
910 if name == TOOLS_SEARCH_NAME {
911 search_calls.insert(call_id.to_string());
912 }
913}
914
915fn content_block(part: &Value) -> Result<Option<Value>> {
916 let mut block = match part.get("type").and_then(Value::as_str) {
917 Some("input_text" | "output_text") => {
918 serde_json::json!({"type":"text", "text": required_string(part, "text")?})
919 }
920 Some("input_image") => {
921 let Some((media_type, data)) = image_input(part, "Anthropic")? else {
922 return Ok(None);
923 };
924 serde_json::json!({"type":"image", "source":{"type":"base64", "media_type":media_type,"data":data}})
925 }
926 Some("file") => {
927 serde_json::json!({"type":"text", "text":format!("Stored file: {}", part["file"])})
928 }
929 _ => return Ok(None),
930 };
931 if part
932 .get(PROMPT_CACHE_BREAKPOINT_FIELD)
933 .and_then(Value::as_bool)
934 == Some(true)
935 {
936 block["cache_control"] = serde_json::json!({"type":"ephemeral"});
937 }
938 Ok(Some(block))
939}
940
941fn tool_content_blocks(output: &Value) -> Result<Vec<Value>> {
942 output
943 .as_array()
944 .ok_or_else(|| Error::Provider("tool result content must be an array".into()))?
945 .iter()
946 .map(|part| {
947 content_block(part)?
948 .ok_or_else(|| Error::Provider("unsupported tool result content".into()))
949 })
950 .collect()
951}
952
953fn tool_result_block(
954 item: &Value,
955 next: Option<&Value>,
956 discovery: ToolDiscoveryMode,
957 catalog_revision: &str,
958 search_calls: &BTreeSet<String>,
959 deferred_tool_names: &BTreeSet<&str>,
960) -> Result<Value> {
961 let call_id = required_string(item, "call_id")?;
962 let load = next.map(ToolLoad::from_input).transpose()?.flatten();
963 let references = load
964 .filter(|_| discovery == ToolDiscoveryMode::Native && search_calls.contains(call_id))
965 .map_or_else(Vec::new, |load| {
966 tool_references(load, catalog_revision, deferred_tool_names)
967 });
968 let content = if references.is_empty() {
969 Value::Array(tool_content_blocks(item.get("output").ok_or_else(
970 || Error::Provider("tool result omitted content".into()),
971 )?)?)
972 } else {
973 Value::Array(references)
974 };
975 Ok(serde_json::json!({
976 "type": "tool_result",
977 "tool_use_id": call_id,
978 "content": content,
979 "is_error": item.get(TOOL_ERROR_FIELD).and_then(Value::as_bool).unwrap_or(false)
980 }))
981}
982
983fn follows_search_result(previous: Option<&Value>, search_calls: &BTreeSet<String>) -> bool {
984 previous
985 .filter(|item| item.get("type").and_then(Value::as_str) == Some("function_call_output"))
986 .and_then(|item| item.get("call_id").and_then(Value::as_str))
987 .is_some_and(|call_id| search_calls.contains(call_id))
988}
989
990fn replay_standalone_tool_load(
991 messages: &mut Vec<Value>,
992 load: Option<ToolLoad>,
993 follows_search_result: bool,
994 index: usize,
995 discovery: ToolDiscoveryMode,
996 catalog_revision: &str,
997 deferred_tool_names: &BTreeSet<&str>,
998) {
999 let Some(load) = load else {
1000 return;
1001 };
1002 if discovery != ToolDiscoveryMode::Native || follows_search_result {
1003 return;
1004 }
1005 let references = tool_references(load, catalog_revision, deferred_tool_names);
1006 if references.is_empty() {
1007 return;
1008 }
1009 let call_id = format!("mobius-tool-load-{index}");
1010 push_message(
1011 messages,
1012 "assistant",
1013 vec![serde_json::json!({
1014 "type": "tool_use",
1015 "id": call_id,
1016 "name": TOOLS_SEARCH_NAME,
1017 "input": {"query": "restore loaded session tools"}
1018 })],
1019 );
1020 push_message(
1021 messages,
1022 "user",
1023 vec![serde_json::json!({
1024 "type": "tool_result",
1025 "tool_use_id": call_id,
1026 "content": references,
1027 "is_error": false
1028 })],
1029 );
1030}
1031
1032fn tool_references(
1033 load: ToolLoad,
1034 catalog_revision: &str,
1035 deferred_tool_names: &BTreeSet<&str>,
1036) -> Vec<Value> {
1037 if load.catalog_revision != catalog_revision {
1038 return Vec::new();
1039 }
1040 load.tools
1041 .into_iter()
1042 .filter(|name| deferred_tool_names.contains(name.as_str()))
1043 .map(|name| {
1044 serde_json::json!({
1045 "type": "tool_reference",
1046 "tool_name": name
1047 })
1048 })
1049 .collect()
1050}
1051
1052fn push_message(messages: &mut Vec<Value>, role: &str, blocks: Vec<Value>) {
1053 if blocks.is_empty() {
1054 return;
1055 }
1056 if let Some(last) = messages.last_mut()
1057 && last.get("role").and_then(Value::as_str) == Some(role)
1058 && let Some(content) = last.get_mut("content").and_then(Value::as_array_mut)
1059 {
1060 content.extend(blocks);
1061 return;
1062 }
1063 messages.push(serde_json::json!({"role": role, "content": blocks}));
1064}
1065
1066fn wire_tools(
1067 tools: &[ToolDefinition],
1068 deferred_tools: &[ToolDefinition],
1069 web_search: bool,
1070) -> Vec<Value> {
1071 let mut output = tools
1072 .iter()
1073 .map(|tool| (tool, false))
1074 .chain(deferred_tools.iter().map(|tool| (tool, true)))
1075 .map(|(tool, deferred)| {
1076 let mut wire = serde_json::json!({
1077 "name": tool.name,
1078 "description": tool.description,
1079 "input_schema": tool.parameters
1080 });
1081 if deferred {
1082 wire["defer_loading"] = Value::Bool(true);
1083 }
1084 wire
1085 })
1086 .collect::<Vec<_>>();
1087 if web_search {
1088 output.push(serde_json::json!({
1089 "type": "web_search_20260318",
1090 "name": "web_search"
1091 }));
1092 }
1093 output
1094}
1095
1096fn event_index(event: &Value) -> Result<usize> {
1097 event
1098 .get("index")
1099 .and_then(Value::as_u64)
1100 .and_then(|index| usize::try_from(index).ok())
1101 .ok_or_else(|| Error::Provider("Anthropic event omitted block index".into()))
1102}
1103
1104fn required_string<'a>(value: &'a Value, field: &str) -> Result<&'a str> {
1105 let value = string_field(value, field)?;
1106 if value.is_empty() {
1107 return Err(Error::Provider(
1108 format!("Anthropic value omitted {field}").into(),
1109 ));
1110 }
1111 Ok(value)
1112}
1113
1114fn string_field<'a>(value: &'a Value, field: &str) -> Result<&'a str> {
1115 value
1116 .get(field)
1117 .and_then(Value::as_str)
1118 .ok_or_else(|| Error::Provider(format!("Anthropic value omitted {field}").into()))
1119}
1120
1121fn append_string(value: &mut Value, field: &str, addition: &str) {
1122 if let Some(Value::String(current)) = value.get_mut(field) {
1123 current.push_str(addition);
1124 } else {
1125 value[field] = Value::String(addition.to_string());
1126 }
1127}
1128
1129fn update_i64(target: &mut i64, value: &Value, path: &str) -> Result<()> {
1130 if let Some(value) = usage_i64(Some(value), path, "Anthropic")? {
1131 *target = value;
1132 }
1133 Ok(())
1134}
1135
1136pub(super) const fn provider() -> ProviderDefinition {
1137 ProviderDefinition::new(
1138 "anthropic",
1139 manifest::PROVIDER_LABEL,
1140 "claude",
1141 manifest::PROVIDER_DESCRIPTION,
1142 ProviderAuth::ApiKey("ANTHROPIC_API_KEY"),
1143 manifest::MODELS,
1144 manifest::DEFAULT_MODEL,
1145 manifest::SEARCH,
1146 build_provider,
1147 )
1148 .with_image_input()
1149 .with_base_url(DEFAULT_BASE_URL)
1150 .with_tool_discovery(
1151 manifest::TOOL_DISCOVERY,
1152 manifest::CUSTOM_ENDPOINT_TOOL_DISCOVERY,
1153 )
1154 .with_credentialless_endpoints()
1155}
1156
1157fn build_provider(config: ProviderBuildConfig) -> Result<Arc<dyn Model>> {
1158 let base_url = config
1159 .base_url
1160 .ok_or_else(|| Error::Config("Anthropic requires a base URL".into()))?;
1161 let api_key = config.credential.into_optional_api_key("anthropic")?;
1162 let provider = Anthropic::with_client(api_key, base_url, config.model, config.http)?;
1163 let provider = match config.reasoning_effort {
1164 Some(effort) => provider.with_reasoning_effort(effort)?,
1165 None => provider,
1166 };
1167 let provider = match config.web_search {
1168 HostedWebSearch::Off => provider,
1169 HostedWebSearch::Cached => {
1170 return Err(Error::Config(
1171 "Anthropic does not support cached web search".into(),
1172 ));
1173 }
1174 HostedWebSearch::Live => provider.with_web_search(),
1175 };
1176 Ok(Arc::new(provider))
1177}
1178
1179#[cfg(test)]
1180#[path = "anthropic_tests.rs"]
1181mod tests;