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