1use crate::{Api, CompatSettings, Cost, InputModality, MaxTokensField, Model, ThinkingFormat};
8use parking_lot::RwLock;
9use std::collections::HashMap;
10use std::sync::LazyLock;
11
12fn extract_model_name(id: &str) -> &str {
14 id.rsplit_once('/').map(|(_, name)| name).unwrap_or(id)
15}
16
17fn default_compat_for_provider(provider: &str) -> Option<CompatSettings> {
22 match provider {
23 "openai" | "openai-responses" | "openai-completions" => Some(CompatSettings {
24 thinking_format: Some(ThinkingFormat::OpenAI),
25 max_tokens_field: Some(MaxTokensField::MaxCompletionTokens),
26 ..CompatSettings::default()
27 }),
28 "openrouter" => Some(CompatSettings {
29 thinking_format: Some(ThinkingFormat::OpenRouter),
30 requires_tool_result_name: true,
31 ..CompatSettings::default()
32 }),
33 "deepseek" => Some(CompatSettings {
34 thinking_format: Some(ThinkingFormat::DeepSeek),
35 max_tokens_field: Some(MaxTokensField::MaxTokens),
36 ..CompatSettings::default()
37 }),
38 "zai" => Some(CompatSettings {
39 thinking_format: Some(ThinkingFormat::Zai),
40 ..CompatSettings::default()
41 }),
42 _ => None,
45 }
46}
47
48static STATIC_MODELS: LazyLock<HashMap<String, Model>> = LazyLock::new(|| {
50 let mut map = HashMap::new();
51
52 add_openai_models(&mut map);
54
55 add_anthropic_models(&mut map);
57
58 add_google_models(&mut map);
60
61 add_deepseek_models(&mut map);
63
64 add_mistral_models(&mut map);
66
67 add_groq_models(&mut map);
69
70 add_cerebras_models(&mut map);
72
73 add_xai_models(&mut map);
75
76 add_openrouter_models(&mut map);
78
79 add_azure_models(&mut map);
81
82 add_zai_models(&mut map);
84 add_minimax_models(&mut map);
86
87 refresh_numerics_from_catalog(&mut map);
94
95 map
96});
97
98fn refresh_numerics_from_catalog(map: &mut HashMap<String, Model>) {
102 for (key, model) in map.iter_mut() {
103 let Some((provider, id)) = key.split_once('/') else {
104 continue;
105 };
106 let Some(entry) = crate::model_db::get_model_entry(provider, id) else {
107 continue;
108 };
109 if entry.context_window > 0 {
110 model.context_window = entry.context_window as usize;
111 }
112 if entry.max_tokens > 0 {
113 model.max_tokens = entry.max_tokens as usize;
114 }
115 if entry.cost_input > 0.0 {
116 model.cost.input = entry.cost_input;
117 }
118 if entry.cost_output > 0.0 {
119 model.cost.output = entry.cost_output;
120 }
121 if entry.cost_cache_read > 0.0 {
122 model.cost.cache_read = entry.cost_cache_read;
123 }
124 if entry.cost_cache_write > 0.0 {
125 model.cost.cache_write = entry.cost_cache_write;
126 }
127 }
128}
129
130fn add_openai_models(map: &mut HashMap<String, Model>) {
131 let models = [
132 ("openai/gpt-4o", "GPT-4o", true, 2.5, 10.0),
133 ("openai/gpt-4o-mini", "GPT-4o Mini", true, 0.15, 0.60),
134 ("openai/gpt-4-turbo", "GPT-4 Turbo", true, 10.0, 30.0),
135 ("openai/gpt-4", "GPT-4", false, 30.0, 60.0),
136 ("openai/gpt-3.5-turbo", "GPT-3.5 Turbo", false, 0.5, 1.5),
137 ("openai/o1-preview", "OpenAI o1 Preview", true, 15.0, 60.0),
138 ("openai/o1-mini", "OpenAI o1 Mini", true, 15.0, 60.0),
139 ("openai/o1", "OpenAI o1", true, 15.0, 60.0),
140 ("openai/o3", "OpenAI o3", true, 15.0, 60.0),
141 ("openai/o3-mini", "OpenAI o3 Mini", true, 15.0, 60.0),
142 ];
143
144 for (id, name, reasoning, input_cost, output_cost) in models {
145 map.insert(
146 id.to_string(),
147 Model {
148 id: extract_model_name(id).to_string(),
149 name: name.to_string(),
150 api: Api::OpenAiCompletions,
151 provider: "openai".to_string(),
152 base_url: "https://api.openai.com/v1".to_string(),
153 reasoning,
154 input: if reasoning {
155 vec![InputModality::Text]
156 } else {
157 vec![InputModality::Text, InputModality::Image]
158 },
159 cost: Cost {
160 input: input_cost,
161 output: output_cost,
162 cache_read: input_cost * 0.5,
163 cache_write: input_cost * 7.5,
164 },
165 context_window: 128_000,
166 max_tokens: 32_000,
167 headers: Default::default(),
168 compat: default_compat_for_provider("openai"),
169 },
170 );
171 }
172}
173
174fn add_anthropic_models(map: &mut HashMap<String, Model>) {
175 let models = [
176 (
177 "anthropic/claude-sonnet-4-20250514",
178 "Claude Sonnet 4",
179 true,
180 3.0,
181 15.0,
182 ),
183 (
184 "anthropic/claude-opus-4-20250514",
185 "Claude Opus 4",
186 true,
187 15.0,
188 75.0,
189 ),
190 (
191 "anthropic/claude-3-5-sonnet-20241022",
192 "Claude 3.5 Sonnet",
193 true,
194 3.0,
195 15.0,
196 ),
197 (
198 "anthropic/claude-3-5-haiku-20241022",
199 "Claude 3.5 Haiku",
200 false,
201 0.8,
202 4.0,
203 ),
204 (
205 "anthropic/claude-3-opus",
206 "Claude 3 Opus",
207 false,
208 15.0,
209 75.0,
210 ),
211 (
212 "anthropic/claude-3-sonnet",
213 "Claude 3 Sonnet",
214 false,
215 3.0,
216 15.0,
217 ),
218 (
219 "anthropic/claude-3-haiku",
220 "Claude 3 Haiku",
221 false,
222 0.25,
223 1.25,
224 ),
225 ];
226
227 for (id, name, reasoning, input_cost, output_cost) in models {
228 map.insert(
229 id.to_string(),
230 Model {
231 id: extract_model_name(id).to_string(),
232 name: name.to_string(),
233 api: Api::AnthropicMessages,
234 provider: "anthropic".to_string(),
235 base_url: "https://api.anthropic.com".to_string(),
236 reasoning,
237 input: vec![InputModality::Text, InputModality::Image],
238 cost: Cost {
239 input: input_cost,
240 output: output_cost,
241 cache_read: input_cost * 0.1,
242 cache_write: input_cost * 1.25,
243 },
244 context_window: 200_000,
245 max_tokens: 8192,
246 headers: Default::default(),
247 compat: default_compat_for_provider("anthropic"),
248 },
249 );
250 }
251}
252
253fn add_google_models(map: &mut HashMap<String, Model>) {
254 let models = [
255 (
256 "google/gemini-2.0-flash",
257 "Gemini 2.0 Flash",
258 0.0,
259 0.0,
260 1_000_000,
261 ),
262 (
263 "google/gemini-2.5-flash",
264 "Gemini 2.5 Flash",
265 0.0,
266 0.0,
267 1_000_000,
268 ),
269 (
270 "google/gemini-2.5-pro",
271 "Gemini 2.5 Pro",
272 1.25,
273 5.0,
274 2_000_000,
275 ),
276 (
277 "google/gemini-1.5-flash",
278 "Gemini 1.5 Flash",
279 0.0,
280 0.0,
281 1_000_000,
282 ),
283 (
284 "google/gemini-1.5-pro",
285 "Gemini 1.5 Pro",
286 1.25,
287 5.0,
288 2_000_000,
289 ),
290 ("google/gemini-pro", "Gemini Pro", 0.125, 0.5, 32_000),
291 ];
292
293 for (id, name, input_cost, output_cost, ctx) in models {
294 map.insert(
295 id.to_string(),
296 Model {
297 id: extract_model_name(id).to_string(),
298 name: name.to_string(),
299 api: Api::GoogleGenerativeAi,
300 provider: "google".to_string(),
301 base_url: "https://generativelanguage.googleapis.com".to_string(),
302 reasoning: false,
303 input: vec![InputModality::Text, InputModality::Image],
304 cost: Cost {
305 input: input_cost,
306 output: output_cost,
307 cache_read: 0.0,
308 cache_write: 0.0,
309 },
310 context_window: ctx,
311 max_tokens: 8192,
312 headers: Default::default(),
313 compat: default_compat_for_provider("google"),
314 },
315 );
316 }
317}
318
319fn add_deepseek_models(map: &mut HashMap<String, Model>) {
320 let legacy_models = [
322 (
323 "deepseek/deepseek-chat",
324 "DeepSeek Chat",
325 false,
326 0.27,
327 1.1,
328 64_000,
329 8192,
330 ),
331 (
332 "deepseek/deepseek-chat-v3",
333 "DeepSeek Chat V3",
334 false,
335 0.27,
336 1.1,
337 64_000,
338 8192,
339 ),
340 (
341 "deepseek/deepseek-reasoner",
342 "DeepSeek Reasoner",
343 true,
344 0.55,
345 2.19,
346 64_000,
347 8192,
348 ),
349 (
350 "deepseek/deepseek-coder",
351 "DeepSeek Coder",
352 false,
353 0.27,
354 1.1,
355 64_000,
356 8192,
357 ),
358 ];
359
360 for (id, name, reasoning, input_cost, output_cost, ctx, max_out) in legacy_models {
361 map.insert(
362 id.to_string(),
363 Model {
364 id: extract_model_name(id).to_string(),
365 name: name.to_string(),
366 api: Api::OpenAiCompletions,
367 provider: "deepseek".to_string(),
368 base_url: "https://api.deepseek.com".to_string(),
369 reasoning,
370 input: vec![InputModality::Text],
371 cost: Cost {
372 input: input_cost,
373 output: output_cost,
374 cache_read: 0.1,
375 cache_write: 1.0,
376 },
377 context_window: ctx,
378 max_tokens: max_out,
379 headers: Default::default(),
380 compat: default_compat_for_provider("deepseek"),
381 },
382 );
383 }
384
385 let v4_models = [
387 (
389 "deepseek/deepseek-v4-flash",
390 "DeepSeek V4 Flash",
391 true,
392 0.14,
393 0.28,
394 1_000_000,
395 384_000,
396 ),
397 (
399 "deepseek/deepseek-v4-pro",
400 "DeepSeek V4 Pro",
401 true,
402 0.435,
403 0.87,
404 1_000_000,
405 384_000,
406 ),
407 ];
408
409 for (id, name, reasoning, input_cost, output_cost, ctx, max_out) in v4_models {
410 map.insert(
411 id.to_string(),
412 Model {
413 id: extract_model_name(id).to_string(),
414 name: name.to_string(),
415 api: Api::OpenAiCompletions,
416 provider: "deepseek".to_string(),
417 base_url: "https://api.deepseek.com".to_string(),
418 reasoning,
419 input: vec![InputModality::Text],
420 cost: Cost {
421 input: input_cost,
422 output: output_cost,
423 cache_read: if input_cost < 0.2 { 0.0028 } else { 0.003625 },
425 cache_write: 0.0, },
427 context_window: ctx,
428 max_tokens: max_out,
429 headers: Default::default(),
430 compat: default_compat_for_provider("deepseek"),
431 },
432 );
433 }
434}
435
436fn add_mistral_models(map: &mut HashMap<String, Model>) {
437 let models = [
438 (
439 "mistral/mistral-large-latest",
440 "Mistral Large",
441 false,
442 2.0,
443 6.0,
444 ),
445 (
446 "mistral/mistral-medium-latest",
447 "Mistral Medium",
448 false,
449 0.5,
450 1.5,
451 ),
452 (
453 "mistral/mistral-small-latest",
454 "Mistral Small",
455 false,
456 0.2,
457 0.6,
458 ),
459 ("mistral/mistral-nemo", "Mistral Nemo", false, 0.15, 0.15),
460 ("mistral/codestral", "Codestral", false, 0.3, 0.9),
461 (
462 "mistral/codestral-mamba",
463 "Codestral Mamba",
464 false,
465 0.25,
466 0.25,
467 ),
468 (
469 "mistral/open-mixtral-8x22b",
470 "Mixtral 8x22B",
471 false,
472 0.45,
473 1.4,
474 ),
475 (
476 "mistral/open-mixtral-8x7b",
477 "Mixtral 8x7B",
478 false,
479 0.24,
480 0.24,
481 ),
482 ];
483
484 for (id, name, reasoning, input_cost, output_cost) in models {
485 map.insert(
486 id.to_string(),
487 Model {
488 id: extract_model_name(id).to_string(),
489 name: name.to_string(),
490 api: Api::OpenAiCompletions,
491 provider: "mistral".to_string(),
492 base_url: "https://api.mistral.ai".to_string(),
493 reasoning,
494 input: vec![InputModality::Text],
495 cost: Cost {
496 input: input_cost,
497 output: output_cost,
498 cache_read: 0.0,
499 cache_write: 0.0,
500 },
501 context_window: 128_000,
502 max_tokens: 32_000,
503 headers: Default::default(),
504 compat: default_compat_for_provider("mistral"),
505 },
506 );
507 }
508}
509
510fn add_groq_models(map: &mut HashMap<String, Model>) {
511 let models = [
512 (
513 "groq/llama-3.3-70b-versatile",
514 "Llama 3.3 70B Versatile",
515 false,
516 0.0,
517 0.0,
518 ),
519 (
520 "groq/llama-3.1-70b-versatile",
521 "Llama 3.1 70B Versatile",
522 false,
523 0.0,
524 0.0,
525 ),
526 (
527 "groq/llama-3.1-8b-instant",
528 "Llama 3.1 8B Instant",
529 false,
530 0.0,
531 0.0,
532 ),
533 (
534 "groq/llama-3-70b-versatile",
535 "Llama 3 70B Versatile",
536 false,
537 0.0,
538 0.0,
539 ),
540 (
541 "groq/llama-3-8b-versatile",
542 "Llama 3 8B Versatile",
543 false,
544 0.0,
545 0.0,
546 ),
547 ("groq/mixtral-8x7b-32768", "Mixtral 8x7B", false, 0.0, 0.0),
548 ("groq/gemma2-9b-it", "Gemma 2 9B", false, 0.0, 0.0),
549 ("groq/gemma-7b-it", "Gemma 7B", false, 0.0, 0.0),
550 ];
551
552 for (id, name, reasoning, input_cost, output_cost) in models {
553 map.insert(
554 id.to_string(),
555 Model {
556 id: extract_model_name(id).to_string(),
557 name: name.to_string(),
558 api: Api::OpenAiCompletions,
559 provider: "groq".to_string(),
560 base_url: "https://api.groq.com/openai/v1".to_string(),
561 reasoning,
562 input: vec![InputModality::Text],
563 cost: Cost {
564 input: input_cost,
565 output: output_cost,
566 cache_read: 0.0,
567 cache_write: 0.0,
568 },
569 context_window: 128_000,
570 max_tokens: 8192,
571 headers: Default::default(),
572 compat: default_compat_for_provider("groq"),
573 },
574 );
575 }
576}
577
578fn add_cerebras_models(map: &mut HashMap<String, Model>) {
579 let models = [
580 ("cerebras/llama-3.3-70b", "Llama 3.3 70B", false, 0.0, 0.0),
581 ("cerebras/llama-3.1-8b", "Llama 3.1 8B", false, 0.0, 0.0),
582 ("cerebras/qwen-2.5-32b", "Qwen 2.5 32B", false, 0.0, 0.0),
583 ("cerebras/qwen-2.5-7b", "Qwen 2.5 7B", false, 0.0, 0.0),
584 ];
585
586 for (id, name, reasoning, input_cost, output_cost) in models {
587 map.insert(
588 id.to_string(),
589 Model {
590 id: extract_model_name(id).to_string(),
591 name: name.to_string(),
592 api: Api::OpenAiCompletions,
593 provider: "cerebras".to_string(),
594 base_url: "https://api.cerebras.ai".to_string(),
595 reasoning,
596 input: vec![InputModality::Text],
597 cost: Cost {
598 input: input_cost,
599 output: output_cost,
600 cache_read: 0.0,
601 cache_write: 0.0,
602 },
603 context_window: 128_000,
604 max_tokens: 8192,
605 headers: Default::default(),
606 compat: default_compat_for_provider("cerebras"),
607 },
608 );
609 }
610}
611
612fn add_xai_models(map: &mut HashMap<String, Model>) {
613 let models = [
614 ("xai/grok-2", "Grok 2", false, 5.0, 15.0),
615 ("xai/grok-2-mini", "Grok 2 Mini", false, 0.3, 0.5),
616 ("xai/grok-1", "Grok 1", false, 5.0, 15.0),
617 ("xai/grok-1.5", "Grok 1.5", false, 5.0, 15.0),
618 ];
619
620 for (id, name, reasoning, input_cost, output_cost) in models {
621 map.insert(
622 id.to_string(),
623 Model {
624 id: extract_model_name(id).to_string(),
625 name: name.to_string(),
626 api: Api::OpenAiCompletions,
627 provider: "xai".to_string(),
628 base_url: "https://api.x.ai/v1".to_string(),
629 reasoning,
630 input: vec![InputModality::Text],
631 cost: Cost {
632 input: input_cost,
633 output: output_cost,
634 cache_read: 0.0,
635 cache_write: 0.0,
636 },
637 context_window: 131_072,
638 max_tokens: 8192,
639 headers: Default::default(),
640 compat: default_compat_for_provider("xai"),
641 },
642 );
643 }
644}
645
646fn add_openrouter_models(map: &mut HashMap<String, Model>) {
647 let models = [
648 (
649 "openrouter/anthropic/claude-3.5-sonnet",
650 "Claude 3.5 Sonnet",
651 false,
652 3.0,
653 15.0,
654 ),
655 (
656 "openrouter/anthropic/claude-3-opus",
657 "Claude 3 Opus",
658 false,
659 15.0,
660 75.0,
661 ),
662 (
663 "openrouter/google/gemini-pro-1.5",
664 "Gemini Pro 1.5",
665 false,
666 1.25,
667 5.0,
668 ),
669 (
670 "openrouter/meta-llama/llama-3-70b",
671 "Llama 3 70B",
672 false,
673 0.65,
674 2.75,
675 ),
676 (
677 "openrouter/meta-llama/llama-3-8b",
678 "Llama 3 8B",
679 false,
680 0.2,
681 0.2,
682 ),
683 (
684 "openrouter/mistralai/mistral-large",
685 "Mistral Large",
686 false,
687 2.0,
688 6.0,
689 ),
690 (
691 "openrouter/deepseek/deepseek-chat",
692 "DeepSeek Chat",
693 false,
694 0.27,
695 1.1,
696 ),
697 ("openrouter/qwen/qwen-2-72b", "Qwen 2 72B", false, 0.9, 0.9),
698 (
699 "openrouter/nousresearch/hermes-3-llama-3-70b",
700 "Hermes 3 70B",
701 false,
702 0.5,
703 1.5,
704 ),
705 ];
706
707 for (id, name, reasoning, input_cost, output_cost) in models {
708 map.insert(
709 id.to_string(),
710 Model {
711 id: extract_model_name(id).to_string(),
712 name: name.to_string(),
713 api: Api::OpenAiCompletions,
714 provider: "openrouter".to_string(),
715 base_url: "https://openrouter.ai/api/v1".to_string(),
716 reasoning,
717 input: vec![InputModality::Text],
718 cost: Cost {
719 input: input_cost,
720 output: output_cost,
721 cache_read: 0.0,
722 cache_write: 0.0,
723 },
724 context_window: 128_000,
725 max_tokens: 32_000,
726 headers: [
727 ("HTTP-Referer".to_string(), "https://oxicode-ai".to_string()),
728 ("X-Title".to_string(), "oxicode-ai".to_string()),
729 ]
730 .into_iter()
731 .collect(),
732 compat: default_compat_for_provider("openrouter"),
733 },
734 );
735 }
736}
737
738fn add_azure_models(map: &mut HashMap<String, Model>) {
739 let models = [
740 ("azure-openai/gpt-4o", "GPT-4o", false, 2.5, 10.0),
741 ("azure-openai/gpt-4o-mini", "GPT-4o Mini", false, 0.15, 0.60),
742 ("azure-openai/gpt-4-turbo", "GPT-4 Turbo", false, 10.0, 30.0),
743 ];
744
745 for (id, name, reasoning, input_cost, output_cost) in models {
746 map.insert(
747 id.to_string(),
748 Model {
749 id: extract_model_name(id).to_string(),
750 name: name.to_string(),
751 api: Api::AzureOpenAiResponses,
752 provider: "azure-openai".to_string(),
753 base_url: "https://{your-resource-name}.openai.azure.com".to_string(),
754 reasoning,
755 input: vec![InputModality::Text, InputModality::Image],
756 cost: Cost {
757 input: input_cost,
758 output: output_cost,
759 cache_read: 0.0,
760 cache_write: 0.0,
761 },
762 context_window: 128_000,
763 max_tokens: 32_000,
764 headers: Default::default(),
765 compat: Some(crate::CompatSettings {
766 supports_store: false,
767 supports_developer_role: false,
768 supports_reasoning_effort: false,
769 supports_usage_in_streaming: false,
770 max_tokens_field: Some(crate::MaxTokensField::MaxCompletionTokens),
771 requires_tool_result_name: true,
772 requires_assistant_after_tool_result: false,
773 requires_thinking_as_text: false,
774 thinking_format: None,
775 }),
776 },
777 );
778 }
779}
780
781fn add_zai_models(map: &mut HashMap<String, Model>) {
782 let models = [
783 ("zai/glm-4.7", "GLM-4.7", true, 0.0, 0.0),
784 ("zai/glm-5-turbo", "GLM-5-Turbo", true, 0.0, 0.0),
785 ("zai/glm-5.1", "GLM-5.1", true, 0.0, 0.0),
786 ("zai/glm-5v-turbo", "GLM-5V-Turbo", true, 0.0, 0.0),
787 ("zai/glm-4.5-air", "GLM-4.5-Air", true, 0.0, 0.0),
788 ];
789
790 for (id, name, reasoning, input_cost, output_cost) in models {
791 map.insert(
792 id.to_string(),
793 Model {
794 id: extract_model_name(id).to_string(),
795 name: name.to_string(),
796 api: Api::OpenAiCompletions,
797 provider: "zai".to_string(),
798 base_url: "https://api.z.ai/api/coding/paas/v4".to_string(),
799 reasoning,
800 input: vec![InputModality::Text],
801 cost: Cost {
802 input: input_cost,
803 output: output_cost,
804 cache_read: 0.0,
805 cache_write: 0.0,
806 },
807 context_window: 200_000,
808 max_tokens: 131_072,
809 headers: Default::default(),
810 compat: default_compat_for_provider("zai"),
811 },
812 );
813 }
814}
815
816fn add_minimax_models(map: &mut HashMap<String, Model>) {
817 let models = [
818 ("minimax/MiniMax-M2.7", "MiniMax-M2.7", true, 0.0, 0.0),
819 (
820 "minimax/MiniMax-M2.7-highspeed",
821 "MiniMax-M2.7-highspeed",
822 true,
823 0.0,
824 0.0,
825 ),
826 ];
827
828 for (id, name, reasoning, input_cost, output_cost) in models {
829 map.insert(
830 id.to_string(),
831 Model {
832 id: extract_model_name(id).to_string(),
833 name: name.to_string(),
834 api: Api::AnthropicMessages,
835 provider: "minimax".to_string(),
836 base_url: "https://api.minimax.io".to_string(),
837 reasoning,
838 input: vec![InputModality::Text],
839 cost: Cost {
840 input: input_cost,
841 output: output_cost,
842 cache_read: 0.06,
843 cache_write: 0.375,
844 },
845 context_window: 204_800,
846 max_tokens: 131_072,
847 headers: Default::default(),
848 compat: default_compat_for_provider("minimax"),
849 },
850 );
851 }
852}
853
854#[derive(Default)]
860pub struct ModelRegistry {
861 static_models: HashMap<String, Model>,
862 dynamic_models: parking_lot::RwLock<HashMap<String, Model>>,
863}
864
865impl ModelRegistry {
866 pub fn new() -> Self {
868 Self {
869 static_models: HashMap::new(),
870 dynamic_models: RwLock::new(HashMap::new()),
871 }
872 }
873
874 pub fn from_static() -> Self {
878 Self {
879 static_models: STATIC_MODELS.clone(),
880 dynamic_models: RwLock::new(HashMap::new()),
881 }
882 }
883
884 pub fn register(&self, model: Model) {
889 let key = format!("{}/{}", model.provider, model.id);
890 self.dynamic_models.write().insert(key, model);
891 }
892
893 pub fn unregister(&self, provider: &str, model_id: &str) {
895 let key = format!("{}/{}", provider, model_id);
896 self.dynamic_models.write().remove(&key);
897 }
898
899 pub fn lookup(&self, provider: &str, model_id: &str) -> Option<Model> {
903 let key = format!("{}/{}", provider, model_id);
904 if let Some(m) = self.dynamic_models.read().get(&key) {
906 return Some(m.clone());
907 }
908 self.static_models.get(&key).cloned()
910 }
911
912 pub fn get(provider: &str, model_id: &str) -> Option<&'static Model> {
914 let key = format!("{}/{}", provider, model_id);
915 STATIC_MODELS.get(&key)
916 }
917
918 pub fn get_by_provider(provider: &str) -> Vec<&'static Model> {
920 STATIC_MODELS
921 .values()
922 .filter(|m| m.provider == provider)
923 .collect()
924 }
925
926 pub fn all() -> Vec<&'static Model> {
928 STATIC_MODELS.values().collect()
929 }
930
931 pub fn dynamic_models(&self) -> Vec<Model> {
933 self.dynamic_models.read().values().cloned().collect()
934 }
935
936 pub fn model_ids(&self) -> Vec<String> {
938 let static_ids: Vec<String> = self.static_models.keys().cloned().collect();
939 let dynamic_ids: Vec<String> = self.dynamic_models.read().keys().cloned().collect();
940 static_ids.into_iter().chain(dynamic_ids).collect()
941 }
942
943 pub fn search(pattern: &str) -> Vec<&'static Model> {
945 let pattern_lower = pattern.to_lowercase();
946 STATIC_MODELS
947 .values()
948 .filter(|m| {
949 m.id.to_lowercase().contains(&pattern_lower)
950 || m.name.to_lowercase().contains(&pattern_lower)
951 })
952 .collect()
953 }
954}
955
956static GLOBAL_REGISTRY: LazyLock<ModelRegistry> = LazyLock::new(ModelRegistry::from_static);
960
961pub fn register_model(model: Model) {
969 GLOBAL_REGISTRY.register(model);
970}
971
972pub fn unregister_model(provider: &str, model_id: &str) {
974 GLOBAL_REGISTRY.unregister(provider, model_id);
975}
976
977pub fn lookup_model(provider: &str, model_id: &str) -> Option<Model> {
981 GLOBAL_REGISTRY.lookup(provider, model_id)
982}
983
984pub fn get_model(provider: &str, model_id: &str) -> Option<&'static Model> {
986 ModelRegistry::get(provider, model_id)
987}
988
989pub fn get_providers() -> Vec<&'static str> {
991 let mut providers: Vec<&'static str> = STATIC_MODELS
992 .values()
993 .map(|m| m.provider.as_str())
994 .collect();
995 providers.sort();
996 providers.dedup();
997 providers
998}
999
1000pub fn get_models(provider: &str) -> Vec<&'static Model> {
1002 ModelRegistry::get_by_provider(provider)
1003}
1004
1005pub fn dynamic_models() -> Vec<Model> {
1007 GLOBAL_REGISTRY.dynamic_models()
1008}
1009
1010#[cfg(test)]
1011mod tests {
1012 use super::*;
1013
1014 #[test]
1015 fn test_get_model() {
1016 let model = get_model("openai", "gpt-4o");
1017 assert!(model.is_some());
1018 let model = model.unwrap();
1019 assert_eq!(model.provider, "openai");
1020 }
1022
1023 #[test]
1024 fn test_get_providers() {
1025 let providers = get_providers();
1026 assert!(providers.contains(&"openai"));
1027 assert!(providers.contains(&"anthropic"));
1028 assert!(providers.contains(&"google"));
1029 assert!(providers.contains(&"deepseek"));
1030 assert!(providers.contains(&"mistral"));
1031 assert!(providers.contains(&"groq"));
1032 }
1033
1034 #[test]
1035 fn test_deepseek_model() {
1036 let model = get_model("deepseek", "deepseek-chat");
1037 assert!(model.is_some());
1038 let model = model.unwrap();
1039 assert_eq!(model.provider, "deepseek");
1040 assert_eq!(model.base_url, "https://api.deepseek.com");
1041 }
1042
1043 #[test]
1044 fn test_deepseek_v4_models() {
1045 let flash = get_model("deepseek", "deepseek-v4-flash");
1046 assert!(flash.is_some(), "deepseek-v4-flash should be registered");
1047 let flash = flash.unwrap();
1048 assert_eq!(flash.provider, "deepseek");
1049 assert_eq!(flash.context_window, 1_000_000);
1050 assert_eq!(flash.max_tokens, 384_000);
1051 assert!(flash.reasoning);
1052
1053 let pro = get_model("deepseek", "deepseek-v4-pro");
1054 assert!(pro.is_some(), "deepseek-v4-pro should be registered");
1055 let pro = pro.unwrap();
1056 assert_eq!(pro.provider, "deepseek");
1057 assert_eq!(pro.context_window, 1_000_000);
1058 assert_eq!(pro.max_tokens, 384_000);
1059 assert!(pro.reasoning);
1060 assert!(pro.cost.input > flash.cost.input);
1062 }
1063
1064 #[test]
1065 fn test_minimax_m3_available_via_catalog() {
1066 let m3 = crate::model_db::get_model_entry("minimax", "MiniMax-M3")
1071 .expect("models.dev snapshot must carry MiniMax-M3");
1072 assert_eq!(m3.context_window, 1_000_000);
1073 assert_eq!(m3.max_tokens, 128_000);
1074 assert!(m3.reasoning, "M3 supports thinking");
1075 assert!(m3.input.contains(&InputModality::Text));
1076 assert!(
1077 m3.input.contains(&InputModality::Image),
1078 "M3 is natively multimodal — must accept image input"
1079 );
1080 }
1081
1082 #[test]
1083 fn test_search_models() {
1084 let results = ModelRegistry::search("gpt");
1085 assert!(!results.is_empty());
1086 assert!(
1087 results
1088 .iter()
1089 .all(|m| m.name.to_lowercase().contains("gpt"))
1090 );
1091 }
1092
1093 #[test]
1094 fn test_model_registry_instance() {
1095 let registry = ModelRegistry::from_static();
1096 assert!(registry.lookup("openai", "gpt-4o").is_some());
1097 assert!(registry.lookup("fake", "fake-model").is_none());
1098 }
1099
1100 #[test]
1101 fn static_registry_numerics_match_catalog() {
1102 let mut checked = 0;
1108 for provider in get_providers() {
1109 for model in get_models(provider) {
1110 let Some(entry) = crate::model_db::get_model_entry(provider, &model.id) else {
1111 continue; };
1113 if entry.context_window > 0 {
1114 assert_eq!(
1115 model.context_window, entry.context_window as usize,
1116 "{provider}/{} context window must match models.dev",
1117 model.id
1118 );
1119 }
1120 if entry.max_tokens > 0 {
1121 assert_eq!(
1122 model.max_tokens, entry.max_tokens as usize,
1123 "{provider}/{} max output tokens must match models.dev",
1124 model.id
1125 );
1126 }
1127 checked += 1;
1128 }
1129 }
1130 assert!(checked > 20, "expected a meaningful overlap, got {checked}");
1131 }
1132
1133 #[test]
1134 fn gemini_2_5_pro_uses_models_dev_window() {
1135 let m = get_model("google", "gemini-2.5-pro").expect("static gemini-2.5-pro");
1138 assert_eq!(m.context_window, 1_048_576);
1139 }
1140
1141 #[test]
1142 fn test_model_registry_register_dynamic() {
1143 let registry = ModelRegistry::new();
1144 let custom_model = Model {
1145 id: "custom-model".to_string(),
1146 name: "Custom Model".to_string(),
1147 api: Api::OpenAiCompletions,
1148 provider: "custom".to_string(),
1149 base_url: "https://custom.example.com".to_string(),
1150 reasoning: false,
1151 input: vec![InputModality::Text],
1152 cost: Cost {
1153 input: 1.0,
1154 output: 2.0,
1155 cache_read: 0.5,
1156 cache_write: 5.0,
1157 },
1158 context_window: 100_000,
1159 max_tokens: 8192,
1160 headers: Default::default(),
1161 compat: None,
1162 };
1163 registry.register(custom_model.clone());
1164 assert!(registry.lookup("custom", "custom-model").is_some());
1165 }
1166}