use async_trait::async_trait;
use serde::Deserialize;
use tracing::{debug, error};
use crate::error::BaochuanError;
use crate::provider::{ChunkStream, Provider};
use crate::providers::openai_compat::OpenAICompatClient;
use crate::types::{ChatRequest, ChatResponse, ModelInfo};
const DEFAULT_BASE_URL: &str = "https://api.perplexity.ai";
#[derive(Deserialize)]
struct PerplexityResponse {
id: String,
model: String,
choices: Vec<crate::types::ChatChoice>,
usage: Option<crate::types::Usage>,
#[serde(default)]
citations: Vec<String>,
}
#[derive(Deserialize)]
struct PerplexityModelList {
data: Vec<PerplexityModel>,
}
#[derive(Deserialize)]
struct PerplexityModel {
id: String,
owned_by: Option<String>,
context_length: Option<u32>,
}
pub struct PerplexityProvider {
inner: OpenAICompatClient,
}
impl PerplexityProvider {
pub fn new(api_key: impl Into<String>) -> Self {
Self { inner: OpenAICompatClient::with_key(api_key, DEFAULT_BASE_URL) }
}
pub fn with_base_url(mut self, base_url: impl Into<String>) -> Self {
self.inner.base_url = base_url.into();
self
}
}
#[async_trait]
impl Provider for PerplexityProvider {
fn name(&self) -> &str {
"perplexity"
}
async fn models(&self) -> Result<Vec<ModelInfo>, BaochuanError> {
debug!("listing models from Perplexity");
let response = self
.inner
.auth(self.inner.client.get(self.inner.models_url()))
.send()
.await?;
let status = response.status();
if !status.is_success() {
let body = response.text().await.unwrap_or_default();
error!(status = %status, body = %body, "Perplexity models error");
return Err(BaochuanError::Api { status: status.as_u16(), message: body });
}
let list: PerplexityModelList = response.json().await?;
Ok(list.data.into_iter().map(|m| ModelInfo {
id: m.id,
owned_by: m.owned_by,
context_length: m.context_length,
display_name: None,
}).collect())
}
async fn chat(&self, request: &ChatRequest) -> Result<ChatResponse, BaochuanError> {
debug!(model = %request.model, "sending chat request to Perplexity");
let response = self
.inner
.auth(self.inner.client.post(self.inner.chat_url()))
.json(request)
.send()
.await?;
let status = response.status();
if !status.is_success() {
let body = response.text().await.unwrap_or_default();
error!(status = %status, body = %body, "Perplexity API error");
return Err(BaochuanError::Api { status: status.as_u16(), message: body });
}
let ppl: PerplexityResponse = response.json().await?;
debug!(id = %ppl.id, "received Perplexity response");
Ok(ChatResponse {
id: ppl.id,
model: ppl.model,
choices: ppl.choices,
usage: ppl.usage,
citations: if ppl.citations.is_empty() { None } else { Some(ppl.citations) },
})
}
async fn stream_chat(&self, request: &ChatRequest) -> Result<ChunkStream, BaochuanError> {
self.inner.stream_chat(request, self.name()).await
}
}