1use crate::api::ChatApi;
2use crate::types::{
3 ChatCompletionRequest, ChatCompletionResponse, Message, Role,
4};
5use crate::types::AiLibError;
6use reqwest::Client;
7use serde::{Deserialize, Serialize};
8use futures::Stream;
9use async_trait::async_trait;
10
11pub struct PerplexityAdapter {
16 client: Client,
17 api_key: String,
18}
19
20impl PerplexityAdapter {
21 pub fn new() -> Result<Self, AiLibError> {
22 let api_key = std::env::var("PERPLEXITY_API_KEY")
23 .map_err(|_| AiLibError::ConfigurationError(
24 "PERPLEXITY_API_KEY environment variable not set".to_string()
25 ))?;
26
27 Ok(Self {
28 client: Client::new(),
29 api_key,
30 })
31 }
32
33 pub async fn chat_completion(
34 &self,
35 request: ChatCompletionRequest,
36 ) -> Result<ChatCompletionResponse, AiLibError> {
37 let perplexity_request = self.convert_request(&request)?;
38
39 let response = self
40 .client
41 .post("https://api.perplexity.ai/chat/completions")
42 .header("Authorization", format!("Bearer {}", self.api_key))
43 .header("Content-Type", "application/json")
44 .json(&perplexity_request)
45 .send()
46 .await
47 .map_err(|e| AiLibError::NetworkError(format!("Perplexity API request failed: {}", e)))?;
48
49 if !response.status().is_success() {
50 let status = response.status();
51 let error_text = response.text().await.unwrap_or_else(|_| "Unknown error".to_string());
52 return Err(AiLibError::ProviderError(format!(
53 "Perplexity API error {}: {}",
54 status, error_text
55 )));
56 }
57
58 let perplexity_response: PerplexityResponse = response
59 .json()
60 .await
61 .map_err(|e| AiLibError::DeserializationError(format!("Failed to parse Perplexity response: {}", e)))?;
62
63 self.convert_response(perplexity_response)
64 }
65
66 pub async fn chat_completion_stream(
67 &self,
68 request: ChatCompletionRequest,
69 ) -> Result<Box<dyn futures::Stream<Item = Result<crate::api::ChatCompletionChunk, AiLibError>> + Send + Unpin>, AiLibError> {
70 let response = self.chat_completion(request.clone()).await?;
72
73 let chunk = crate::api::ChatCompletionChunk {
75 id: response.id.clone(),
76 object: "chat.completion.chunk".to_string(),
77 created: response.created,
78 model: response.model.clone(),
79 choices: response.choices.into_iter().map(|choice| {
80 crate::api::ChoiceDelta {
81 index: choice.index,
82 delta: crate::api::MessageDelta {
83 role: Some(choice.message.role),
84 content: Some(match &choice.message.content {
85 crate::Content::Text(text) => text.clone(),
86 _ => "".to_string(),
87 }),
88 },
89 finish_reason: choice.finish_reason,
90 }
91 }).collect(),
92 };
93
94 let stream = futures::stream::once(async move { Ok(chunk) });
95 Ok(Box::new(Box::pin(stream)))
96 }
97
98 fn convert_request(&self, request: &ChatCompletionRequest) -> Result<PerplexityRequest, AiLibError> {
99 let messages = request
101 .messages
102 .iter()
103 .map(|msg| PerplexityMessage {
104 role: match msg.role {
105 Role::System => "system".to_string(),
106 Role::User => "user".to_string(),
107 Role::Assistant => "assistant".to_string(),
108 },
109 content: match &msg.content {
110 crate::Content::Text(text) => text.clone(),
111 _ => "Unsupported content type".to_string(),
112 },
113 })
114 .collect();
115
116 Ok(PerplexityRequest {
117 model: request.model.clone(),
118 messages,
119 max_tokens: request.max_tokens,
120 temperature: request.temperature,
121 top_p: request.top_p,
122 stream: Some(false),
123 })
124 }
125
126 fn convert_response(&self, response: PerplexityResponse) -> Result<ChatCompletionResponse, AiLibError> {
127 let choice = response.choices.first()
128 .ok_or_else(|| AiLibError::InvalidModelResponse("No choices in Perplexity response".to_string()))?;
129
130 let message = Message {
131 role: match choice.message.role.as_str() {
132 "assistant" => Role::Assistant,
133 "user" => Role::User,
134 "system" => Role::System,
135 _ => Role::Assistant,
136 },
137 content: crate::Content::Text(choice.message.content.clone().unwrap_or_default()),
138 function_call: None,
139 };
140
141 Ok(ChatCompletionResponse {
142 id: response.id,
143 object: "chat.completion".to_string(),
144 created: response.created,
145 model: response.model,
146 choices: vec![crate::types::Choice {
147 index: 0,
148 message,
149 finish_reason: choice.finish_reason.clone(),
150 }],
151 usage: response.usage.map(|u| crate::types::Usage {
152 prompt_tokens: u.prompt_tokens,
153 completion_tokens: u.completion_tokens,
154 total_tokens: u.total_tokens,
155 }).unwrap_or_else(|| crate::types::Usage {
156 prompt_tokens: 0,
157 completion_tokens: 0,
158 total_tokens: 0,
159 }),
160 usage_status: crate::types::response::UsageStatus::Finalized,
161 })
162 }
163}
164
165#[async_trait]
166impl ChatApi for PerplexityAdapter {
167 async fn chat_completion(
168 &self,
169 request: ChatCompletionRequest,
170 ) -> Result<ChatCompletionResponse, AiLibError> {
171 self.chat_completion(request).await
172 }
173
174 async fn chat_completion_stream(
175 &self,
176 request: ChatCompletionRequest,
177 ) -> Result<
178 Box<dyn Stream<Item = Result<crate::api::ChatCompletionChunk, AiLibError>> + Send + Unpin>,
179 AiLibError,
180 > {
181 self.chat_completion_stream(request).await
182 }
183
184 async fn list_models(&self) -> Result<Vec<String>, AiLibError> {
185 Ok(vec![
187 "llama-3.1-sonar-small-128k-online".to_string(),
188 "llama-3.1-sonar-large-128k-online".to_string(),
189 ])
190 }
191
192 async fn get_model_info(&self, model_id: &str) -> Result<crate::api::ModelInfo, AiLibError> {
193 Ok(crate::api::ModelInfo {
194 id: model_id.to_string(),
195 object: "model".to_string(),
196 created: 0,
197 owned_by: "perplexity".to_string(),
198 permission: vec![],
199 })
200 }
201}
202
203#[derive(Serialize)]
204struct PerplexityRequest {
205 model: String,
206 messages: Vec<PerplexityMessage>,
207 max_tokens: Option<u32>,
208 temperature: Option<f32>,
209 top_p: Option<f32>,
210 stream: Option<bool>,
211}
212
213#[derive(Serialize)]
214struct PerplexityMessage {
215 role: String,
216 content: String,
217}
218
219#[derive(Deserialize)]
220struct PerplexityResponse {
221 id: String,
222 #[allow(dead_code)]
223 object: String,
224 created: u64,
225 model: String,
226 choices: Vec<PerplexityChoice>,
227 usage: Option<PerplexityUsage>,
228}
229
230#[derive(Deserialize)]
231struct PerplexityChoice {
232 message: PerplexityMessageResponse,
233 finish_reason: Option<String>,
234}
235
236#[derive(Deserialize)]
237struct PerplexityMessageResponse {
238 role: String,
239 content: Option<String>,
240}
241
242#[derive(Deserialize)]
243struct PerplexityUsage {
244 prompt_tokens: u32,
245 completion_tokens: u32,
246 total_tokens: u32,
247}