Skip to main content

mobius/backend/model/
anthropic.rs

1//! Native Anthropic Messages API provider.
2
3use 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
142/// Anthropic's native Messages API provider.
143pub 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    /// Creates a provider for an Anthropic Messages API endpoint.
155    /// # Errors
156    ///
157    /// Returns an error if configuration is invalid or a required resource cannot be initialized.
158    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    /// Enables adaptive thinking at one supported Anthropic effort level.
194    /// # Errors
195    ///
196    /// Returns an error if validation or an operation required by this function fails.
197    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    /// Enables Anthropic-hosted web search.
214    #[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;