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 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
135pub const SONAR_PRO: &str = "sonar-pro";
140pub 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 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 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 full_history.extend(
265 partial_history
266 .into_iter()
267 .map(message::Message::try_into)
268 .collect::<Result<Vec<Message>, _>>()?,
269 );
270
271 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 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}