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