use serde::{Deserialize, Serialize};
pub use super::effective_policy::is_country;
pub const CATALOG_SCHEMA: &str = "leanctx.hive.model-catalog/v1";
pub const MODEL_ID_PREFIX: &str = "leanctx/";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ModelCatalog {
pub schema: String,
pub catalog_version: String,
pub price_version: String,
#[serde(default)]
pub models: Vec<CatalogModel>,
#[serde(default)]
pub direct: Vec<DirectVendor>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ModelStatus {
Available,
Unpriced,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct CatalogModel {
pub id: String,
pub display_name: String,
pub vendor: String,
pub origin_country: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub license: Option<String>,
pub status: ModelStatus,
pub plans: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub context_window: Option<u32>,
pub hosts: Vec<CatalogHost>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct CatalogHost {
pub provider: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub upstream_model: Option<String>,
pub host_country: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub host_region: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub retention: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub price_in_per_mtok_micros: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub price_cached_in_per_mtok_micros: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub price_out_per_mtok_micros: Option<u64>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct DirectVendor {
pub vendor: String,
pub hosts: Vec<String>,
pub host_country: String,
}
impl ModelCatalog {
pub fn from_json(text: &str) -> Result<Self, String> {
let catalog: Self =
serde_json::from_str(text).map_err(|e| format!("model catalog: {e}"))?;
if catalog.schema != CATALOG_SCHEMA {
return Err(format!("model catalog: schema {:?}", catalog.schema));
}
for m in &catalog.models {
if !is_country(&m.origin_country)
|| m.hosts.is_empty()
|| m.hosts.iter().any(|h| !is_country(&h.host_country))
{
return Err(format!("model catalog: model {}", m.id));
}
}
if let Some(d) = catalog
.direct
.iter()
.find(|d| !is_country(&d.host_country) || d.hosts.is_empty())
{
return Err(format!("model catalog: direct {}", d.vendor));
}
Ok(catalog)
}
#[must_use]
pub fn model(&self, id: &str) -> Option<&CatalogModel> {
self.models.iter().find(|m| m.id == id)
}
#[must_use]
pub fn direct_vendor(&self, host: &str) -> Option<&DirectVendor> {
let host = host.trim_end_matches('.').to_ascii_lowercase();
self.direct.iter().find(|d| {
d.hosts.iter().any(|h| {
host == *h
|| host
.strip_suffix(h.as_str())
.is_some_and(|p| p.ends_with('.'))
})
})
}
}
impl CatalogModel {
#[must_use]
pub fn origin_host_label(&self) -> String {
let hosts: Vec<String> = self
.hosts
.iter()
.map(|h| {
let mut s = format!("{} ({}", h.host_country, h.provider);
if let Some(r) = &h.host_region {
s.push_str(", ");
s.push_str(r);
}
s.push(')');
if h.retention.as_deref() == Some("zero") {
s.push_str(" · zero retention");
}
s
})
.collect();
format!(
"origin {} ({}) · hosted {}",
self.origin_country,
self.vendor,
hosts.join(" / ")
)
}
}