1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
//! Provider management subcommand handler
use clap::Subcommand;
use nika::error::NikaError;
/// Provider management actions
#[derive(Subcommand)]
pub enum ProviderAction {
/// List all providers and their status
List,
/// Set API key for a provider (stored in system keychain)
Set {
/// Provider name (anthropic, openai, mistral, groq, deepseek)
provider: String,
/// API key (or use --prompt to enter interactively)
key: Option<String>,
/// Prompt for key interactively (hidden input)
#[arg(short, long)]
prompt: bool,
},
/// Get API key for a provider (masked for security)
Get {
/// Provider name
provider: String,
},
/// Delete API key for a provider
Delete {
/// Provider name
provider: String,
},
/// Migrate API keys from environment variables to keychain
Migrate,
/// Test connection to a provider
Test {
/// Provider name
provider: String,
},
}
/// Get all LLM provider IDs (from nika::core::KNOWN_PROVIDERS).
fn llm_provider_ids() -> Vec<&'static str> {
use nika::core::{ProviderCategory, KNOWN_PROVIDERS};
KNOWN_PROVIDERS
.iter()
.filter(|p| p.category == ProviderCategory::Llm)
.map(|p| p.id)
.collect()
}
/// Handle provider management commands
pub async fn handle_provider_command(action: ProviderAction) -> Result<(), NikaError> {
use colored::Colorize;
use nika::core::provider_to_env_var;
use nika::secrets::{mask_api_key, migrate_env_to_keyring, validate_key_format, NikaKeyring};
use std::io::{self, Write};
// Get LLM provider IDs from nika::core
let all_providers = llm_provider_ids();
match action {
ProviderAction::List => {
println!("{}", "LLM Providers".bold());
println!("{}", "─".repeat(60));
for provider in &all_providers {
let env_var = provider_to_env_var(provider).unwrap_or("UNKNOWN_API_KEY");
let has_keychain = NikaKeyring::exists(provider);
let has_env = std::env::var(env_var).is_ok();
let status = match (has_keychain, has_env) {
(true, true) => format!("{} (keychain + env)", "✓".green()),
(true, false) => format!("{} (keychain)", "✓".green()),
(false, true) => format!("{} (env only)", "~".yellow()),
(false, false) => format!("{}", "✗".red()),
};
let masked = if has_keychain {
NikaKeyring::get_masked(provider).unwrap_or_default()
} else if has_env {
std::env::var(env_var)
.ok()
.map(|k| mask_api_key(&k))
.unwrap_or_default()
} else {
String::new()
};
println!(
" {:12} {} {}",
provider,
status,
if masked.is_empty() {
String::new()
} else {
format!("[{}]", masked.dimmed())
}
);
}
println!();
println!(
"{}",
"Use 'nika provider set <name>' to add an API key".dimmed()
);
Ok(())
}
ProviderAction::Set {
provider,
key,
prompt,
} => {
// Validate provider name using nika::core
if !all_providers.contains(&provider.as_str()) {
return Err(NikaError::ValidationError {
reason: format!(
"Unknown provider '{}'. Valid: {}",
provider,
all_providers.join(", ")
),
});
}
// Get key from argument or prompt
let api_key = match (prompt, key) {
// If prompt flag is set or no key provided, read from stdin
(true, _) | (false, None) => {
print!("Enter API key for {}: ", provider);
let _ = io::stdout().flush();
let mut input = String::new();
io::stdin().read_line(&mut input).map_err(|e| {
NikaError::Execution(format!("Failed to read input: {}", e))
})?;
input.trim().to_string()
}
// Key provided as argument
(false, Some(k)) => k,
};
// Validate key format
if let Err(e) = validate_key_format(&provider, &api_key) {
return Err(NikaError::ValidationError { reason: e });
}
// Store in keychain
NikaKeyring::set(&provider, &api_key)
.map_err(|e| NikaError::Execution(format!("Failed to store key: {}", e)))?;
println!(
"{} API key for {} stored in system keychain",
"✓".green(),
provider.bold()
);
Ok(())
}
ProviderAction::Get { provider } => {
match NikaKeyring::get_masked(&provider) {
Some(masked) => {
println!("{}: {}", provider, masked);
}
None => {
let env_var = provider_to_env_var(&provider).unwrap_or("UNKNOWN_API_KEY");
match std::env::var(env_var) {
Ok(key) => {
println!("{}: {} (from env)", provider, mask_api_key(&key));
}
Err(_) => {
println!("{}: {}", provider, "Not configured".red());
}
}
}
}
Ok(())
}
ProviderAction::Delete { provider } => {
match NikaKeyring::delete(&provider) {
Ok(()) => {
println!(
"{} API key for {} deleted from keychain",
"✓".green(),
provider.bold()
);
}
Err(e) => {
return Err(NikaError::Execution(format!("Failed to delete key: {}", e)));
}
}
Ok(())
}
ProviderAction::Migrate => {
println!(
"{}",
"Migrating API keys from environment variables...".cyan()
);
let report = migrate_env_to_keyring();
println!();
println!("{}", report.summary());
Ok(())
}
ProviderAction::Test { provider } => {
println!("Testing connection to {}...", provider.bold());
// First check if API key is configured
let env_var = provider_to_env_var(&provider).unwrap_or("UNKNOWN_API_KEY");
let has_key = NikaKeyring::exists(&provider)
|| std::env::var(env_var).is_ok_and(|v| !v.is_empty());
if !has_key && provider != "native" {
println!(
"{} No API key configured for {}",
"✗".red(),
provider.bold()
);
println!(" Use 'nika provider set {}' to add your API key", provider);
return Ok(());
}
// Try to create provider and make a simple request
use nika::provider::rig::RigProvider;
let prov = match provider.as_str() {
"anthropic" => RigProvider::claude(),
"openai" => RigProvider::openai(),
"mistral" => RigProvider::mistral(),
"groq" => RigProvider::groq(),
"deepseek" => RigProvider::deepseek(),
"gemini" => RigProvider::gemini(),
"xai" => RigProvider::xai(),
"native" => {
#[cfg(feature = "native-inference")]
{
RigProvider::native()
}
#[cfg(not(feature = "native-inference"))]
{
return Err(NikaError::ValidationError {
reason: "Native inference requires --features native-inference"
.to_string(),
});
}
}
_ => {
return Err(NikaError::ValidationError {
reason: format!("Unknown provider: {}", provider),
})
}
};
// Simple test inference
match prov.infer("Say 'OK' if you can hear me.", None).await {
Ok(response) => {
println!("{} Connection successful!", "✓".green());
let truncated: String = response.chars().take(100).collect();
println!(" Response: {}", truncated);
}
Err(e) => {
println!("{} Connection failed: {}", "✗".red(), e);
}
}
Ok(())
}
}
}