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