228 lines
6.0 KiB
Rust
228 lines
6.0 KiB
Rust
use super::{create_http_client, LlmProvider};
|
|
use anyhow::{bail, Context, Result};
|
|
use async_trait::async_trait;
|
|
use serde::{Deserialize, Serialize};
|
|
use std::time::Duration;
|
|
|
|
/// Anthropic Claude API client
|
|
pub struct AnthropicClient {
|
|
api_key: String,
|
|
model: String,
|
|
client: reqwest::Client,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
struct MessagesRequest {
|
|
model: String,
|
|
max_tokens: u32,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
temperature: Option<f32>,
|
|
messages: Vec<AnthropicMessage>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
system: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
struct AnthropicMessage {
|
|
role: String,
|
|
content: String,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
struct MessagesResponse {
|
|
content: Vec<ContentBlock>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
struct ContentBlock {
|
|
#[serde(rename = "type")]
|
|
content_type: String,
|
|
text: String,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
struct ErrorResponse {
|
|
error: AnthropicError,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
struct AnthropicError {
|
|
#[serde(rename = "type")]
|
|
error_type: String,
|
|
message: String,
|
|
}
|
|
|
|
impl AnthropicClient {
|
|
/// Create new Anthropic client
|
|
pub fn new(api_key: &str, model: &str) -> Result<Self> {
|
|
let client = create_http_client(Duration::from_secs(60))?;
|
|
|
|
Ok(Self {
|
|
api_key: api_key.to_string(),
|
|
model: model.to_string(),
|
|
client,
|
|
})
|
|
}
|
|
|
|
/// Set timeout
|
|
pub fn with_timeout(mut self, timeout: Duration) -> Result<Self> {
|
|
self.client = create_http_client(timeout)?;
|
|
Ok(self)
|
|
}
|
|
|
|
/// Validate API key
|
|
pub async fn validate_key(&self) -> Result<bool> {
|
|
let url = "https://api.anthropic.com/v1/messages";
|
|
|
|
let request = MessagesRequest {
|
|
model: self.model.clone(),
|
|
max_tokens: 5,
|
|
temperature: Some(0.0),
|
|
messages: vec![AnthropicMessage {
|
|
role: "user".to_string(),
|
|
content: "Hi".to_string(),
|
|
}],
|
|
system: None,
|
|
};
|
|
|
|
let response = self.client
|
|
.post(url)
|
|
.header("x-api-key", &self.api_key)
|
|
.header("anthropic-version", "2023-06-01")
|
|
.header("Content-Type", "application/json")
|
|
.json(&request)
|
|
.send()
|
|
.await;
|
|
|
|
match response {
|
|
Ok(resp) => {
|
|
if resp.status().is_success() {
|
|
Ok(true)
|
|
} else {
|
|
let status = resp.status();
|
|
if status.as_u16() == 401 {
|
|
Ok(false)
|
|
} else {
|
|
let text = resp.text().await.unwrap_or_default();
|
|
bail!("Anthropic API error: {} - {}", status, text)
|
|
}
|
|
}
|
|
}
|
|
Err(e) => Err(e.into()),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
impl LlmProvider for AnthropicClient {
|
|
async fn generate(&self, prompt: &str) -> Result<String> {
|
|
let messages = vec![AnthropicMessage {
|
|
role: "user".to_string(),
|
|
content: prompt.to_string(),
|
|
}];
|
|
|
|
self.messages_request(messages, None).await
|
|
}
|
|
|
|
async fn generate_with_system(&self, system: &str, user: &str) -> Result<String> {
|
|
let messages = vec![AnthropicMessage {
|
|
role: "user".to_string(),
|
|
content: user.to_string(),
|
|
}];
|
|
|
|
let system = if system.is_empty() {
|
|
None
|
|
} else {
|
|
Some(system.to_string())
|
|
};
|
|
|
|
self.messages_request(messages, system).await
|
|
}
|
|
|
|
async fn is_available(&self) -> bool {
|
|
self.validate_key().await.unwrap_or(false)
|
|
}
|
|
|
|
fn name(&self) -> &str {
|
|
"anthropic"
|
|
}
|
|
}
|
|
|
|
impl AnthropicClient {
|
|
async fn messages_request(
|
|
&self,
|
|
messages: Vec<AnthropicMessage>,
|
|
system: Option<String>,
|
|
) -> Result<String> {
|
|
let url = "https://api.anthropic.com/v1/messages";
|
|
|
|
let request = MessagesRequest {
|
|
model: self.model.clone(),
|
|
max_tokens: 500,
|
|
temperature: Some(0.7),
|
|
messages,
|
|
system,
|
|
};
|
|
|
|
let response = self.client
|
|
.post(url)
|
|
.header("x-api-key", &self.api_key)
|
|
.header("anthropic-version", "2023-06-01")
|
|
.header("Content-Type", "application/json")
|
|
.json(&request)
|
|
.send()
|
|
.await
|
|
.context("Failed to send request to Anthropic")?;
|
|
|
|
let status = response.status();
|
|
|
|
if !status.is_success() {
|
|
let text = response.text().await.unwrap_or_default();
|
|
|
|
// Try to parse error
|
|
if let Ok(error) = serde_json::from_str::<ErrorResponse>(&text) {
|
|
bail!("Anthropic API error: {} ({})", error.error.message, error.error.error_type);
|
|
}
|
|
|
|
bail!("Anthropic API error: {} - {}", status, text);
|
|
}
|
|
|
|
let result: MessagesResponse = response
|
|
.json()
|
|
.await
|
|
.context("Failed to parse Anthropic response")?;
|
|
|
|
result.content
|
|
.into_iter()
|
|
.find(|c| c.content_type == "text")
|
|
.map(|c| c.text.trim().to_string())
|
|
.ok_or_else(|| anyhow::anyhow!("No text response from Anthropic"))
|
|
}
|
|
}
|
|
|
|
/// Available Anthropic models
|
|
pub const ANTHROPIC_MODELS: &[&str] = &[
|
|
"claude-3-opus-20240229",
|
|
"claude-3-sonnet-20240229",
|
|
"claude-3-haiku-20240307",
|
|
"claude-2.1",
|
|
"claude-2.0",
|
|
"claude-instant-1.2",
|
|
];
|
|
|
|
/// Check if a model name is valid
|
|
pub fn is_valid_model(model: &str) -> bool {
|
|
ANTHROPIC_MODELS.contains(&model)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_model_validation() {
|
|
assert!(is_valid_model("claude-3-sonnet-20240229"));
|
|
assert!(!is_valid_model("invalid-model"));
|
|
}
|
|
}
|