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