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::{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    /// Create provider from account configuration
67    #[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
179/// ProviderBuilder for Perplexity
180pub 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}