Skip to main content

nika_cli/
model_cloud.rs

1//! Cloud model listing — always available (no native-inference feature required).
2//!
3//! Shows all cloud LLM models with pricing, grouped by provider.
4
5use colored::Colorize;
6use nika_engine::error::NikaError;
7use nika_engine::provider::cost::{list_provider_models, ModelPricing, ProviderKind};
8
9/// Provider display info.
10struct ProviderDisplay {
11    name: &'static str,
12    kind: ProviderKind,
13    env_var: &'static str,
14}
15
16const CLOUD_PROVIDERS: &[ProviderDisplay] = &[
17    ProviderDisplay {
18        name: "ANTHROPIC",
19        kind: ProviderKind::Claude,
20        env_var: "ANTHROPIC_API_KEY",
21    },
22    ProviderDisplay {
23        name: "OPENAI",
24        kind: ProviderKind::OpenAI,
25        env_var: "OPENAI_API_KEY",
26    },
27    ProviderDisplay {
28        name: "MISTRAL",
29        kind: ProviderKind::Mistral,
30        env_var: "MISTRAL_API_KEY",
31    },
32    ProviderDisplay {
33        name: "GROQ",
34        kind: ProviderKind::Groq,
35        env_var: "GROQ_API_KEY",
36    },
37    ProviderDisplay {
38        name: "DEEPSEEK",
39        kind: ProviderKind::DeepSeek,
40        env_var: "DEEPSEEK_API_KEY",
41    },
42    ProviderDisplay {
43        name: "GEMINI",
44        kind: ProviderKind::Gemini,
45        env_var: "GEMINI_API_KEY",
46    },
47    ProviderDisplay {
48        name: "XAI",
49        kind: ProviderKind::XAi,
50        env_var: "XAI_API_KEY",
51    },
52];
53
54/// Display all cloud models with pricing, grouped by provider.
55pub fn print_cloud_models(filter_provider: Option<&str>) -> Result<(), NikaError> {
56    println!();
57    println!(
58        "  {}{}",
59        "Available Models".bold(),
60        "                          input / output per M tokens".dimmed()
61    );
62    println!("  {}", "─".repeat(62));
63
64    let mut any_shown = false;
65
66    for p in CLOUD_PROVIDERS {
67        // Filter by provider if specified
68        if let Some(filter) = filter_provider {
69            if !p.name.eq_ignore_ascii_case(filter) {
70                continue;
71            }
72        }
73
74        let has_key = std::env::var(p.env_var).is_ok_and(|v| !v.trim().is_empty());
75        let status = if has_key {
76            format!("{} key set", "✓".green())
77        } else {
78            format!("{} no key", "✗".red())
79        };
80
81        let models = list_provider_models(p.kind);
82        if models.is_empty() {
83            continue;
84        }
85
86        println!();
87        println!("  {} ({})", p.name.bold(), status);
88
89        if !has_key {
90            println!(
91                "  {} nika provider set {}",
92                "→".dimmed(),
93                p.name.to_lowercase().dimmed()
94            );
95        } else {
96            for (i, (name, pricing)) in models.iter().enumerate() {
97                let is_last = i == models.len() - 1;
98                let prefix = if is_last { "└──" } else { "├──" };
99                println!(
100                    "  {} {:<30} ${:.2} / ${:.2}",
101                    prefix.dimmed(),
102                    name.cyan(),
103                    pricing.input_per_million,
104                    pricing.output_per_million,
105                );
106            }
107        }
108        any_shown = true;
109    }
110
111    if !any_shown {
112        println!("  No providers matched filter.");
113    }
114
115    println!();
116    println!("  {} nika infer \"...\" -m <model>", "Use:".dimmed());
117    println!(
118        "  {} nika config set default_model <model>",
119        "Default:".dimmed()
120    );
121    println!();
122
123    Ok(())
124}
125
126/// Show detailed info for a specific cloud model.
127pub fn print_model_info(model_name: &str) -> Result<(), NikaError> {
128    // Search all providers for this model
129    for p in CLOUD_PROVIDERS {
130        let models = list_provider_models(p.kind);
131        for (name, pricing) in &models {
132            if *name == model_name {
133                let has_key = std::env::var(p.env_var).is_ok_and(|v| !v.trim().is_empty());
134                println!();
135                println!("  {} ({})", name.bold().cyan(), p.name);
136                println!("  {}", "─".repeat(40));
137                println!("  Provider:  {}", p.name.to_lowercase());
138                println!(
139                    "  Pricing:   ${:.2} input / ${:.2} output per M tokens",
140                    pricing.input_per_million, pricing.output_per_million
141                );
142                println!(
143                    "  Status:    {}",
144                    if has_key {
145                        "✓ API key available".green().to_string()
146                    } else {
147                        format!("✗ Set key: nika provider set {}", p.name.to_lowercase())
148                            .red()
149                            .to_string()
150                    }
151                );
152                println!();
153                println!("  Use: nika infer \"...\" -m {}", name);
154                println!();
155                return Ok(());
156            }
157        }
158    }
159
160    Err(NikaError::ValidationError {
161        reason: format!("Model '{}' not found. Run: nika model list", model_name),
162    })
163}
164
165/// Smart model recommendation based on available API keys.
166pub fn print_model_recommend() -> Result<(), NikaError> {
167    println!();
168    println!("  {}", "Model Recommendation".bold());
169    println!("  {}", "─".repeat(40));
170
171    let mut available: Vec<(&str, &str, ModelPricing)> = Vec::new();
172
173    for p in CLOUD_PROVIDERS {
174        let has_key = std::env::var(p.env_var).is_ok_and(|v| !v.trim().is_empty());
175        if !has_key {
176            continue;
177        }
178        let models = list_provider_models(p.kind);
179        for (name, pricing) in models {
180            available.push((name, p.name, pricing));
181        }
182    }
183
184    if available.is_empty() {
185        println!("  No API keys configured.");
186        println!("  Run: nika provider set <provider>");
187        println!();
188        return Ok(());
189    }
190
191    // Sort by output cost (most representative for recommendation)
192    available.sort_by(|(_, _, a), (_, _, b)| {
193        a.output_per_million
194            .partial_cmp(&b.output_per_million)
195            .unwrap_or(std::cmp::Ordering::Equal)
196    });
197
198    // Budget: cheapest
199    if let Some((name, provider, pricing)) = available.first() {
200        println!(
201            "  {} {:<28} ${:.2}/${:.2}  [{}]",
202            "⚡ Budget:".yellow(),
203            name.cyan(),
204            pricing.input_per_million,
205            pricing.output_per_million,
206            provider.to_lowercase().dimmed()
207        );
208    }
209
210    // Quality: most expensive
211    if let Some((name, provider, pricing)) = available.last() {
212        println!(
213            "  {} {:<28} ${:.2}/${:.2}  [{}]",
214            "🏆 Quality:".bold(),
215            name.cyan(),
216            pricing.input_per_million,
217            pricing.output_per_million,
218            provider.to_lowercase().dimmed()
219        );
220    }
221
222    // Balanced: mid-range (prefer sonnet/gpt-4o)
223    let balanced = available
224        .iter()
225        .find(|(name, _, _)| name.contains("sonnet") || *name == "gpt-4o")
226        .or_else(|| available.get(available.len() / 2));
227    if let Some((name, provider, pricing)) = balanced {
228        println!(
229            "  {} {:<28} ${:.2}/${:.2}  [{}]",
230            "★ Balanced:".green(),
231            name.cyan(),
232            pricing.input_per_million,
233            pricing.output_per_million,
234            provider.to_lowercase().dimmed()
235        );
236    }
237
238    println!();
239    println!("  Set default: nika config set default_model <model>");
240    println!();
241
242    Ok(())
243}