rusty_commit/providers/
perplexity.rs1use anyhow::{Context, Result};
2use async_trait::async_trait;
3use reqwest::Client;
4use serde::{Deserialize, Serialize};
5
6use super::prompt::split_prompt;
7use super::AIProvider;
8use crate::config::Config;
9
10pub struct PerplexityProvider {
11 client: Client,
12 model: String,
13 api_key: String,
14}
15
16#[derive(Serialize)]
17struct PerplexityRequest {
18 model: String,
19 messages: Vec<Message>,
20 max_tokens: u32,
21 temperature: f32,
22 stream: bool,
23}
24
25#[derive(Serialize)]
26struct Message {
27 role: String,
28 content: String,
29}
30
31#[derive(Deserialize)]
32struct PerplexityResponse {
33 choices: Vec<Choice>,
34}
35
36#[derive(Deserialize)]
37struct Choice {
38 message: MessageResponse,
39}
40
41#[derive(Deserialize)]
42struct MessageResponse {
43 content: String,
44}
45
46impl PerplexityProvider {
47 pub fn new(config: &Config) -> Result<Self> {
48 let api_key = config
49 .api_key
50 .as_ref()
51 .context("Perplexity API key not configured.\nRun: rco config set RCO_API_KEY=<your_key>\nGet your API key from: https://www.perplexity.ai/settings/api")?;
52
53 let client = Client::new();
54 let model = config
55 .model
56 .as_deref()
57 .unwrap_or("llama-3.1-sonar-small-128k-online")
58 .to_string();
59
60 Ok(Self {
61 client,
62 model,
63 api_key: api_key.clone(),
64 })
65 }
66
67 #[allow(dead_code)]
69 pub fn from_account(
70 _account: &crate::config::accounts::AccountConfig,
71 api_key: &str,
72 config: &Config,
73 ) -> 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("llama-3.1-sonar-small-128k-online")
80 .to_string();
81
82 Ok(Self {
83 client,
84 model,
85 api_key: api_key.to_string(),
86 })
87 }
88}
89
90#[async_trait]
91impl AIProvider for PerplexityProvider {
92 async fn generate_commit_message(
93 &self,
94 diff: &str,
95 context: Option<&str>,
96 full_gitmoji: bool,
97 config: &Config,
98 ) -> Result<String> {
99 let (system_prompt, user_prompt) = split_prompt(diff, context, config, full_gitmoji);
100
101 let messages = vec![
102 Message {
103 role: "system".to_string(),
104 content: system_prompt,
105 },
106 Message {
107 role: "user".to_string(),
108 content: user_prompt,
109 },
110 ];
111
112 let request = PerplexityRequest {
113 model: self.model.clone(),
114 messages,
115 max_tokens: config.tokens_max_output.unwrap_or(500),
116 temperature: 0.7,
117 stream: false,
118 };
119
120 let api_url = config
121 .api_url
122 .as_deref()
123 .unwrap_or("https://api.perplexity.ai/chat/completions");
124
125 let response = match self
126 .client
127 .post(api_url)
128 .header("Authorization", format!("Bearer {}", self.api_key))
129 .header("Content-Type", "application/json")
130 .json(&request)
131 .send()
132 .await
133 {
134 Ok(resp) => resp,
135 Err(e) => {
136 anyhow::bail!("Failed to connect to Perplexity API: {}. Please check your internet connection.", e);
137 }
138 };
139
140 if !response.status().is_success() {
141 let status = response.status();
142 let error_text = response.text().await.unwrap_or_default();
143
144 match status.as_u16() {
145 401 => anyhow::bail!(
146 "Invalid Perplexity API key. Please check your API key configuration."
147 ),
148 429 => anyhow::bail!(
149 "Perplexity API rate limit exceeded. Please wait a moment and try again."
150 ),
151 400 => {
152 if error_text.contains("insufficient_quota") {
153 anyhow::bail!(
154 "Perplexity API quota exceeded. Please check your billing status."
155 );
156 }
157 anyhow::bail!("Bad request to Perplexity API: {}", error_text);
158 }
159 _ => anyhow::bail!("Perplexity API error ({}): {}", status, error_text),
160 }
161 }
162
163 let perplexity_response: PerplexityResponse = response
164 .json()
165 .await
166 .context("Failed to parse Perplexity API response")?;
167
168 let message = perplexity_response
169 .choices
170 .first()
171 .map(|choice| &choice.message.content)
172 .context("Perplexity returned an empty response. The model may be overloaded - please try again.")?
173 .trim()
174 .to_string();
175
176 Ok(message)
177 }
178}
179
180pub struct PerplexityProviderBuilder;
182
183impl super::registry::ProviderBuilder for PerplexityProviderBuilder {
184 fn name(&self) -> &'static str {
185 "perplexity"
186 }
187
188 fn create(&self, config: &Config) -> Result<Box<dyn super::AIProvider>> {
189 Ok(Box::new(PerplexityProvider::new(config)?))
190 }
191
192 fn requires_api_key(&self) -> bool {
193 true
194 }
195
196 fn default_model(&self) -> Option<&'static str> {
197 Some("llama-3.1-sonar-small-128k-online")
198 }
199}