Skip to main content

rusty_commit/providers/
anthropic.rs

1use anyhow::{Context, Result};
2use async_trait::async_trait;
3use reqwest::{header, Client};
4use serde::{Deserialize, Serialize};
5
6use super::{split_prompt, AIProvider};
7use crate::config::accounts::AccountConfig;
8use crate::config::Config;
9use crate::utils::retry::retry_async;
10
11pub struct AnthropicProvider {
12    client: Client,
13    api_key: String,
14    model: String,
15}
16
17#[derive(Serialize)]
18struct AnthropicRequest {
19    model: String,
20    messages: Vec<Message>,
21    max_tokens: u32,
22    temperature: f32,
23}
24
25#[derive(Serialize, Deserialize)]
26struct Message {
27    role: String,
28    content: String,
29}
30
31#[derive(Deserialize)]
32struct AnthropicResponse {
33    content: Vec<Content>,
34}
35
36#[derive(Deserialize)]
37struct Content {
38    text: String,
39}
40
41impl AnthropicProvider {
42    pub fn new(config: &Config) -> Result<Self> {
43        // Try OAuth token first, then fall back to API key
44        let api_key = if let Some(token) = crate::auth::token_storage::get_access_token()? {
45            token
46        } else {
47            config
48                .api_key
49                .as_ref()
50                .context(
51                    "Not authenticated with Claude.\nRun: oco auth login (for OAuth)\nOr: rco config set RCO_API_KEY=<your_key>\nGet your API key from: https://console.anthropic.com/settings/keys",
52                )?
53                .clone()
54        };
55
56        let client = Client::new();
57        let model = config
58            .model
59            .as_deref()
60            .unwrap_or("claude-3-5-sonnet-20241022")
61            .to_string();
62
63        Ok(Self {
64            client,
65            api_key,
66            model,
67        })
68    }
69
70    /// Create provider from account configuration
71    #[allow(dead_code)]
72    pub fn from_account(account: &AccountConfig, _api_key: &str, config: &Config) -> Result<Self> {
73        let client = Client::new();
74        let model = account
75            .model
76            .as_deref()
77            .or(config.model.as_deref())
78            .unwrap_or("claude-3-5-sonnet-20241022")
79            .to_string();
80
81        // For accounts, we'll use the api_key from the function parameter
82        // In a full implementation, this would extract from the account's auth method
83        let api_key = _api_key.to_string();
84
85        Ok(Self {
86            client,
87            api_key,
88            model,
89        })
90    }
91}
92
93#[async_trait]
94impl AIProvider for AnthropicProvider {
95    async fn generate_commit_message(
96        &self,
97        diff: &str,
98        context: Option<&str>,
99        full_gitmoji: bool,
100        config: &Config,
101    ) -> Result<String> {
102        let (system_prompt, user_prompt) = split_prompt(diff, context, config, full_gitmoji);
103
104        let request = AnthropicRequest {
105            model: self.model.clone(),
106            messages: vec![
107                Message {
108                    role: "system".to_string(),
109                    content: system_prompt,
110                },
111                Message {
112                    role: "user".to_string(),
113                    content: user_prompt,
114                },
115            ],
116            max_tokens: config.tokens_max_output.unwrap_or(500),
117            temperature: 0.7,
118        };
119
120        let anthropic_response: AnthropicResponse = retry_async(|| async {
121            // Build request with appropriate auth header
122            let mut req = self
123                .client
124                .post("https://api.anthropic.com/v1/messages");
125
126            // Check if this is an OAuth token (starts with "ey") or API key (starts with "sk-")
127            if self.api_key.starts_with("ey") {
128                // OAuth token - use Authorization header
129                req = req.header(header::AUTHORIZATION, format!("Bearer {}", &self.api_key));
130            } else {
131                // API key - use x-api-key header
132                req = req.header("x-api-key", &self.api_key);
133            }
134
135            let response = req
136                .header("anthropic-version", "2023-06-01")
137                .header(header::CONTENT_TYPE, "application/json")
138                .json(&request)
139                .send()
140                .await
141                .context("Failed to connect to Anthropic")?;
142
143            if !response.status().is_success() {
144                let status = response.status();
145                let error_text = response.text().await?;
146
147                if status.as_u16() == 401 {
148                    return Err(anyhow::anyhow!("Invalid Anthropic API key. Please check your API key configuration."));
149                } else if status.as_u16() == 403 {
150                    return Err(anyhow::anyhow!("Access forbidden. Please check your Anthropic API permissions."));
151                } else {
152                    return Err(anyhow::anyhow!("Anthropic API error ({}): {}", status, error_text));
153                }
154            }
155
156            let anthropic_response: AnthropicResponse = response
157                .json()
158                .await
159                .context("Failed to parse Anthropic response")?;
160
161            Ok(anthropic_response)
162        }).await.context("Failed to generate commit message from Anthropic after retries. Please check your internet connection and API configuration.")?;
163
164        let message = anthropic_response
165            .content
166            .first()
167            .map(|c| c.text.trim().to_string())
168            .context("Anthropic returned an empty response. The model may be overloaded - please try again.")?;
169
170        Ok(message)
171    }
172}
173
174/// ProviderBuilder for Anthropic
175pub struct AnthropicProviderBuilder;
176
177impl super::registry::ProviderBuilder for AnthropicProviderBuilder {
178    fn name(&self) -> &'static str {
179        "anthropic"
180    }
181
182    fn aliases(&self) -> Vec<&'static str> {
183        vec!["claude", "claude-code"]
184    }
185
186    fn create(&self, config: &Config) -> Result<Box<dyn AIProvider>> {
187        Ok(Box::new(AnthropicProvider::new(config)?))
188    }
189
190    fn requires_api_key(&self) -> bool {
191        true
192    }
193
194    fn default_model(&self) -> Option<&'static str> {
195        Some("claude-3-5-sonnet-20241022")
196    }
197}