1use crate::{
2 anthropic::{json_error, schema::MessagesRequest},
3 config::AliasProvider,
4 provider::{CliHandlers, Provider, RequestContext},
5};
6use anyhow::{Result, anyhow};
7use async_trait::async_trait;
8use axum::{http::StatusCode, response::Response};
9use std::collections::{BTreeMap, HashSet};
10use std::sync::Arc;
11
12pub const ANTHROPIC_STYLE_ALIASES: &[&str] = &[
13 "haiku",
14 "claude-haiku-4-5",
15 "claude-haiku-4-5-20251001",
16 "sonnet",
17 "claude-sonnet-4-6",
18 "claude-sonnet-5",
19 "opus",
20 "claude-opus-4-7",
21 "claude-opus-4-8",
22 "claude-opus-5",
23 "fable",
24 "claude-fable-5",
25];
26
27pub const CURSOR_PREFIXES: &[&str] = &["cursor:", "cursor-plan:", "cursor-ask:"];
28
29const CURSOR_LEGACY_MODELS: &[&str] = &[
30 "cursor",
31 "cursor-agent",
32 "cursor-composer",
33 "cursor-composer-fast",
34 "cursor-plan",
35 "cursor-ask",
36 "composer-2.5",
37 "composer-2.5-fast",
38];
39
40pub(crate) const CODEX_MODELS: &[&str] = &[
41 "gpt-5.2",
42 "gpt-5.3-codex",
43 "gpt-5.3-codex-spark",
44 "gpt-5.4",
45 "gpt-5.4-mini",
46 "gpt-5.5",
47 "gpt-5.6-luna",
48 "gpt-5.6-sol",
49 "gpt-5.6-terra",
50];
51
52pub(crate) const KIMI_MODELS: &[&str] = &["kimi-for-coding", "kimi-k2.6", "kimi-k3", "k2.6", "k3"];
53pub(crate) const GROK_MODELS: &[&str] = &["grok-composer-2.5-fast", "grok-4.5"];
54
55pub struct Registry {
56 alias_provider: AliasProvider,
57 models: BTreeMap<String, Vec<String>>,
58 handlers: BTreeMap<String, Arc<dyn Provider>>,
59}
60
61impl Registry {
62 pub fn new(alias_provider: AliasProvider) -> Self {
63 let mut models: BTreeMap<String, Vec<String>> = BTreeMap::new();
64 models.insert(
65 "anthropic".into(),
66 ANTHROPIC_STYLE_ALIASES
67 .iter()
68 .map(|alias| (*alias).to_string())
69 .collect(),
70 );
71 models.insert("codex".into(), expand_codex_models());
72 models.insert(
73 "kimi".into(),
74 KIMI_MODELS.iter().map(|m| (*m).to_string()).collect(),
75 );
76 models.insert("cursor".into(), build_cursor_models());
77 models.insert(
78 "grok".into(),
79 GROK_MODELS
80 .iter()
81 .map(|model| (*model).to_string())
82 .collect(),
83 );
84 let mut handlers = BTreeMap::new();
85 for (name, entries) in &models {
86 let handler: Arc<dyn Provider> = match name.as_str() {
87 "anthropic" => Arc::new(crate::providers::anthropic::AnthropicProvider::new()),
88 "codex" => Arc::new(crate::providers::codex::CodexProvider::new()),
89 "kimi" => Arc::new(crate::providers::kimi::KimiProvider::new()),
90 "cursor" => Arc::new(crate::providers::cursor::CursorProvider::new()),
91 "grok" => Arc::new(crate::providers::grok::GrokProvider::new()),
92 _ => Arc::new(PlaceholderProvider::new(name, entries.clone())),
93 };
94 handlers.insert(name.clone(), handler);
95 }
96
97 Self {
98 alias_provider,
99 models,
100 handlers,
101 }
102 }
103
104 pub fn with_default_alias() -> Self {
105 Self::new(crate::config::alias_provider())
106 }
107
108 pub fn from_providers(
109 alias_provider: AliasProvider,
110 providers: impl IntoIterator<Item = Arc<dyn Provider>>,
111 ) -> Self {
112 let mut models = BTreeMap::new();
113 let mut handlers = BTreeMap::new();
114 for provider in providers {
115 let name = provider.name().to_string();
116 models.insert(name.clone(), provider.supported_models());
117 handlers.insert(name, provider);
118 }
119 Self {
120 alias_provider,
121 models,
122 handlers,
123 }
124 }
125
126 pub fn list_provider_names(&self) -> Vec<String> {
127 let mut names: Vec<String> = self.handlers.keys().cloned().collect();
128 names.sort_unstable();
129 names
130 }
131
132 pub fn provider(&self, name: &str) -> Option<Arc<dyn Provider>> {
133 self.handlers.get(name).cloned()
134 }
135
136 pub fn supported_models_for(&self, provider: &str) -> Vec<String> {
137 let mut models = self.models.get(provider).cloned().unwrap_or_default();
138 if provider == self.alias_provider.as_str() {
139 for alias in ANTHROPIC_STYLE_ALIASES {
140 if !models.iter().any(|value| value == alias) {
141 models.push((*alias).to_string());
142 }
143 }
144 }
145 models.sort_unstable();
146 models
147 }
148
149 pub fn all_supported_models(&self) -> Vec<(String, String)> {
150 let mut out = Vec::new();
151 for provider in self.handlers.keys() {
152 for model in self.supported_models_for(provider) {
153 out.push((model, provider.clone()));
154 }
155 }
156 out
157 }
158
159 pub fn grouped_models(&self) -> BTreeMap<String, Vec<String>> {
160 let mut out = BTreeMap::new();
161 for provider in self.handlers.keys() {
162 out.insert(provider.clone(), self.supported_models_for(provider));
163 }
164 out
165 }
166
167 pub fn provider_for_model(
168 &self,
169 raw_model: &str,
170 session_affinity: Option<&AliasProvider>,
171 ) -> Option<Arc<dyn Provider>> {
172 let normalized = normalize_incoming_model(raw_model);
173 let _ = session_affinity;
179 if is_anthropic_alias(&normalized) || normalized.starts_with("claude-") {
180 return self.handlers.get(self.alias_provider.as_str()).cloned();
181 }
182 if is_cursor_model(&normalized) {
183 return self.handlers.get("cursor").cloned();
184 }
185
186 for (name, models) in &self.models {
190 if name == "anthropic" {
191 continue;
192 }
193 if models.iter().any(|candidate| candidate == &normalized) {
194 return self.handlers.get(name).cloned();
195 }
196 }
197
198 None
199 }
200
201 pub fn unknown_model_message(&self) -> String {
202 let mut parts = Vec::new();
203 for (provider, models) in self.grouped_models() {
204 let mut models = models;
205 models.sort_unstable();
206 parts.push(format!("{}: {}", provider, models.join(", ")));
207 }
208 format!("Supported: {}.", parts.join("; "))
209 }
210}
211
212pub fn normalize_incoming_model(model: &str) -> String {
213 let suffix = "[1m]";
214 if model.len() >= suffix.len() && model.to_ascii_lowercase().ends_with(suffix) {
215 return model[..model.len() - suffix.len()].to_string();
216 }
217 model.to_string()
218}
219
220pub fn is_anthropic_alias(model: &str) -> bool {
221 ANTHROPIC_STYLE_ALIASES.contains(&model)
222}
223
224pub fn is_cursor_model(model: &str) -> bool {
225 if CURSOR_LEGACY_MODELS.contains(&model) {
226 return true;
227 }
228
229 CURSOR_PREFIXES
230 .iter()
231 .any(|prefix| model.starts_with(prefix))
232}
233
234struct PlaceholderProvider {
235 name: &'static str,
236 models: Vec<String>,
237}
238
239impl PlaceholderProvider {
240 fn new(name: &str, models: Vec<String>) -> Self {
241 let name = match name {
242 "codex" => "codex",
243 "kimi" => "kimi",
244 "cursor" => "cursor",
245 "grok" => "grok",
246 _ => "codex",
247 };
248 Self { name, models }
249 }
250}
251
252#[async_trait]
253impl Provider for PlaceholderProvider {
254 fn name(&self) -> &'static str {
255 self.name
256 }
257
258 fn supported_models(&self) -> Vec<String> {
259 self.models.clone()
260 }
261
262 fn cli(&self) -> &'static dyn CliHandlers {
263 match self.name {
264 "codex" => &CODEX_CLI,
265 "kimi" => &KIMI_CLI,
266 "cursor" => &CURSOR_CLI,
267 "grok" => &GROK_CLI,
268 _ => &CODEX_CLI,
269 }
270 }
271
272 async fn handle_messages(&self, _body: MessagesRequest, ctx: RequestContext) -> Response {
273 placeholder_provider_response("messages", &ctx.provider)
274 }
275
276 async fn handle_count_tokens(&self, _body: MessagesRequest, ctx: RequestContext) -> Response {
277 placeholder_provider_response("count_tokens", &ctx.provider)
278 }
279}
280
281fn placeholder_provider_response(route: &str, provider: &str) -> Response {
282 let _ = route;
283 json_error(
284 StatusCode::NOT_IMPLEMENTED,
285 "unsupported_provider_error",
286 format!("provider '{}' is not yet implemented", provider),
287 )
288}
289
290#[derive(Clone, Copy)]
291struct PlaceholderCli {
292 provider: &'static str,
293}
294
295impl CliHandlers for PlaceholderCli {
296 fn login(&self) -> Result<()> {
297 Err(anyhow!("{}: browser login not supported", self.provider))
298 }
299
300 fn device(&self) -> Result<()> {
301 Err(anyhow!("{}: device login not supported", self.provider))
302 }
303
304 fn status(&self) -> Result<()> {
305 use serde_json::Value;
306 let path = crate::paths::provider_auth_file(self.provider);
307 let legacy = crate::paths::provider_legacy_auth_file(self.provider);
308 if crate::auth::load_auth_file_with_legacy::<Value>(&path, &legacy).is_some() {
309 Ok(())
310 } else {
311 Err(anyhow!("Not authenticated"))
312 }
313 }
314
315 fn logout(&self) -> Result<()> {
316 let path = crate::paths::provider_auth_file(self.provider);
317 let legacy = crate::paths::provider_legacy_auth_file(self.provider);
318 let _ = crate::auth::delete_auth_file(&path, &legacy);
319 Ok(())
320 }
321}
322
323const CODEX_CLI: PlaceholderCli = PlaceholderCli { provider: "codex" };
324const KIMI_CLI: PlaceholderCli = PlaceholderCli { provider: "kimi" };
325const CURSOR_CLI: PlaceholderCli = PlaceholderCli { provider: "cursor" };
326const GROK_CLI: PlaceholderCli = PlaceholderCli { provider: "grok" };
327fn expand_codex_models() -> Vec<String> {
328 let mut set = HashSet::new();
329 let mut out = Vec::new();
330 for model in CODEX_MODELS {
331 if set.insert((*model).to_string()) {
332 out.push((*model).to_string());
333 }
334 let fast = format!("{model}-fast");
335 if set.insert(fast.clone()) {
336 out.push(fast);
337 }
338 }
339 out.sort_unstable();
340 out
341}
342
343fn build_cursor_models() -> Vec<String> {
344 let mut out: Vec<String> = CURSOR_LEGACY_MODELS
345 .iter()
346 .map(|s| (*s).to_string())
347 .collect();
348 out.sort_unstable();
349 out
350}
351
352#[cfg(test)]
353mod tests {
354 use super::*;
355
356 #[test]
357 fn normalize_model_trims_hint() {
358 assert_eq!(normalize_incoming_model("gpt-5.4-fast[1m]"), "gpt-5.4-fast");
359 assert_eq!(normalize_incoming_model("gpt-5.4-fast"), "gpt-5.4-fast");
360 }
361
362 #[test]
363 fn alias_routes_to_configured_provider() {
364 let registry = Registry::new(AliasProvider::Kimi);
365 let p = registry.provider_for_model("haiku", None);
366 assert!(p.is_some());
367 assert_eq!(p.expect("provider").name(), "kimi");
368 }
369
370 #[test]
371 fn opus_4_8_routes_to_configured_provider() {
372 let registry = Registry::new(AliasProvider::Codex);
373 let p = registry.provider_for_model("claude-opus-4-8", None);
374 assert!(p.is_some());
375 assert_eq!(p.expect("provider").name(), "codex");
376 }
377
378 #[test]
379 fn claude_5_aliases_route_to_configured_provider() {
380 let registry = Registry::new(AliasProvider::Codex);
381 for model in [
382 "claude-sonnet-5",
383 "claude-opus-5",
384 "fable",
385 "claude-fable-5",
386 ] {
387 let p = registry.provider_for_model(model, None);
388 assert!(p.is_some(), "{model} should route to a provider");
389 assert_eq!(p.expect("provider").name(), "codex");
390 }
391 }
392
393 #[test]
394 fn claude_models_route_to_anthropic_passthrough_by_default() {
395 let registry = Registry::new(AliasProvider::Anthropic);
396 for model in [
397 "opus",
398 "claude-opus-4-8",
399 "sonnet",
400 "haiku",
401 "claude-3-5-haiku-20241022",
402 ] {
403 let p = registry.provider_for_model(model, None);
404 assert!(p.is_some(), "{model} should route");
405 assert_eq!(p.expect("provider").name(), "anthropic", "{model}");
406 }
407 }
408
409 #[test]
410 fn explicit_codex_model_routes_to_codex_while_default_is_anthropic() {
411 let registry = Registry::new(AliasProvider::Anthropic);
412 let p = registry.provider_for_model("gpt-5.6-terra", None);
413 assert_eq!(p.expect("provider").name(), "codex");
414 }
415
416 #[test]
417 fn session_affinity_cannot_hijack_claude_slot() {
418 let registry = Registry::new(AliasProvider::Anthropic);
420 let p = registry.provider_for_model("claude-opus-4-8", Some(&AliasProvider::Codex));
421 assert_eq!(p.expect("provider").name(), "anthropic");
422 }
423
424 #[test]
425 fn cursor_prefix_routes() {
426 let registry = Registry::new(AliasProvider::Codex);
427 assert_eq!(
428 registry
429 .provider_for_model("cursor:gpt-5.5", None)
430 .unwrap()
431 .name(),
432 "cursor"
433 );
434 assert_eq!(
435 registry
436 .provider_for_model("cursor-plan:gpt-5.5", None)
437 .unwrap()
438 .name(),
439 "cursor"
440 );
441 assert_eq!(
442 registry
443 .provider_for_model("cursor-ask:gpt-5.5", None)
444 .unwrap()
445 .name(),
446 "cursor"
447 );
448 }
449}