mod deepseek;
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) enum Provider {
DeepSeek,
}
impl Provider {
pub(crate) fn from_base_url(url: &str) -> Option<Self> {
if deepseek::matches_base_url(url) {
Some(Self::DeepSeek)
} else {
None
}
}
pub(crate) fn display_name(self) -> &'static str {
match self {
Self::DeepSeek => deepseek::DISPLAY_NAME,
}
}
}
fn url_matches_host(url: &str, base: &str) -> bool {
let url = url.to_ascii_lowercase();
let base = base.to_ascii_lowercase();
match url.strip_prefix(&base) {
Some("") => true,
Some(rest) => rest.starts_with(['/', ':', '?', '#']),
None => false,
}
}
pub(crate) fn fetch_third_party_usage(
provider: Provider,
api_key: &str,
) -> Result<ThirdPartyStats, ThirdPartyError> {
match provider {
Provider::DeepSeek => deepseek::fetch(api_key),
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct ThirdPartyStats {
pub(crate) is_available: bool,
pub(crate) rows: Vec<StatRow>,
}
impl ThirdPartyStats {
fn from_rows(rows: Vec<StatRow>) -> Self {
Self {
is_available: true,
rows,
}
}
fn unavailable(reason: &str) -> Self {
Self {
is_available: false,
rows: vec![StatRow {
label: String::new(),
value: reason.to_string(),
kind: StatRowKind::Danger,
}],
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct StatRow {
pub(crate) label: String,
pub(crate) value: String,
pub(crate) kind: StatRowKind,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub(crate) enum StatRowKind {
Heading,
Body,
Danger,
Faint,
}
#[derive(Debug)]
pub(crate) enum ThirdPartyError {
Status,
RateLimited {
retry_after: Option<std::time::Duration>,
},
Network,
Parse,
}
fn get_json(url: &str, api_key: &str) -> Result<String, ThirdPartyError> {
let mut response = crate::usage::http_agent()
.get(url)
.header("Authorization", &format!("Bearer {api_key}"))
.call()
.map_err(|_| ThirdPartyError::Network)?;
let status = response.status().as_u16();
if status == 429 {
let retry_after = response
.headers()
.get("retry-after")
.and_then(|v| v.to_str().ok())
.and_then(crate::usage::parse_retry_after);
return Err(ThirdPartyError::RateLimited { retry_after });
}
if status >= 400 {
return Err(ThirdPartyError::Status);
}
response
.body_mut()
.read_to_string()
.map_err(|_| ThirdPartyError::Network)
}
fn cache_path(profile_name: &str) -> Option<PathBuf> {
crate::profile::profile_dir(profile_name)
.ok()
.map(|p| p.join("third_party_cache.json"))
}
pub(crate) fn load_third_party_disk_cache(name: &str) -> Option<ThirdPartyStats> {
cache_path(name).and_then(|p| {
let text = std::fs::read_to_string(p).ok()?;
serde_json::from_str::<ThirdPartyStats>(&text).ok()
})
}
pub(crate) fn write_third_party_disk_cache(name: &str, stats: &ThirdPartyStats) {
let Some(path) = cache_path(name) else {
return;
};
let Ok(json) = serde_json::to_string(stats) else {
return;
};
let _ = crate::profile::atomic_write(&path, json);
}
#[cfg(test)]
#[path = "../../tests/inline/providers.rs"]
mod tests;