1use axum::{extract::State, http::StatusCode, response::Json};
2use serde::{Deserialize, Serialize};
3use serde_json::json;
4use tracing::{info, error};
5use std::sync::atomic::{AtomicU64, Ordering};
6
7use crate::api::AppState;
8
9fn mask_api_key(api_key: &str) -> String {
11 if api_key.len() <= 8 {
12 "*".repeat(api_key.len())
13 } else {
14 format!("{}***{}", &api_key[..4], &api_key[api_key.len()-4..])
15 }
16}
17
18fn validate_api_key(provider: &str, api_key: &str) -> Result<(), String> {
20 if api_key.trim().is_empty() {
21 return Err("API key cannot be empty".to_string());
22 }
23
24 match provider {
25 "openai" => {
26 if !api_key.starts_with("sk-") {
27 return Err("OpenAI API key should start with 'sk-'".to_string());
28 }
29 }
30 "anthropic" => {
31 if !api_key.starts_with("sk-ant-") {
32 return Err("Anthropic API key should start with 'sk-ant-'".to_string());
33 }
34 }
35 "zhipu" => {
36 if api_key.len() < 10 {
38 return Err("Zhipu API key seems too short".to_string());
39 }
40 }
41 "ollama" => {
42 }
44 _ => {
45 if api_key.len() < 10 {
47 return Err("API key seems too short".to_string());
48 }
49 }
50 }
51
52 Ok(())
53}
54
55fn validate_provider(provider: &str) -> Result<(), String> {
57 match provider {
58 "openai" | "anthropic" | "zhipu" | "ollama" | "aliyun" | "volcengine" | "tencent" => Ok(()),
59 _ => Err(format!("Unsupported provider: {}", provider)),
60 }
61}
62
63static INSTANCE_ID: AtomicU64 = AtomicU64::new(0);
65
66pub fn init_instance_id() {
67 use std::time::{SystemTime, UNIX_EPOCH};
68 let timestamp = SystemTime::now()
69 .duration_since(UNIX_EPOCH)
70 .unwrap()
71 .as_secs();
72 INSTANCE_ID.store(timestamp, Ordering::SeqCst);
73}
74
75pub fn get_instance_id() -> u64 {
76 INSTANCE_ID.load(Ordering::SeqCst)
77}
78
79#[derive(Debug, Deserialize)]
80pub struct UpdateConfigRequest {
81 pub provider: String,
82 pub api_key: String,
83 #[serde(default)]
84 pub model: Option<String>,
85 #[serde(default)]
86 pub base_url: Option<String>,
87}
88
89#[derive(Debug, Deserialize)]
90pub struct UpdateKeyRequest {
91 pub provider: String,
92 pub api_key: String,
93 #[serde(default)]
94 pub base_url: Option<String>,
95}
96
97#[derive(Debug, Deserialize)]
98pub struct SwitchProviderRequest {
99 pub provider: String,
100 #[serde(default)]
101 pub model: Option<String>,
102 #[serde(default)]
103 pub api_key: Option<String>,
104 #[serde(default)]
105 pub base_url: Option<String>,
106}
107
108#[derive(Debug, Serialize)]
109pub struct CurrentConfigResponse {
110 pub provider: String,
111 pub model: String,
112 pub has_api_key: bool,
113 pub has_base_url: bool,
114 pub supports_hot_reload: bool,
115}
116
117pub async fn get_current_config(
119 State(state): State<AppState>,
120) -> Result<Json<CurrentConfigResponse>, StatusCode> {
121 use crate::settings::LlmBackendSettings;
122
123 let config = state.config.read().unwrap();
124 let (provider, model, has_api_key, has_base_url) = match &config.llm_backend {
125 LlmBackendSettings::OpenAI { model, base_url, .. } => {
126 ("openai", model.clone(), true, base_url.is_some())
127 }
128 LlmBackendSettings::Anthropic { model, .. } => {
129 ("anthropic", model.clone(), true, false)
130 }
131 LlmBackendSettings::Zhipu { model, base_url, .. } => {
132 ("zhipu", model.clone(), true, base_url.is_some())
133 }
134 LlmBackendSettings::Ollama { model, base_url } => {
135 ("ollama", model.clone(), false, base_url.is_some())
136 }
137 LlmBackendSettings::Aliyun { model, .. } => {
138 ("aliyun", model.clone(), true, false)
139 }
140 LlmBackendSettings::Volcengine { model, .. } => {
141 ("volcengine", model.clone(), true, false)
142 }
143 LlmBackendSettings::Tencent { model, .. } => {
144 ("tencent", model.clone(), true, false)
145 }
146 };
147
148 Ok(Json(CurrentConfigResponse {
149 provider: provider.to_string(),
150 model,
151 has_api_key,
152 has_base_url,
153 supports_hot_reload: true, }))
155}
156
157pub async fn get_health(
161 State(state): State<AppState>,
162) -> Json<serde_json::Value> {
163 use crate::settings::LlmBackendSettings;
164
165 let config = state.config.read().unwrap();
166 let (provider, model) = match &config.llm_backend {
167 LlmBackendSettings::OpenAI { model, .. } => ("openai", model.clone()),
168 LlmBackendSettings::Anthropic { model, .. } => ("anthropic", model.clone()),
169 LlmBackendSettings::Zhipu { model, .. } => ("zhipu", model.clone()),
170 LlmBackendSettings::Ollama { model, .. } => ("ollama", model.clone()),
171 LlmBackendSettings::Aliyun { model, .. } => ("aliyun", model.clone()),
172 LlmBackendSettings::Volcengine { model, .. } => ("volcengine", model.clone()),
173 LlmBackendSettings::Tencent { model, .. } => ("tencent", model.clone()),
174 };
175
176 Json(json!({
177 "status": "ok",
178 "instance_id": get_instance_id(),
179 "pid": std::process::id(),
180 "provider": provider,
181 "model": model,
182 }))
183}
184
185pub async fn update_config_for_restart(
196 State(_state): State<AppState>,
197 Json(request): Json<UpdateConfigRequest>,
198) -> Result<Json<serde_json::Value>, StatusCode> {
199 info!("🔧 Preparing config update for provider: {}", request.provider);
200
201 let default_model = request.model.clone().or_else(|| {
203 match request.provider.as_str() {
204 "openai" => Some("gpt-4o".to_string()),
205 "anthropic" => Some("claude-3-5-sonnet-20241022".to_string()),
206 "zhipu" => Some("glm-4-flash".to_string()),
207 "ollama" => Some("llama2".to_string()),
208 "aliyun" => Some("qwen-turbo".to_string()),
209 "volcengine" => Some("ep-20241023xxxxx-xxxxx".to_string()),
210 "tencent" => Some("hunyuan-lite".to_string()),
211 _ => None,
212 }
213 });
214
215 let model = match default_model {
216 Some(m) => m,
217 None => {
218 error!("❌ Unknown provider: {}", request.provider);
219 return Err(StatusCode::BAD_REQUEST);
220 }
221 };
222
223 let mut env_vars = serde_json::Map::new();
225
226 let api_key_var = match request.provider.as_str() {
228 "openai" => "OPENAI_API_KEY",
229 "anthropic" => "ANTHROPIC_API_KEY",
230 "zhipu" => "ZHIPU_API_KEY",
231 "aliyun" => "ALIYUN_API_KEY",
232 "volcengine" => "VOLCENGINE_API_KEY",
233 "tencent" => "TENCENT_API_KEY",
234 "ollama" => "", _ => return Err(StatusCode::BAD_REQUEST),
236 };
237
238 if !api_key_var.is_empty() {
239 env_vars.insert(api_key_var.to_string(), json!(request.api_key));
240 }
241
242 if let Some(base_url) = request.base_url {
244 let base_url_var = match request.provider.as_str() {
245 "openai" => "OPENAI_BASE_URL",
246 "zhipu" => "ZHIPU_BASE_URL",
247 "ollama" => "OLLAMA_BASE_URL",
248 _ => "",
249 };
250 if !base_url_var.is_empty() {
251 env_vars.insert(base_url_var.to_string(), json!(base_url));
252 }
253 }
254
255 info!("✅ Config prepared for restart with provider: {}", request.provider);
256
257 Ok(Json(json!({
258 "status": "success",
259 "message": format!("Config prepared for provider: {}", request.provider),
260 "restart_required": true,
261 "current_instance_id": get_instance_id(),
262 "env_vars": env_vars,
263 "cli_args": {
264 "provider": request.provider,
265 "model": model,
266 }
267 })))
268}
269
270pub async fn validate_key(
274 State(_state): State<AppState>,
275 Json(request): Json<UpdateConfigRequest>,
276) -> Result<Json<serde_json::Value>, StatusCode> {
277 use crate::settings::LlmBackendSettings;
278 use crate::service::Service;
279
280 info!("🔍 Validating API key for provider: {} (key: {})", request.provider, mask_api_key(&request.api_key));
281
282 let model = request.model.clone().unwrap_or_else(|| "test-model".to_string());
284
285 let test_backend = match request.provider.as_str() {
286 "openai" => LlmBackendSettings::OpenAI {
287 api_key: request.api_key.clone(),
288 base_url: request.base_url.clone(),
289 model,
290 },
291 "anthropic" => LlmBackendSettings::Anthropic {
292 api_key: request.api_key.clone(),
293 model,
294 },
295 "zhipu" => LlmBackendSettings::Zhipu {
296 api_key: request.api_key.clone(),
297 base_url: request.base_url.clone(),
298 model,
299 },
300 "ollama" => LlmBackendSettings::Ollama {
301 base_url: request.base_url.clone(),
302 model,
303 },
304 "aliyun" => LlmBackendSettings::Aliyun {
305 api_key: request.api_key.clone(),
306 model,
307 },
308 "volcengine" => LlmBackendSettings::Volcengine {
309 api_key: request.api_key.clone(),
310 model,
311 },
312 "tencent" => LlmBackendSettings::Tencent {
313 api_key: request.api_key.clone(),
314 model,
315 },
316 _ => {
317 error!("❌ Unsupported provider: {}", request.provider);
318 return Err(StatusCode::BAD_REQUEST);
319 }
320 };
321
322 match Service::new(&test_backend) {
324 Ok(service) => {
325 match service.list_models().await {
326 Ok(models) => {
327 info!("✅ API key validated successfully, found {} models", models.len());
328 Ok(Json(json!({
329 "status": "valid",
330 "message": "API key is valid",
331 "models": models.iter().map(|m| &m.id).collect::<Vec<_>>(),
332 })))
333 }
334 Err(e) => {
335 error!("❌ API key validation failed: {:?}", e);
336 Ok(Json(json!({
337 "status": "invalid",
338 "message": format!("Failed to list models: {}", e),
339 })))
340 }
341 }
342 }
343 Err(e) => {
344 error!("❌ Failed to create service: {:?}", e);
345 Ok(Json(json!({
346 "status": "error",
347 "message": format!("Failed to create service: {}", e),
348 })))
349 }
350 }
351}
352
353pub async fn get_pid() -> Json<serde_json::Value> {
357 let pid = std::process::id();
358
359 Json(json!({
360 "pid": pid,
361 "message": "Use this PID to restart the service"
362 }))
363}
364
365pub async fn validate_key_for_update(
369 State(_state): State<AppState>,
370 Json(request): Json<UpdateKeyRequest>,
371) -> Result<Json<serde_json::Value>, StatusCode> {
372 use crate::settings::LlmBackendSettings;
373 use crate::service::Service;
374
375 info!("🔍 Validating API key for hot update - provider: {} (key: {})", request.provider, mask_api_key(&request.api_key));
376
377 let model = match request.provider.as_str() {
379 "openai" => "gpt-4o".to_string(),
380 "anthropic" => "claude-3-5-sonnet-20241022".to_string(),
381 "zhipu" => "glm-4-flash".to_string(),
382 "ollama" => "llama2".to_string(),
383 "aliyun" => "qwen-turbo".to_string(),
384 "volcengine" => "ep-20241023xxxxx-xxxxx".to_string(),
385 "tencent" => "hunyuan-lite".to_string(),
386 _ => {
387 error!("❌ Unsupported provider: {}", request.provider);
388 return Err(StatusCode::BAD_REQUEST);
389 }
390 };
391
392 let test_backend = match request.provider.as_str() {
393 "openai" => LlmBackendSettings::OpenAI {
394 api_key: request.api_key.clone(),
395 base_url: request.base_url.clone(),
396 model,
397 },
398 "anthropic" => LlmBackendSettings::Anthropic {
399 api_key: request.api_key.clone(),
400 model,
401 },
402 "zhipu" => LlmBackendSettings::Zhipu {
403 api_key: request.api_key.clone(),
404 base_url: request.base_url.clone(),
405 model,
406 },
407 "ollama" => LlmBackendSettings::Ollama {
408 base_url: request.base_url.clone(),
409 model,
410 },
411 "aliyun" => LlmBackendSettings::Aliyun {
412 api_key: request.api_key.clone(),
413 model,
414 },
415 "volcengine" => LlmBackendSettings::Volcengine {
416 api_key: request.api_key.clone(),
417 model,
418 },
419 "tencent" => LlmBackendSettings::Tencent {
420 api_key: request.api_key.clone(),
421 model,
422 },
423 _ => {
424 error!("❌ Unsupported provider: {}", request.provider);
425 return Err(StatusCode::BAD_REQUEST);
426 }
427 };
428
429 match Service::new(&test_backend) {
431 Ok(service) => {
432 match service.list_models().await {
433 Ok(models) => {
434 info!("✅ API key validated successfully for hot update, found {} models", models.len());
435 Ok(Json(json!({
436 "status": "valid",
437 "message": "API key is valid and ready for hot update",
438 "provider": request.provider,
439 "models": models.iter().map(|m| &m.id).collect::<Vec<_>>(),
440 "supports_hot_reload": true,
441 })))
442 }
443 Err(e) => {
444 error!("❌ API key validation failed for hot update: {:?}", e);
445 Ok(Json(json!({
446 "status": "invalid",
447 "message": format!("Failed to list models: {}", e),
448 "provider": request.provider,
449 })))
450 }
451 }
452 }
453 Err(e) => {
454 error!("❌ Failed to create service for hot update validation: {:?}", e);
455 Ok(Json(json!({
456 "status": "error",
457 "message": format!("Failed to create service: {}", e),
458 "provider": request.provider,
459 })))
460 }
461 }
462}
463
464pub async fn update_key(
468 State(state): State<AppState>,
469 Json(request): Json<UpdateKeyRequest>,
470) -> Result<Json<serde_json::Value>, StatusCode> {
471 if let Err(e) = validate_provider(&request.provider) {
473 error!("❌ Invalid provider: {}", e);
474 return Err(StatusCode::BAD_REQUEST);
475 }
476
477 if request.provider != "ollama" {
478 if let Err(e) = validate_api_key(&request.provider, &request.api_key) {
479 error!("❌ Invalid API key format: {}", e);
480 return Ok(Json(json!({
481 "status": "error",
482 "message": format!("Invalid API key format: {}", e),
483 })));
484 }
485 }
486
487 info!("🔧 Updating API key for provider: {} (key: {})", request.provider, mask_api_key(&request.api_key));
488
489 let current_config = state.get_current_config();
491
492 let new_backend = match request.provider.as_str() {
494 "openai" => {
495 if let crate::settings::LlmBackendSettings::OpenAI { model, .. } = ¤t_config.llm_backend {
496 crate::settings::LlmBackendSettings::OpenAI {
497 api_key: request.api_key.clone(),
498 base_url: request.base_url.clone(),
499 model: model.clone(),
500 }
501 } else {
502 crate::settings::LlmBackendSettings::OpenAI {
504 api_key: request.api_key.clone(),
505 base_url: request.base_url.clone(),
506 model: "gpt-4o".to_string(),
507 }
508 }
509 }
510 "anthropic" => {
511 if let crate::settings::LlmBackendSettings::Anthropic { model, .. } = ¤t_config.llm_backend {
512 crate::settings::LlmBackendSettings::Anthropic {
513 api_key: request.api_key.clone(),
514 model: model.clone(),
515 }
516 } else {
517 crate::settings::LlmBackendSettings::Anthropic {
518 api_key: request.api_key.clone(),
519 model: "claude-3-5-sonnet-20241022".to_string(),
520 }
521 }
522 }
523 "zhipu" => {
524 if let crate::settings::LlmBackendSettings::Zhipu { model, .. } = ¤t_config.llm_backend {
525 crate::settings::LlmBackendSettings::Zhipu {
526 api_key: request.api_key.clone(),
527 base_url: request.base_url.clone(),
528 model: model.clone(),
529 }
530 } else {
531 crate::settings::LlmBackendSettings::Zhipu {
532 api_key: request.api_key.clone(),
533 base_url: request.base_url.clone(),
534 model: "glm-4-flash".to_string(),
535 }
536 }
537 }
538 "aliyun" => {
539 if let crate::settings::LlmBackendSettings::Aliyun { model, .. } = ¤t_config.llm_backend {
540 crate::settings::LlmBackendSettings::Aliyun {
541 api_key: request.api_key.clone(),
542 model: model.clone(),
543 }
544 } else {
545 crate::settings::LlmBackendSettings::Aliyun {
546 api_key: request.api_key.clone(),
547 model: "qwen-turbo".to_string(),
548 }
549 }
550 }
551 "volcengine" => {
552 if let crate::settings::LlmBackendSettings::Volcengine { model, .. } = ¤t_config.llm_backend {
553 crate::settings::LlmBackendSettings::Volcengine {
554 api_key: request.api_key.clone(),
555 model: model.clone(),
556 }
557 } else {
558 crate::settings::LlmBackendSettings::Volcengine {
559 api_key: request.api_key.clone(),
560 model: "ep-20241023xxxxx-xxxxx".to_string(),
561 }
562 }
563 }
564 "tencent" => {
565 if let crate::settings::LlmBackendSettings::Tencent { model, .. } = ¤t_config.llm_backend {
566 crate::settings::LlmBackendSettings::Tencent {
567 api_key: request.api_key.clone(),
568 model: model.clone(),
569 }
570 } else {
571 crate::settings::LlmBackendSettings::Tencent {
572 api_key: request.api_key.clone(),
573 model: "hunyuan-lite".to_string(),
574 }
575 }
576 }
577 "ollama" => {
578 if let crate::settings::LlmBackendSettings::Ollama { model, .. } = ¤t_config.llm_backend {
579 crate::settings::LlmBackendSettings::Ollama {
580 base_url: request.base_url.clone(),
581 model: model.clone(),
582 }
583 } else {
584 crate::settings::LlmBackendSettings::Ollama {
585 base_url: request.base_url.clone(),
586 model: "llama2".to_string(),
587 }
588 }
589 }
590 _ => {
591 error!("❌ Unsupported provider: {}", request.provider);
592 return Err(StatusCode::BAD_REQUEST);
593 }
594 };
595
596 match state.update_llm_service(&new_backend) {
598 Ok(()) => {
599 info!("✅ API key updated successfully for provider: {}", request.provider);
600 Ok(Json(json!({
601 "status": "success",
602 "message": format!("API key updated for provider: {}", request.provider),
603 "provider": request.provider,
604 "restart_required": false,
605 })))
606 }
607 Err(e) => {
608 error!("❌ Failed to update API key: {:?}", e);
609 Ok(Json(json!({
610 "status": "error",
611 "message": format!("Failed to update API key: {}", e),
612 })))
613 }
614 }
615}
616
617pub async fn switch_provider(
621 State(state): State<AppState>,
622 Json(request): Json<SwitchProviderRequest>,
623) -> Result<Json<serde_json::Value>, StatusCode> {
624 if let Err(e) = validate_provider(&request.provider) {
626 error!("❌ Invalid provider: {}", e);
627 return Err(StatusCode::BAD_REQUEST);
628 }
629
630 let masked_key = request.api_key.as_ref().map(|k| mask_api_key(k)).unwrap_or_else(|| "none".to_string());
631 info!("🔄 Switching to provider: {} (key: {})", request.provider, masked_key);
632
633 let current_config = state.get_current_config();
635
636 let api_key = if let Some(key) = request.api_key {
638 key
639 } else {
640 match request.provider.as_str() {
642 "openai" => {
643 if let crate::settings::LlmBackendSettings::OpenAI { api_key, .. } = ¤t_config.llm_backend {
644 api_key.clone()
645 } else {
646 error!("❌ No API key provided for OpenAI and none found in current config");
647 return Err(StatusCode::BAD_REQUEST);
648 }
649 }
650 "anthropic" => {
651 if let crate::settings::LlmBackendSettings::Anthropic { api_key, .. } = ¤t_config.llm_backend {
652 api_key.clone()
653 } else {
654 error!("❌ No API key provided for Anthropic and none found in current config");
655 return Err(StatusCode::BAD_REQUEST);
656 }
657 }
658 "zhipu" => {
659 if let crate::settings::LlmBackendSettings::Zhipu { api_key, .. } = ¤t_config.llm_backend {
660 api_key.clone()
661 } else {
662 error!("❌ No API key provided for Zhipu and none found in current config");
663 return Err(StatusCode::BAD_REQUEST);
664 }
665 }
666 "ollama" => String::new(), _ => {
668 error!("❌ Unsupported provider: {}", request.provider);
669 return Err(StatusCode::BAD_REQUEST);
670 }
671 }
672 };
673
674 let model = request.model.unwrap_or_else(|| {
676 match request.provider.as_str() {
677 "openai" => "gpt-4o".to_string(),
678 "anthropic" => "claude-3-5-sonnet-20241022".to_string(),
679 "zhipu" => "glm-4-flash".to_string(),
680 "ollama" => "llama2".to_string(),
681 "aliyun" => "qwen-turbo".to_string(),
682 "volcengine" => "ep-20241023xxxxx-xxxxx".to_string(),
683 "tencent" => "hunyuan-lite".to_string(),
684 _ => "default-model".to_string(),
685 }
686 });
687
688 let new_backend = match request.provider.as_str() {
690 "openai" => crate::settings::LlmBackendSettings::OpenAI {
691 api_key,
692 base_url: request.base_url,
693 model,
694 },
695 "anthropic" => crate::settings::LlmBackendSettings::Anthropic {
696 api_key,
697 model,
698 },
699 "zhipu" => crate::settings::LlmBackendSettings::Zhipu {
700 api_key,
701 base_url: request.base_url,
702 model,
703 },
704 "ollama" => crate::settings::LlmBackendSettings::Ollama {
705 base_url: request.base_url,
706 model,
707 },
708 "aliyun" => crate::settings::LlmBackendSettings::Aliyun {
709 api_key,
710 model,
711 },
712 "volcengine" => crate::settings::LlmBackendSettings::Volcengine {
713 api_key,
714 model,
715 },
716 "tencent" => crate::settings::LlmBackendSettings::Tencent {
717 api_key,
718 model,
719 },
720 _ => {
721 error!("❌ Unsupported provider: {}", request.provider);
722 return Err(StatusCode::BAD_REQUEST);
723 }
724 };
725
726 match state.update_llm_service(&new_backend) {
728 Ok(()) => {
729 info!("✅ Provider switched successfully to: {}", request.provider);
730 Ok(Json(json!({
731 "status": "success",
732 "message": format!("Provider switched to: {}", request.provider),
733 "provider": request.provider,
734 "model": new_backend.get_model(),
735 "restart_required": false,
736 })))
737 }
738 Err(e) => {
739 error!("❌ Failed to switch provider: {:?}", e);
740 Ok(Json(json!({
741 "status": "error",
742 "message": format!("Failed to switch provider: {}", e),
743 })))
744 }
745 }
746}
747
748pub async fn shutdown() -> Json<serde_json::Value> {
753 info!("🛑 Shutdown requested via API");
754
755 Json(json!({
759 "status": "success",
760 "message": "Shutdown signal sent. Please restart with new configuration.",
761 }))
762}