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