lean_ctx/core/
hive_models.rs1use std::sync::{Arc, RwLock};
15
16pub const PROVIDER_ID: &str = "leanctx";
18
19pub const UNAVAILABLE_CODE: &str = "subscription_models_unavailable";
22
23pub const TOKEN_UNAVAILABLE_CODE: &str = "hive_token_unavailable";
25
26pub struct ModelsAdmission {
28 pub origin: String,
30 pub token: zeroize::Zeroizing<String>,
32 pub probe: bool,
35}
36
37impl std::fmt::Debug for ModelsAdmission {
39 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40 f.debug_struct("ModelsAdmission")
41 .field("origin", &self.origin)
42 .field("token", &"[redacted]")
43 .field("probe", &self.probe)
44 .finish()
45 }
46}
47
48#[derive(Debug, Clone, PartialEq, Eq)]
50pub enum ModelsRefusal {
51 Unavailable,
54 OriginRefused(String),
57 Held {
60 status: u16,
61 code: String,
62 retry_after_s: u64,
63 line: String,
64 },
65 TokenUnavailable,
67}
68
69pub trait HiveTokenSource: Send + Sync {
73 fn admit_route(&self, fresh_token: bool) -> Result<ModelsAdmission, ModelsRefusal>;
76 fn note_answer(&self, status: u16, retry_after: Option<&str>, body: &[u8]);
79 fn note_passed(&self);
81}
82
83static SOURCE: RwLock<Option<Arc<dyn HiveTokenSource>>> = RwLock::new(None);
84
85pub fn set_hive_token_source(source: Arc<dyn HiveTokenSource>) {
87 *SOURCE
88 .write()
89 .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(source);
90}
91
92pub fn hive_token_source() -> Option<Arc<dyn HiveTokenSource>> {
94 SOURCE
95 .read()
96 .unwrap_or_else(std::sync::PoisonError::into_inner)
97 .clone()
98}
99
100fn is_loopback(host: &str) -> bool {
101 let host = host.trim_start_matches('[').trim_end_matches(']');
102 host == "localhost"
103 || host
104 .parse::<std::net::IpAddr>()
105 .is_ok_and(|ip| ip.is_loopback())
106}
107
108fn origin_of(url: &reqwest::Url) -> String {
109 url.origin().ascii_serialization()
110}
111
112pub fn models_origin(hive_url: &str, models_url: Option<&str>) -> Result<String, String> {
120 let hive = reqwest::Url::parse(hive_url.trim()).map_err(|e| format!("Hive URL: {e}"))?;
121 let hive_host = hive
122 .host_str()
123 .ok_or("Hive URL has no host")?
124 .to_ascii_lowercase();
125 let Some(raw) = models_url.map(str::trim).filter(|s| !s.is_empty()) else {
126 if is_loopback(&hive_host) {
127 return Ok(origin_of(&hive));
128 }
129 if hive.scheme() != "https" {
130 return Err("Hive URL is not https".into());
131 }
132 let port = hive.port().map(|p| format!(":{p}")).unwrap_or_default();
133 return Ok(format!("https://models.{hive_host}{port}"));
134 };
135 let url = reqwest::Url::parse(raw).map_err(|e| format!("models_url: {e}"))?;
136 if !matches!(url.path(), "" | "/")
137 || url.query().is_some()
138 || url.fragment().is_some()
139 || !url.username().is_empty()
140 || url.password().is_some()
141 {
142 return Err("models_url must be a bare origin (scheme, host, port)".into());
143 }
144 let host = url
145 .host_str()
146 .ok_or("models_url has no host")?
147 .to_ascii_lowercase();
148 if is_loopback(&host) && matches!(url.scheme(), "http" | "https") {
149 return Ok(origin_of(&url));
150 }
151 if url.scheme() != "https" {
152 return Err("models_url must use https".into());
153 }
154 let direct_subdomain = host
155 .strip_suffix(&hive_host)
156 .and_then(|rest| rest.strip_suffix('.'))
157 .is_some_and(|label| !label.is_empty() && !label.contains('.'));
158 if host == hive_host || direct_subdomain {
159 Ok(origin_of(&url))
160 } else {
161 Err(format!(
162 "models_url host {host} is not the Hive host {hive_host} or a direct subdomain of it"
163 ))
164 }
165}
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170
171 #[test]
172 fn the_models_origin_stays_on_the_hive_host() {
173 let hive = "https://hive.example.com";
174 assert_eq!(
175 models_origin(hive, None).unwrap(),
176 "https://models.hive.example.com"
177 );
178 for ok in [
179 "https://models.hive.example.com",
180 "https://gw.hive.example.com/",
181 "https://hive.example.com:8443",
182 "http://127.0.0.1:9000",
183 ] {
184 assert!(
185 models_origin(hive, Some(ok)).is_ok(),
186 "{ok} must be honoured"
187 );
188 }
189 for refused in [
190 "https://models.evil.example",
191 "https://hive.example.com.evil.example",
192 "https://a.b.hive.example.com",
193 "https://evilhive.example.com",
194 "http://models.hive.example.com",
195 "https://models.hive.example.com/v1",
196 "https://user@models.hive.example.com",
197 ] {
198 assert!(
199 models_origin(hive, Some(refused)).is_err(),
200 "{refused} must be refused"
201 );
202 }
203 assert_eq!(
204 models_origin("http://127.0.0.1:7000", None).unwrap(),
205 "http://127.0.0.1:7000"
206 );
207 assert!(models_origin("http://hive.example.com", None).is_err());
208 }
209}