#![allow(deprecated)]
use anyhow::{anyhow, Result};
use async_trait::async_trait;
use log::info;
use reqwest::{header, Client};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use crate::completions::ThinkingLevel;
use crate::constants::PERPLEXITY_API_URL;
use crate::domain::{PerplexityAPICompletionsResponse, RateLimit};
use crate::llm_models::{LLMModel, LLMTools};
use crate::utils::{map_to_range_f32, remove_json_wrapper, remove_think_reasoner_wrapper};
#[derive(Deserialize, Serialize, Debug, Clone, Eq, PartialEq)]
pub enum PerplexityModels {
SonarPro,
Sonar,
SonarReasoning,
#[deprecated(
since = "0.12.0",
note = "`Llama3_1SonarSmall` is deprecated starting February 22, 2025, please use `Sonar` or `SonarPro` instead."
)]
Llama3_1SonarSmall,
#[deprecated(
since = "0.12.0",
note = "`Llama3_1SonarLarge` is deprecated starting February 22, 2025, please use `Sonar` or `SonarPro` instead."
)]
Llama3_1SonarLarge,
#[deprecated(
since = "0.12.0",
note = "`Llama3_1SonarHuge` is deprecated starting February 22, 2025, please use `Sonar` or `SonarPro` instead."
)]
Llama3_1SonarHuge,
}
#[async_trait(?Send)]
impl LLMModel for PerplexityModels {
fn as_str(&self) -> &str {
match self {
PerplexityModels::SonarPro => "sonar-pro",
PerplexityModels::Sonar => "sonar",
PerplexityModels::SonarReasoning => "sonar-reasoning",
#[allow(deprecated)]
PerplexityModels::Llama3_1SonarSmall => "llama-3.1-sonar-small-128k-online",
#[allow(deprecated)]
PerplexityModels::Llama3_1SonarLarge => "llama-3.1-sonar-large-128k-online",
#[allow(deprecated)]
PerplexityModels::Llama3_1SonarHuge => "llama-3.1-sonar-huge-128k-online",
}
}
fn try_from_str(name: &str) -> Option<Self> {
match name.to_lowercase().as_str() {
"sonar-pro" => Some(PerplexityModels::SonarPro),
"sonar" => Some(PerplexityModels::Sonar),
"sonar-reasoning" => Some(PerplexityModels::SonarReasoning),
#[allow(deprecated)]
"llama-3.1-sonar-small-128k-online" => Some(PerplexityModels::Llama3_1SonarSmall),
#[allow(deprecated)]
"llama-3.1-sonar-large-128k-online" => Some(PerplexityModels::Llama3_1SonarLarge),
#[allow(deprecated)]
"llama-3.1-sonar-huge-128k-online" => Some(PerplexityModels::Llama3_1SonarHuge),
_ => None,
}
}
fn default_max_tokens(&self) -> usize {
match self {
PerplexityModels::SonarPro => 200_000,
PerplexityModels::Sonar => 127_072,
PerplexityModels::SonarReasoning => 127_072,
#[allow(deprecated)]
PerplexityModels::Llama3_1SonarSmall
| PerplexityModels::Llama3_1SonarLarge
| PerplexityModels::Llama3_1SonarHuge => 127_072,
}
}
fn get_endpoint(&self) -> String {
PERPLEXITY_API_URL.to_string()
}
fn get_body(
&self,
instructions: &str,
json_schema: &Value,
function_call: bool,
_max_tokens: &usize,
temperature: &f32,
_tools: Option<&[LLMTools]>,
_thinking_level: Option<&ThinkingLevel>,
) -> serde_json::Value {
let base_instructions = self.get_base_instructions(Some(function_call));
let system_message = json!({
"role": "system",
"content": base_instructions,
});
let user_message = json!({
"role": "user",
"content": format!(
"<instructions>
{instructions}
</instructions>
<output json schema>
{json_schema}
</output json schema>"
),
});
json!({
"model": self.as_str(),
"temperature": temperature,
"messages": vec![
system_message,
user_message,
],
})
}
async fn call_api(
&self,
api_key: &str,
_version: Option<String>,
body: &serde_json::Value,
debug: bool,
_tools: Option<&[LLMTools]>,
) -> Result<String> {
let model_url = self.get_endpoint();
if debug {
info!("[debug] Perplexity API URL: {:#?}", model_url);
}
let client = Client::new();
let response = client
.post(model_url)
.header(header::CONTENT_TYPE, "application/json")
.bearer_auth(api_key)
.json(&body)
.send()
.await?;
let response_status = response.status();
let response_text = response.text().await?;
if debug {
info!(
"[debug] Perplexity API response: [{}] {:#?}",
&response_status, &response_text
);
}
Ok(response_text)
}
fn get_data(&self, response_text: &str, _function_call: bool) -> Result<String> {
let completions_response: PerplexityAPICompletionsResponse =
serde_json::from_str(response_text)?;
completions_response
.choices
.iter()
.filter_map(|choice| choice.message.as_ref())
.find(|&message| message.role == Some("assistant".to_string()))
.and_then(|message| {
message
.content
.as_ref()
.map(|content| self.sanitize_json_response(content))
})
.ok_or_else(|| anyhow!("Assistant role content not found"))
}
fn sanitize_json_response(&self, json_response: &str) -> String {
let no_json_text = remove_json_wrapper(json_response);
if self == &PerplexityModels::SonarReasoning {
remove_think_reasoner_wrapper(&no_json_text)
} else {
no_json_text
}
}
fn get_rate_limit(&self) -> RateLimit {
RateLimit {
tpm: 50 * 127_072, rpm: 50, }
}
fn get_normalized_temperature(&self, relative_temp: u32) -> f32 {
let min = 0.0f32;
let max = 1.99999f32;
map_to_range_f32(min, max, relative_temp)
}
}