use super::LlmProvider;
use anyhow::{Context, Result};
use async_trait::async_trait;
use serde_json::json;
use std::time::Duration;
pub struct AnthropicProvider {
client: reqwest::Client,
api_key: String,
model: String,
}
impl AnthropicProvider {
pub fn new(api_key: String, model: Option<String>, timeout_secs: u64) -> Result<Self> {
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(timeout_secs))
.build()
.context("Failed to build reqwest client")?;
Ok(Self {
client,
api_key,
model: model.unwrap_or_else(|| "claude-3-5-haiku-20241022".to_string()),
})
}
}
#[async_trait]
impl LlmProvider for AnthropicProvider {
async fn complete(&self, prompt: &str, _json_mode: bool) -> Result<String> {
let response = self
.client
.post("https://api.anthropic.com/v1/messages")
.header("x-api-key", &self.api_key)
.header("anthropic-version", "2023-06-01")
.header("Content-Type", "application/json")
.json(&json!({
"model": self.model,
"max_tokens": 4000,
"temperature": 0.1,
"messages": [
{
"role": "user",
"content": prompt
}
]
}))
.send()
.await
.context("Failed to send request to Anthropic API")?;
if !response.status().is_success() {
let status = response.status();
let error_text = response.text().await.unwrap_or_else(|_| "Unknown error".to_string());
anyhow::bail!("Anthropic API error ({}): {}", status, error_text);
}
let data: serde_json::Value = response
.json()
.await
.context("Failed to parse Anthropic response as JSON")?;
let content = data["content"][0]["text"]
.as_str()
.context("No content in Anthropic response")?;
Ok(content.to_string())
}
fn name(&self) -> &str {
"anthropic"
}
fn default_model(&self) -> &str {
"claude-3-5-haiku-20241022"
}
}
pub async fn fetch_models(api_key: &str) -> Result<Vec<String>> {
let client = reqwest::Client::new();
let response = client
.get("https://api.anthropic.com/v1/models?limit=1000")
.header("x-api-key", api_key)
.header("anthropic-version", "2023-06-01")
.timeout(std::time::Duration::from_secs(10))
.send()
.await
.context("Failed to fetch models from Anthropic")?;
if !response.status().is_success() {
let status = response.status();
let body = response
.text()
.await
.unwrap_or_else(|_| "Unknown error".to_string());
anyhow::bail!("Anthropic API error ({}): {}", status, body);
}
let data: serde_json::Value = response
.json()
.await
.context("Failed to parse Anthropic models response")?;
if data["has_more"].as_bool() == Some(true) {
log::warn!(
"Anthropic /v1/models returned has_more=true with limit=1000; pagination may be needed"
);
}
let arr = data["data"]
.as_array()
.context("No 'data' array in Anthropic models response")?;
let mut ids: Vec<String> = arr
.iter()
.filter_map(|m| m["id"].as_str().map(String::from))
.collect();
sort_anthropic_models(&mut ids);
Ok(ids)
}
fn sort_anthropic_models(ids: &mut Vec<String>) {
const PREFERRED: &str = "claude-sonnet-4-5";
if let Some(pos) = ids.iter().position(|id| id == PREFERRED) {
let pinned = ids.remove(pos);
ids.insert(0, pinned);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_with_default_model() {
let provider = AnthropicProvider::new("test-key".to_string(), None, 300).unwrap();
assert_eq!(provider.name(), "anthropic");
assert_eq!(provider.model, "claude-3-5-haiku-20241022");
}
#[test]
fn test_new_with_custom_model() {
let provider = AnthropicProvider::new(
"test-key".to_string(),
Some("claude-3-5-sonnet-20241022".to_string()),
300
).unwrap();
assert_eq!(provider.model, "claude-3-5-sonnet-20241022");
}
#[test]
fn test_sort_pins_preferred_first() {
let mut ids = vec![
"claude-opus-4-7".to_string(),
"claude-sonnet-4-6".to_string(),
"claude-sonnet-4-5".to_string(),
"claude-haiku-4-5".to_string(),
];
sort_anthropic_models(&mut ids);
assert_eq!(ids[0], "claude-sonnet-4-5");
}
#[test]
fn test_sort_preserves_order_when_preferred_absent() {
let mut ids = vec![
"claude-opus-4-7".to_string(),
"claude-sonnet-4-6".to_string(),
"claude-haiku-4-5".to_string(),
];
let before = ids.clone();
sort_anthropic_models(&mut ids);
assert_eq!(ids, before);
}
}