Skip to main content

leviath_cli/commands/
models.rs

1//! `lev models` - Inspect available models and their capabilities.
2
3use clap::{Args, Subcommand};
4use leviath_providers::{ModelCapabilities, ModelInfo};
5
6use super::run::build_provider_registry_from_config;
7use crate::config::Config;
8
9// ─── CLI types ────────────────────────────────────────────────────────────────
10
11/// Arguments for `lev models`.
12#[derive(Args)]
13pub struct ModelsArgs {
14    /// Which models subcommand to run.
15    #[command(subcommand)]
16    pub command: ModelsCommand,
17}
18
19/// The `lev models` subcommands.
20#[derive(Subcommand)]
21pub enum ModelsCommand {
22    /// List available models and their capabilities
23    List(ListArgs),
24    /// Show capabilities for a specific model
25    Show(ShowArgs),
26}
27
28/// Arguments for `lev models list`.
29#[derive(Args)]
30pub struct ListArgs {
31    /// Filter by provider name (anthropic, openai, ollama, openrouter)
32    #[arg(short, long)]
33    pub provider: Option<String>,
34    /// Fetch live model list from provider APIs (slower but complete)
35    #[arg(short = 'r', long)]
36    pub remote: bool,
37    /// Include models from providers this install has no credential for
38    #[arg(short = 'a', long)]
39    pub all: bool,
40    /// Report the table as JSON, one object per model.
41    #[arg(long)]
42    pub json: bool,
43}
44
45/// One model in `lev models list --json`.
46///
47/// Carries the full capability set rather than the six columns the table has
48/// room for, and turns the table's `*` marker into a field, since a caller
49/// choosing a model needs to know a capability came from config rather than
50/// from the provider.
51#[derive(serde::Serialize)]
52struct ModelRow {
53    id: String,
54    provider: String,
55    display_name: Option<String>,
56    capabilities: ModelCapabilities,
57    /// True when `[model_capabilities]` in config.toml replaced what Leviath
58    /// knows about this model.
59    capabilities_overridden: bool,
60}
61
62/// Arguments for `lev models show`.
63#[derive(Args)]
64pub struct ShowArgs {
65    /// Model ID to look up
66    pub model: String,
67    /// Provider to query (required for remote lookup)
68    #[arg(short, long)]
69    pub provider: Option<String>,
70    /// Fetch live model list from provider APIs (slower but complete)
71    #[arg(short = 'r', long)]
72    pub remote: bool,
73}
74
75// ─── Entrypoint ───────────────────────────────────────────────────────────────
76
77/// Run `lev models`: show which provider/model pairs are reachable.
78pub 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
85// ─── Built-in model table ─────────────────────────────────────────────────────
86
87/// A single row in the built-in model table.
88struct BuiltinEntry {
89    provider: &'static str,
90    model_id: &'static str,
91    display_name: &'static str,
92    caps: ModelCapabilities,
93}
94
95/// Providers whose entry in [`builtin_table`] is complete enough to say "this
96/// model does not exist" about.
97///
98/// Anthropic, OpenAI and Google publish a short list and this table tracks it.
99/// The rest do not: Ollama serves whatever has been pulled locally, OpenRouter
100/// proxies hundreds of models of which the table lists a sample, and a script
101/// provider defines its own catalog at run time. Naming a model those hosts
102/// have and this build has not heard of is normal, so they are never checked.
103const CLOSED_CATALOG_PROVIDERS: &[&str] = &["anthropic", "openai", "google"];
104
105/// The `(provider, model)` rows [`crate::lint`] checks a blueprint's model
106/// references against, limited to the providers with a closed catalog.
107pub 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
115/// Hard-coded capability table for well-known models.
116///
117/// This is used when the provider API is not reachable or `--remote` is not
118/// specified.  Remote results override these values for identical model IDs.
119fn builtin_table() -> Vec<BuiltinEntry> {
120    macro_rules! entry {
121        // Short form - tools defaults to true
122        ($provider:expr_2021, $id:expr_2021, $name:expr_2021,
123         temp=$t:expr_2021, ctx=$ctx:expr_2021, out=$out:expr_2021) => {
124            entry!(
125                $provider,
126                $id,
127                $name,
128                temp = $t,
129                tools = true,
130                ctx = $ctx,
131                out = $out
132            )
133        };
134        // Full form - explicit tools flag
135        ($provider:expr_2021, $id:expr_2021, $name:expr_2021,
136         temp=$t:expr_2021, tools=$to:expr_2021, ctx=$ctx:expr_2021, out=$out:expr_2021) => {
137            BuiltinEntry {
138                provider: $provider,
139                model_id: $id,
140                display_name: $name,
141                caps: ModelCapabilities {
142                    supports_temperature: $t,
143                    supports_streaming: true,
144                    supports_tools: $to,
145                    supports_system_prompt: true,
146                    max_context_tokens: $ctx,
147                    max_output_tokens: $out,
148                },
149            }
150        };
151    }
152
153    vec![
154        // ── Anthropic ──────────────────────────────────────────────────────────
155        entry!(
156            "anthropic",
157            "claude-opus-5",
158            "Claude Opus 5",
159            temp = false,
160            ctx = 1_000_000,
161            out = 128_000
162        ),
163        entry!(
164            "anthropic",
165            "claude-sonnet-5",
166            "Claude Sonnet 5",
167            temp = false,
168            ctx = 1_000_000,
169            out = 128_000
170        ),
171        entry!(
172            "anthropic",
173            "claude-fable-5",
174            "Claude Fable 5",
175            temp = false,
176            ctx = 1_000_000,
177            out = 128_000
178        ),
179        entry!(
180            "anthropic",
181            "claude-opus-4-8",
182            "Claude Opus 4.8",
183            temp = false,
184            ctx = 1_000_000,
185            out = 128_000
186        ),
187        entry!(
188            "anthropic",
189            "claude-opus-4-7",
190            "Claude Opus 4.7",
191            temp = false,
192            ctx = 1_000_000,
193            out = 128_000
194        ),
195        entry!(
196            "anthropic",
197            "claude-opus-4-6",
198            "Claude Opus 4.6",
199            temp = true,
200            ctx = 1_000_000,
201            out = 128_000
202        ),
203        entry!(
204            "anthropic",
205            "claude-sonnet-4-6",
206            "Claude Sonnet 4.6",
207            temp = true,
208            ctx = 1_000_000,
209            out = 128_000
210        ),
211        entry!(
212            "anthropic",
213            "claude-haiku-4-5-20251001",
214            "Claude Haiku 4.5",
215            temp = true,
216            ctx = 200_000,
217            out = 65_536
218        ),
219        // ── OpenAI ─────────────────────────────────────────────────────────────
220        // GPT-5.5 - flagship (Apr 2026), 1M+ context
221        entry!(
222            "openai",
223            "gpt-5.5",
224            "GPT-5.5",
225            temp = true,
226            ctx = 1_050_000,
227            out = 128_000
228        ),
229        entry!(
230            "openai",
231            "gpt-5.4",
232            "GPT-5.4",
233            temp = true,
234            ctx = 1_050_000,
235            out = 128_000
236        ),
237        entry!(
238            "openai",
239            "gpt-5.4-mini",
240            "GPT-5.4 Mini",
241            temp = true,
242            ctx = 400_000,
243            out = 128_000
244        ),
245        entry!(
246            "openai",
247            "gpt-5.4-nano",
248            "GPT-5.4 Nano",
249            temp = true,
250            ctx = 400_000,
251            out = 128_000
252        ),
253        // ── Google (Gemini) ────────────────────────────────────────────────────
254        // Native Google provider entries. Without these, a user whose only key
255        // is a Gemini key saw no model they could run: every Gemini row in this
256        // table routed through OpenRouter, which needs a different key.
257        entry!(
258            "google",
259            "gemini-3.5-flash",
260            "Gemini 3.5 Flash",
261            temp = true,
262            ctx = 1_048_576,
263            out = 65_535
264        ),
265        entry!(
266            "google",
267            "gemini-3.1-pro-preview",
268            "Gemini 3.1 Pro (preview)",
269            temp = true,
270            ctx = 1_048_576,
271            out = 65_535
272        ),
273        entry!(
274            "google",
275            "gemini-3-flash",
276            "Gemini 3 Flash",
277            temp = true,
278            ctx = 1_048_576,
279            out = 65_535
280        ),
281        entry!(
282            "google",
283            "gemini-3.1-flash-lite",
284            "Gemini 3.1 Flash Lite",
285            temp = true,
286            ctx = 1_048_576,
287            out = 65_535
288        ),
289        // ── OpenRouter: Google Gemini ──────────────────────────────────────────
290        entry!(
291            "openrouter",
292            "google/gemini-3.5-flash",
293            "Gemini 3.5 Flash",
294            temp = true,
295            ctx = 1_048_576,
296            out = 65_536
297        ),
298        entry!(
299            "openrouter",
300            "google/gemini-2.5-pro",
301            "Gemini 2.5 Pro",
302            temp = true,
303            ctx = 1_048_576,
304            out = 65_536
305        ),
306        entry!(
307            "openrouter",
308            "google/gemini-2.5-flash",
309            "Gemini 2.5 Flash",
310            temp = true,
311            ctx = 1_048_576,
312            out = 65_536
313        ),
314        entry!(
315            "openrouter",
316            "google/gemini-2.5-flash-lite",
317            "Gemini 2.5 Flash Lite",
318            temp = true,
319            ctx = 1_048_576,
320            out = 65_536
321        ),
322        // ── OpenRouter: Meta Llama 4 ───────────────────────────────────────────
323        entry!(
324            "openrouter",
325            "meta-llama/llama-4-maverick",
326            "Llama 4 Maverick",
327            temp = true,
328            ctx = 1_048_576,
329            out = 32_768
330        ),
331        entry!(
332            "openrouter",
333            "meta-llama/llama-4-scout",
334            "Llama 4 Scout",
335            temp = true,
336            ctx = 10_000_000,
337            out = 32_768
338        ),
339        // ── OpenRouter: DeepSeek ───────────────────────────────────────────────
340        entry!(
341            "openrouter",
342            "deepseek/deepseek-v4-pro",
343            "DeepSeek V4 Pro",
344            temp = true,
345            ctx = 1_048_576,
346            out = 393_216
347        ),
348        entry!(
349            "openrouter",
350            "deepseek/deepseek-v4-flash",
351            "DeepSeek V4 Flash",
352            temp = true,
353            ctx = 1_048_576,
354            out = 65_536
355        ),
356        entry!(
357            "openrouter",
358            "deepseek/deepseek-v3.2",
359            "DeepSeek V3.2",
360            temp = true,
361            ctx = 131_072,
362            out = 65_536
363        ),
364        entry!(
365            "openrouter",
366            "deepseek/deepseek-r1-0528",
367            "DeepSeek R1 (0528)",
368            temp = false,
369            tools = false,
370            ctx = 163_840,
371            out = 32_768
372        ),
373        entry!(
374            "openrouter",
375            "deepseek/deepseek-r1",
376            "DeepSeek R1",
377            temp = false,
378            tools = false,
379            ctx = 163_840,
380            out = 16_384
381        ),
382        // ── OpenRouter: Mistral ────────────────────────────────────────────────
383        entry!(
384            "openrouter",
385            "mistralai/mistral-large-2512",
386            "Mistral Large 3",
387            temp = true,
388            ctx = 262_144,
389            out = 32_768
390        ),
391        entry!(
392            "openrouter",
393            "mistralai/mistral-medium-3-5",
394            "Mistral Medium 3.5",
395            temp = true,
396            ctx = 256_000,
397            out = 32_768
398        ),
399        entry!(
400            "openrouter",
401            "mistralai/mistral-small-2603",
402            "Mistral Small 4",
403            temp = true,
404            ctx = 128_000,
405            out = 32_768
406        ),
407        // ── OpenRouter: Qwen (Alibaba) ─────────────────────────────────────────
408        entry!(
409            "openrouter",
410            "qwen/qwen3.6-plus",
411            "Qwen 3.6 Plus",
412            temp = true,
413            ctx = 1_048_576,
414            out = 65_536
415        ),
416        entry!(
417            "openrouter",
418            "qwen/qwen3-max",
419            "Qwen3 Max",
420            temp = true,
421            ctx = 131_072,
422            out = 32_768
423        ),
424        entry!(
425            "openrouter",
426            "qwen/qwen3-coder",
427            "Qwen3 Coder 480B",
428            temp = true,
429            ctx = 1_048_576,
430            out = 262_144
431        ),
432    ]
433}
434
435// ─── list ─────────────────────────────────────────────────────────────────────
436
437/// Core of [`list`], with provider-registry construction injected so tests
438/// can drive the `--remote` merge/override/error paths with a
439/// [`Provider`](leviath_providers::Provider) mock instead of hitting a real
440/// network endpoint (ollama) or spawning a real subprocess (claude-code) --
441/// both of which [`build_provider_registry`] always registers.
442///
443/// `build_registry` is a `&dyn Fn` trait object, not a generic
444/// `impl FnOnce`, deliberately: every test below passes a distinct closure
445/// type (each `mock_registry(...)` call site produces its own closure type,
446/// separate again from the production `build_provider_registry` function
447/// item type). A generic parameter would make `cargo-llvm-cov` instrument
448/// each call site's monomorphization of this function separately, and it
449/// has been observed to report the production instantiation as 0-hit even
450/// though it's genuinely exercised by `execute_list_command_runs_without_error`
451/// et al. - the same instantiation-merging undercount `run/task.rs`'s
452/// `resolve_task_with` documents at length and fixes the same way. A
453/// `&dyn Fn` trait object is one concrete type regardless of what closure is
454/// passed, so every call site shares a single instrumented instantiation.
455async fn list_with_registry(
456    args: ListArgs,
457    build_registry: &dyn Fn(
458        &Config,
459    ) -> Result<
460        leviath_runtime::ProviderRegistry,
461        leviath_providers::ProviderError,
462    >,
463) -> anyhow::Result<()> {
464    let config = Config::load()?;
465    for warning in config.validate_keys() {
466        eprintln!("Warning: {}", warning);
467    }
468
469    // Start with the built-in table, indexed by model_id for easy overriding.
470    let mut entries: Vec<ModelInfo> = builtin_table()
471        .into_iter()
472        .map(|e| ModelInfo {
473            id: e.model_id.to_string(),
474            display_name: Some(e.display_name.to_string()),
475            provider: e.provider.to_string(),
476            capabilities: e.caps,
477        })
478        .collect();
479
480    // Only what this install can actually run. Listing every model the binary
481    // knows about made a user with one key scroll past dozens of models they
482    // had no credential for, and hid whether their own key had been picked up
483    // at all. `--all` restores the full catalogue for shopping around.
484    let registry = build_registry(&config)?;
485    let available: std::collections::HashSet<String> = registry
486        .provider_names()
487        .into_iter()
488        .map(str::to_string)
489        .collect();
490    if !args.all {
491        entries.retain(|e| available.contains(&e.provider));
492    }
493
494    // --remote: fetch live model lists and merge (remote wins on same ID).
495    if args.remote {
496        for provider_name in registry.provider_names() {
497            // If the caller filtered to a specific provider, skip others.
498            if let Some(ref filter) = args.provider
499                && filter != provider_name
500            {
501                continue;
502            }
503
504            // `registry.get(provider_name)` is structurally guaranteed
505            // `Some` here - `provider_name` comes from
506            // `registry.provider_names()` just above, and both methods read
507            // the same underlying map (see `leviath-runtime/src/providers.rs`'s
508            // `ProviderRegistry`). There is no way to construct a registry
509            // where a name from `provider_names()` isn't `get()`-able, so
510            // `.expect()` documents that invariant instead of leaving a
511            // defensive-but-unreachable `if let` branch permanently
512            // uncovered - the same choice already made by
513            // `commands/serve/config.rs`'s `get_models` for this identical
514            // pattern.
515            let provider = registry
516                .get(provider_name)
517                .expect("provider_names returns registered names");
518            match provider.list_models().await {
519                Ok(remote_models) => {
520                    for rm in remote_models {
521                        // Override builtin entry with the same ID, or append.
522                        if let Some(existing) = entries.iter_mut().find(|e| e.id == rm.id) {
523                            *existing = rm;
524                        } else {
525                            entries.push(rm);
526                        }
527                    }
528                }
529                Err(e) => {
530                    eprintln!(
531                        "Warning: could not fetch models from '{}': {}",
532                        provider_name, e
533                    );
534                }
535            }
536        }
537    }
538
539    // Apply provider filter (after remote merge so we respect the filter).
540    if let Some(ref filter) = args.provider {
541        entries.retain(|e| &e.provider == filter);
542    }
543
544    // Apply user-defined capability overrides from config; track which IDs are overridden.
545    let overridden: std::collections::HashSet<String> =
546        config.model_capabilities.keys().cloned().collect();
547
548    for entry in entries.iter_mut() {
549        if let Some(user_caps) = config.model_capabilities.get(&entry.id) {
550            entry.capabilities = user_caps.apply_to(entry.capabilities.clone());
551        }
552    }
553
554    // JSON before the emptiness guard: an empty catalog is an empty array, not
555    // an error, and a caller polling this should not have to parse a nudge.
556    if args.json {
557        let rows: Vec<ModelRow> = entries
558            .into_iter()
559            .map(|e| ModelRow {
560                capabilities_overridden: overridden.contains(&e.id),
561                id: e.id,
562                provider: e.provider,
563                display_name: e.display_name,
564                capabilities: e.capabilities,
565            })
566            .collect();
567        // Owned scalars with no map keys to reject, so this cannot fail.
568        println!(
569            "{}",
570            serde_json::to_string_pretty(&rows).expect("a model listing serializes")
571        );
572        return Ok(());
573    }
574
575    if entries.is_empty() {
576        // Reachable two ways now: a `--provider` filter that matches nothing,
577        // or no configured provider at all (a fresh install). Both want the
578        // same nudge, and the second is the one worth naming.
579        println!("No models available.");
580        println!(
581            "(configure a provider with `lev setup`, or pass --all to see every \
582             model Leviath knows about)"
583        );
584        return Ok(());
585    }
586
587    // Print table header.
588    println!(
589        "{:<12} {:<40} {:<6} {:<7} {:<8} {:<8}",
590        "PROVIDER", "MODEL ID", "TEMP", "TOOLS", "CTX", "OUTPUT"
591    );
592    println!("{}", "-".repeat(85));
593
594    for entry in &entries {
595        let provider_col = if overridden.contains(&entry.id) {
596            format!("*{}", entry.provider)
597        } else {
598            entry.provider.clone()
599        };
600
601        let temp = bool_icon(entry.capabilities.supports_temperature);
602        let tools = bool_icon(entry.capabilities.supports_tools);
603        let ctx = fmt_tokens(entry.capabilities.max_context_tokens);
604        let out = fmt_tokens(entry.capabilities.max_output_tokens);
605
606        println!(
607            "{:<12} {:<40} {:<6} {:<7} {:<8} {:<8}",
608            provider_col, entry.id, temp, tools, ctx, out
609        );
610    }
611
612    if overridden
613        .iter()
614        .any(|id| entries.iter().any(|e| &e.id == id))
615    {
616        println!("\n* = capabilities overridden via [model_capabilities] in config");
617    }
618
619    Ok(())
620}
621
622// ─── show ─────────────────────────────────────────────────────────────────────
623
624/// Core of [`show`], with provider-registry construction injected - see
625/// [`list_with_registry`] for why.
626async fn show_with_registry(
627    args: ShowArgs,
628    build_registry: &dyn Fn(
629        &Config,
630    ) -> Result<
631        leviath_runtime::ProviderRegistry,
632        leviath_providers::ProviderError,
633    >,
634) -> anyhow::Result<()> {
635    let config = Config::load()?;
636    for warning in config.validate_keys() {
637        eprintln!("Warning: {}", warning);
638    }
639
640    let model_id = &args.model;
641
642    // 1. The built-in row, which a `[model_capabilities]` entry corrects rather
643    //    than replaces - so it has to be found before the override is applied.
644    //    Printing the override alone would report `Default` for every field the
645    //    operator did not mention, which is not what the run will use.
646    let builtin = builtin_table();
647    let builtin_entry = builtin.iter().find(|e| e.model_id == model_id);
648    let user_caps = config.model_capabilities.get(model_id);
649
650    if let Some(user_caps) = user_caps {
651        let base = builtin_entry.map(|e| e.caps.clone()).unwrap_or_default();
652        print_model_detail(
653            model_id,
654            builtin_entry.map(|e| e.display_name),
655            "config (user override)",
656            &user_caps.apply_to(base),
657            true,
658        );
659        return Ok(());
660    }
661
662    // 2. The built-in table on its own.
663    if let Some(entry) = builtin_entry {
664        print_model_detail(
665            model_id,
666            Some(entry.display_name),
667            entry.provider,
668            &entry.caps,
669            false,
670        );
671        return Ok(());
672    }
673
674    // 3. Optionally fetch from provider API if --remote and --provider are both given.
675    if args.remote
676        && let Some(ref provider_name) = args.provider
677    {
678        let registry = build_registry(&config)?;
679        if let Some(provider) = registry.get(provider_name) {
680            match provider.list_models().await {
681                Ok(models) => {
682                    if let Some(info) = models.iter().find(|m| &m.id == model_id) {
683                        print_model_detail(
684                            model_id,
685                            info.display_name.as_deref(),
686                            &info.provider,
687                            &info.capabilities,
688                            false,
689                        );
690                        return Ok(());
691                    }
692                }
693                Err(e) => {
694                    eprintln!(
695                        "Warning: could not fetch models from '{}': {}",
696                        provider_name, e
697                    );
698                }
699            }
700        } else {
701            eprintln!(
702                "Warning: provider '{}' is not configured (missing API key?)",
703                provider_name
704            );
705        }
706    }
707
708    // 4. Not found anywhere - print a helpful message with a TOML snippet.
709    println!("Model '{}' not found.", model_id);
710    println!(
711        "Add it to {} under [model_capabilities.'{}']",
712        Config::config_path().display(),
713        model_id
714    );
715    println!();
716    println!("Example:");
717    println!("[model_capabilities.'{}']", model_id);
718    println!("supports_temperature  = true");
719    println!("supports_streaming    = true");
720    println!("supports_tools        = true");
721    println!("supports_system_prompt = true");
722    println!("max_context_tokens    = 8192");
723    println!("max_output_tokens     = 4096");
724
725    Ok(())
726}
727
728// ─── Display helpers ──────────────────────────────────────────────────────────
729
730fn bool_icon(b: bool) -> &'static str {
731    if b { "✓" } else { "✗" }
732}
733
734/// Format a raw token count as a human-friendly string (e.g. 1M, 200K, 128K, 8K).
735fn fmt_tokens(n: usize) -> String {
736    if n >= 1_000_000 {
737        format!("{}M", n / 1_000_000)
738    } else if n >= 1_000 {
739        format!("{}K", n / 1_000)
740    } else {
741        n.to_string()
742    }
743}
744
745/// Print a detailed capability sheet for a single model.
746fn print_model_detail(
747    id: &str,
748    display_name: Option<&str>,
749    provider: &str,
750    caps: &ModelCapabilities,
751    is_user_override: bool,
752) {
753    println!("Model:    {}", id);
754    if let Some(name) = display_name {
755        println!("Name:     {}", name);
756    }
757    println!("Provider: {}", provider);
758    if is_user_override {
759        println!("Source:   user override (config)");
760    }
761    println!();
762    println!("Capabilities");
763    println!("------------");
764    println!("  Temperature:    {}", bool_icon(caps.supports_temperature));
765    println!("  Streaming:      {}", bool_icon(caps.supports_streaming));
766    println!("  Tool calling:   {}", bool_icon(caps.supports_tools));
767    println!(
768        "  System prompt:  {}",
769        bool_icon(caps.supports_system_prompt)
770    );
771    println!(
772        "  Context window: {} tokens ({})",
773        caps.max_context_tokens,
774        fmt_tokens(caps.max_context_tokens)
775    );
776    println!(
777        "  Max output:     {} tokens ({})",
778        caps.max_output_tokens,
779        fmt_tokens(caps.max_output_tokens)
780    );
781}
782
783#[cfg(test)]
784mod tests {
785    use super::*;
786
787    // ─── fmt_tokens ─────────────────────────────────────────────────────────
788
789    #[test]
790    fn fmt_tokens_millions() {
791        assert_eq!(fmt_tokens(1_000_000), "1M");
792        assert_eq!(fmt_tokens(2_000_000), "2M");
793    }
794
795    #[test]
796    fn fmt_tokens_thousands() {
797        assert_eq!(fmt_tokens(128_000), "128K");
798        assert_eq!(fmt_tokens(4_096), "4K");
799        assert_eq!(fmt_tokens(1_000), "1K");
800    }
801
802    #[test]
803    fn fmt_tokens_small() {
804        assert_eq!(fmt_tokens(512), "512");
805        assert_eq!(fmt_tokens(0), "0");
806    }
807
808    // ─── bool_icon ──────────────────────────────────────────────────────────
809
810    #[test]
811    fn bool_icon_values() {
812        assert_eq!(bool_icon(true), "✓");
813        assert_eq!(bool_icon(false), "✗");
814    }
815
816    // ─── builtin_table ──────────────────────────────────────────────────────
817
818    #[test]
819    fn builtin_table_is_not_empty() {
820        let table = builtin_table();
821        assert!(!table.is_empty());
822    }
823
824    #[test]
825    fn builtin_table_has_anthropic_models() {
826        let table = builtin_table();
827        let anthropic: Vec<_> = table.iter().filter(|e| e.provider == "anthropic").collect();
828        assert!(!anthropic.is_empty());
829    }
830
831    #[test]
832    fn builtin_table_has_openai_models() {
833        let table = builtin_table();
834        let openai: Vec<_> = table.iter().filter(|e| e.provider == "openai").collect();
835        assert!(!openai.is_empty());
836    }
837
838    #[test]
839    fn builtin_table_has_openrouter_models() {
840        let table = builtin_table();
841        let openrouter: Vec<_> = table
842            .iter()
843            .filter(|e| e.provider == "openrouter")
844            .collect();
845        assert!(!openrouter.is_empty());
846    }
847
848    #[test]
849    fn builtin_entries_have_valid_capabilities() {
850        for entry in builtin_table() {
851            assert!(entry.caps.max_context_tokens > 0);
852            assert!(entry.caps.max_output_tokens > 0);
853            assert!(entry.caps.supports_streaming);
854            assert!(entry.caps.supports_system_prompt);
855        }
856    }
857
858    #[test]
859    fn builtin_entries_have_unique_model_ids() {
860        let table = builtin_table();
861        let ids: Vec<&str> = table.iter().map(|e| e.model_id).collect();
862        let unique: std::collections::HashSet<&str> = ids.iter().copied().collect();
863        assert_eq!(ids.len(), unique.len());
864    }
865
866    #[test]
867    fn deepseek_r1_models_no_tools() {
868        let table = builtin_table();
869        for entry in &table {
870            if entry.model_id.contains("deepseek-r1") {
871                assert!(!entry.caps.supports_tools);
872            }
873        }
874    }
875
876    // ─── print_model_detail ─────────────────────────────────────────────────
877
878    #[test]
879    fn print_model_detail_does_not_panic() {
880        let caps = ModelCapabilities {
881            supports_temperature: true,
882            supports_streaming: true,
883            supports_tools: true,
884            supports_system_prompt: true,
885            max_context_tokens: 100_000,
886            max_output_tokens: 8_192,
887        };
888        // Should not panic
889        print_model_detail("test-model", Some("Test Model"), "test", &caps, false);
890        print_model_detail("test-model", None, "test", &caps, true);
891    }
892
893    // ─── fmt_tokens edge cases ──────────────────────────────────────────────
894
895    #[test]
896    fn fmt_tokens_exact_boundary() {
897        assert_eq!(fmt_tokens(999), "999");
898        assert_eq!(fmt_tokens(999_999), "999K");
899    }
900
901    #[test]
902    fn fmt_tokens_large_millions() {
903        assert_eq!(fmt_tokens(10_000_000), "10M");
904    }
905
906    // ─── builtin_table provider coverage ────────────────────────────────────
907
908    #[test]
909    fn builtin_table_claude_opus_no_temperature() {
910        let table = builtin_table();
911        for entry in &table {
912            if entry.model_id == "claude-opus-4-8" || entry.model_id == "claude-opus-4-7" {
913                assert!(!entry.caps.supports_temperature);
914            }
915        }
916    }
917
918    #[test]
919    fn builtin_table_claude_sonnet_supports_temperature() {
920        let table = builtin_table();
921        let sonnet = table
922            .iter()
923            .find(|e| e.model_id == "claude-sonnet-4-6")
924            .expect("claude-sonnet-4-6 should be in table");
925        assert!(sonnet.caps.supports_temperature);
926    }
927
928    #[test]
929    fn builtin_table_has_display_names() {
930        let table = builtin_table();
931        for entry in &table {
932            assert!(!entry.display_name.is_empty());
933        }
934    }
935
936    #[test]
937    fn builtin_table_context_larger_than_output() {
938        let table = builtin_table();
939        for entry in &table {
940            assert!(entry.caps.max_context_tokens >= entry.caps.max_output_tokens);
941        }
942    }
943
944    // ─── bool_icon edge ─────────────────────────────────────────────────────
945
946    #[test]
947    fn bool_icon_returns_unicode() {
948        assert!(!bool_icon(true).is_empty());
949        assert!(!bool_icon(false).is_empty());
950        assert_ne!(bool_icon(true), bool_icon(false));
951    }
952
953    // ─── builtin_table model coverage ───────────────────────────────────
954
955    #[test]
956    fn builtin_table_openai_models_support_temperature() {
957        let table = builtin_table();
958        for entry in &table {
959            if entry.provider == "openai" {
960                assert!(entry.caps.supports_temperature);
961            }
962        }
963    }
964
965    /// A user whose only key is a Gemini key must find models they can run.
966    /// Every Gemini row in this table used to route through OpenRouter, which
967    /// needs a different key, so `lev models list` showed that user nothing
968    /// their key could reach.
969    #[test]
970    fn builtin_table_offers_native_google_models() {
971        let table = builtin_table();
972        let native: Vec<&str> = table
973            .iter()
974            .filter(|e| e.provider == "google")
975            .map(|e| e.model_id)
976            .collect();
977        assert!(
978            !native.is_empty(),
979            "the native google provider must offer models of its own"
980        );
981        // Native ids are bare (`gemini-3.5-flash`), never OpenRouter-prefixed.
982        for id in &native {
983            assert!(
984                !id.contains('/'),
985                "native google model id must not be vendor-prefixed: {id}"
986            );
987        }
988    }
989
990    #[test]
991    fn builtin_table_gemini_flash_models_exist() {
992        let table = builtin_table();
993        let flash: Vec<_> = table
994            .iter()
995            .filter(|e| e.model_id.contains("gemini") && e.model_id.contains("flash"))
996            .collect();
997        assert!(!flash.is_empty());
998    }
999
1000    #[test]
1001    fn builtin_table_deepseek_r1_no_temperature() {
1002        let table = builtin_table();
1003        for entry in &table {
1004            if entry.model_id.contains("deepseek-r1") {
1005                assert!(!entry.caps.supports_temperature);
1006            }
1007        }
1008    }
1009
1010    #[test]
1011    fn builtin_table_qwen_models_exist() {
1012        let table = builtin_table();
1013        let qwen: Vec<_> = table
1014            .iter()
1015            .filter(|e| e.model_id.contains("qwen"))
1016            .collect();
1017        assert!(!qwen.is_empty());
1018    }
1019
1020    #[test]
1021    fn builtin_table_mistral_models_exist() {
1022        let table = builtin_table();
1023        let mistral: Vec<_> = table
1024            .iter()
1025            .filter(|e| e.model_id.contains("mistral"))
1026            .collect();
1027        assert!(!mistral.is_empty());
1028    }
1029
1030    #[test]
1031    fn builtin_table_all_entries_have_provider() {
1032        let table = builtin_table();
1033        for entry in &table {
1034            assert!(!entry.provider.is_empty());
1035        }
1036    }
1037
1038    #[test]
1039    fn builtin_table_all_entries_have_model_id() {
1040        let table = builtin_table();
1041        for entry in &table {
1042            assert!(!entry.model_id.is_empty());
1043        }
1044    }
1045
1046    // ─── execute() / list() / show() async entry points ──────────────────
1047
1048    #[tokio::test]
1049    async fn execute_list_command_runs_without_error() {
1050        crate::config::with_isolated_config_path_async(
1051            "models-execute_list_command_runs_without_error",
1052            |_fake_dir| async move {
1053                let args = ModelsArgs {
1054                    command: ModelsCommand::List(ListArgs {
1055                        provider: None,
1056                        remote: false,
1057                        all: false,
1058                        json: false,
1059                    }),
1060                };
1061                // Should succeed: prints the builtin table
1062                let result = execute(args).await;
1063                assert!(result.is_ok());
1064            },
1065        )
1066        .await;
1067    }
1068
1069    #[tokio::test]
1070    async fn execute_list_with_provider_filter_runs_without_error() {
1071        crate::config::with_isolated_config_path_async(
1072            "models-execute_list_with_provider_filter_runs_without_error",
1073            |_fake_dir| async move {
1074                let args = ModelsArgs {
1075                    command: ModelsCommand::List(ListArgs {
1076                        provider: Some("anthropic".to_string()),
1077                        remote: false,
1078                        all: false,
1079                        json: false,
1080                    }),
1081                };
1082                let result = execute(args).await;
1083                assert!(result.is_ok());
1084            },
1085        )
1086        .await;
1087    }
1088
1089    #[tokio::test]
1090    async fn execute_list_with_nonexistent_provider_filter() {
1091        crate::config::with_isolated_config_path_async(
1092            "models-execute_list_with_nonexistent_provider_filter",
1093            |_fake_dir| async move {
1094                let args = ModelsArgs {
1095                    command: ModelsCommand::List(ListArgs {
1096                        provider: Some("nonexistent_provider".to_string()),
1097                        remote: false,
1098                        all: false,
1099                        json: false,
1100                    }),
1101                };
1102                // Should succeed but print "No models found."
1103                let result = execute(args).await;
1104                assert!(result.is_ok());
1105            },
1106        )
1107        .await;
1108    }
1109
1110    #[tokio::test]
1111    async fn execute_show_known_model_runs_without_error() {
1112        crate::config::with_isolated_config_path_async(
1113            "models-execute_show_known_model_runs_without_error",
1114            |_fake_dir| async move {
1115                let args = ModelsArgs {
1116                    command: ModelsCommand::Show(ShowArgs {
1117                        model: "claude-sonnet-4-6".to_string(),
1118                        provider: None,
1119                        remote: false,
1120                    }),
1121                };
1122                // Should find model in builtin table and print details
1123                let result = execute(args).await;
1124                assert!(result.is_ok());
1125            },
1126        )
1127        .await;
1128    }
1129
1130    #[tokio::test]
1131    async fn execute_show_unknown_model_runs_without_error() {
1132        crate::config::with_isolated_config_path_async(
1133            "models-execute_show_unknown_model_runs_without_error",
1134            |_fake_dir| async move {
1135                let args = ModelsArgs {
1136                    command: ModelsCommand::Show(ShowArgs {
1137                        model: "totally-unknown-model-xyz".to_string(),
1138                        provider: None,
1139                        remote: false,
1140                    }),
1141                };
1142                // Should print "Model not found" message without error
1143                let result = execute(args).await;
1144                assert!(result.is_ok());
1145            },
1146        )
1147        .await;
1148    }
1149
1150    #[tokio::test]
1151    async fn execute_show_unknown_model_with_remote_no_provider() {
1152        crate::config::with_isolated_config_path_async(
1153            "models-execute_show_unknown_model_with_remote_no_provider",
1154            |_fake_dir| async move {
1155                let args = ModelsArgs {
1156                    command: ModelsCommand::Show(ShowArgs {
1157                        model: "totally-unknown-model-xyz".to_string(),
1158                        provider: None,
1159                        remote: true, // remote but no provider = skips remote lookup
1160                    }),
1161                };
1162                let result = execute(args).await;
1163                assert!(result.is_ok());
1164            },
1165        )
1166        .await;
1167    }
1168
1169    #[tokio::test]
1170    async fn execute_show_unknown_model_with_remote_unconfigured_provider() {
1171        crate::config::with_isolated_config_path_async(
1172            "models-execute_show_unknown_model_with_remote_unconfigured_provider",
1173            |_fake_dir| async move {
1174                let args = ModelsArgs {
1175                    command: ModelsCommand::Show(ShowArgs {
1176                        model: "totally-unknown-model-xyz".to_string(),
1177                        provider: Some("anthropic".to_string()),
1178                        remote: true,
1179                        // Provider won't be configured in test env (no API key)
1180                    }),
1181                };
1182                // Should warn about unconfigured provider and then show not-found message
1183                let result = execute(args).await;
1184                assert!(result.is_ok());
1185            },
1186        )
1187        .await;
1188    }
1189
1190    // ─── list() with builtin model having overrides in config ─────────────
1191
1192    #[tokio::test]
1193    async fn list_with_openrouter_filter() {
1194        // execute() -> list_with_registry() calls the real Config::load(),
1195        // which reads the process-global LEVIATH_CONFIG_PATH. Without
1196        // isolating it here, a concurrently-running test that points that
1197        // var at a temporarily-invalid-TOML fake config (e.g.
1198        // list_with_registry_propagates_config_load_error) can make this
1199        // test observe that torn state and fail nondeterministically --
1200        // exactly what happened on CI.
1201        crate::config::with_isolated_config_path_async(
1202            "models-list-openrouter-filter",
1203            |_fake_dir| async move {
1204                let args = ModelsArgs {
1205                    command: ModelsCommand::List(ListArgs {
1206                        provider: Some("openrouter".to_string()),
1207                        remote: false,
1208                        all: false,
1209                        json: false,
1210                    }),
1211                };
1212                let result = execute(args).await;
1213                assert!(result.is_ok());
1214            },
1215        )
1216        .await;
1217    }
1218
1219    #[tokio::test]
1220    async fn list_with_openai_filter() {
1221        // See the comment on list_with_openrouter_filter - same real
1222        // Config::load() race.
1223        crate::config::with_isolated_config_path_async(
1224            "models-list-openai-filter",
1225            |_fake_dir| async move {
1226                let args = ModelsArgs {
1227                    command: ModelsCommand::List(ListArgs {
1228                        provider: Some("openai".to_string()),
1229                        remote: false,
1230                        all: false,
1231                        json: false,
1232                    }),
1233                };
1234                let result = execute(args).await;
1235                assert!(result.is_ok());
1236            },
1237        )
1238        .await;
1239    }
1240
1241    #[tokio::test]
1242    async fn show_builtin_anthropic_opus() {
1243        // See the comment on list_with_openrouter_filter - same real
1244        // Config::load() race.
1245        crate::config::with_isolated_config_path_async(
1246            "models-show-anthropic-opus",
1247            |_fake_dir| async move {
1248                let args = ModelsArgs {
1249                    command: ModelsCommand::Show(ShowArgs {
1250                        model: "claude-opus-4-6".to_string(),
1251                        provider: None,
1252                        remote: false,
1253                    }),
1254                };
1255                let result = execute(args).await;
1256                assert!(result.is_ok());
1257            },
1258        )
1259        .await;
1260    }
1261
1262    #[tokio::test]
1263    async fn show_builtin_openai_model() {
1264        // See the comment on list_with_openrouter_filter - same real
1265        // Config::load() race.
1266        crate::config::with_isolated_config_path_async(
1267            "models-show-openai-model",
1268            |_fake_dir| async move {
1269                let args = ModelsArgs {
1270                    command: ModelsCommand::Show(ShowArgs {
1271                        model: "gpt-5.5".to_string(),
1272                        provider: None,
1273                        remote: false,
1274                    }),
1275                };
1276                let result = execute(args).await;
1277                assert!(result.is_ok());
1278            },
1279        )
1280        .await;
1281    }
1282
1283    #[tokio::test]
1284    async fn show_builtin_deepseek_r1() {
1285        // See the comment on list_with_openrouter_filter - same real
1286        // Config::load() race.
1287        crate::config::with_isolated_config_path_async(
1288            "models-show-deepseek-r1",
1289            |_fake_dir| async move {
1290                let args = ModelsArgs {
1291                    command: ModelsCommand::Show(ShowArgs {
1292                        model: "deepseek/deepseek-r1".to_string(),
1293                        provider: None,
1294                        remote: false,
1295                    }),
1296                };
1297                let result = execute(args).await;
1298                assert!(result.is_ok());
1299            },
1300        )
1301        .await;
1302    }
1303
1304    // ─── builtin_table as ModelInfo conversion ──────────────────────────
1305
1306    #[test]
1307    fn builtin_table_to_model_info_preserves_data() {
1308        let table = builtin_table();
1309        let infos: Vec<ModelInfo> = table
1310            .into_iter()
1311            .map(|e| ModelInfo {
1312                id: e.model_id.to_string(),
1313                display_name: Some(e.display_name.to_string()),
1314                provider: e.provider.to_string(),
1315                capabilities: e.caps,
1316            })
1317            .collect();
1318
1319        assert!(!infos.is_empty());
1320        for info in &infos {
1321            assert!(!info.id.is_empty());
1322            assert!(info.display_name.is_some());
1323            assert!(!info.provider.is_empty());
1324        }
1325    }
1326
1327    // ─── print_model_detail coverage ────────────────────────────────────
1328
1329    #[test]
1330    fn print_model_detail_with_no_tools_no_temp() {
1331        let caps = ModelCapabilities {
1332            supports_temperature: false,
1333            supports_streaming: false,
1334            supports_tools: false,
1335            supports_system_prompt: false,
1336            max_context_tokens: 1000,
1337            max_output_tokens: 500,
1338        };
1339        // Should not panic with all features disabled
1340        print_model_detail("test-model", Some("Test"), "test", &caps, false);
1341    }
1342
1343    #[test]
1344    fn print_model_detail_user_override_source() {
1345        let caps = ModelCapabilities::default();
1346        // Should not panic with user override flag set
1347        print_model_detail("override-model", None, "custom", &caps, true);
1348    }
1349
1350    // ─── fmt_tokens additional ──────────────────────────────────────────
1351
1352    #[test]
1353    fn fmt_tokens_just_below_thousand() {
1354        assert_eq!(fmt_tokens(999), "999");
1355    }
1356
1357    #[test]
1358    fn fmt_tokens_just_at_thousand() {
1359        assert_eq!(fmt_tokens(1000), "1K");
1360    }
1361
1362    #[test]
1363    fn fmt_tokens_just_below_million() {
1364        assert_eq!(fmt_tokens(999_999), "999K");
1365    }
1366
1367    #[test]
1368    fn fmt_tokens_just_at_million() {
1369        assert_eq!(fmt_tokens(1_000_000), "1M");
1370    }
1371
1372    #[test]
1373    fn fmt_tokens_non_round_thousands() {
1374        // Integer division: 1500 / 1000 = 1
1375        assert_eq!(fmt_tokens(1500), "1K");
1376        assert_eq!(fmt_tokens(65_536), "65K");
1377    }
1378
1379    // ─── list() / show() non-remote paths ──────────────────────────────
1380    //
1381    // Config::load() gracefully falls back to defaults when
1382    // ~/.leviath/config.toml doesn't exist, so these are safe to call
1383    // directly without touching the real environment. `list`/`show` are thin
1384    // wrappers around `list_with_registry`/`show_with_registry` (see below
1385    // for the --remote-path tests using a mock registry).
1386
1387    #[tokio::test]
1388    async fn list_builtin_no_filter_succeeds() {
1389        crate::config::with_isolated_config_path_async(
1390            "models-list_builtin_no_filter_succeeds",
1391            |_fake_dir| async move {
1392                let args = ListArgs {
1393                    remote: false,
1394                    provider: None,
1395                    all: false,
1396                    json: false,
1397                };
1398                let result = list_with_registry(args, &build_provider_registry_from_config).await;
1399                assert!(result.is_ok());
1400            },
1401        )
1402        .await;
1403    }
1404
1405    #[tokio::test]
1406    async fn list_json_with_no_configured_provider_is_an_empty_array() {
1407        // The prose report prints a nudge here. JSON must not: a caller reading
1408        // this branches on the array's length, and a sentence would not parse.
1409        crate::config::with_isolated_config_path_async(
1410            "models-list_json_empty",
1411            |_fake_dir| async move {
1412                let args = ListArgs {
1413                    remote: false,
1414                    provider: Some("no-such-provider".to_string()),
1415                    all: false,
1416                    json: true,
1417                };
1418                let result = list_with_registry(args, &build_provider_registry_from_config).await;
1419                assert!(result.is_ok());
1420            },
1421        )
1422        .await;
1423    }
1424
1425    #[tokio::test]
1426    async fn list_json_with_all_succeeds() {
1427        crate::config::with_isolated_config_path_async(
1428            "models-list_json_all",
1429            |_fake_dir| async move {
1430                let args = ListArgs {
1431                    remote: false,
1432                    provider: None,
1433                    all: true,
1434                    json: true,
1435                };
1436                let result = list_with_registry(args, &build_provider_registry_from_config).await;
1437                assert!(result.is_ok());
1438            },
1439        )
1440        .await;
1441    }
1442
1443    #[test]
1444    fn model_row_serializes_capabilities_and_the_override_flag() {
1445        // The table shows an override as a `*` prefix on the provider column.
1446        // JSON has to say so in a field, or a caller cannot tell a capability
1447        // Leviath knows from one config asserted.
1448        let row = ModelRow {
1449            id: "m".to_string(),
1450            provider: "p".to_string(),
1451            display_name: Some("M".to_string()),
1452            capabilities: ModelCapabilities::default(),
1453            capabilities_overridden: true,
1454        };
1455        let value: serde_json::Value =
1456            serde_json::from_str(&serde_json::to_string(&row).unwrap()).unwrap();
1457        assert_eq!(value["id"], serde_json::json!("m"));
1458        assert_eq!(value["capabilities_overridden"], serde_json::json!(true));
1459        assert!(value["capabilities"]["supports_tools"].is_boolean());
1460    }
1461
1462    #[tokio::test]
1463    async fn list_builtin_with_provider_filter_succeeds() {
1464        crate::config::with_isolated_config_path_async(
1465            "models-list_builtin_with_provider_filter_succeeds",
1466            |_fake_dir| async move {
1467                let args = ListArgs {
1468                    remote: false,
1469                    provider: Some("anthropic".to_string()),
1470                    all: false,
1471                    json: false,
1472                };
1473                let result = list_with_registry(args, &build_provider_registry_from_config).await;
1474                assert!(result.is_ok());
1475            },
1476        )
1477        .await;
1478    }
1479
1480    #[tokio::test]
1481    async fn list_unknown_provider_filter_finds_nothing() {
1482        crate::config::with_isolated_config_path_async(
1483            "models-list_unknown_provider_filter_finds_nothing",
1484            |_fake_dir| async move {
1485                let args = ListArgs {
1486                    remote: false,
1487                    provider: Some("no-such-provider".to_string()),
1488                    all: false,
1489                    json: false,
1490                };
1491                // Should print "No models found." and still succeed, not error.
1492                let result = list_with_registry(args, &build_provider_registry_from_config).await;
1493                assert!(result.is_ok());
1494            },
1495        )
1496        .await;
1497    }
1498
1499    #[tokio::test]
1500    async fn show_builtin_model_succeeds() {
1501        crate::config::with_isolated_config_path_async(
1502            "models-show_builtin_model_succeeds",
1503            |_fake_dir| async move {
1504                // Use a model ID guaranteed to be in the builtin table.
1505                let known_id = builtin_table()[0].model_id.to_string();
1506                let args = ShowArgs {
1507                    model: known_id,
1508                    remote: false,
1509                    provider: None,
1510                };
1511                let result = show_with_registry(args, &build_provider_registry_from_config).await;
1512                assert!(result.is_ok());
1513            },
1514        )
1515        .await;
1516    }
1517
1518    #[tokio::test]
1519    async fn show_unknown_model_without_remote_succeeds_with_warning() {
1520        crate::config::with_isolated_config_path_async(
1521            "models-show_unknown_model_without_remote_succeeds_with_warning",
1522            |_fake_dir| async move {
1523                let args = ShowArgs {
1524                    model: "totally-unknown-model-xyz".to_string(),
1525                    remote: false,
1526                    provider: None,
1527                };
1528                // Falls through all lookup tiers; must not error even when not found.
1529                let result = show_with_registry(args, &build_provider_registry_from_config).await;
1530                assert!(result.is_ok());
1531            },
1532        )
1533        .await;
1534    }
1535
1536    #[tokio::test]
1537    async fn show_remote_without_provider_falls_through_gracefully() {
1538        crate::config::with_isolated_config_path_async(
1539            "models-show_remote_without_provider_falls_through_gracefully",
1540            |_fake_dir| async move {
1541                // args.remote = true but no --provider given -> the remote-fetch
1542                // branch's inner `if let Some(ref provider_name)` is skipped.
1543                let args = ShowArgs {
1544                    model: "totally-unknown-model-xyz".to_string(),
1545                    remote: true,
1546                    provider: None,
1547                };
1548                let result = show_with_registry(args, &build_provider_registry_from_config).await;
1549                assert!(result.is_ok());
1550            },
1551        )
1552        .await;
1553    }
1554
1555    // ─── list()/show() --remote paths, with a mock provider ────────────────
1556    //
1557    // `build_provider_registry` always registers real `ollama`/`claude-code`
1558    // providers regardless of config, so these can't safely be exercised via
1559    // the real registry (a real network call to localhost:11434, or spawning
1560    // a real `claude` subprocess). `list_with_registry`/`show_with_registry`
1561    // take an injectable registry builder for exactly this reason: tests
1562    // register a `MockProvider` under a name of their choosing and filter to
1563    // just that provider via `--provider`, so no real ollama/claude-code
1564    // provider is ever touched.
1565
1566    struct MockProvider {
1567        models: Vec<ModelInfo>,
1568        fail: bool,
1569    }
1570
1571    #[async_trait::async_trait]
1572    impl leviath_providers::Provider for MockProvider {
1573        async fn infer(
1574            &self,
1575            _request: &leviath_providers::InferenceRequest,
1576        ) -> Result<leviath_providers::InferenceResponse, leviath_providers::ProviderError>
1577        {
1578            Err(leviath_providers::ProviderError::Other(
1579                "MockProvider does not support infer".to_string(),
1580            ))
1581        }
1582
1583        async fn count_tokens(&self, text: &str, _model: &str) -> usize {
1584            leviath_core::estimate_tokens(text)
1585        }
1586
1587        fn max_context_tokens(&self, _model: &str) -> usize {
1588            100_000
1589        }
1590
1591        fn name(&self) -> &str {
1592            "mock"
1593        }
1594
1595        fn capabilities(&self, _model: &str) -> ModelCapabilities {
1596            ModelCapabilities::default()
1597        }
1598
1599        async fn list_models(&self) -> Result<Vec<ModelInfo>, leviath_providers::ProviderError> {
1600            if self.fail {
1601                Err(leviath_providers::ProviderError::Other(
1602                    "mock provider failure".to_string(),
1603                ))
1604            } else {
1605                Ok(self.models.clone())
1606            }
1607        }
1608    }
1609
1610    fn mock_registry(
1611        provider_name: &'static str,
1612        models: Vec<ModelInfo>,
1613        fail: bool,
1614    ) -> impl Fn(&Config) -> Result<leviath_runtime::ProviderRegistry, leviath_providers::ProviderError>
1615    {
1616        // `Fn` (not `FnOnce`) so the closure can be called through the
1617        // `&dyn Fn` trait object `list_with_registry`/`show_with_registry`
1618        // now take - see the doc comment on `list_with_registry` for why.
1619        // Only ever actually invoked once per test, but `Fn`'s "may be
1620        // called more than once" contract means captured state can't be
1621        // moved out on each call, hence the clone.
1622        move |_config: &Config| {
1623            let mut registry = leviath_runtime::ProviderRegistry::new();
1624            registry.register(
1625                provider_name.to_string(),
1626                std::sync::Arc::new(MockProvider {
1627                    models: models.clone(),
1628                    fail,
1629                }),
1630            );
1631            Ok(registry)
1632        }
1633    }
1634
1635    #[tokio::test]
1636    async fn list_remote_merges_new_model_from_provider() {
1637        crate::config::with_isolated_config_path_async(
1638            "models-list_remote_merges_new_model_from_provider",
1639            |_fake_dir| async move {
1640                let args = ListArgs {
1641                    remote: true,
1642                    provider: Some("mock".to_string()),
1643                    all: false,
1644                    json: false,
1645                };
1646                let new_model = ModelInfo {
1647                    id: "mock-brand-new-model".to_string(),
1648                    display_name: Some("Mock Brand New Model".to_string()),
1649                    provider: "mock".to_string(),
1650                    capabilities: ModelCapabilities::default(),
1651                };
1652                let result =
1653                    list_with_registry(args, &mock_registry("mock", vec![new_model], false)).await;
1654                assert!(result.is_ok());
1655            },
1656        )
1657        .await;
1658    }
1659
1660    #[tokio::test]
1661    async fn list_remote_without_provider_filter_queries_all_providers() {
1662        // No `--provider` filter set: every provider in the registry should
1663        // be queried for remote models (the `if let Some(ref filter) = ...`
1664        // pattern-doesn't-match arm, never exercised by the other
1665        // `list_remote_*` tests below, which all pass a provider filter).
1666        crate::config::with_isolated_config_path_async(
1667            "models-list_remote_without_provider_filter_queries_all_providers",
1668            |_fake_dir| async move {
1669                let args = ListArgs {
1670                    remote: true,
1671                    provider: None,
1672                    all: false,
1673                    json: false,
1674                };
1675                let new_model = ModelInfo {
1676                    id: "mock-brand-new-model".to_string(),
1677                    display_name: Some("Mock Brand New Model".to_string()),
1678                    provider: "mock".to_string(),
1679                    capabilities: ModelCapabilities::default(),
1680                };
1681                let result =
1682                    list_with_registry(args, &mock_registry("mock", vec![new_model], false)).await;
1683                assert!(result.is_ok());
1684            },
1685        )
1686        .await;
1687    }
1688
1689    #[tokio::test]
1690    async fn list_remote_overrides_builtin_entry_with_same_id() {
1691        crate::config::with_isolated_config_path_async(
1692            "models-list_remote_overrides_builtin_entry_with_same_id",
1693            |_fake_dir| async move {
1694                let known_id = builtin_table()[0].model_id.to_string();
1695                let args = ListArgs {
1696                    remote: true,
1697                    provider: Some("mock".to_string()),
1698                    all: false,
1699                    json: false,
1700                };
1701                let overriding_model = ModelInfo {
1702                    id: known_id,
1703                    display_name: Some("Overridden".to_string()),
1704                    provider: "mock".to_string(),
1705                    capabilities: ModelCapabilities::default(),
1706                };
1707                let result =
1708                    list_with_registry(args, &mock_registry("mock", vec![overriding_model], false))
1709                        .await;
1710                assert!(result.is_ok());
1711            },
1712        )
1713        .await;
1714    }
1715
1716    /// Only models the install can reach are listed: a registry holding just
1717    /// `anthropic` must not print google/openai/openrouter rows. Before this,
1718    /// a user with one key scrolled past dozens of models they could not run,
1719    /// with no way to tell whether their own key had registered.
1720    #[tokio::test]
1721    async fn list_shows_only_providers_the_install_has_credentials_for() {
1722        crate::config::with_isolated_config_path_async(
1723            "models-list_only_available",
1724            |_fake_dir| async move {
1725                let args = ListArgs {
1726                    remote: false,
1727                    provider: None,
1728                    all: false,
1729                    json: false,
1730                };
1731                // A registry with exactly one provider that the builtin table
1732                // also knows: its rows survive, everything else is filtered.
1733                let result =
1734                    list_with_registry(args, &mock_registry("anthropic", vec![], false)).await;
1735                assert!(result.is_ok());
1736            },
1737        )
1738        .await;
1739    }
1740
1741    /// A remote fetch that returns a model id the builtin table already lists
1742    /// replaces that row (remote wins), which requires the row to have survived
1743    /// the availability filter.
1744    #[tokio::test]
1745    async fn list_remote_overrides_a_builtin_entry_with_the_same_id() {
1746        crate::config::with_isolated_config_path_async(
1747            "models-list_remote_override",
1748            |_fake_dir| async move {
1749                let remote = vec![ModelInfo {
1750                    id: "claude-sonnet-5".to_string(),
1751                    display_name: Some("Claude Sonnet 5 (remote)".to_string()),
1752                    provider: "anthropic".to_string(),
1753                    capabilities: leviath_providers::ModelCapabilities::default(),
1754                }];
1755                let args = ListArgs {
1756                    remote: true,
1757                    provider: None,
1758                    all: false,
1759                    json: false,
1760                };
1761                let result =
1762                    list_with_registry(args, &mock_registry("anthropic", remote, false)).await;
1763                assert!(result.is_ok());
1764            },
1765        )
1766        .await;
1767    }
1768
1769    /// `--all` restores the full catalogue for shopping around before choosing
1770    /// a provider.
1771    #[tokio::test]
1772    async fn list_all_includes_providers_without_credentials() {
1773        crate::config::with_isolated_config_path_async(
1774            "models-list_all_includes_everything",
1775            |_fake_dir| async move {
1776                let args = ListArgs {
1777                    remote: false,
1778                    provider: None,
1779                    all: true,
1780                    json: false,
1781                };
1782                let result = list_with_registry(args, &mock_registry("mock", vec![], false)).await;
1783                assert!(result.is_ok());
1784            },
1785        )
1786        .await;
1787    }
1788
1789    /// A capability override marks its row with `*`, which requires the row to
1790    /// survive the availability filter first.
1791    #[tokio::test]
1792    async fn list_marks_overridden_capabilities_for_an_available_provider() {
1793        crate::config::with_isolated_config_path_async(
1794            "models-list_overridden_available",
1795            |_fake_dir| async move {
1796                let mut config = Config::default();
1797                config.model_capabilities.insert(
1798                    "claude-sonnet-5".to_string(),
1799                    leviath_providers::ModelCapabilityOverride::default(),
1800                );
1801                config
1802                    .save_to_path(&Config::config_path())
1803                    .expect("the isolated config path is writable");
1804                let args = ListArgs {
1805                    remote: false,
1806                    provider: None,
1807                    all: false,
1808                    json: false,
1809                };
1810                let result =
1811                    list_with_registry(args, &mock_registry("anthropic", vec![], false)).await;
1812                assert!(result.is_ok());
1813            },
1814        )
1815        .await;
1816    }
1817
1818    #[tokio::test]
1819    async fn list_remote_provider_error_warns_and_continues() {
1820        crate::config::with_isolated_config_path_async(
1821            "models-list_remote_provider_error_warns_and_continues",
1822            |_fake_dir| async move {
1823                let args = ListArgs {
1824                    remote: true,
1825                    provider: Some("mock".to_string()),
1826                    all: false,
1827                    json: false,
1828                };
1829                let result = list_with_registry(args, &mock_registry("mock", vec![], true)).await;
1830                assert!(result.is_ok());
1831            },
1832        )
1833        .await;
1834    }
1835
1836    #[tokio::test]
1837    async fn list_remote_skips_providers_not_matching_filter() {
1838        crate::config::with_isolated_config_path_async(
1839            "models-list_remote_skips_providers_not_matching_filter",
1840            |_fake_dir| async move {
1841                // provider filter is "mock-other", but the registry only has "mock"
1842                // registered -> the `if filter != provider_name { continue; }`
1843                // branch is exercised, and the mock is never queried.
1844                let args = ListArgs {
1845                    remote: true,
1846                    provider: Some("mock-other".to_string()),
1847                    all: false,
1848                    json: false,
1849                };
1850                let result = list_with_registry(args, &mock_registry("mock", vec![], false)).await;
1851                assert!(result.is_ok());
1852            },
1853        )
1854        .await;
1855    }
1856
1857    #[tokio::test]
1858    async fn show_remote_finds_model_from_provider() {
1859        crate::config::with_isolated_config_path_async(
1860            "models-show_remote_finds_model_from_provider",
1861            |_fake_dir| async move {
1862                let args = ShowArgs {
1863                    model: "mock-remote-model".to_string(),
1864                    remote: true,
1865                    provider: Some("mock".to_string()),
1866                };
1867                let remote_model = ModelInfo {
1868                    id: "mock-remote-model".to_string(),
1869                    display_name: Some("Mock Remote Model".to_string()),
1870                    provider: "mock".to_string(),
1871                    capabilities: ModelCapabilities::default(),
1872                };
1873                let result =
1874                    show_with_registry(args, &mock_registry("mock", vec![remote_model], false))
1875                        .await;
1876                assert!(result.is_ok());
1877            },
1878        )
1879        .await;
1880    }
1881
1882    #[tokio::test]
1883    async fn show_remote_model_not_found_in_provider_list_falls_through() {
1884        crate::config::with_isolated_config_path_async(
1885            "models-show_remote_model_not_found_in_provider_list_falls_through",
1886            |_fake_dir| async move {
1887                let args = ShowArgs {
1888                    model: "totally-unknown-model-xyz".to_string(),
1889                    remote: true,
1890                    provider: Some("mock".to_string()),
1891                };
1892                let result = show_with_registry(args, &mock_registry("mock", vec![], false)).await;
1893                assert!(result.is_ok());
1894            },
1895        )
1896        .await;
1897    }
1898
1899    #[tokio::test]
1900    async fn show_remote_provider_error_warns_and_falls_through() {
1901        crate::config::with_isolated_config_path_async(
1902            "models-show_remote_provider_error_warns_and_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![], true)).await;
1910                assert!(result.is_ok());
1911            },
1912        )
1913        .await;
1914    }
1915
1916    #[tokio::test]
1917    async fn show_remote_unconfigured_provider_warns_and_falls_through() {
1918        crate::config::with_isolated_config_path_async(
1919            "models-show_remote_unconfigured_provider_warns_and_falls_through",
1920            |_fake_dir| async move {
1921                // provider filter names a provider that isn't in the registry at all
1922                // -> the `if let Some(provider) = registry.get(...)` else branch.
1923                let args = ShowArgs {
1924                    model: "totally-unknown-model-xyz".to_string(),
1925                    remote: true,
1926                    provider: Some("nonexistent-provider".to_string()),
1927                };
1928                let result = show_with_registry(args, &mock_registry("mock", vec![], false)).await;
1929                assert!(result.is_ok());
1930            },
1931        )
1932        .await;
1933    }
1934
1935    // ─── validate_keys() warnings + [model_capabilities] overrides ─────────
1936    //
1937    // `list_with_registry`/`show_with_registry` take an injectable registry
1938    // builder, so a malformed API key in the isolated test config can safely
1939    // exercise the `validate_keys()` warning-print branch without the
1940    // registry ever actually using that key (the mock registry below ignores
1941    // `_config` entirely).
1942
1943    #[tokio::test]
1944    async fn list_prints_warning_and_applies_model_capabilities_override() {
1945        crate::config::with_isolated_config_path_async(
1946            "models-list-override",
1947            |_fake_dir| async move {
1948                let known_id = builtin_table()[0].model_id.to_string();
1949                let mut fake_config = Config::default();
1950                fake_config.providers.anthropic_api_key = Some("not-a-real-key".to_string());
1951                fake_config.model_capabilities.insert(
1952                    known_id,
1953                    ModelCapabilities {
1954                        supports_temperature: false,
1955                        supports_streaming: false,
1956                        supports_tools: false,
1957                        supports_system_prompt: false,
1958                        max_context_tokens: 1,
1959                        max_output_tokens: 1,
1960                    }
1961                    .into(),
1962                );
1963                std::fs::write(
1964                    Config::config_path(),
1965                    toml::to_string(&fake_config).unwrap(),
1966                )
1967                .unwrap();
1968
1969                let args = ListArgs {
1970                    remote: false,
1971                    provider: None,
1972                    all: false,
1973                    json: false,
1974                };
1975                let result = list_with_registry(args, &mock_registry("mock", vec![], false)).await;
1976                assert!(result.is_ok());
1977            },
1978        )
1979        .await;
1980    }
1981
1982    #[tokio::test]
1983    async fn show_prints_warning_and_uses_model_capabilities_override() {
1984        crate::config::with_isolated_config_path_async(
1985            "models-show-override",
1986            |_fake_dir| async move {
1987                let known_id = builtin_table()[0].model_id.to_string();
1988                let mut fake_config = Config::default();
1989                fake_config.providers.anthropic_api_key = Some("not-a-real-key".to_string());
1990                fake_config.model_capabilities.insert(
1991                    known_id.clone(),
1992                    ModelCapabilities {
1993                        supports_temperature: false,
1994                        supports_streaming: false,
1995                        supports_tools: false,
1996                        supports_system_prompt: false,
1997                        max_context_tokens: 1,
1998                        max_output_tokens: 1,
1999                    }
2000                    .into(),
2001                );
2002                std::fs::write(
2003                    Config::config_path(),
2004                    toml::to_string(&fake_config).unwrap(),
2005                )
2006                .unwrap();
2007
2008                let args = ShowArgs {
2009                    model: known_id,
2010                    remote: false,
2011                    provider: None,
2012                };
2013                let result = show_with_registry(args, &mock_registry("mock", vec![], false)).await;
2014                assert!(result.is_ok());
2015            },
2016        )
2017        .await;
2018    }
2019
2020    #[tokio::test]
2021    async fn mock_provider_trivial_trait_methods() {
2022        use leviath_providers::Provider;
2023        let provider = MockProvider {
2024            models: vec![],
2025            fail: false,
2026        };
2027        assert_eq!(provider.count_tokens("abcd", "mock-model").await, 1);
2028        assert_eq!(provider.max_context_tokens("mock-model"), 100_000);
2029        assert_eq!(provider.name(), "mock");
2030        let _ = provider.capabilities("mock-model");
2031    }
2032
2033    #[tokio::test]
2034    async fn mock_provider_infer_returns_err() {
2035        use leviath_providers::Provider;
2036        let provider = MockProvider {
2037            models: vec![],
2038            fail: false,
2039        };
2040        let request = leviath_providers::InferenceRequest {
2041            system: vec![],
2042            messages: vec![],
2043            model: "mock".to_string(),
2044            max_tokens: 100,
2045            temperature: 0.0,
2046            tools: vec![],
2047            extra: serde_json::Value::Null,
2048            request_timeout_secs: None,
2049        };
2050        let result = provider.infer(&request).await;
2051        assert!(result.is_err());
2052    }
2053
2054    #[tokio::test]
2055    async fn list_with_registry_propagates_config_load_error() {
2056        crate::config::with_isolated_config_path_async(
2057            "models-list_with_registry_propagates_config_load_error",
2058            |fake_dir| async move {
2059                std::fs::write(fake_dir.join("config.toml"), "not valid toml [[[").unwrap();
2060                let args = ListArgs {
2061                    remote: false,
2062                    provider: None,
2063                    all: false,
2064                    json: false,
2065                };
2066                let result = list_with_registry(args, &mock_registry("mock", vec![], false)).await;
2067                assert!(result.is_err());
2068            },
2069        )
2070        .await;
2071    }
2072
2073    // ─── CLI argument parsing (clap derive) ────────────────────────────────
2074    //
2075    // `ModelsArgs`/`ModelsCommand`/`ListArgs`/`ShowArgs` only ever get
2076    // constructed as plain struct literals elsewhere in this file's tests,
2077    // which never exercises clap's derive-generated `Args`/`FromArgMatches`
2078    // parsing implementations (`augment_args`, `from_arg_matches`, etc.) --
2079    // those are only reached in production via `main.rs`'s real
2080    // `Cli::parse()`, which isn't part of this crate's `--lib` test target.
2081    // Wrapping `ModelsArgs` in a minimal local `Parser` and driving it
2082    // through `try_parse_from` exercises that derive machinery directly and
2083    // doubles as a real regression test for the actual flag/positional
2084    // contract (short flags, long flags, subcommand names).
2085
2086    use clap::Parser as _;
2087
2088    #[derive(clap::Parser)]
2089    struct TestCli {
2090        #[command(flatten)]
2091        models: ModelsArgs,
2092    }
2093
2094    /// Unwraps the `List` variant, panicking otherwise. A bare `match ... =>
2095    /// panic!(...)` inline in each test would leave that panic arm a
2096    /// permanent 0-hit region in a green suite (it only fires on failure) --
2097    /// extracting it here lets a single `#[should_panic]` test exercise it
2098    /// once, matching the pattern already used in `serve/blueprints.rs`.
2099    fn expect_list(cmd: ModelsCommand) -> ListArgs {
2100        match cmd {
2101            ModelsCommand::List(args) => args,
2102            ModelsCommand::Show(_) => panic!("expected List"),
2103        }
2104    }
2105
2106    #[test]
2107    #[should_panic(expected = "expected List")]
2108    fn expect_list_panics_on_show() {
2109        expect_list(ModelsCommand::Show(ShowArgs {
2110            model: "x".to_string(),
2111            provider: None,
2112            remote: false,
2113        }));
2114    }
2115
2116    /// Unwraps the `Show` variant, panicking otherwise. See [`expect_list`].
2117    fn expect_show(cmd: ModelsCommand) -> ShowArgs {
2118        match cmd {
2119            ModelsCommand::Show(args) => args,
2120            ModelsCommand::List(_) => panic!("expected Show"),
2121        }
2122    }
2123
2124    #[test]
2125    #[should_panic(expected = "expected Show")]
2126    fn expect_show_panics_on_list() {
2127        expect_show(ModelsCommand::List(ListArgs {
2128            provider: None,
2129            remote: false,
2130            all: false,
2131            json: false,
2132        }));
2133    }
2134
2135    #[test]
2136    fn parses_list_with_no_flags() {
2137        let cli = TestCli::try_parse_from(["lev", "list"]).unwrap();
2138        let args = expect_list(cli.models.command);
2139        assert!(args.provider.is_none());
2140        assert!(!args.remote);
2141    }
2142
2143    #[test]
2144    fn parses_list_with_long_flags() {
2145        let cli = TestCli::try_parse_from(["lev", "list", "--provider", "anthropic", "--remote"])
2146            .unwrap();
2147        let args = expect_list(cli.models.command);
2148        assert_eq!(args.provider.as_deref(), Some("anthropic"));
2149        assert!(args.remote);
2150    }
2151
2152    #[test]
2153    fn parses_list_with_short_flags() {
2154        let cli = TestCli::try_parse_from(["lev", "list", "-p", "openai", "-r"]).unwrap();
2155        let args = expect_list(cli.models.command);
2156        assert_eq!(args.provider.as_deref(), Some("openai"));
2157        assert!(args.remote);
2158    }
2159
2160    #[test]
2161    fn parses_show_with_positional_model_and_long_flags() {
2162        let cli = TestCli::try_parse_from([
2163            "lev",
2164            "show",
2165            "claude-sonnet-4-6",
2166            "--provider",
2167            "anthropic",
2168            "--remote",
2169        ])
2170        .unwrap();
2171        let args = expect_show(cli.models.command);
2172        assert_eq!(args.model, "claude-sonnet-4-6");
2173        assert_eq!(args.provider.as_deref(), Some("anthropic"));
2174        assert!(args.remote);
2175    }
2176
2177    #[test]
2178    fn parses_show_with_short_flags() {
2179        let cli =
2180            TestCli::try_parse_from(["lev", "show", "gpt-5.5", "-p", "openai", "-r"]).unwrap();
2181        let args = expect_show(cli.models.command);
2182        assert_eq!(args.model, "gpt-5.5");
2183        assert_eq!(args.provider.as_deref(), Some("openai"));
2184        assert!(args.remote);
2185    }
2186
2187    #[test]
2188    fn parses_show_missing_required_positional_errors() {
2189        let result = TestCli::try_parse_from(["lev", "show"]);
2190        assert!(result.is_err());
2191    }
2192
2193    #[test]
2194    fn parses_unknown_subcommand_errors() {
2195        let result = TestCli::try_parse_from(["lev", "not-a-subcommand"]);
2196        assert!(result.is_err());
2197    }
2198
2199    #[tokio::test]
2200    async fn show_with_registry_propagates_config_load_error() {
2201        crate::config::with_isolated_config_path_async(
2202            "models-show_with_registry_propagates_config_load_error",
2203            |fake_dir| async move {
2204                std::fs::write(fake_dir.join("config.toml"), "not valid toml [[[").unwrap();
2205                let args = ShowArgs {
2206                    model: "any-model".to_string(),
2207                    remote: false,
2208                    provider: None,
2209                };
2210                let result = show_with_registry(args, &mock_registry("mock", vec![], false)).await;
2211                assert!(result.is_err());
2212            },
2213        )
2214        .await;
2215    }
2216
2217    /// A builder that fails the way a machine with no readable root
2218    /// certificate store would.
2219    fn cannot_build(
2220        _config: &Config,
2221    ) -> Result<leviath_runtime::ProviderRegistry, leviath_providers::ProviderError> {
2222        Err(leviath_providers::ProviderError::ClientBuild(
2223            "no roots".to_string(),
2224        ))
2225    }
2226
2227    #[tokio::test]
2228    async fn list_reports_a_registry_that_will_not_build() {
2229        crate::config::with_isolated_config_path_async(
2230            "models-list_reports_a_registry_that_will_not_build",
2231            |_fake_dir| async move {
2232                let args = ListArgs {
2233                    remote: false,
2234                    provider: None,
2235                    all: false,
2236                    json: false,
2237                };
2238                let err = list_with_registry(args, &cannot_build)
2239                    .await
2240                    .expect_err("a failing registry builder should fail the command");
2241                assert!(err.to_string().contains("root certificate store"));
2242            },
2243        )
2244        .await;
2245    }
2246
2247    #[tokio::test]
2248    async fn show_reports_a_registry_that_will_not_build() {
2249        crate::config::with_isolated_config_path_async(
2250            "models-show_reports_a_registry_that_will_not_build",
2251            |_fake_dir| async move {
2252                // Both `remote` and `provider`: the registry is only built
2253                // when the two are given together.
2254                let args = ShowArgs {
2255                    // A model the built-in table does not know, so the lookup
2256                    // falls through to the registry instead of returning early.
2257                    model: "not-a-built-in-model".to_string(),
2258                    provider: Some("anthropic".to_string()),
2259                    remote: true,
2260                };
2261                let err = show_with_registry(args, &cannot_build)
2262                    .await
2263                    .expect_err("a failing registry builder should fail the command");
2264                assert!(err.to_string().contains("root certificate store"));
2265            },
2266        )
2267        .await;
2268    }
2269}