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::ModelProvider;
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_thinking_signature();
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 chat(&self, request: ChatRequest, ctx: StreamContext) -> Result<FinalResponse> {
53 let config = build_model_config(&request);
54 let (relay_tx, relay_handle) = super::stream_bridge::ordered_relay(ctx.sink.clone());
56 let callback = forward_callback(relay_tx.clone());
57 let chat_fut = self
58 .adapter
59 .chat(&request.messages, &config, Some(callback));
60
61 let response = tokio::select! {
62 biased;
63 _ = ctx.token.cancelled() => {
64 return Err(ModelError::Cancelled);
65 },
66 r = chat_fut => r?,
67 };
68
69 let usage = response.usage.clone();
70 let thinking_signature = response.thinking_signature.clone();
71 let stop_reason = response.stop_reason.clone();
72 let _ = relay_tx.send(StreamEvent::Done {
74 usage: usage.clone(),
75 thinking_signature: thinking_signature.clone(),
76 stop_reason: stop_reason.clone(),
77 });
78 drop(relay_tx);
79 let _ = relay_handle.await;
80
81 Ok(FinalResponse {
82 usage,
83 thinking_signature,
84 tool_calls: response.tool_calls.unwrap_or_default(),
85 stop_reason,
86 })
87 }
88}
89
90fn build_model_config(request: &ChatRequest) -> ModelConfig {
91 ModelConfig {
92 model: request.model_id.clone(),
93 temperature: request.temperature,
94 max_tokens: request.max_tokens,
95 reasoning: request.reasoning,
96 system_prompt: Some(request.system_prompt.clone()),
97 dynamic_system_suffix: request.instructions.clone(),
98 tools: request.tools.iter().map(|t| t.to_openai_json()).collect(),
99 ..Default::default()
100 }
101}
102
103fn forward_callback(sink: tokio::sync::mpsc::UnboundedSender<StreamEvent>) -> StreamCallback {
104 Arc::new(move |event: ModelStreamEvent| {
105 let mapped = match event {
106 ModelStreamEvent::Text(s) => StreamEvent::Text(s),
107 ModelStreamEvent::Reasoning(chunk) => StreamEvent::Reasoning(ReasoningChunk {
108 text: chunk.text,
109 signature: chunk.signature,
110 }),
111 ModelStreamEvent::ToolCall(tc) => StreamEvent::ToolCall(tc),
112 ModelStreamEvent::Done { tokens } => StreamEvent::Done {
113 usage: if tokens > 0 {
114 Some(crate::models::TokenUsage::provider(0, tokens, tokens))
115 } else {
116 None
117 },
118 thinking_signature: None,
119 stop_reason: None,
120 },
121 };
122 let _ = sink.send(mapped);
123 })
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129
130 #[test]
131 fn build_model_config_maps_fields() {
132 let req = ChatRequest {
133 model_id: "anthropic/claude-opus-4-7".to_string(),
134 messages: vec![],
135 system_prompt: "sys".to_string(),
136 instructions: Some("MERMAID.md content".to_string()),
137 reasoning: crate::models::ReasoningLevel::XHigh,
138 temperature: 0.7,
139 max_tokens: 8192,
140 tools: vec![],
141
142 ollama_num_ctx: None,
143 ollama_allow_ram_offload: None,
144 };
145 let cfg = build_model_config(&req);
146 assert_eq!(cfg.reasoning, crate::models::ReasoningLevel::XHigh);
147 assert_eq!(cfg.max_tokens, 8192);
148 assert_eq!(
149 cfg.dynamic_system_suffix.as_deref(),
150 Some("MERMAID.md content")
151 );
152 }
153}