rig/providers/
perplexity.rs

1//! Perplexity API client and Rig integration
2//!
3//! # Example
4//! ```
5//! use rig::providers::perplexity;
6//!
7//! let client = perplexity::Client::new("YOUR_API_KEY");
8//!
9//! let llama_3_1_sonar_small_online = client.completion_model(perplexity::LLAMA_3_1_SONAR_SMALL_ONLINE);
10//! ```
11
12use crate::{
13    agent::AgentBuilder,
14    completion::{self, message, CompletionError, MessageError},
15    extractor::ExtractorBuilder,
16    json_utils, OneOrMany,
17};
18
19use crate::completion::CompletionRequest;
20use crate::json_utils::merge;
21use crate::providers::openai::send_compatible_streaming_request;
22use crate::streaming::{StreamingCompletionModel, StreamingResult};
23use schemars::JsonSchema;
24use serde::{Deserialize, Serialize};
25use serde_json::{json, Value};
26
27// ================================================================
28// Main Cohere Client
29// ================================================================
30const PERPLEXITY_API_BASE_URL: &str = "https://api.perplexity.ai";
31
32#[derive(Clone)]
33pub struct Client {
34    base_url: String,
35    http_client: reqwest::Client,
36}
37
38impl Client {
39    pub fn new(api_key: &str) -> Self {
40        Self::from_url(api_key, PERPLEXITY_API_BASE_URL)
41    }
42
43    /// Create a new Perplexity client from the `PERPLEXITY_API_KEY` environment variable.
44    /// Panics if the environment variable is not set.
45    pub fn from_env() -> Self {
46        let api_key = std::env::var("PERPLEXITY_API_KEY").expect("PERPLEXITY_API_KEY not set");
47        Self::new(&api_key)
48    }
49
50    pub fn from_url(api_key: &str, base_url: &str) -> Self {
51        Self {
52            base_url: base_url.to_string(),
53            http_client: reqwest::Client::builder()
54                .default_headers({
55                    let mut headers = reqwest::header::HeaderMap::new();
56                    headers.insert(
57                        "Authorization",
58                        format!("Bearer {}", api_key)
59                            .parse()
60                            .expect("Bearer token should parse"),
61                    );
62                    headers
63                })
64                .build()
65                .expect("Perplexity reqwest client should build"),
66        }
67    }
68
69    pub fn post(&self, path: &str) -> reqwest::RequestBuilder {
70        let url = format!("{}/{}", self.base_url, path).replace("//", "/");
71        self.http_client.post(url)
72    }
73
74    pub fn completion_model(&self, model: &str) -> CompletionModel {
75        CompletionModel::new(self.clone(), model)
76    }
77
78    pub fn agent(&self, model: &str) -> AgentBuilder<CompletionModel> {
79        AgentBuilder::new(self.completion_model(model))
80    }
81
82    pub fn extractor<T: JsonSchema + for<'a> Deserialize<'a> + Serialize + Send + Sync>(
83        &self,
84        model: &str,
85    ) -> ExtractorBuilder<T, CompletionModel> {
86        ExtractorBuilder::new(self.completion_model(model))
87    }
88}
89
90#[derive(Debug, Deserialize)]
91struct ApiErrorResponse {
92    message: String,
93}
94
95#[derive(Debug, Deserialize)]
96#[serde(untagged)]
97enum ApiResponse<T> {
98    Ok(T),
99    Err(ApiErrorResponse),
100}
101
102// ================================================================
103// Perplexity Completion API
104// ================================================================
105/// `sonar-pro` completion model
106pub const SONAR_PRO: &str = "sonar-pro";
107/// `sonar` completion model
108pub const SONAR: &str = "sonar";
109
110#[derive(Debug, Deserialize)]
111pub struct CompletionResponse {
112    pub id: String,
113    pub model: String,
114    pub object: String,
115    pub created: u64,
116    #[serde(default)]
117    pub choices: Vec<Choice>,
118    pub usage: Usage,
119}
120
121#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
122pub struct Message {
123    pub role: Role,
124    pub content: String,
125}
126
127#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
128#[serde(rename_all = "lowercase")]
129pub enum Role {
130    System,
131    User,
132    Assistant,
133}
134
135#[derive(Deserialize, Debug)]
136pub struct Delta {
137    pub role: Role,
138    pub content: String,
139}
140
141#[derive(Deserialize, Debug)]
142pub struct Choice {
143    pub index: usize,
144    pub finish_reason: String,
145    pub message: Message,
146    pub delta: Delta,
147}
148
149#[derive(Deserialize, Debug)]
150pub struct Usage {
151    pub prompt_tokens: u32,
152    pub completion_tokens: u32,
153    pub total_tokens: u32,
154}
155
156impl std::fmt::Display for Usage {
157    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
158        write!(
159            f,
160            "Prompt tokens: {}\nCompletion tokens: {} Total tokens: {}",
161            self.prompt_tokens, self.completion_tokens, self.total_tokens
162        )
163    }
164}
165
166impl TryFrom<CompletionResponse> for completion::CompletionResponse<CompletionResponse> {
167    type Error = CompletionError;
168
169    fn try_from(response: CompletionResponse) -> Result<Self, Self::Error> {
170        let choice = response.choices.first().ok_or_else(|| {
171            CompletionError::ResponseError("Response contained no choices".to_owned())
172        })?;
173
174        match &choice.message {
175            Message {
176                role: Role::Assistant,
177                content,
178            } => Ok(completion::CompletionResponse {
179                choice: OneOrMany::one(content.clone().into()),
180                raw_response: response,
181            }),
182            _ => Err(CompletionError::ResponseError(
183                "Response contained no assistant message".to_owned(),
184            )),
185        }
186    }
187}
188
189#[derive(Clone)]
190pub struct CompletionModel {
191    client: Client,
192    pub model: String,
193}
194
195impl CompletionModel {
196    pub fn new(client: Client, model: &str) -> Self {
197        Self {
198            client,
199            model: model.to_string(),
200        }
201    }
202
203    fn create_completion_request(
204        &self,
205        completion_request: CompletionRequest,
206    ) -> Result<Value, CompletionError> {
207        // Add context documents to current prompt
208        let prompt_with_context = completion_request.prompt_with_context();
209
210        // Add preamble to messages (if available)
211        let mut messages: Vec<Message> = if let Some(preamble) = completion_request.preamble {
212            vec![Message {
213                role: Role::System,
214                content: preamble,
215            }]
216        } else {
217            vec![]
218        };
219
220        // Add chat history to messages
221        for message in completion_request.chat_history {
222            messages.push(
223                message
224                    .try_into()
225                    .map_err(|e: MessageError| CompletionError::RequestError(e.into()))?,
226            );
227        }
228
229        // Add user prompt to messages
230        messages.push(
231            prompt_with_context
232                .try_into()
233                .map_err(|e: MessageError| CompletionError::RequestError(e.into()))?,
234        );
235
236        // Compose request
237        let request = json!({
238            "model": self.model,
239            "messages": messages,
240            "temperature": completion_request.temperature,
241        });
242
243        let request = if let Some(ref params) = completion_request.additional_params {
244            json_utils::merge(request, params.clone())
245        } else {
246            request
247        };
248
249        Ok(request)
250    }
251}
252
253impl TryFrom<message::Message> for Message {
254    type Error = MessageError;
255
256    fn try_from(message: message::Message) -> Result<Self, Self::Error> {
257        Ok(match message {
258            message::Message::User { content } => {
259                let collapsed_content = content
260                    .into_iter()
261                    .map(|content| match content {
262                        message::UserContent::Text(message::Text { text }) => Ok(text),
263                        _ => Err(MessageError::ConversionError(
264                            "Only text content is supported by Perplexity".to_owned(),
265                        )),
266                    })
267                    .collect::<Result<Vec<_>, _>>()?
268                    .join("\n");
269
270                Message {
271                    role: Role::User,
272                    content: collapsed_content,
273                }
274            }
275
276            message::Message::Assistant { content } => {
277                let collapsed_content = content
278                    .into_iter()
279                    .map(|content| {
280                        Ok(match content {
281                            message::AssistantContent::Text(message::Text { text }) => text,
282                            _ => return Err(MessageError::ConversionError(
283                                "Only text assistant message content is supported by Perplexity"
284                                    .to_owned(),
285                            )),
286                        })
287                    })
288                    .collect::<Result<Vec<_>, _>>()?
289                    .join("\n");
290
291                Message {
292                    role: Role::Assistant,
293                    content: collapsed_content,
294                }
295            }
296        })
297    }
298}
299
300impl From<Message> for message::Message {
301    fn from(message: Message) -> Self {
302        match message.role {
303            Role::User => message::Message::user(message.content),
304            Role::Assistant => message::Message::assistant(message.content),
305
306            // System messages get coerced into user messages for ease of error handling.
307            // They should be handled on the outside of `Message` conversions via the preamble.
308            Role::System => message::Message::user(message.content),
309        }
310    }
311}
312
313impl completion::CompletionModel for CompletionModel {
314    type Response = CompletionResponse;
315
316    #[cfg_attr(feature = "worker", worker::send)]
317    async fn completion(
318        &self,
319        completion_request: completion::CompletionRequest,
320    ) -> Result<completion::CompletionResponse<CompletionResponse>, CompletionError> {
321        let request = self.create_completion_request(completion_request)?;
322
323        let response = self
324            .client
325            .post("/chat/completions")
326            .json(&request)
327            .send()
328            .await?;
329
330        if response.status().is_success() {
331            match response.json::<ApiResponse<CompletionResponse>>().await? {
332                ApiResponse::Ok(completion) => {
333                    tracing::info!(target: "rig",
334                        "Perplexity completion token usage: {}",
335                        completion.usage
336                    );
337                    Ok(completion.try_into()?)
338                }
339                ApiResponse::Err(error) => Err(CompletionError::ProviderError(error.message)),
340            }
341        } else {
342            Err(CompletionError::ProviderError(response.text().await?))
343        }
344    }
345}
346
347impl StreamingCompletionModel for CompletionModel {
348    async fn stream(
349        &self,
350        completion_request: completion::CompletionRequest,
351    ) -> Result<StreamingResult, CompletionError> {
352        let mut request = self.create_completion_request(completion_request)?;
353
354        request = merge(request, json!({"stream": true}));
355
356        let builder = self.client.post("/chat/completions").json(&request);
357
358        send_compatible_streaming_request(builder).await
359    }
360}
361
362#[cfg(test)]
363mod tests {
364    use super::*;
365
366    #[test]
367    fn test_deserialize_message() {
368        let json_data = r#"
369        {
370            "role": "user",
371            "content": "Hello, how can I help you?"
372        }
373        "#;
374
375        let message: Message = serde_json::from_str(json_data).unwrap();
376        assert_eq!(message.role, Role::User);
377        assert_eq!(message.content, "Hello, how can I help you?");
378    }
379
380    #[test]
381    fn test_serialize_message() {
382        let message = Message {
383            role: Role::Assistant,
384            content: "I am here to assist you.".to_string(),
385        };
386
387        let json_data = serde_json::to_string(&message).unwrap();
388        let expected_json = r#"{"role":"assistant","content":"I am here to assist you."}"#;
389        assert_eq!(json_data, expected_json);
390    }
391
392    #[test]
393    fn test_message_to_message_conversion() {
394        let user_message = message::Message::user("User message");
395        let assistant_message = message::Message::assistant("Assistant message");
396
397        let converted_user_message: Message = user_message.clone().try_into().unwrap();
398        let converted_assistant_message: Message = assistant_message.clone().try_into().unwrap();
399
400        assert_eq!(converted_user_message.role, Role::User);
401        assert_eq!(converted_user_message.content, "User message");
402
403        assert_eq!(converted_assistant_message.role, Role::Assistant);
404        assert_eq!(converted_assistant_message.content, "Assistant message");
405
406        let back_to_user_message: message::Message = converted_user_message.into();
407        let back_to_assistant_message: message::Message = converted_assistant_message.into();
408
409        assert_eq!(user_message, back_to_user_message);
410        assert_eq!(assistant_message, back_to_assistant_message);
411    }
412}