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