use std::sync::Arc; use anyhow::{Context, Result, anyhow}; use crate::chatbot::anthropic::AnthropicClient; use crate::llm::{LlmModelRecord, LlmProviderRecord}; use crate::llm::providers::{RemoteLlmModelInfo, extra_with_reasoning}; use crate::provider::{ApiProvider, BuiltLlmClient, ProviderField, ProviderUiMeta, ReasoningMode, ServiceType}; pub struct AnthropicProvider { http: reqwest::Client, } impl AnthropicProvider { pub fn new() -> Self { Self { http: reqwest::Client::new() } } } #[async_trait::async_trait] impl ApiProvider for AnthropicProvider { fn type_id(&self) -> &'static str { "anthropic" } fn display_name(&self) -> &'static str { "Anthropic" } fn supported_types(&self) -> &'static [ServiceType] { &[ServiceType::Llm] } async fn list_llm_models(&self, _record: &LlmProviderRecord) -> Result>> { Ok(None) } async fn llm_model_info(&self, record: &LlmProviderRecord, model_id: &str) -> Result> { let api_key = record.api_key.as_deref() .ok_or_else(|| anyhow!("provider '{}': api_key required for anthropic model_info", record.name))?; let url = format!("https://api.anthropic.com/v1/models/{model_id}"); let resp: serde_json::Value = self.http .get(&url) .header("x-api-key", api_key) .header("anthropic-version", "2023-06-01") .send() .await .map_err(|e| anyhow!("Anthropic model_info request failed: {e}"))? .json() .await .map_err(|e| anyhow!("Anthropic model_info response parse failed: {e}"))?; let id = resp["id"].as_str().ok_or_else(|| anyhow!("missing 'id' in Anthropic response"))?.to_string(); let name = resp["display_name"].as_str().unwrap_or(&id).to_string(); Ok(Some(RemoteLlmModelInfo { id, name, context_length: resp["context_window"].as_u64(), max_completion_tokens: resp["max_output_tokens"].as_u64(), knowledge_cutoff: None, capabilities: vec![], vision: None, price_input_per_million: None, price_output_per_million: None, reasoning: None, })) } fn reasoning_mode(&self, model_id: &str, capabilities: &[String]) -> Option { // Extended thinking → numeric token budget. Available on Claude 3.7 and // the 4.x/5.x families (not the 3.5/3-opus generation). let id = model_id.to_lowercase(); let supports = capabilities.iter().any(|c| c == "reasoning") || id.contains("3-7") || id.contains("-4") || id.contains("-5") || id.contains("opus-4") || id.contains("sonnet-4") || id.contains("haiku-4"); if supports { Some(ReasoningMode::Range { min: 1024, max: 32_000, step: Some(1024), default: Some(8192), unit: Some("tokens".to_string()), }) } else { None } } fn reasoning_request(&self, value: &serde_json::Value) -> Option { // value is a JSON number (budget_tokens). let budget = value.as_i64().filter(|n| *n > 0)?; Some(serde_json::json!({ "thinking": { "type": "enabled", "budget_tokens": budget } })) } fn build_llm(&self, record: &LlmProviderRecord, model: &LlmModelRecord) -> Option> { Some((|| { let key = record.api_key.as_deref() .with_context(|| format!("provider '{}': api_key required for anthropic", record.name))?; // Merge model extra_params + reasoning (thinking) into the request body. let extra = extra_with_reasoning(self, model); Ok(BuiltLlmClient { client: Arc::new(AnthropicClient::with_extra_body(key, extra)), prompt_cache: false, }) })()) } fn ui_meta(&self) -> ProviderUiMeta { ProviderUiMeta { type_id: "anthropic", display_name: "Anthropic", description: None, color: "#d4a574", icon: "bi-chat-square-dots", fields: &[ ProviderField { key: "api_key", label: "API Key", required: true, secret: true }, ], } } }