1use 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
30const 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 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 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
101impl CompletionClient for Client {
102 type CompletionModel = CompletionModel;
103
104 fn completion_model(&self, model: &str) -> CompletionModel {
105 CompletionModel::new(self.clone(), model)
106 }
107}
108
109impl_conversion_traits!(
110 AsTranscription,
111 AsEmbeddings,
112 AsImageGeneration,
113 AsAudioGeneration for Client
114);
115
116#[derive(Debug, Deserialize)]
117struct ApiErrorResponse {
118 message: String,
119}
120
121#[derive(Debug, Deserialize)]
122#[serde(untagged)]
123enum ApiResponse<T> {
124 Ok(T),
125 Err(ApiErrorResponse),
126}
127
128pub const SONAR_PRO: &str = "sonar-pro";
133pub const SONAR: &str = "sonar";
135
136#[derive(Debug, Deserialize)]
137pub struct CompletionResponse {
138 pub id: String,
139 pub model: String,
140 pub object: String,
141 pub created: u64,
142 #[serde(default)]
143 pub choices: Vec<Choice>,
144 pub usage: Usage,
145}
146
147#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
148pub struct Message {
149 pub role: Role,
150 pub content: String,
151}
152
153#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
154#[serde(rename_all = "lowercase")]
155pub enum Role {
156 System,
157 User,
158 Assistant,
159}
160
161#[derive(Deserialize, Debug)]
162pub struct Delta {
163 pub role: Role,
164 pub content: String,
165}
166
167#[derive(Deserialize, Debug)]
168pub struct Choice {
169 pub index: usize,
170 pub finish_reason: String,
171 pub message: Message,
172 pub delta: Delta,
173}
174
175#[derive(Deserialize, Debug)]
176pub struct Usage {
177 pub prompt_tokens: u32,
178 pub completion_tokens: u32,
179 pub total_tokens: u32,
180}
181
182impl std::fmt::Display for Usage {
183 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
184 write!(
185 f,
186 "Prompt tokens: {}\nCompletion tokens: {} Total tokens: {}",
187 self.prompt_tokens, self.completion_tokens, self.total_tokens
188 )
189 }
190}
191
192impl TryFrom<CompletionResponse> for completion::CompletionResponse<CompletionResponse> {
193 type Error = CompletionError;
194
195 fn try_from(response: CompletionResponse) -> Result<Self, Self::Error> {
196 let choice = response.choices.first().ok_or_else(|| {
197 CompletionError::ResponseError("Response contained no choices".to_owned())
198 })?;
199
200 match &choice.message {
201 Message {
202 role: Role::Assistant,
203 content,
204 } => Ok(completion::CompletionResponse {
205 choice: OneOrMany::one(content.clone().into()),
206 raw_response: response,
207 }),
208 _ => Err(CompletionError::ResponseError(
209 "Response contained no assistant message".to_owned(),
210 )),
211 }
212 }
213}
214
215#[derive(Clone)]
216pub struct CompletionModel {
217 client: Client,
218 pub model: String,
219}
220
221impl CompletionModel {
222 pub fn new(client: Client, model: &str) -> Self {
223 Self {
224 client,
225 model: model.to_string(),
226 }
227 }
228
229 fn create_completion_request(
230 &self,
231 completion_request: CompletionRequest,
232 ) -> Result<Value, CompletionError> {
233 let mut partial_history = vec![];
235 if let Some(docs) = completion_request.normalized_documents() {
236 partial_history.push(docs);
237 }
238 partial_history.extend(completion_request.chat_history);
239
240 let mut full_history: Vec<Message> =
242 completion_request
243 .preamble
244 .map_or_else(Vec::new, |preamble| {
245 vec![Message {
246 role: Role::System,
247 content: preamble,
248 }]
249 });
250
251 full_history.extend(
253 partial_history
254 .into_iter()
255 .map(message::Message::try_into)
256 .collect::<Result<Vec<Message>, _>>()?,
257 );
258
259 let request = json!({
261 "model": self.model,
262 "messages": full_history,
263 "temperature": completion_request.temperature,
264 });
265
266 let request = if let Some(ref params) = completion_request.additional_params {
267 json_utils::merge(request, params.clone())
268 } else {
269 request
270 };
271
272 Ok(request)
273 }
274}
275
276impl TryFrom<message::Message> for Message {
277 type Error = MessageError;
278
279 fn try_from(message: message::Message) -> Result<Self, Self::Error> {
280 Ok(match message {
281 message::Message::User { content } => {
282 let collapsed_content = content
283 .into_iter()
284 .map(|content| match content {
285 message::UserContent::Text(message::Text { text }) => Ok(text),
286 _ => Err(MessageError::ConversionError(
287 "Only text content is supported by Perplexity".to_owned(),
288 )),
289 })
290 .collect::<Result<Vec<_>, _>>()?
291 .join("\n");
292
293 Message {
294 role: Role::User,
295 content: collapsed_content,
296 }
297 }
298
299 message::Message::Assistant { content, .. } => {
300 let collapsed_content = content
301 .into_iter()
302 .map(|content| {
303 Ok(match content {
304 message::AssistantContent::Text(message::Text { text }) => text,
305 _ => return Err(MessageError::ConversionError(
306 "Only text assistant message content is supported by Perplexity"
307 .to_owned(),
308 )),
309 })
310 })
311 .collect::<Result<Vec<_>, _>>()?
312 .join("\n");
313
314 Message {
315 role: Role::Assistant,
316 content: collapsed_content,
317 }
318 }
319 })
320 }
321}
322
323impl From<Message> for message::Message {
324 fn from(message: Message) -> Self {
325 match message.role {
326 Role::User => message::Message::user(message.content),
327 Role::Assistant => message::Message::assistant(message.content),
328
329 Role::System => message::Message::user(message.content),
332 }
333 }
334}
335
336impl completion::CompletionModel for CompletionModel {
337 type Response = CompletionResponse;
338 type StreamingResponse = openai::StreamingCompletionResponse;
339
340 #[cfg_attr(feature = "worker", worker::send)]
341 async fn completion(
342 &self,
343 completion_request: completion::CompletionRequest,
344 ) -> Result<completion::CompletionResponse<CompletionResponse>, CompletionError> {
345 let request = self.create_completion_request(completion_request)?;
346
347 let response = self
348 .client
349 .post("/chat/completions")
350 .json(&request)
351 .send()
352 .await?;
353
354 if response.status().is_success() {
355 match response.json::<ApiResponse<CompletionResponse>>().await? {
356 ApiResponse::Ok(completion) => {
357 tracing::info!(target: "rig",
358 "Perplexity completion token usage: {}",
359 completion.usage
360 );
361 Ok(completion.try_into()?)
362 }
363 ApiResponse::Err(error) => Err(CompletionError::ProviderError(error.message)),
364 }
365 } else {
366 Err(CompletionError::ProviderError(response.text().await?))
367 }
368 }
369
370 #[cfg_attr(feature = "worker", worker::send)]
371 async fn stream(
372 &self,
373 completion_request: completion::CompletionRequest,
374 ) -> Result<StreamingCompletionResponse<Self::StreamingResponse>, CompletionError> {
375 let mut request = self.create_completion_request(completion_request)?;
376
377 request = merge(request, json!({"stream": true}));
378
379 let builder = self.client.post("/chat/completions").json(&request);
380
381 send_compatible_streaming_request(builder).await
382 }
383}
384
385#[cfg(test)]
386mod tests {
387 use super::*;
388
389 #[test]
390 fn test_deserialize_message() {
391 let json_data = r#"
392 {
393 "role": "user",
394 "content": "Hello, how can I help you?"
395 }
396 "#;
397
398 let message: Message = serde_json::from_str(json_data).unwrap();
399 assert_eq!(message.role, Role::User);
400 assert_eq!(message.content, "Hello, how can I help you?");
401 }
402
403 #[test]
404 fn test_serialize_message() {
405 let message = Message {
406 role: Role::Assistant,
407 content: "I am here to assist you.".to_string(),
408 };
409
410 let json_data = serde_json::to_string(&message).unwrap();
411 let expected_json = r#"{"role":"assistant","content":"I am here to assist you."}"#;
412 assert_eq!(json_data, expected_json);
413 }
414
415 #[test]
416 fn test_message_to_message_conversion() {
417 let user_message = message::Message::user("User message");
418 let assistant_message = message::Message::assistant("Assistant message");
419
420 let converted_user_message: Message = user_message.clone().try_into().unwrap();
421 let converted_assistant_message: Message = assistant_message.clone().try_into().unwrap();
422
423 assert_eq!(converted_user_message.role, Role::User);
424 assert_eq!(converted_user_message.content, "User message");
425
426 assert_eq!(converted_assistant_message.role, Role::Assistant);
427 assert_eq!(converted_assistant_message.content, "Assistant message");
428
429 let back_to_user_message: message::Message = converted_user_message.into();
430 let back_to_assistant_message: message::Message = converted_assistant_message.into();
431
432 assert_eq!(user_message, back_to_user_message);
433 assert_eq!(assistant_message, back_to_assistant_message);
434 }
435}