llm_link/api/config/
mod.rs

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
9/// 安全地掩盖 API Key 用于日志记录
10fn 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
18/// 验证 API Key 格式
19fn 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            // Zhipu API keys have specific format, but we'll be lenient
37            if api_key.len() < 10 {
38                return Err("Zhipu API key seems too short".to_string());
39            }
40        }
41        "ollama" => {
42            // Ollama doesn't require API key, so this is always valid
43        }
44        _ => {
45            // For other providers, just check minimum length
46            if api_key.len() < 10 {
47                return Err("API key seems too short".to_string());
48            }
49        }
50    }
51
52    Ok(())
53}
54
55/// 验证 provider 名称
56fn 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
63// 全局计数器,每次启动时递增
64static 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
117/// 获取当前配置信息(不包含敏感的 API Key)
118pub 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, // 现在支持热重载
154    }))
155}
156
157/// 获取健康状态和实例信息
158///
159/// 用于验证服务是否重启成功
160pub 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
185/// 更新配置并请求重启
186/// 
187/// 这个端点会:
188/// 1. 验证配置的有效性
189/// 2. 将配置保存为环境变量格式(供调用者重启时使用)
190/// 3. 返回需要设置的环境变量
191/// 
192/// z-agent 需要:
193/// 1. 调用此端点获取环境变量
194/// 2. 使用新的环境变量重启 llm-link 进程
195pub 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    // 验证 provider 和生成默认 model
202    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    // 构建环境变量
224    let mut env_vars = serde_json::Map::new();
225    
226    // 添加 provider 对应的 API key 环境变量
227    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" => "", // Ollama 不需要 API key
235        _ => 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    // 添加 base_url(如果提供)
243    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
270/// 验证 API Key 是否有效
271/// 
272/// 通过尝试创建一个临时的 Service 并列出模型来验证
273pub 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    // 构建测试用的 backend settings
283    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    // 尝试创建 service 并列出模型
323    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
353/// 获取当前进程 PID
354/// 
355/// z-agent 可以使用这个 PID 来管理进程(如重启)
356pub 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
365/// 验证 API Key(用于热更新)
366///
367/// 专门用于热更新场景的 API Key 验证
368pub 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    // 使用默认模型进行测试
378    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    // 尝试创建 service 并列出模型
430    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
464/// 运行时更新 API Key
465///
466/// 这个端点允许在不重启服务的情况下更新指定 provider 的 API Key
467pub async fn update_key(
468    State(state): State<AppState>,
469    Json(request): Json<UpdateKeyRequest>,
470) -> Result<Json<serde_json::Value>, StatusCode> {
471    // 验证输入
472    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    // 获取当前配置
490    let current_config = state.get_current_config();
491
492    // 构建新的 backend settings
493    let new_backend = match request.provider.as_str() {
494        "openai" => {
495            if let crate::settings::LlmBackendSettings::OpenAI { model, .. } = &current_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                // 如果当前不是 OpenAI,使用默认模型
503                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, .. } = &current_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, .. } = &current_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, .. } = &current_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, .. } = &current_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, .. } = &current_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, .. } = &current_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    // 尝试更新服务
597    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
617/// 切换 Provider
618///
619/// 这个端点允许动态切换当前使用的 LLM 服务商
620pub async fn switch_provider(
621    State(state): State<AppState>,
622    Json(request): Json<SwitchProviderRequest>,
623) -> Result<Json<serde_json::Value>, StatusCode> {
624    // 验证输入
625    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    // 获取当前配置
634    let current_config = state.get_current_config();
635
636    // 确定 API key
637    let api_key = if let Some(key) = request.api_key {
638        key
639    } else {
640        // 尝试从当前配置中获取对应 provider 的 API key
641        match request.provider.as_str() {
642            "openai" => {
643                if let crate::settings::LlmBackendSettings::OpenAI { api_key, .. } = &current_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, .. } = &current_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, .. } = &current_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(), // Ollama 不需要 API key
667            _ => {
668                error!("❌ Unsupported provider: {}", request.provider);
669                return Err(StatusCode::BAD_REQUEST);
670            }
671        }
672    };
673
674    // 确定模型
675    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    // 构建新的 backend settings
689    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    // 尝试更新服务
727    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
748/// 触发优雅关闭
749///
750/// 注意:这需要配合信号处理才能实现优雅关闭
751/// z-agent 应该先调用此端点,等待响应后再启动新进程
752pub async fn shutdown() -> Json<serde_json::Value> {
753    info!("🛑 Shutdown requested via API");
754
755    // 在实际应用中,这里应该触发优雅关闭
756    // 目前只返回成功,让调用方决定如何重启
757
758    Json(json!({
759        "status": "success",
760        "message": "Shutdown signal sent. Please restart with new configuration.",
761    }))
762}