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//! ```
11use crate::{
12    OneOrMany,
13    client::{VerifyClient, VerifyError},
14    completion::{self, CompletionError, MessageError, message},
15    http_client::{self, HttpClientExt},
16    impl_conversion_traits, json_utils,
17};
18
19use crate::client::{CompletionClient, ProviderClient};
20use crate::completion::CompletionRequest;
21use crate::json_utils::merge;
22use crate::providers::openai;
23use crate::providers::openai::send_compatible_streaming_request;
24use crate::streaming::StreamingCompletionResponse;
25use bytes::Bytes;
26use http::Method;
27use serde::{Deserialize, Serialize};
28use serde_json::{Value, json};
29use tracing::{Instrument, info_span};
30
31// ================================================================
32// Main Cohere Client
33// ================================================================
34const PERPLEXITY_API_BASE_URL: &str = "https://api.perplexity.ai";
35
36pub struct ClientBuilder<'a, T = reqwest::Client> {
37    api_key: &'a str,
38    base_url: &'a str,
39    http_client: T,
40}
41
42impl<'a, T> ClientBuilder<'a, T>
43where
44    T: Default,
45{
46    pub fn new(api_key: &'a str) -> Self {
47        Self {
48            api_key,
49            base_url: PERPLEXITY_API_BASE_URL,
50            http_client: Default::default(),
51        }
52    }
53}
54
55impl<'a, T> ClientBuilder<'a, T> {
56    pub fn new_with_client(api_key: &'a str, http_client: T) -> Self {
57        Self {
58            api_key,
59            base_url: PERPLEXITY_API_BASE_URL,
60            http_client,
61        }
62    }
63
64    pub fn base_url(mut self, base_url: &'a str) -> Self {
65        self.base_url = base_url;
66        self
67    }
68
69    pub fn with_client<U>(self, http_client: U) -> ClientBuilder<'a, U> {
70        ClientBuilder {
71            api_key: self.api_key,
72            base_url: self.base_url,
73            http_client,
74        }
75    }
76
77    pub fn build(self) -> Client<T> {
78        Client {
79            base_url: self.base_url.to_string(),
80            api_key: self.api_key.to_string(),
81            http_client: self.http_client,
82        }
83    }
84}
85
86#[derive(Clone)]
87pub struct Client<T = reqwest::Client> {
88    base_url: String,
89    api_key: String,
90    http_client: T,
91}
92
93impl<T> std::fmt::Debug for Client<T>
94where
95    T: std::fmt::Debug,
96{
97    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
98        f.debug_struct("Client")
99            .field("base_url", &self.base_url)
100            .field("http_client", &self.http_client)
101            .field("api_key", &"<REDACTED>")
102            .finish()
103    }
104}
105
106impl<T> Client<T>
107where
108    T: HttpClientExt,
109{
110    fn req(
111        &self,
112        method: http_client::Method,
113        path: &str,
114    ) -> http_client::Result<http_client::Builder> {
115        let url = format!("{}/{}", self.base_url, path.trim_start_matches('/'));
116        let req = http_client::Builder::new().method(method).uri(url);
117
118        http_client::with_bearer_auth(req, &self.api_key)
119    }
120}
121
122impl Client<reqwest::Client> {
123    pub fn builder(api_key: &str) -> ClientBuilder<'_, reqwest::Client> {
124        ClientBuilder::new(api_key)
125    }
126
127    pub fn new(api_key: &str) -> Self {
128        Self::builder(api_key).build()
129    }
130
131    pub fn from_env() -> Self {
132        <Self as ProviderClient>::from_env()
133    }
134}
135
136impl<T> ProviderClient for Client<T>
137where
138    T: HttpClientExt + Clone + std::fmt::Debug + Default + Send + 'static,
139{
140    /// Create a new Perplexity client from the `PERPLEXITY_API_KEY` environment variable.
141    /// Panics if the environment variable is not set.
142    fn from_env() -> Self {
143        let api_key = std::env::var("PERPLEXITY_API_KEY").expect("PERPLEXITY_API_KEY not set");
144        ClientBuilder::<T>::new(&api_key).build()
145    }
146
147    fn from_val(input: crate::client::ProviderValue) -> Self {
148        let crate::client::ProviderValue::Simple(api_key) = input else {
149            panic!("Incorrect provider value type")
150        };
151        ClientBuilder::<T>::new(&api_key).build()
152    }
153}
154
155impl<T> CompletionClient for Client<T>
156where
157    T: HttpClientExt + Clone + std::fmt::Debug + Default + Send + 'static,
158{
159    type CompletionModel = CompletionModel<T>;
160
161    fn completion_model(&self, model: &str) -> Self::CompletionModel {
162        CompletionModel::new(self.clone(), model)
163    }
164}
165
166impl<T> VerifyClient for Client<T>
167where
168    T: HttpClientExt + Clone + std::fmt::Debug + Default + Send + 'static,
169{
170    #[cfg_attr(feature = "worker", worker::send)]
171    async fn verify(&self) -> Result<(), VerifyError> {
172        // No API endpoint to verify the API key
173        Ok(())
174    }
175}
176
177impl_conversion_traits!(
178    AsTranscription,
179    AsEmbeddings,
180    AsImageGeneration,
181    AsAudioGeneration for Client<T>
182);
183
184#[derive(Debug, Deserialize)]
185struct ApiErrorResponse {
186    message: String,
187}
188
189#[derive(Debug, Deserialize)]
190#[serde(untagged)]
191enum ApiResponse<T> {
192    Ok(T),
193    Err(ApiErrorResponse),
194}
195
196// ================================================================
197// Perplexity Completion API
198// ================================================================
199/// `sonar-pro` completion model
200pub const SONAR_PRO: &str = "sonar-pro";
201/// `sonar` completion model
202pub const SONAR: &str = "sonar";
203
204#[derive(Debug, Deserialize, Serialize)]
205pub struct CompletionResponse {
206    pub id: String,
207    pub model: String,
208    pub object: String,
209    pub created: u64,
210    #[serde(default)]
211    pub choices: Vec<Choice>,
212    pub usage: Usage,
213}
214
215#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
216pub struct Message {
217    pub role: Role,
218    pub content: String,
219}
220
221#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
222#[serde(rename_all = "lowercase")]
223pub enum Role {
224    System,
225    User,
226    Assistant,
227}
228
229#[derive(Deserialize, Debug, Serialize)]
230pub struct Delta {
231    pub role: Role,
232    pub content: String,
233}
234
235#[derive(Deserialize, Debug, Serialize)]
236pub struct Choice {
237    pub index: usize,
238    pub finish_reason: String,
239    pub message: Message,
240    pub delta: Delta,
241}
242
243#[derive(Deserialize, Debug, Serialize)]
244pub struct Usage {
245    pub prompt_tokens: u32,
246    pub completion_tokens: u32,
247    pub total_tokens: u32,
248}
249
250impl std::fmt::Display for Usage {
251    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
252        write!(
253            f,
254            "Prompt tokens: {}\nCompletion tokens: {} Total tokens: {}",
255            self.prompt_tokens, self.completion_tokens, self.total_tokens
256        )
257    }
258}
259
260impl TryFrom<CompletionResponse> for completion::CompletionResponse<CompletionResponse> {
261    type Error = CompletionError;
262
263    fn try_from(response: CompletionResponse) -> Result<Self, Self::Error> {
264        let choice = response.choices.first().ok_or_else(|| {
265            CompletionError::ResponseError("Response contained no choices".to_owned())
266        })?;
267
268        match &choice.message {
269            Message {
270                role: Role::Assistant,
271                content,
272            } => Ok(completion::CompletionResponse {
273                choice: OneOrMany::one(content.clone().into()),
274                usage: completion::Usage {
275                    input_tokens: response.usage.prompt_tokens as u64,
276                    output_tokens: response.usage.completion_tokens as u64,
277                    total_tokens: response.usage.total_tokens as u64,
278                },
279                raw_response: response,
280            }),
281            _ => Err(CompletionError::ResponseError(
282                "Response contained no assistant message".to_owned(),
283            )),
284        }
285    }
286}
287
288#[derive(Clone)]
289pub struct CompletionModel<T> {
290    client: Client<T>,
291    pub model: String,
292}
293
294impl<T> CompletionModel<T> {
295    pub fn new(client: Client<T>, model: &str) -> Self {
296        Self {
297            client,
298            model: model.to_string(),
299        }
300    }
301
302    fn create_completion_request(
303        &self,
304        completion_request: CompletionRequest,
305    ) -> Result<Value, CompletionError> {
306        if completion_request.tool_choice.is_some() {
307            tracing::warn!("WARNING: `tool_choice` not supported on Perplexity");
308        }
309
310        // Build up the order of messages (context, chat_history, prompt)
311        let mut partial_history = vec![];
312        if let Some(docs) = completion_request.normalized_documents() {
313            partial_history.push(docs);
314        }
315        partial_history.extend(completion_request.chat_history);
316
317        // Initialize full history with preamble (or empty if non-existent)
318        let mut full_history: Vec<Message> =
319            completion_request
320                .preamble
321                .map_or_else(Vec::new, |preamble| {
322                    vec![Message {
323                        role: Role::System,
324                        content: preamble,
325                    }]
326                });
327
328        // Convert and extend the rest of the history
329        full_history.extend(
330            partial_history
331                .into_iter()
332                .map(message::Message::try_into)
333                .collect::<Result<Vec<Message>, _>>()?,
334        );
335
336        // Compose request
337        let request = json!({
338            "model": self.model,
339            "messages": full_history,
340            "temperature": completion_request.temperature,
341        });
342
343        let request = if let Some(ref params) = completion_request.additional_params {
344            json_utils::merge(request, params.clone())
345        } else {
346            request
347        };
348
349        Ok(request)
350    }
351}
352
353impl TryFrom<message::Message> for Message {
354    type Error = MessageError;
355
356    fn try_from(message: message::Message) -> Result<Self, Self::Error> {
357        Ok(match message {
358            message::Message::User { content } => {
359                let collapsed_content = content
360                    .into_iter()
361                    .map(|content| match content {
362                        message::UserContent::Text(message::Text { text }) => Ok(text),
363                        _ => Err(MessageError::ConversionError(
364                            "Only text content is supported by Perplexity".to_owned(),
365                        )),
366                    })
367                    .collect::<Result<Vec<_>, _>>()?
368                    .join("\n");
369
370                Message {
371                    role: Role::User,
372                    content: collapsed_content,
373                }
374            }
375
376            message::Message::Assistant { content, .. } => {
377                let collapsed_content = content
378                    .into_iter()
379                    .map(|content| {
380                        Ok(match content {
381                            message::AssistantContent::Text(message::Text { text }) => text,
382                            _ => return Err(MessageError::ConversionError(
383                                "Only text assistant message content is supported by Perplexity"
384                                    .to_owned(),
385                            )),
386                        })
387                    })
388                    .collect::<Result<Vec<_>, _>>()?
389                    .join("\n");
390
391                Message {
392                    role: Role::Assistant,
393                    content: collapsed_content,
394                }
395            }
396        })
397    }
398}
399
400impl From<Message> for message::Message {
401    fn from(message: Message) -> Self {
402        match message.role {
403            Role::User => message::Message::user(message.content),
404            Role::Assistant => message::Message::assistant(message.content),
405
406            // System messages get coerced into user messages for ease of error handling.
407            // They should be handled on the outside of `Message` conversions via the preamble.
408            Role::System => message::Message::user(message.content),
409        }
410    }
411}
412
413impl<T> completion::CompletionModel for CompletionModel<T>
414where
415    T: HttpClientExt + Clone + Default + std::fmt::Debug + Send + 'static,
416{
417    type Response = CompletionResponse;
418    type StreamingResponse = openai::StreamingCompletionResponse;
419
420    #[cfg_attr(feature = "worker", worker::send)]
421    async fn completion(
422        &self,
423        completion_request: completion::CompletionRequest,
424    ) -> Result<completion::CompletionResponse<CompletionResponse>, CompletionError> {
425        let preamble = completion_request.preamble.clone();
426        let request = self.create_completion_request(completion_request)?;
427
428        let span = if tracing::Span::current().is_disabled() {
429            info_span!(
430                target: "rig::completions",
431                "chat",
432                gen_ai.operation.name = "chat",
433                gen_ai.provider.name = "perplexity",
434                gen_ai.request.model = self.model,
435                gen_ai.system_instructions = preamble,
436                gen_ai.response.id = tracing::field::Empty,
437                gen_ai.response.model = tracing::field::Empty,
438                gen_ai.usage.output_tokens = tracing::field::Empty,
439                gen_ai.usage.input_tokens = tracing::field::Empty,
440                gen_ai.input.messages = serde_json::to_string(&request.get("messages").unwrap()).unwrap(),
441                gen_ai.output.messages = tracing::field::Empty,
442            )
443        } else {
444            tracing::Span::current()
445        };
446
447        let body = serde_json::to_vec(&request)?;
448
449        let req = self
450            .client
451            .req(Method::POST, "/v1/chat/completions")?
452            .header("Content-Type", "application/json")
453            .body(body)
454            .map_err(http_client::Error::from)?;
455
456        let async_block = async move {
457            let response = self.client.http_client.send::<_, Bytes>(req).await?;
458
459            let status = response.status();
460            let response_body = response.into_body().into_future().await?.to_vec();
461
462            if status.is_success() {
463                match serde_json::from_slice::<ApiResponse<CompletionResponse>>(&response_body)? {
464                    ApiResponse::Ok(completion) => {
465                        let span = tracing::Span::current();
466                        span.record("gen_ai.usage.input_tokens", completion.usage.prompt_tokens);
467                        span.record(
468                            "gen_ai.usage.output_tokens",
469                            completion.usage.completion_tokens,
470                        );
471                        span.record(
472                            "gen_ai.output.messages",
473                            serde_json::to_string(&completion.choices).unwrap(),
474                        );
475                        span.record("gen_ai.response.id", completion.id.to_string());
476                        span.record("gen_ai.response.model_name", completion.model.to_string());
477                        Ok(completion.try_into()?)
478                    }
479                    ApiResponse::Err(error) => Err(CompletionError::ProviderError(error.message)),
480                }
481            } else {
482                Err(CompletionError::ProviderError(
483                    String::from_utf8_lossy(&response_body).to_string(),
484                ))
485            }
486        };
487
488        async_block.instrument(span).await
489    }
490
491    #[cfg_attr(feature = "worker", worker::send)]
492    async fn stream(
493        &self,
494        completion_request: completion::CompletionRequest,
495    ) -> Result<StreamingCompletionResponse<Self::StreamingResponse>, CompletionError> {
496        let preamble = completion_request.preamble.clone();
497        let mut request = self.create_completion_request(completion_request)?;
498
499        request = merge(request, json!({"stream": true}));
500        let body = serde_json::to_vec(&request)?;
501
502        let req = self
503            .client
504            .req(Method::POST, "/chat/completions")?
505            .header("Content-Type", "application/json")
506            .body(body)
507            .map_err(http_client::Error::from)?;
508
509        let span = if tracing::Span::current().is_disabled() {
510            info_span!(
511                target: "rig::completions",
512                "chat_streaming",
513                gen_ai.operation.name = "chat_streaming",
514                gen_ai.provider.name = "perplexity",
515                gen_ai.request.model = self.model,
516                gen_ai.system_instructions = preamble,
517                gen_ai.response.id = tracing::field::Empty,
518                gen_ai.response.model = tracing::field::Empty,
519                gen_ai.usage.output_tokens = tracing::field::Empty,
520                gen_ai.usage.input_tokens = tracing::field::Empty,
521                gen_ai.input.messages = serde_json::to_string(&request.get("messages").unwrap()).unwrap(),
522                gen_ai.output.messages = tracing::field::Empty,
523            )
524        } else {
525            tracing::Span::current()
526        };
527        send_compatible_streaming_request(self.client.http_client.clone(), req)
528            .instrument(span)
529            .await
530    }
531}
532
533#[cfg(test)]
534mod tests {
535    use super::*;
536
537    #[test]
538    fn test_deserialize_message() {
539        let json_data = r#"
540        {
541            "role": "user",
542            "content": "Hello, how can I help you?"
543        }
544        "#;
545
546        let message: Message = serde_json::from_str(json_data).unwrap();
547        assert_eq!(message.role, Role::User);
548        assert_eq!(message.content, "Hello, how can I help you?");
549    }
550
551    #[test]
552    fn test_serialize_message() {
553        let message = Message {
554            role: Role::Assistant,
555            content: "I am here to assist you.".to_string(),
556        };
557
558        let json_data = serde_json::to_string(&message).unwrap();
559        let expected_json = r#"{"role":"assistant","content":"I am here to assist you."}"#;
560        assert_eq!(json_data, expected_json);
561    }
562
563    #[test]
564    fn test_message_to_message_conversion() {
565        let user_message = message::Message::user("User message");
566        let assistant_message = message::Message::assistant("Assistant message");
567
568        let converted_user_message: Message = user_message.clone().try_into().unwrap();
569        let converted_assistant_message: Message = assistant_message.clone().try_into().unwrap();
570
571        assert_eq!(converted_user_message.role, Role::User);
572        assert_eq!(converted_user_message.content, "User message");
573
574        assert_eq!(converted_assistant_message.role, Role::Assistant);
575        assert_eq!(converted_assistant_message.content, "Assistant message");
576
577        let back_to_user_message: message::Message = converted_user_message.into();
578        let back_to_assistant_message: message::Message = converted_assistant_message.into();
579
580        assert_eq!(user_message, back_to_user_message);
581        assert_eq!(assistant_message, back_to_assistant_message);
582    }
583}