1use clap::{Args, Subcommand};
4use leviath_providers::{ModelCapabilities, ModelInfo};
5
6use super::run::build_provider_registry_from_config;
7use crate::config::Config;
8
9#[derive(Args)]
13pub struct ModelsArgs {
14 #[command(subcommand)]
16 pub command: ModelsCommand,
17}
18
19#[derive(Subcommand)]
21pub enum ModelsCommand {
22 List(ListArgs),
24 Show(ShowArgs),
26}
27
28#[derive(Args)]
30pub struct ListArgs {
31 #[arg(short, long)]
33 pub provider: Option<String>,
34 #[arg(short = 'r', long)]
36 pub remote: bool,
37 #[arg(short = 'a', long)]
39 pub all: bool,
40 #[arg(long)]
42 pub json: bool,
43}
44
45#[derive(serde::Serialize)]
52struct ModelRow {
53 id: String,
54 provider: String,
55 display_name: Option<String>,
56 capabilities: ModelCapabilities,
57 capabilities_overridden: bool,
60}
61
62#[derive(Args)]
64pub struct ShowArgs {
65 pub model: String,
67 #[arg(short, long)]
69 pub provider: Option<String>,
70 #[arg(short = 'r', long)]
72 pub remote: bool,
73}
74
75pub async fn execute(args: ModelsArgs) -> anyhow::Result<()> {
79 match args.command {
80 ModelsCommand::List(a) => list_with_registry(a, &build_provider_registry_from_config).await,
81 ModelsCommand::Show(a) => show_with_registry(a, &build_provider_registry_from_config).await,
82 }
83}
84
85struct BuiltinEntry {
89 provider: &'static str,
90 model_id: &'static str,
91 display_name: &'static str,
92 caps: ModelCapabilities,
93}
94
95const CLOSED_CATALOG_PROVIDERS: &[&str] = &["anthropic", "openai", "google"];
104
105pub fn closed_catalog_models() -> Vec<(String, String)> {
108 builtin_table()
109 .into_iter()
110 .filter(|e| CLOSED_CATALOG_PROVIDERS.contains(&e.provider))
111 .map(|e| (e.provider.to_string(), e.model_id.to_string()))
112 .collect()
113}
114
115pub fn builtin_model_windows() -> std::collections::HashMap<(String, String), usize> {
121 builtin_table()
122 .into_iter()
123 .map(|e| {
124 (
125 (e.provider.to_string(), e.model_id.to_string()),
126 e.caps.max_context_tokens,
127 )
128 })
129 .collect()
130}
131
132fn builtin_table() -> Vec<BuiltinEntry> {
137 macro_rules! entry {
138 ($provider:expr_2021, $id:expr_2021, $name:expr_2021,
140 temp=$t:expr_2021, ctx=$ctx:expr_2021, out=$out:expr_2021) => {
141 entry!(
142 $provider,
143 $id,
144 $name,
145 temp = $t,
146 tools = true,
147 ctx = $ctx,
148 out = $out
149 )
150 };
151 ($provider:expr_2021, $id:expr_2021, $name:expr_2021,
153 temp=$t:expr_2021, tools=$to:expr_2021, ctx=$ctx:expr_2021, out=$out:expr_2021) => {
154 BuiltinEntry {
155 provider: $provider,
156 model_id: $id,
157 display_name: $name,
158 caps: ModelCapabilities {
159 supports_temperature: $t,
160 supports_streaming: true,
161 supports_tools: $to,
162 supports_system_prompt: true,
163 max_context_tokens: $ctx,
164 max_output_tokens: $out,
165 },
166 }
167 };
168 }
169
170 vec![
171 entry!(
173 "anthropic",
174 "claude-opus-5",
175 "Claude Opus 5",
176 temp = false,
177 ctx = 1_000_000,
178 out = 128_000
179 ),
180 entry!(
181 "anthropic",
182 "claude-sonnet-5",
183 "Claude Sonnet 5",
184 temp = false,
185 ctx = 1_000_000,
186 out = 128_000
187 ),
188 entry!(
189 "anthropic",
190 "claude-fable-5",
191 "Claude Fable 5",
192 temp = false,
193 ctx = 1_000_000,
194 out = 128_000
195 ),
196 entry!(
197 "anthropic",
198 "claude-opus-4-8",
199 "Claude Opus 4.8",
200 temp = false,
201 ctx = 1_000_000,
202 out = 128_000
203 ),
204 entry!(
205 "anthropic",
206 "claude-opus-4-7",
207 "Claude Opus 4.7",
208 temp = false,
209 ctx = 1_000_000,
210 out = 128_000
211 ),
212 entry!(
213 "anthropic",
214 "claude-opus-4-6",
215 "Claude Opus 4.6",
216 temp = true,
217 ctx = 1_000_000,
218 out = 128_000
219 ),
220 entry!(
221 "anthropic",
222 "claude-sonnet-4-6",
223 "Claude Sonnet 4.6",
224 temp = true,
225 ctx = 1_000_000,
226 out = 128_000
227 ),
228 entry!(
229 "anthropic",
230 "claude-haiku-4-5-20251001",
231 "Claude Haiku 4.5",
232 temp = true,
233 ctx = 200_000,
234 out = 65_536
235 ),
236 entry!(
239 "openai",
240 "gpt-5.5",
241 "GPT-5.5",
242 temp = true,
243 ctx = 1_050_000,
244 out = 128_000
245 ),
246 entry!(
247 "openai",
248 "gpt-5.4",
249 "GPT-5.4",
250 temp = true,
251 ctx = 1_050_000,
252 out = 128_000
253 ),
254 entry!(
255 "openai",
256 "gpt-5.4-mini",
257 "GPT-5.4 Mini",
258 temp = true,
259 ctx = 400_000,
260 out = 128_000
261 ),
262 entry!(
263 "openai",
264 "gpt-5.4-nano",
265 "GPT-5.4 Nano",
266 temp = true,
267 ctx = 400_000,
268 out = 128_000
269 ),
270 entry!(
275 "google",
276 "gemini-3.5-flash",
277 "Gemini 3.5 Flash",
278 temp = true,
279 ctx = 1_048_576,
280 out = 65_535
281 ),
282 entry!(
283 "google",
284 "gemini-3.1-pro-preview",
285 "Gemini 3.1 Pro (preview)",
286 temp = true,
287 ctx = 1_048_576,
288 out = 65_535
289 ),
290 entry!(
291 "google",
292 "gemini-3-flash",
293 "Gemini 3 Flash",
294 temp = true,
295 ctx = 1_048_576,
296 out = 65_535
297 ),
298 entry!(
299 "google",
300 "gemini-3.1-flash-lite",
301 "Gemini 3.1 Flash Lite",
302 temp = true,
303 ctx = 1_048_576,
304 out = 65_535
305 ),
306 entry!(
308 "openrouter",
309 "google/gemini-3.5-flash",
310 "Gemini 3.5 Flash",
311 temp = true,
312 ctx = 1_048_576,
313 out = 65_536
314 ),
315 entry!(
316 "openrouter",
317 "google/gemini-2.5-pro",
318 "Gemini 2.5 Pro",
319 temp = true,
320 ctx = 1_048_576,
321 out = 65_536
322 ),
323 entry!(
324 "openrouter",
325 "google/gemini-2.5-flash",
326 "Gemini 2.5 Flash",
327 temp = true,
328 ctx = 1_048_576,
329 out = 65_536
330 ),
331 entry!(
332 "openrouter",
333 "google/gemini-2.5-flash-lite",
334 "Gemini 2.5 Flash Lite",
335 temp = true,
336 ctx = 1_048_576,
337 out = 65_536
338 ),
339 entry!(
341 "openrouter",
342 "meta-llama/llama-4-maverick",
343 "Llama 4 Maverick",
344 temp = true,
345 ctx = 1_048_576,
346 out = 32_768
347 ),
348 entry!(
349 "openrouter",
350 "meta-llama/llama-4-scout",
351 "Llama 4 Scout",
352 temp = true,
353 ctx = 10_000_000,
354 out = 32_768
355 ),
356 entry!(
358 "openrouter",
359 "deepseek/deepseek-v4-pro",
360 "DeepSeek V4 Pro",
361 temp = true,
362 ctx = 1_048_576,
363 out = 393_216
364 ),
365 entry!(
366 "openrouter",
367 "deepseek/deepseek-v4-flash",
368 "DeepSeek V4 Flash",
369 temp = true,
370 ctx = 1_048_576,
371 out = 65_536
372 ),
373 entry!(
374 "openrouter",
375 "deepseek/deepseek-v3.2",
376 "DeepSeek V3.2",
377 temp = true,
378 ctx = 131_072,
379 out = 65_536
380 ),
381 entry!(
382 "openrouter",
383 "deepseek/deepseek-r1-0528",
384 "DeepSeek R1 (0528)",
385 temp = false,
386 tools = false,
387 ctx = 163_840,
388 out = 32_768
389 ),
390 entry!(
391 "openrouter",
392 "deepseek/deepseek-r1",
393 "DeepSeek R1",
394 temp = false,
395 tools = false,
396 ctx = 163_840,
397 out = 16_384
398 ),
399 entry!(
401 "openrouter",
402 "mistralai/mistral-large-2512",
403 "Mistral Large 3",
404 temp = true,
405 ctx = 262_144,
406 out = 32_768
407 ),
408 entry!(
409 "openrouter",
410 "mistralai/mistral-medium-3-5",
411 "Mistral Medium 3.5",
412 temp = true,
413 ctx = 256_000,
414 out = 32_768
415 ),
416 entry!(
417 "openrouter",
418 "mistralai/mistral-small-2603",
419 "Mistral Small 4",
420 temp = true,
421 ctx = 128_000,
422 out = 32_768
423 ),
424 entry!(
426 "openrouter",
427 "qwen/qwen3.6-plus",
428 "Qwen 3.6 Plus",
429 temp = true,
430 ctx = 1_048_576,
431 out = 65_536
432 ),
433 entry!(
434 "openrouter",
435 "qwen/qwen3-max",
436 "Qwen3 Max",
437 temp = true,
438 ctx = 131_072,
439 out = 32_768
440 ),
441 entry!(
442 "openrouter",
443 "qwen/qwen3-coder",
444 "Qwen3 Coder 480B",
445 temp = true,
446 ctx = 1_048_576,
447 out = 262_144
448 ),
449 ]
450}
451
452async fn list_with_registry(
473 args: ListArgs,
474 build_registry: &dyn Fn(
475 &Config,
476 ) -> Result<
477 leviath_runtime::ProviderRegistry,
478 leviath_providers::ProviderError,
479 >,
480) -> anyhow::Result<()> {
481 let config = Config::load()?;
482 for warning in config.validate_keys() {
483 eprintln!("Warning: {}", warning);
484 }
485
486 let mut entries: Vec<ModelInfo> = builtin_table()
488 .into_iter()
489 .map(|e| ModelInfo {
490 id: e.model_id.to_string(),
491 display_name: Some(e.display_name.to_string()),
492 provider: e.provider.to_string(),
493 capabilities: e.caps,
494 })
495 .collect();
496
497 let registry = build_registry(&config)?;
502 let available: std::collections::HashSet<String> = registry
503 .provider_names()
504 .into_iter()
505 .map(str::to_string)
506 .collect();
507 if !args.all {
508 entries.retain(|e| available.contains(&e.provider));
509 }
510
511 if args.remote {
513 for provider_name in registry.provider_names() {
514 if let Some(ref filter) = args.provider
516 && filter != provider_name
517 {
518 continue;
519 }
520
521 let provider = registry
533 .get(provider_name)
534 .expect("provider_names returns registered names");
535 match provider.list_models().await {
536 Ok(remote_models) => {
537 for rm in remote_models {
538 if let Some(existing) = entries.iter_mut().find(|e| e.id == rm.id) {
540 *existing = rm;
541 } else {
542 entries.push(rm);
543 }
544 }
545 }
546 Err(e) => {
547 eprintln!(
548 "Warning: could not fetch models from '{}': {}",
549 provider_name, e
550 );
551 }
552 }
553 }
554 }
555
556 if let Some(ref filter) = args.provider {
558 entries.retain(|e| &e.provider == filter);
559 }
560
561 let overridden: std::collections::HashSet<String> =
563 config.model_capabilities.keys().cloned().collect();
564
565 for entry in entries.iter_mut() {
566 if let Some(user_caps) = config.model_capabilities.get(&entry.id) {
567 entry.capabilities = user_caps.apply_to(entry.capabilities.clone());
568 }
569 }
570
571 if args.json {
574 let rows: Vec<ModelRow> = entries
575 .into_iter()
576 .map(|e| ModelRow {
577 capabilities_overridden: overridden.contains(&e.id),
578 id: e.id,
579 provider: e.provider,
580 display_name: e.display_name,
581 capabilities: e.capabilities,
582 })
583 .collect();
584 println!(
586 "{}",
587 serde_json::to_string_pretty(&rows).expect("a model listing serializes")
588 );
589 return Ok(());
590 }
591
592 if entries.is_empty() {
593 println!("No models available.");
597 println!(
598 "(configure a provider with `lev setup`, or pass --all to see every \
599 model Leviath knows about)"
600 );
601 return Ok(());
602 }
603
604 println!(
606 "{:<12} {:<40} {:<6} {:<7} {:<8} {:<8}",
607 "PROVIDER", "MODEL ID", "TEMP", "TOOLS", "CTX", "OUTPUT"
608 );
609 println!("{}", "-".repeat(85));
610
611 for entry in &entries {
612 let provider_col = if overridden.contains(&entry.id) {
613 format!("*{}", entry.provider)
614 } else {
615 entry.provider.clone()
616 };
617
618 let temp = bool_icon(entry.capabilities.supports_temperature);
619 let tools = bool_icon(entry.capabilities.supports_tools);
620 let ctx = fmt_tokens(entry.capabilities.max_context_tokens);
621 let out = fmt_tokens(entry.capabilities.max_output_tokens);
622
623 println!(
624 "{:<12} {:<40} {:<6} {:<7} {:<8} {:<8}",
625 provider_col, entry.id, temp, tools, ctx, out
626 );
627 }
628
629 if overridden
630 .iter()
631 .any(|id| entries.iter().any(|e| &e.id == id))
632 {
633 println!("\n* = capabilities overridden via [model_capabilities] in config");
634 }
635
636 Ok(())
637}
638
639async fn show_with_registry(
644 args: ShowArgs,
645 build_registry: &dyn Fn(
646 &Config,
647 ) -> Result<
648 leviath_runtime::ProviderRegistry,
649 leviath_providers::ProviderError,
650 >,
651) -> anyhow::Result<()> {
652 let config = Config::load()?;
653 for warning in config.validate_keys() {
654 eprintln!("Warning: {}", warning);
655 }
656
657 let model_id = &args.model;
658
659 let builtin = builtin_table();
664 let builtin_entry = builtin.iter().find(|e| e.model_id == model_id);
665 let user_caps = config.model_capabilities.get(model_id);
666
667 if let Some(user_caps) = user_caps {
668 let base = builtin_entry.map(|e| e.caps.clone()).unwrap_or_default();
669 print_model_detail(
670 model_id,
671 builtin_entry.map(|e| e.display_name),
672 "config (user override)",
673 &user_caps.apply_to(base),
674 true,
675 );
676 return Ok(());
677 }
678
679 if let Some(entry) = builtin_entry {
681 print_model_detail(
682 model_id,
683 Some(entry.display_name),
684 entry.provider,
685 &entry.caps,
686 false,
687 );
688 return Ok(());
689 }
690
691 if args.remote
693 && let Some(ref provider_name) = args.provider
694 {
695 let registry = build_registry(&config)?;
696 if let Some(provider) = registry.get(provider_name) {
697 match provider.list_models().await {
698 Ok(models) => {
699 if let Some(info) = models.iter().find(|m| &m.id == model_id) {
700 print_model_detail(
701 model_id,
702 info.display_name.as_deref(),
703 &info.provider,
704 &info.capabilities,
705 false,
706 );
707 return Ok(());
708 }
709 }
710 Err(e) => {
711 eprintln!(
712 "Warning: could not fetch models from '{}': {}",
713 provider_name, e
714 );
715 }
716 }
717 } else {
718 eprintln!(
719 "Warning: provider '{}' is not configured (missing API key?)",
720 provider_name
721 );
722 }
723 }
724
725 println!("Model '{}' not found.", model_id);
727 println!(
728 "Add it to {} under [model_capabilities.'{}']",
729 Config::config_path().display(),
730 model_id
731 );
732 println!();
733 println!("Example:");
734 println!("[model_capabilities.'{}']", model_id);
735 println!("supports_temperature = true");
736 println!("supports_streaming = true");
737 println!("supports_tools = true");
738 println!("supports_system_prompt = true");
739 println!("max_context_tokens = 8192");
740 println!("max_output_tokens = 4096");
741
742 Ok(())
743}
744
745fn bool_icon(b: bool) -> &'static str {
748 if b { "✓" } else { "✗" }
749}
750
751fn fmt_tokens(n: usize) -> String {
753 if n >= 1_000_000 {
754 format!("{}M", n / 1_000_000)
755 } else if n >= 1_000 {
756 format!("{}K", n / 1_000)
757 } else {
758 n.to_string()
759 }
760}
761
762fn print_model_detail(
764 id: &str,
765 display_name: Option<&str>,
766 provider: &str,
767 caps: &ModelCapabilities,
768 is_user_override: bool,
769) {
770 println!("Model: {}", id);
771 if let Some(name) = display_name {
772 println!("Name: {}", name);
773 }
774 println!("Provider: {}", provider);
775 if is_user_override {
776 println!("Source: user override (config)");
777 }
778 println!();
779 println!("Capabilities");
780 println!("------------");
781 println!(" Temperature: {}", bool_icon(caps.supports_temperature));
782 println!(" Streaming: {}", bool_icon(caps.supports_streaming));
783 println!(" Tool calling: {}", bool_icon(caps.supports_tools));
784 println!(
785 " System prompt: {}",
786 bool_icon(caps.supports_system_prompt)
787 );
788 println!(
789 " Context window: {} tokens ({})",
790 caps.max_context_tokens,
791 fmt_tokens(caps.max_context_tokens)
792 );
793 println!(
794 " Max output: {} tokens ({})",
795 caps.max_output_tokens,
796 fmt_tokens(caps.max_output_tokens)
797 );
798}
799
800#[cfg(test)]
801mod tests {
802 use super::*;
803
804 #[test]
807 fn fmt_tokens_millions() {
808 assert_eq!(fmt_tokens(1_000_000), "1M");
809 assert_eq!(fmt_tokens(2_000_000), "2M");
810 }
811
812 #[test]
813 fn fmt_tokens_thousands() {
814 assert_eq!(fmt_tokens(128_000), "128K");
815 assert_eq!(fmt_tokens(4_096), "4K");
816 assert_eq!(fmt_tokens(1_000), "1K");
817 }
818
819 #[test]
820 fn fmt_tokens_small() {
821 assert_eq!(fmt_tokens(512), "512");
822 assert_eq!(fmt_tokens(0), "0");
823 }
824
825 #[test]
828 fn bool_icon_values() {
829 assert_eq!(bool_icon(true), "✓");
830 assert_eq!(bool_icon(false), "✗");
831 }
832
833 #[test]
836 fn builtin_table_is_not_empty() {
837 let table = builtin_table();
838 assert!(!table.is_empty());
839 }
840
841 #[test]
842 fn builtin_table_has_anthropic_models() {
843 let table = builtin_table();
844 let anthropic: Vec<_> = table.iter().filter(|e| e.provider == "anthropic").collect();
845 assert!(!anthropic.is_empty());
846 }
847
848 #[test]
849 fn builtin_table_has_openai_models() {
850 let table = builtin_table();
851 let openai: Vec<_> = table.iter().filter(|e| e.provider == "openai").collect();
852 assert!(!openai.is_empty());
853 }
854
855 #[test]
856 fn builtin_table_has_openrouter_models() {
857 let table = builtin_table();
858 let openrouter: Vec<_> = table
859 .iter()
860 .filter(|e| e.provider == "openrouter")
861 .collect();
862 assert!(!openrouter.is_empty());
863 }
864
865 #[test]
866 fn builtin_entries_have_valid_capabilities() {
867 for entry in builtin_table() {
868 assert!(entry.caps.max_context_tokens > 0);
869 assert!(entry.caps.max_output_tokens > 0);
870 assert!(entry.caps.supports_streaming);
871 assert!(entry.caps.supports_system_prompt);
872 }
873 }
874
875 #[test]
876 fn builtin_entries_have_unique_model_ids() {
877 let table = builtin_table();
878 let ids: Vec<&str> = table.iter().map(|e| e.model_id).collect();
879 let unique: std::collections::HashSet<&str> = ids.iter().copied().collect();
880 assert_eq!(ids.len(), unique.len());
881 }
882
883 #[test]
884 fn deepseek_r1_models_no_tools() {
885 let table = builtin_table();
886 for entry in &table {
887 if entry.model_id.contains("deepseek-r1") {
888 assert!(!entry.caps.supports_tools);
889 }
890 }
891 }
892
893 #[test]
896 fn print_model_detail_does_not_panic() {
897 let caps = ModelCapabilities {
898 supports_temperature: true,
899 supports_streaming: true,
900 supports_tools: true,
901 supports_system_prompt: true,
902 max_context_tokens: 100_000,
903 max_output_tokens: 8_192,
904 };
905 print_model_detail("test-model", Some("Test Model"), "test", &caps, false);
907 print_model_detail("test-model", None, "test", &caps, true);
908 }
909
910 #[test]
913 fn fmt_tokens_exact_boundary() {
914 assert_eq!(fmt_tokens(999), "999");
915 assert_eq!(fmt_tokens(999_999), "999K");
916 }
917
918 #[test]
919 fn fmt_tokens_large_millions() {
920 assert_eq!(fmt_tokens(10_000_000), "10M");
921 }
922
923 #[test]
926 fn builtin_table_claude_opus_no_temperature() {
927 let table = builtin_table();
928 for entry in &table {
929 if entry.model_id == "claude-opus-4-8" || entry.model_id == "claude-opus-4-7" {
930 assert!(!entry.caps.supports_temperature);
931 }
932 }
933 }
934
935 #[test]
936 fn builtin_table_claude_sonnet_supports_temperature() {
937 let table = builtin_table();
938 let sonnet = table
939 .iter()
940 .find(|e| e.model_id == "claude-sonnet-4-6")
941 .expect("claude-sonnet-4-6 should be in table");
942 assert!(sonnet.caps.supports_temperature);
943 }
944
945 #[test]
946 fn builtin_table_has_display_names() {
947 let table = builtin_table();
948 for entry in &table {
949 assert!(!entry.display_name.is_empty());
950 }
951 }
952
953 #[test]
954 fn builtin_table_context_larger_than_output() {
955 let table = builtin_table();
956 for entry in &table {
957 assert!(entry.caps.max_context_tokens >= entry.caps.max_output_tokens);
958 }
959 }
960
961 #[test]
964 fn bool_icon_returns_unicode() {
965 assert!(!bool_icon(true).is_empty());
966 assert!(!bool_icon(false).is_empty());
967 assert_ne!(bool_icon(true), bool_icon(false));
968 }
969
970 #[test]
973 fn builtin_table_openai_models_support_temperature() {
974 let table = builtin_table();
975 for entry in &table {
976 if entry.provider == "openai" {
977 assert!(entry.caps.supports_temperature);
978 }
979 }
980 }
981
982 #[test]
987 fn builtin_table_offers_native_google_models() {
988 let table = builtin_table();
989 let native: Vec<&str> = table
990 .iter()
991 .filter(|e| e.provider == "google")
992 .map(|e| e.model_id)
993 .collect();
994 assert!(
995 !native.is_empty(),
996 "the native google provider must offer models of its own"
997 );
998 for id in &native {
1000 assert!(
1001 !id.contains('/'),
1002 "native google model id must not be vendor-prefixed: {id}"
1003 );
1004 }
1005 }
1006
1007 #[test]
1008 fn builtin_table_gemini_flash_models_exist() {
1009 let table = builtin_table();
1010 let flash: Vec<_> = table
1011 .iter()
1012 .filter(|e| e.model_id.contains("gemini") && e.model_id.contains("flash"))
1013 .collect();
1014 assert!(!flash.is_empty());
1015 }
1016
1017 #[test]
1018 fn builtin_table_deepseek_r1_no_temperature() {
1019 let table = builtin_table();
1020 for entry in &table {
1021 if entry.model_id.contains("deepseek-r1") {
1022 assert!(!entry.caps.supports_temperature);
1023 }
1024 }
1025 }
1026
1027 #[test]
1028 fn builtin_table_qwen_models_exist() {
1029 let table = builtin_table();
1030 let qwen: Vec<_> = table
1031 .iter()
1032 .filter(|e| e.model_id.contains("qwen"))
1033 .collect();
1034 assert!(!qwen.is_empty());
1035 }
1036
1037 #[test]
1038 fn builtin_table_mistral_models_exist() {
1039 let table = builtin_table();
1040 let mistral: Vec<_> = table
1041 .iter()
1042 .filter(|e| e.model_id.contains("mistral"))
1043 .collect();
1044 assert!(!mistral.is_empty());
1045 }
1046
1047 #[test]
1048 fn builtin_table_all_entries_have_provider() {
1049 let table = builtin_table();
1050 for entry in &table {
1051 assert!(!entry.provider.is_empty());
1052 }
1053 }
1054
1055 #[test]
1056 fn builtin_table_all_entries_have_model_id() {
1057 let table = builtin_table();
1058 for entry in &table {
1059 assert!(!entry.model_id.is_empty());
1060 }
1061 }
1062
1063 #[tokio::test]
1066 async fn execute_list_command_runs_without_error() {
1067 crate::config::with_isolated_config_path_async(
1068 "models-execute_list_command_runs_without_error",
1069 |_fake_dir| async move {
1070 let args = ModelsArgs {
1071 command: ModelsCommand::List(ListArgs {
1072 provider: None,
1073 remote: false,
1074 all: false,
1075 json: false,
1076 }),
1077 };
1078 let result = execute(args).await;
1080 assert!(result.is_ok());
1081 },
1082 )
1083 .await;
1084 }
1085
1086 #[tokio::test]
1087 async fn execute_list_with_provider_filter_runs_without_error() {
1088 crate::config::with_isolated_config_path_async(
1089 "models-execute_list_with_provider_filter_runs_without_error",
1090 |_fake_dir| async move {
1091 let args = ModelsArgs {
1092 command: ModelsCommand::List(ListArgs {
1093 provider: Some("anthropic".to_string()),
1094 remote: false,
1095 all: false,
1096 json: false,
1097 }),
1098 };
1099 let result = execute(args).await;
1100 assert!(result.is_ok());
1101 },
1102 )
1103 .await;
1104 }
1105
1106 #[tokio::test]
1107 async fn execute_list_with_nonexistent_provider_filter() {
1108 crate::config::with_isolated_config_path_async(
1109 "models-execute_list_with_nonexistent_provider_filter",
1110 |_fake_dir| async move {
1111 let args = ModelsArgs {
1112 command: ModelsCommand::List(ListArgs {
1113 provider: Some("nonexistent_provider".to_string()),
1114 remote: false,
1115 all: false,
1116 json: false,
1117 }),
1118 };
1119 let result = execute(args).await;
1121 assert!(result.is_ok());
1122 },
1123 )
1124 .await;
1125 }
1126
1127 #[tokio::test]
1128 async fn execute_show_known_model_runs_without_error() {
1129 crate::config::with_isolated_config_path_async(
1130 "models-execute_show_known_model_runs_without_error",
1131 |_fake_dir| async move {
1132 let args = ModelsArgs {
1133 command: ModelsCommand::Show(ShowArgs {
1134 model: "claude-sonnet-4-6".to_string(),
1135 provider: None,
1136 remote: false,
1137 }),
1138 };
1139 let result = execute(args).await;
1141 assert!(result.is_ok());
1142 },
1143 )
1144 .await;
1145 }
1146
1147 #[tokio::test]
1148 async fn execute_show_unknown_model_runs_without_error() {
1149 crate::config::with_isolated_config_path_async(
1150 "models-execute_show_unknown_model_runs_without_error",
1151 |_fake_dir| async move {
1152 let args = ModelsArgs {
1153 command: ModelsCommand::Show(ShowArgs {
1154 model: "totally-unknown-model-xyz".to_string(),
1155 provider: None,
1156 remote: false,
1157 }),
1158 };
1159 let result = execute(args).await;
1161 assert!(result.is_ok());
1162 },
1163 )
1164 .await;
1165 }
1166
1167 #[tokio::test]
1168 async fn execute_show_unknown_model_with_remote_no_provider() {
1169 crate::config::with_isolated_config_path_async(
1170 "models-execute_show_unknown_model_with_remote_no_provider",
1171 |_fake_dir| async move {
1172 let args = ModelsArgs {
1173 command: ModelsCommand::Show(ShowArgs {
1174 model: "totally-unknown-model-xyz".to_string(),
1175 provider: None,
1176 remote: true, }),
1178 };
1179 let result = execute(args).await;
1180 assert!(result.is_ok());
1181 },
1182 )
1183 .await;
1184 }
1185
1186 #[tokio::test]
1187 async fn execute_show_unknown_model_with_remote_unconfigured_provider() {
1188 crate::config::with_isolated_config_path_async(
1189 "models-execute_show_unknown_model_with_remote_unconfigured_provider",
1190 |_fake_dir| async move {
1191 let args = ModelsArgs {
1192 command: ModelsCommand::Show(ShowArgs {
1193 model: "totally-unknown-model-xyz".to_string(),
1194 provider: Some("anthropic".to_string()),
1195 remote: true,
1196 }),
1198 };
1199 let result = execute(args).await;
1201 assert!(result.is_ok());
1202 },
1203 )
1204 .await;
1205 }
1206
1207 #[tokio::test]
1210 async fn list_with_openrouter_filter() {
1211 crate::config::with_isolated_config_path_async(
1219 "models-list-openrouter-filter",
1220 |_fake_dir| async move {
1221 let args = ModelsArgs {
1222 command: ModelsCommand::List(ListArgs {
1223 provider: Some("openrouter".to_string()),
1224 remote: false,
1225 all: false,
1226 json: false,
1227 }),
1228 };
1229 let result = execute(args).await;
1230 assert!(result.is_ok());
1231 },
1232 )
1233 .await;
1234 }
1235
1236 #[tokio::test]
1237 async fn list_with_openai_filter() {
1238 crate::config::with_isolated_config_path_async(
1241 "models-list-openai-filter",
1242 |_fake_dir| async move {
1243 let args = ModelsArgs {
1244 command: ModelsCommand::List(ListArgs {
1245 provider: Some("openai".to_string()),
1246 remote: false,
1247 all: false,
1248 json: false,
1249 }),
1250 };
1251 let result = execute(args).await;
1252 assert!(result.is_ok());
1253 },
1254 )
1255 .await;
1256 }
1257
1258 #[tokio::test]
1259 async fn show_builtin_anthropic_opus() {
1260 crate::config::with_isolated_config_path_async(
1263 "models-show-anthropic-opus",
1264 |_fake_dir| async move {
1265 let args = ModelsArgs {
1266 command: ModelsCommand::Show(ShowArgs {
1267 model: "claude-opus-4-6".to_string(),
1268 provider: None,
1269 remote: false,
1270 }),
1271 };
1272 let result = execute(args).await;
1273 assert!(result.is_ok());
1274 },
1275 )
1276 .await;
1277 }
1278
1279 #[tokio::test]
1280 async fn show_builtin_openai_model() {
1281 crate::config::with_isolated_config_path_async(
1284 "models-show-openai-model",
1285 |_fake_dir| async move {
1286 let args = ModelsArgs {
1287 command: ModelsCommand::Show(ShowArgs {
1288 model: "gpt-5.5".to_string(),
1289 provider: None,
1290 remote: false,
1291 }),
1292 };
1293 let result = execute(args).await;
1294 assert!(result.is_ok());
1295 },
1296 )
1297 .await;
1298 }
1299
1300 #[tokio::test]
1301 async fn show_builtin_deepseek_r1() {
1302 crate::config::with_isolated_config_path_async(
1305 "models-show-deepseek-r1",
1306 |_fake_dir| async move {
1307 let args = ModelsArgs {
1308 command: ModelsCommand::Show(ShowArgs {
1309 model: "deepseek/deepseek-r1".to_string(),
1310 provider: None,
1311 remote: false,
1312 }),
1313 };
1314 let result = execute(args).await;
1315 assert!(result.is_ok());
1316 },
1317 )
1318 .await;
1319 }
1320
1321 #[test]
1324 fn builtin_table_to_model_info_preserves_data() {
1325 let table = builtin_table();
1326 let infos: Vec<ModelInfo> = table
1327 .into_iter()
1328 .map(|e| ModelInfo {
1329 id: e.model_id.to_string(),
1330 display_name: Some(e.display_name.to_string()),
1331 provider: e.provider.to_string(),
1332 capabilities: e.caps,
1333 })
1334 .collect();
1335
1336 assert!(!infos.is_empty());
1337 for info in &infos {
1338 assert!(!info.id.is_empty());
1339 assert!(info.display_name.is_some());
1340 assert!(!info.provider.is_empty());
1341 }
1342 }
1343
1344 #[test]
1347 fn print_model_detail_with_no_tools_no_temp() {
1348 let caps = ModelCapabilities {
1349 supports_temperature: false,
1350 supports_streaming: false,
1351 supports_tools: false,
1352 supports_system_prompt: false,
1353 max_context_tokens: 1000,
1354 max_output_tokens: 500,
1355 };
1356 print_model_detail("test-model", Some("Test"), "test", &caps, false);
1358 }
1359
1360 #[test]
1361 fn print_model_detail_user_override_source() {
1362 let caps = ModelCapabilities::default();
1363 print_model_detail("override-model", None, "custom", &caps, true);
1365 }
1366
1367 #[test]
1370 fn fmt_tokens_just_below_thousand() {
1371 assert_eq!(fmt_tokens(999), "999");
1372 }
1373
1374 #[test]
1375 fn fmt_tokens_just_at_thousand() {
1376 assert_eq!(fmt_tokens(1000), "1K");
1377 }
1378
1379 #[test]
1380 fn fmt_tokens_just_below_million() {
1381 assert_eq!(fmt_tokens(999_999), "999K");
1382 }
1383
1384 #[test]
1385 fn fmt_tokens_just_at_million() {
1386 assert_eq!(fmt_tokens(1_000_000), "1M");
1387 }
1388
1389 #[test]
1390 fn fmt_tokens_non_round_thousands() {
1391 assert_eq!(fmt_tokens(1500), "1K");
1393 assert_eq!(fmt_tokens(65_536), "65K");
1394 }
1395
1396 #[tokio::test]
1405 async fn list_builtin_no_filter_succeeds() {
1406 crate::config::with_isolated_config_path_async(
1407 "models-list_builtin_no_filter_succeeds",
1408 |_fake_dir| async move {
1409 let args = ListArgs {
1410 remote: false,
1411 provider: None,
1412 all: false,
1413 json: false,
1414 };
1415 let result = list_with_registry(args, &build_provider_registry_from_config).await;
1416 assert!(result.is_ok());
1417 },
1418 )
1419 .await;
1420 }
1421
1422 #[tokio::test]
1423 async fn list_json_with_no_configured_provider_is_an_empty_array() {
1424 crate::config::with_isolated_config_path_async(
1427 "models-list_json_empty",
1428 |_fake_dir| async move {
1429 let args = ListArgs {
1430 remote: false,
1431 provider: Some("no-such-provider".to_string()),
1432 all: false,
1433 json: true,
1434 };
1435 let result = list_with_registry(args, &build_provider_registry_from_config).await;
1436 assert!(result.is_ok());
1437 },
1438 )
1439 .await;
1440 }
1441
1442 #[tokio::test]
1443 async fn list_json_with_all_succeeds() {
1444 crate::config::with_isolated_config_path_async(
1445 "models-list_json_all",
1446 |_fake_dir| async move {
1447 let args = ListArgs {
1448 remote: false,
1449 provider: None,
1450 all: true,
1451 json: true,
1452 };
1453 let result = list_with_registry(args, &build_provider_registry_from_config).await;
1454 assert!(result.is_ok());
1455 },
1456 )
1457 .await;
1458 }
1459
1460 #[test]
1461 fn model_row_serializes_capabilities_and_the_override_flag() {
1462 let row = ModelRow {
1466 id: "m".to_string(),
1467 provider: "p".to_string(),
1468 display_name: Some("M".to_string()),
1469 capabilities: ModelCapabilities::default(),
1470 capabilities_overridden: true,
1471 };
1472 let value: serde_json::Value =
1473 serde_json::from_str(&serde_json::to_string(&row).unwrap()).unwrap();
1474 assert_eq!(value["id"], serde_json::json!("m"));
1475 assert_eq!(value["capabilities_overridden"], serde_json::json!(true));
1476 assert!(value["capabilities"]["supports_tools"].is_boolean());
1477 }
1478
1479 #[tokio::test]
1480 async fn list_builtin_with_provider_filter_succeeds() {
1481 crate::config::with_isolated_config_path_async(
1482 "models-list_builtin_with_provider_filter_succeeds",
1483 |_fake_dir| async move {
1484 let args = ListArgs {
1485 remote: false,
1486 provider: Some("anthropic".to_string()),
1487 all: false,
1488 json: false,
1489 };
1490 let result = list_with_registry(args, &build_provider_registry_from_config).await;
1491 assert!(result.is_ok());
1492 },
1493 )
1494 .await;
1495 }
1496
1497 #[tokio::test]
1498 async fn list_unknown_provider_filter_finds_nothing() {
1499 crate::config::with_isolated_config_path_async(
1500 "models-list_unknown_provider_filter_finds_nothing",
1501 |_fake_dir| async move {
1502 let args = ListArgs {
1503 remote: false,
1504 provider: Some("no-such-provider".to_string()),
1505 all: false,
1506 json: false,
1507 };
1508 let result = list_with_registry(args, &build_provider_registry_from_config).await;
1510 assert!(result.is_ok());
1511 },
1512 )
1513 .await;
1514 }
1515
1516 #[tokio::test]
1517 async fn show_builtin_model_succeeds() {
1518 crate::config::with_isolated_config_path_async(
1519 "models-show_builtin_model_succeeds",
1520 |_fake_dir| async move {
1521 let known_id = builtin_table()[0].model_id.to_string();
1523 let args = ShowArgs {
1524 model: known_id,
1525 remote: false,
1526 provider: None,
1527 };
1528 let result = show_with_registry(args, &build_provider_registry_from_config).await;
1529 assert!(result.is_ok());
1530 },
1531 )
1532 .await;
1533 }
1534
1535 #[tokio::test]
1536 async fn show_unknown_model_without_remote_succeeds_with_warning() {
1537 crate::config::with_isolated_config_path_async(
1538 "models-show_unknown_model_without_remote_succeeds_with_warning",
1539 |_fake_dir| async move {
1540 let args = ShowArgs {
1541 model: "totally-unknown-model-xyz".to_string(),
1542 remote: false,
1543 provider: None,
1544 };
1545 let result = show_with_registry(args, &build_provider_registry_from_config).await;
1547 assert!(result.is_ok());
1548 },
1549 )
1550 .await;
1551 }
1552
1553 #[tokio::test]
1554 async fn show_remote_without_provider_falls_through_gracefully() {
1555 crate::config::with_isolated_config_path_async(
1556 "models-show_remote_without_provider_falls_through_gracefully",
1557 |_fake_dir| async move {
1558 let args = ShowArgs {
1561 model: "totally-unknown-model-xyz".to_string(),
1562 remote: true,
1563 provider: None,
1564 };
1565 let result = show_with_registry(args, &build_provider_registry_from_config).await;
1566 assert!(result.is_ok());
1567 },
1568 )
1569 .await;
1570 }
1571
1572 struct MockProvider {
1584 models: Vec<ModelInfo>,
1585 fail: bool,
1586 }
1587
1588 #[async_trait::async_trait]
1589 impl leviath_providers::Provider for MockProvider {
1590 async fn infer(
1591 &self,
1592 _request: &leviath_providers::InferenceRequest,
1593 ) -> Result<leviath_providers::InferenceResponse, leviath_providers::ProviderError>
1594 {
1595 Err(leviath_providers::ProviderError::Other(
1596 "MockProvider does not support infer".to_string(),
1597 ))
1598 }
1599
1600 async fn count_tokens(&self, text: &str, _model: &str) -> usize {
1601 leviath_core::estimate_tokens(text)
1602 }
1603
1604 fn max_context_tokens(&self, _model: &str) -> usize {
1605 100_000
1606 }
1607
1608 fn name(&self) -> &str {
1609 "mock"
1610 }
1611
1612 fn capabilities(&self, _model: &str) -> ModelCapabilities {
1613 ModelCapabilities::default()
1614 }
1615
1616 async fn list_models(&self) -> Result<Vec<ModelInfo>, leviath_providers::ProviderError> {
1617 if self.fail {
1618 Err(leviath_providers::ProviderError::Other(
1619 "mock provider failure".to_string(),
1620 ))
1621 } else {
1622 Ok(self.models.clone())
1623 }
1624 }
1625 }
1626
1627 fn mock_registry(
1628 provider_name: &'static str,
1629 models: Vec<ModelInfo>,
1630 fail: bool,
1631 ) -> impl Fn(&Config) -> Result<leviath_runtime::ProviderRegistry, leviath_providers::ProviderError>
1632 {
1633 move |_config: &Config| {
1640 let mut registry = leviath_runtime::ProviderRegistry::new();
1641 registry.register(
1642 provider_name.to_string(),
1643 std::sync::Arc::new(MockProvider {
1644 models: models.clone(),
1645 fail,
1646 }),
1647 );
1648 Ok(registry)
1649 }
1650 }
1651
1652 #[tokio::test]
1653 async fn list_remote_merges_new_model_from_provider() {
1654 crate::config::with_isolated_config_path_async(
1655 "models-list_remote_merges_new_model_from_provider",
1656 |_fake_dir| async move {
1657 let args = ListArgs {
1658 remote: true,
1659 provider: Some("mock".to_string()),
1660 all: false,
1661 json: false,
1662 };
1663 let new_model = ModelInfo {
1664 id: "mock-brand-new-model".to_string(),
1665 display_name: Some("Mock Brand New Model".to_string()),
1666 provider: "mock".to_string(),
1667 capabilities: ModelCapabilities::default(),
1668 };
1669 let result =
1670 list_with_registry(args, &mock_registry("mock", vec![new_model], false)).await;
1671 assert!(result.is_ok());
1672 },
1673 )
1674 .await;
1675 }
1676
1677 #[tokio::test]
1678 async fn list_remote_without_provider_filter_queries_all_providers() {
1679 crate::config::with_isolated_config_path_async(
1684 "models-list_remote_without_provider_filter_queries_all_providers",
1685 |_fake_dir| async move {
1686 let args = ListArgs {
1687 remote: true,
1688 provider: None,
1689 all: false,
1690 json: false,
1691 };
1692 let new_model = ModelInfo {
1693 id: "mock-brand-new-model".to_string(),
1694 display_name: Some("Mock Brand New Model".to_string()),
1695 provider: "mock".to_string(),
1696 capabilities: ModelCapabilities::default(),
1697 };
1698 let result =
1699 list_with_registry(args, &mock_registry("mock", vec![new_model], false)).await;
1700 assert!(result.is_ok());
1701 },
1702 )
1703 .await;
1704 }
1705
1706 #[tokio::test]
1707 async fn list_remote_overrides_builtin_entry_with_same_id() {
1708 crate::config::with_isolated_config_path_async(
1709 "models-list_remote_overrides_builtin_entry_with_same_id",
1710 |_fake_dir| async move {
1711 let known_id = builtin_table()[0].model_id.to_string();
1712 let args = ListArgs {
1713 remote: true,
1714 provider: Some("mock".to_string()),
1715 all: false,
1716 json: false,
1717 };
1718 let overriding_model = ModelInfo {
1719 id: known_id,
1720 display_name: Some("Overridden".to_string()),
1721 provider: "mock".to_string(),
1722 capabilities: ModelCapabilities::default(),
1723 };
1724 let result =
1725 list_with_registry(args, &mock_registry("mock", vec![overriding_model], false))
1726 .await;
1727 assert!(result.is_ok());
1728 },
1729 )
1730 .await;
1731 }
1732
1733 #[tokio::test]
1738 async fn list_shows_only_providers_the_install_has_credentials_for() {
1739 crate::config::with_isolated_config_path_async(
1740 "models-list_only_available",
1741 |_fake_dir| async move {
1742 let args = ListArgs {
1743 remote: false,
1744 provider: None,
1745 all: false,
1746 json: false,
1747 };
1748 let result =
1751 list_with_registry(args, &mock_registry("anthropic", vec![], false)).await;
1752 assert!(result.is_ok());
1753 },
1754 )
1755 .await;
1756 }
1757
1758 #[tokio::test]
1762 async fn list_remote_overrides_a_builtin_entry_with_the_same_id() {
1763 crate::config::with_isolated_config_path_async(
1764 "models-list_remote_override",
1765 |_fake_dir| async move {
1766 let remote = vec![ModelInfo {
1767 id: "claude-sonnet-5".to_string(),
1768 display_name: Some("Claude Sonnet 5 (remote)".to_string()),
1769 provider: "anthropic".to_string(),
1770 capabilities: leviath_providers::ModelCapabilities::default(),
1771 }];
1772 let args = ListArgs {
1773 remote: true,
1774 provider: None,
1775 all: false,
1776 json: false,
1777 };
1778 let result =
1779 list_with_registry(args, &mock_registry("anthropic", remote, false)).await;
1780 assert!(result.is_ok());
1781 },
1782 )
1783 .await;
1784 }
1785
1786 #[tokio::test]
1789 async fn list_all_includes_providers_without_credentials() {
1790 crate::config::with_isolated_config_path_async(
1791 "models-list_all_includes_everything",
1792 |_fake_dir| async move {
1793 let args = ListArgs {
1794 remote: false,
1795 provider: None,
1796 all: true,
1797 json: false,
1798 };
1799 let result = list_with_registry(args, &mock_registry("mock", vec![], false)).await;
1800 assert!(result.is_ok());
1801 },
1802 )
1803 .await;
1804 }
1805
1806 #[tokio::test]
1809 async fn list_marks_overridden_capabilities_for_an_available_provider() {
1810 crate::config::with_isolated_config_path_async(
1811 "models-list_overridden_available",
1812 |_fake_dir| async move {
1813 let mut config = Config::default();
1814 config.model_capabilities.insert(
1815 "claude-sonnet-5".to_string(),
1816 leviath_providers::ModelCapabilityOverride::default(),
1817 );
1818 config
1819 .save_to_path(&Config::config_path())
1820 .expect("the isolated config path is writable");
1821 let args = ListArgs {
1822 remote: false,
1823 provider: None,
1824 all: false,
1825 json: false,
1826 };
1827 let result =
1828 list_with_registry(args, &mock_registry("anthropic", vec![], false)).await;
1829 assert!(result.is_ok());
1830 },
1831 )
1832 .await;
1833 }
1834
1835 #[tokio::test]
1836 async fn list_remote_provider_error_warns_and_continues() {
1837 crate::config::with_isolated_config_path_async(
1838 "models-list_remote_provider_error_warns_and_continues",
1839 |_fake_dir| async move {
1840 let args = ListArgs {
1841 remote: true,
1842 provider: Some("mock".to_string()),
1843 all: false,
1844 json: false,
1845 };
1846 let result = list_with_registry(args, &mock_registry("mock", vec![], true)).await;
1847 assert!(result.is_ok());
1848 },
1849 )
1850 .await;
1851 }
1852
1853 #[tokio::test]
1854 async fn list_remote_skips_providers_not_matching_filter() {
1855 crate::config::with_isolated_config_path_async(
1856 "models-list_remote_skips_providers_not_matching_filter",
1857 |_fake_dir| async move {
1858 let args = ListArgs {
1862 remote: true,
1863 provider: Some("mock-other".to_string()),
1864 all: false,
1865 json: false,
1866 };
1867 let result = list_with_registry(args, &mock_registry("mock", vec![], false)).await;
1868 assert!(result.is_ok());
1869 },
1870 )
1871 .await;
1872 }
1873
1874 #[tokio::test]
1875 async fn show_remote_finds_model_from_provider() {
1876 crate::config::with_isolated_config_path_async(
1877 "models-show_remote_finds_model_from_provider",
1878 |_fake_dir| async move {
1879 let args = ShowArgs {
1880 model: "mock-remote-model".to_string(),
1881 remote: true,
1882 provider: Some("mock".to_string()),
1883 };
1884 let remote_model = ModelInfo {
1885 id: "mock-remote-model".to_string(),
1886 display_name: Some("Mock Remote Model".to_string()),
1887 provider: "mock".to_string(),
1888 capabilities: ModelCapabilities::default(),
1889 };
1890 let result =
1891 show_with_registry(args, &mock_registry("mock", vec![remote_model], false))
1892 .await;
1893 assert!(result.is_ok());
1894 },
1895 )
1896 .await;
1897 }
1898
1899 #[tokio::test]
1900 async fn show_remote_model_not_found_in_provider_list_falls_through() {
1901 crate::config::with_isolated_config_path_async(
1902 "models-show_remote_model_not_found_in_provider_list_falls_through",
1903 |_fake_dir| async move {
1904 let args = ShowArgs {
1905 model: "totally-unknown-model-xyz".to_string(),
1906 remote: true,
1907 provider: Some("mock".to_string()),
1908 };
1909 let result = show_with_registry(args, &mock_registry("mock", vec![], false)).await;
1910 assert!(result.is_ok());
1911 },
1912 )
1913 .await;
1914 }
1915
1916 #[tokio::test]
1917 async fn show_remote_provider_error_warns_and_falls_through() {
1918 crate::config::with_isolated_config_path_async(
1919 "models-show_remote_provider_error_warns_and_falls_through",
1920 |_fake_dir| async move {
1921 let args = ShowArgs {
1922 model: "totally-unknown-model-xyz".to_string(),
1923 remote: true,
1924 provider: Some("mock".to_string()),
1925 };
1926 let result = show_with_registry(args, &mock_registry("mock", vec![], true)).await;
1927 assert!(result.is_ok());
1928 },
1929 )
1930 .await;
1931 }
1932
1933 #[tokio::test]
1934 async fn show_remote_unconfigured_provider_warns_and_falls_through() {
1935 crate::config::with_isolated_config_path_async(
1936 "models-show_remote_unconfigured_provider_warns_and_falls_through",
1937 |_fake_dir| async move {
1938 let args = ShowArgs {
1941 model: "totally-unknown-model-xyz".to_string(),
1942 remote: true,
1943 provider: Some("nonexistent-provider".to_string()),
1944 };
1945 let result = show_with_registry(args, &mock_registry("mock", vec![], false)).await;
1946 assert!(result.is_ok());
1947 },
1948 )
1949 .await;
1950 }
1951
1952 #[tokio::test]
1961 async fn list_prints_warning_and_applies_model_capabilities_override() {
1962 crate::config::with_isolated_config_path_async(
1963 "models-list-override",
1964 |_fake_dir| async move {
1965 let known_id = builtin_table()[0].model_id.to_string();
1966 let mut fake_config = Config::default();
1967 fake_config.providers.anthropic_api_key = Some("not-a-real-key".to_string());
1968 fake_config.model_capabilities.insert(
1969 known_id,
1970 ModelCapabilities {
1971 supports_temperature: false,
1972 supports_streaming: false,
1973 supports_tools: false,
1974 supports_system_prompt: false,
1975 max_context_tokens: 1,
1976 max_output_tokens: 1,
1977 }
1978 .into(),
1979 );
1980 std::fs::write(
1981 Config::config_path(),
1982 toml::to_string(&fake_config).unwrap(),
1983 )
1984 .unwrap();
1985
1986 let args = ListArgs {
1987 remote: false,
1988 provider: None,
1989 all: false,
1990 json: false,
1991 };
1992 let result = list_with_registry(args, &mock_registry("mock", vec![], false)).await;
1993 assert!(result.is_ok());
1994 },
1995 )
1996 .await;
1997 }
1998
1999 #[tokio::test]
2000 async fn show_prints_warning_and_uses_model_capabilities_override() {
2001 crate::config::with_isolated_config_path_async(
2002 "models-show-override",
2003 |_fake_dir| async move {
2004 let known_id = builtin_table()[0].model_id.to_string();
2005 let mut fake_config = Config::default();
2006 fake_config.providers.anthropic_api_key = Some("not-a-real-key".to_string());
2007 fake_config.model_capabilities.insert(
2008 known_id.clone(),
2009 ModelCapabilities {
2010 supports_temperature: false,
2011 supports_streaming: false,
2012 supports_tools: false,
2013 supports_system_prompt: false,
2014 max_context_tokens: 1,
2015 max_output_tokens: 1,
2016 }
2017 .into(),
2018 );
2019 std::fs::write(
2020 Config::config_path(),
2021 toml::to_string(&fake_config).unwrap(),
2022 )
2023 .unwrap();
2024
2025 let args = ShowArgs {
2026 model: known_id,
2027 remote: false,
2028 provider: None,
2029 };
2030 let result = show_with_registry(args, &mock_registry("mock", vec![], false)).await;
2031 assert!(result.is_ok());
2032 },
2033 )
2034 .await;
2035 }
2036
2037 #[tokio::test]
2038 async fn mock_provider_trivial_trait_methods() {
2039 use leviath_providers::Provider;
2040 let provider = MockProvider {
2041 models: vec![],
2042 fail: false,
2043 };
2044 assert_eq!(provider.count_tokens("abcd", "mock-model").await, 1);
2045 assert_eq!(provider.max_context_tokens("mock-model"), 100_000);
2046 assert_eq!(provider.name(), "mock");
2047 let _ = provider.capabilities("mock-model");
2048 }
2049
2050 #[tokio::test]
2051 async fn mock_provider_infer_returns_err() {
2052 use leviath_providers::Provider;
2053 let provider = MockProvider {
2054 models: vec![],
2055 fail: false,
2056 };
2057 let request = leviath_providers::InferenceRequest {
2058 system: vec![],
2059 messages: vec![],
2060 model: "mock".to_string(),
2061 max_tokens: 100,
2062 temperature: 0.0,
2063 tools: vec![],
2064 extra: serde_json::Value::Null,
2065 request_timeout_secs: None,
2066 };
2067 let result = provider.infer(&request).await;
2068 assert!(result.is_err());
2069 }
2070
2071 #[tokio::test]
2072 async fn list_with_registry_propagates_config_load_error() {
2073 crate::config::with_isolated_config_path_async(
2074 "models-list_with_registry_propagates_config_load_error",
2075 |fake_dir| async move {
2076 std::fs::write(fake_dir.join("config.toml"), "not valid toml [[[").unwrap();
2077 let args = ListArgs {
2078 remote: false,
2079 provider: None,
2080 all: false,
2081 json: false,
2082 };
2083 let result = list_with_registry(args, &mock_registry("mock", vec![], false)).await;
2084 assert!(result.is_err());
2085 },
2086 )
2087 .await;
2088 }
2089
2090 use clap::Parser as _;
2104
2105 #[derive(clap::Parser)]
2106 struct TestCli {
2107 #[command(flatten)]
2108 models: ModelsArgs,
2109 }
2110
2111 fn expect_list(cmd: ModelsCommand) -> ListArgs {
2117 match cmd {
2118 ModelsCommand::List(args) => args,
2119 ModelsCommand::Show(_) => panic!("expected List"),
2120 }
2121 }
2122
2123 #[test]
2124 #[should_panic(expected = "expected List")]
2125 fn expect_list_panics_on_show() {
2126 expect_list(ModelsCommand::Show(ShowArgs {
2127 model: "x".to_string(),
2128 provider: None,
2129 remote: false,
2130 }));
2131 }
2132
2133 fn expect_show(cmd: ModelsCommand) -> ShowArgs {
2135 match cmd {
2136 ModelsCommand::Show(args) => args,
2137 ModelsCommand::List(_) => panic!("expected Show"),
2138 }
2139 }
2140
2141 #[test]
2142 #[should_panic(expected = "expected Show")]
2143 fn expect_show_panics_on_list() {
2144 expect_show(ModelsCommand::List(ListArgs {
2145 provider: None,
2146 remote: false,
2147 all: false,
2148 json: false,
2149 }));
2150 }
2151
2152 #[test]
2153 fn parses_list_with_no_flags() {
2154 let cli = TestCli::try_parse_from(["lev", "list"]).unwrap();
2155 let args = expect_list(cli.models.command);
2156 assert!(args.provider.is_none());
2157 assert!(!args.remote);
2158 }
2159
2160 #[test]
2161 fn parses_list_with_long_flags() {
2162 let cli = TestCli::try_parse_from(["lev", "list", "--provider", "anthropic", "--remote"])
2163 .unwrap();
2164 let args = expect_list(cli.models.command);
2165 assert_eq!(args.provider.as_deref(), Some("anthropic"));
2166 assert!(args.remote);
2167 }
2168
2169 #[test]
2170 fn parses_list_with_short_flags() {
2171 let cli = TestCli::try_parse_from(["lev", "list", "-p", "openai", "-r"]).unwrap();
2172 let args = expect_list(cli.models.command);
2173 assert_eq!(args.provider.as_deref(), Some("openai"));
2174 assert!(args.remote);
2175 }
2176
2177 #[test]
2178 fn parses_show_with_positional_model_and_long_flags() {
2179 let cli = TestCli::try_parse_from([
2180 "lev",
2181 "show",
2182 "claude-sonnet-4-6",
2183 "--provider",
2184 "anthropic",
2185 "--remote",
2186 ])
2187 .unwrap();
2188 let args = expect_show(cli.models.command);
2189 assert_eq!(args.model, "claude-sonnet-4-6");
2190 assert_eq!(args.provider.as_deref(), Some("anthropic"));
2191 assert!(args.remote);
2192 }
2193
2194 #[test]
2195 fn parses_show_with_short_flags() {
2196 let cli =
2197 TestCli::try_parse_from(["lev", "show", "gpt-5.5", "-p", "openai", "-r"]).unwrap();
2198 let args = expect_show(cli.models.command);
2199 assert_eq!(args.model, "gpt-5.5");
2200 assert_eq!(args.provider.as_deref(), Some("openai"));
2201 assert!(args.remote);
2202 }
2203
2204 #[test]
2205 fn parses_show_missing_required_positional_errors() {
2206 let result = TestCli::try_parse_from(["lev", "show"]);
2207 assert!(result.is_err());
2208 }
2209
2210 #[test]
2211 fn parses_unknown_subcommand_errors() {
2212 let result = TestCli::try_parse_from(["lev", "not-a-subcommand"]);
2213 assert!(result.is_err());
2214 }
2215
2216 #[tokio::test]
2217 async fn show_with_registry_propagates_config_load_error() {
2218 crate::config::with_isolated_config_path_async(
2219 "models-show_with_registry_propagates_config_load_error",
2220 |fake_dir| async move {
2221 std::fs::write(fake_dir.join("config.toml"), "not valid toml [[[").unwrap();
2222 let args = ShowArgs {
2223 model: "any-model".to_string(),
2224 remote: false,
2225 provider: None,
2226 };
2227 let result = show_with_registry(args, &mock_registry("mock", vec![], false)).await;
2228 assert!(result.is_err());
2229 },
2230 )
2231 .await;
2232 }
2233
2234 fn cannot_build(
2237 _config: &Config,
2238 ) -> Result<leviath_runtime::ProviderRegistry, leviath_providers::ProviderError> {
2239 Err(leviath_providers::ProviderError::ClientBuild(
2240 "no roots".to_string(),
2241 ))
2242 }
2243
2244 #[tokio::test]
2245 async fn list_reports_a_registry_that_will_not_build() {
2246 crate::config::with_isolated_config_path_async(
2247 "models-list_reports_a_registry_that_will_not_build",
2248 |_fake_dir| async move {
2249 let args = ListArgs {
2250 remote: false,
2251 provider: None,
2252 all: false,
2253 json: false,
2254 };
2255 let err = list_with_registry(args, &cannot_build)
2256 .await
2257 .expect_err("a failing registry builder should fail the command");
2258 assert!(err.to_string().contains("root certificate store"));
2259 },
2260 )
2261 .await;
2262 }
2263
2264 #[tokio::test]
2265 async fn show_reports_a_registry_that_will_not_build() {
2266 crate::config::with_isolated_config_path_async(
2267 "models-show_reports_a_registry_that_will_not_build",
2268 |_fake_dir| async move {
2269 let args = ShowArgs {
2272 model: "not-a-built-in-model".to_string(),
2275 provider: Some("anthropic".to_string()),
2276 remote: true,
2277 };
2278 let err = show_with_registry(args, &cannot_build)
2279 .await
2280 .expect_err("a failing registry builder should fail the command");
2281 assert!(err.to_string().contains("root certificate store"));
2282 },
2283 )
2284 .await;
2285 }
2286}