1use 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
31const 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 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 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
196pub const SONAR_PRO: &str = "sonar-pro";
201pub 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 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 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 full_history.extend(
330 partial_history
331 .into_iter()
332 .map(message::Message::try_into)
333 .collect::<Result<Vec<Message>, _>>()?,
334 );
335
336 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 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}