Skip to main content

rusty_commit/providers/
perplexity.rs

1use 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    /// Create provider from account configuration
68    #[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
180/// ProviderBuilder for Perplexity
181pub 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}