mod deepseek;
mod generic;
mod zai;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) enum Provider {
DeepSeek,
Zai,
}
impl Provider {
pub(crate) fn from_base_url(url: &str) -> Option<Self> {
if deepseek::matches_base_url(url) {
Some(Self::DeepSeek)
} else if zai::matches_base_url(url) {
Some(Self::Zai)
} else {
None
}
}
pub(crate) fn display_name(self) -> &'static str {
match self {
Self::DeepSeek => deepseek::DISPLAY_NAME,
Self::Zai => zai::DISPLAY_NAME,
}
}
}
#[derive(Debug, Clone)]
pub(crate) enum ThirdPartyTarget {
Known(Provider),
Generic {
base_url: String,
},
}
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 api_origin(base_url: &str) -> Option<String> {
let scheme_end = base_url.find("://")?;
let after = &base_url[scheme_end + 3..];
let auth_end = after.find(['/', '?', '#']).unwrap_or(after.len());
Some(format!(
"{}://{}",
&base_url[..scheme_end],
&after[..auth_end]
))
}
pub(crate) fn fetch_third_party_usage(
target: &ThirdPartyTarget,
api_key: &str,
hint: Option<&str>,
) -> Result<ThirdPartyStats, ThirdPartyError> {
match target {
ThirdPartyTarget::Known(provider) => match provider {
Provider::DeepSeek => deepseek::fetch(api_key),
Provider::Zai => zai::fetch(api_key),
},
ThirdPartyTarget::Generic { base_url } => generic::fetch(base_url, api_key, hint),
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct ThirdPartyStats {
pub(crate) is_available: bool,
pub(crate) rows: Vec<StatRow>,
#[serde(default)]
pub(crate) bars: Vec<UsageBar>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) plan: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) endpoint: Option<String>,
#[serde(default)]
pub(crate) best_effort: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct UsageBar {
pub(crate) label: String,
pub(crate) pct: f64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) resets_at: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) used: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) total: Option<f64>,
}
impl ThirdPartyStats {
fn from_rows(rows: Vec<StatRow>) -> Self {
Self {
is_available: true,
rows,
bars: Vec::new(),
plan: None,
endpoint: None,
best_effort: false,
}
}
fn unavailable(reason: &str) -> Self {
Self {
is_available: false,
rows: vec![StatRow {
label: String::new(),
value: reason.to_string(),
kind: StatRowKind::Danger,
}],
bars: Vec::new(),
plan: None,
endpoint: None,
best_effort: false,
}
}
}
#[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)
}
#[cfg(test)]
#[path = "../../tests/inline/providers.rs"]
mod tests;