mermaid_cli/providers/model/
anthropic.rs1use async_trait::async_trait;
14
15use crate::domain::ChatRequest;
16use crate::models::adapters::anthropic::AnthropicAdapter;
17use crate::models::{Model, ModelConfig, ModelError, Result};
18
19use super::super::capabilities::Capabilities;
20use super::super::ctx::{FinalResponse, StreamContext, StreamEvent};
21use super::{ContextSizing, ModelProvider, resolve_limits_cached};
22
23pub const DEFAULT_BASE_URL: &str = "https://api.anthropic.com/v1";
29pub const DEFAULT_API_KEY_ENV: &str = "ANTHROPIC_API_KEY";
30
31pub struct AnthropicProvider {
33 adapter: AnthropicAdapter,
34 capabilities: Capabilities,
35}
36
37impl AnthropicProvider {
38 pub fn new(api_key: String, model_name: String, base_url: String) -> Result<Self> {
39 let adapter = AnthropicAdapter::new(api_key, model_name, base_url)?;
40 let capabilities =
41 Capabilities::from_legacy(adapter.capabilities()).with_provider_continuation();
42 Ok(Self {
43 adapter,
44 capabilities,
45 })
46 }
47}
48
49#[async_trait]
50impl ModelProvider for AnthropicProvider {
51 fn capabilities(&self) -> &Capabilities {
52 &self.capabilities
53 }
54
55 async fn resolve_context_window(&self, request: &ChatRequest) -> ContextSizing {
60 let _ = request;
61 let model = Model::name(&self.adapter).to_string();
62 let limits =
63 resolve_limits_cached("anthropic", &model, || self.adapter.fetch_model_limits()).await;
64 let window = limits.as_ref().and_then(|l| l.max_context_tokens);
65 ContextSizing {
66 model_max: window,
67 effective: window,
68 source: None,
69 max_output: limits.as_ref().and_then(|l| l.max_output_tokens),
70 }
71 }
72
73 async fn chat(&self, request: ChatRequest, ctx: StreamContext) -> Result<FinalResponse> {
74 let config = build_model_config(&request);
75 let (relay_tx, relay_handle) = super::stream_bridge::ordered_relay(ctx.sink.clone());
77 let callback = super::stream_bridge::forward_callback(relay_tx.clone());
78 let chat_fut = self
79 .adapter
80 .chat(&request.messages, &config, Some(callback));
81
82 let response = tokio::select! {
83 biased;
84 _ = ctx.token.cancelled() => {
85 return Err(ModelError::Cancelled);
86 },
87 r = chat_fut => r?,
88 };
89
90 let usage = response.usage.clone();
91 let provider_continuation = response.provider_continuation.clone();
92 let stop_reason = response.stop_reason.clone();
93 let _ = relay_tx.send(StreamEvent::Done {
95 usage: usage.clone(),
96 provider_continuation: provider_continuation.clone(),
97 stop_reason: stop_reason.clone(),
98 });
99 drop(relay_tx);
100 crate::utils::join_logged(relay_handle.take(), "stream_relay").await;
101
102 Ok(FinalResponse {
103 usage,
104 provider_continuation,
105 tool_calls: response.tool_calls.unwrap_or_default(),
106 stop_reason,
107 })
108 }
109}
110
111fn build_model_config(request: &ChatRequest) -> ModelConfig {
112 ModelConfig {
113 model: request.model_id.clone(),
114 temperature: request.temperature,
115 max_tokens: request.max_tokens,
116 reasoning: request.reasoning,
117 system_prompt: Some(request.system_prompt.clone()),
118 dynamic_system_suffix: request.instructions.clone(),
119 tools: request.tools.iter().map(|t| t.to_openai_json()).collect(),
120 resolved_context_window: request.resolved_context_window,
121 resolved_max_output: request.resolved_max_output,
122 output_schema: request.output_schema.clone(),
125 ..Default::default()
126 }
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132
133 #[test]
134 fn build_model_config_maps_fields() {
135 let req = ChatRequest {
136 model_id: "anthropic/claude-opus-4-7".to_string(),
137 messages: vec![],
138 system_prompt: "sys".to_string(),
139 instructions: Some("MERMAID.md content".to_string()),
140 reasoning: crate::models::ReasoningLevel::XHigh,
141 temperature: 0.7,
142 max_tokens: 8192,
143 tools: vec![],
144
145 ollama_num_ctx: None,
146 ollama_allow_ram_offload: None,
147 resolved_context_window: None,
148 resolved_max_output: None,
149 output_schema: None,
150 suppress_auto_compact: false,
151 suppressed_builtin_tools: Vec::new(),
152 };
153 let cfg = build_model_config(&req);
154 assert_eq!(cfg.reasoning, crate::models::ReasoningLevel::XHigh);
155 assert_eq!(cfg.max_tokens, 8192);
156 assert_eq!(
157 cfg.dynamic_system_suffix.as_deref(),
158 Some("MERMAID.md content")
159 );
160 }
161}