mermaid_cli/providers/model/
anthropic.rs1use std::sync::Arc;
14
15use async_trait::async_trait;
16
17use crate::domain::ChatRequest;
18use crate::models::adapters::anthropic::AnthropicAdapter;
19use crate::models::{
20 Model, ModelConfig, ModelError, ReasoningChunk, Result, StreamCallback,
21 StreamEvent as ModelStreamEvent,
22};
23
24use super::super::capabilities::Capabilities;
25use super::super::ctx::{FinalResponse, StreamContext, StreamEvent};
26use super::{ContextSizing, ModelProvider, resolve_limits_cached};
27
28pub struct AnthropicProvider {
30 adapter: AnthropicAdapter,
31 capabilities: Capabilities,
32}
33
34impl AnthropicProvider {
35 pub fn new(api_key: String, model_name: String, base_url: String) -> Result<Self> {
36 let adapter = AnthropicAdapter::new(api_key, model_name, base_url)?;
37 let capabilities =
38 Capabilities::from_legacy(adapter.capabilities()).with_provider_continuation();
39 Ok(Self {
40 adapter,
41 capabilities,
42 })
43 }
44}
45
46#[async_trait]
47impl ModelProvider for AnthropicProvider {
48 fn capabilities(&self) -> &Capabilities {
49 &self.capabilities
50 }
51
52 async fn resolve_context_window(&self, request: &ChatRequest) -> ContextSizing {
57 let _ = request;
58 let model = Model::name(&self.adapter).to_string();
59 let limits =
60 resolve_limits_cached("anthropic", &model, || self.adapter.fetch_model_limits()).await;
61 let window = limits.as_ref().and_then(|l| l.max_context_tokens);
62 ContextSizing {
63 model_max: window,
64 effective: window,
65 source: None,
66 max_output: limits.as_ref().and_then(|l| l.max_output_tokens),
67 }
68 }
69
70 async fn chat(&self, request: ChatRequest, ctx: StreamContext) -> Result<FinalResponse> {
71 let config = build_model_config(&request);
72 let (relay_tx, relay_handle) = super::stream_bridge::ordered_relay(ctx.sink.clone());
74 let callback = forward_callback(relay_tx.clone());
75 let chat_fut = self
76 .adapter
77 .chat(&request.messages, &config, Some(callback));
78
79 let response = tokio::select! {
80 biased;
81 _ = ctx.token.cancelled() => {
82 return Err(ModelError::Cancelled);
83 },
84 r = chat_fut => r?,
85 };
86
87 let usage = response.usage.clone();
88 let provider_continuation = response.provider_continuation.clone();
89 let stop_reason = response.stop_reason.clone();
90 let _ = relay_tx.send(StreamEvent::Done {
92 usage: usage.clone(),
93 provider_continuation: provider_continuation.clone(),
94 stop_reason: stop_reason.clone(),
95 });
96 drop(relay_tx);
97 crate::utils::join_logged(relay_handle.take(), "stream_relay").await;
98
99 Ok(FinalResponse {
100 usage,
101 provider_continuation,
102 tool_calls: response.tool_calls.unwrap_or_default(),
103 stop_reason,
104 })
105 }
106}
107
108fn build_model_config(request: &ChatRequest) -> ModelConfig {
109 ModelConfig {
110 model: request.model_id.clone(),
111 temperature: request.temperature,
112 max_tokens: request.max_tokens,
113 reasoning: request.reasoning,
114 system_prompt: Some(request.system_prompt.clone()),
115 dynamic_system_suffix: request.instructions.clone(),
116 tools: request.tools.iter().map(|t| t.to_openai_json()).collect(),
117 resolved_context_window: request.resolved_context_window,
118 resolved_max_output: request.resolved_max_output,
119 output_schema: request.output_schema.clone(),
122 ..Default::default()
123 }
124}
125
126fn forward_callback(sink: tokio::sync::mpsc::UnboundedSender<StreamEvent>) -> StreamCallback {
127 Arc::new(move |event: ModelStreamEvent| {
128 let mapped = match event {
129 ModelStreamEvent::Text(s) => StreamEvent::Text(s),
130 ModelStreamEvent::Reasoning(chunk) => StreamEvent::Reasoning(ReasoningChunk {
131 text: chunk.text,
132 signature: chunk.signature,
133 }),
134 ModelStreamEvent::ToolCall(tc) => StreamEvent::ToolCall(tc),
135 ModelStreamEvent::Status(s) => StreamEvent::Status(s),
136 ModelStreamEvent::Done { .. } => StreamEvent::Done {
142 usage: None,
143 provider_continuation: None,
144 stop_reason: None,
145 },
146 };
147 let _ = sink.send(mapped);
148 })
149}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154
155 #[test]
156 fn build_model_config_maps_fields() {
157 let req = ChatRequest {
158 model_id: "anthropic/claude-opus-4-7".to_string(),
159 messages: vec![],
160 system_prompt: "sys".to_string(),
161 instructions: Some("MERMAID.md content".to_string()),
162 reasoning: crate::models::ReasoningLevel::XHigh,
163 temperature: 0.7,
164 max_tokens: 8192,
165 tools: vec![],
166
167 ollama_num_ctx: None,
168 ollama_allow_ram_offload: None,
169 resolved_context_window: None,
170 resolved_max_output: None,
171 output_schema: None,
172 suppress_auto_compact: false,
173 };
174 let cfg = build_model_config(&req);
175 assert_eq!(cfg.reasoning, crate::models::ReasoningLevel::XHigh);
176 assert_eq!(cfg.max_tokens, 8192);
177 assert_eq!(
178 cfg.dynamic_system_suffix.as_deref(),
179 Some("MERMAID.md content")
180 );
181 }
182}