rusty_commit/providers/
anthropic.rs1use 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 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 #[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 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 let mut req = self
124 .client
125 .post("https://api.anthropic.com/v1/messages");
126
127 if self.api_key.starts_with("ey") {
129 req = req.header(header::AUTHORIZATION, format!("Bearer {}", &self.api_key));
131 } else {
132 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
175pub 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}