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