lean_ctx_protocol/hive/
model_catalog.rs1use serde::{Deserialize, Serialize};
14
15pub use super::effective_policy::is_country;
16
17pub const CATALOG_SCHEMA: &str = "leanctx.hive.model-catalog/v1";
19pub const MODEL_ID_PREFIX: &str = "leanctx/";
21
22#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
23#[serde(deny_unknown_fields)]
24pub struct ModelCatalog {
25 pub schema: String,
26 pub catalog_version: String,
27 pub price_version: String,
28 #[serde(default)]
29 pub models: Vec<CatalogModel>,
30 #[serde(default)]
31 pub direct: Vec<DirectVendor>,
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
35#[serde(rename_all = "snake_case")]
36pub enum ModelStatus {
37 Available,
38 Unpriced,
39}
40
41#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
42#[serde(deny_unknown_fields)]
43pub struct CatalogModel {
44 pub id: String,
45 pub display_name: String,
46 pub vendor: String,
47 pub origin_country: String,
49 #[serde(default, skip_serializing_if = "Option::is_none")]
50 pub license: Option<String>,
51 pub status: ModelStatus,
52 pub plans: Vec<String>,
53 #[serde(default, skip_serializing_if = "Option::is_none")]
54 pub context_window: Option<u32>,
55 pub hosts: Vec<CatalogHost>,
57}
58
59#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
60#[serde(deny_unknown_fields)]
61pub struct CatalogHost {
62 pub provider: String,
63 #[serde(default, skip_serializing_if = "Option::is_none")]
64 pub upstream_model: Option<String>,
65 pub host_country: String,
67 #[serde(default, skip_serializing_if = "Option::is_none")]
68 pub host_region: Option<String>,
69 #[serde(default, skip_serializing_if = "Option::is_none")]
70 pub retention: Option<String>,
71 #[serde(default, skip_serializing_if = "Option::is_none")]
72 pub price_in_per_mtok_micros: Option<u64>,
73 #[serde(default, skip_serializing_if = "Option::is_none")]
74 pub price_cached_in_per_mtok_micros: Option<u64>,
75 #[serde(default, skip_serializing_if = "Option::is_none")]
76 pub price_out_per_mtok_micros: Option<u64>,
77}
78
79#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
80#[serde(deny_unknown_fields)]
81pub struct DirectVendor {
82 pub vendor: String,
83 pub hosts: Vec<String>,
85 pub host_country: String,
86}
87
88impl ModelCatalog {
89 pub fn from_json(text: &str) -> Result<Self, String> {
92 let catalog: Self =
93 serde_json::from_str(text).map_err(|e| format!("model catalog: {e}"))?;
94 if catalog.schema != CATALOG_SCHEMA {
95 return Err(format!("model catalog: schema {:?}", catalog.schema));
96 }
97 for m in &catalog.models {
98 if !is_country(&m.origin_country)
99 || m.hosts.is_empty()
100 || m.hosts.iter().any(|h| !is_country(&h.host_country))
101 {
102 return Err(format!("model catalog: model {}", m.id));
103 }
104 }
105 if let Some(d) = catalog
106 .direct
107 .iter()
108 .find(|d| !is_country(&d.host_country) || d.hosts.is_empty())
109 {
110 return Err(format!("model catalog: direct {}", d.vendor));
111 }
112 Ok(catalog)
113 }
114
115 #[must_use]
117 pub fn model(&self, id: &str) -> Option<&CatalogModel> {
118 self.models.iter().find(|m| m.id == id)
119 }
120
121 #[must_use]
124 pub fn direct_vendor(&self, host: &str) -> Option<&DirectVendor> {
125 let host = host.trim_end_matches('.').to_ascii_lowercase();
126 self.direct.iter().find(|d| {
127 d.hosts.iter().any(|h| {
128 host == *h
129 || host
130 .strip_suffix(h.as_str())
131 .is_some_and(|p| p.ends_with('.'))
132 })
133 })
134 }
135}
136
137impl CatalogModel {
138 #[must_use]
141 pub fn origin_host_label(&self) -> String {
142 let hosts: Vec<String> = self
143 .hosts
144 .iter()
145 .map(|h| {
146 let mut s = format!("{} ({}", h.host_country, h.provider);
147 if let Some(r) = &h.host_region {
148 s.push_str(", ");
149 s.push_str(r);
150 }
151 s.push(')');
152 if h.retention.as_deref() == Some("zero") {
153 s.push_str(" · zero retention");
154 }
155 s
156 })
157 .collect();
158 format!(
159 "origin {} ({}) · hosted {}",
160 self.origin_country,
161 self.vendor,
162 hosts.join(" / ")
163 )
164 }
165}