1use colored::Colorize;
6use nika_engine::error::NikaError;
7use nika_engine::provider::cost::{list_provider_models, ModelPricing, ProviderKind};
8
9struct 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
54pub 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 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
126pub fn print_model_info(model_name: &str) -> Result<(), NikaError> {
128 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
165pub 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 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 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 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 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}