feat(llm): T2.1 模型路由 + Fallback —— 单 provider 抖动不再整体宕

现状:单 provider,一家 API 抖动/挂掉全平台不可用。加主备 failover:主模型调用失败
自动按序切备用,compose/ReAct/Chat 全路径透明白嫖。

- llm/failover.go: failoverModel 把多个 ToolCallingChatModel 串成主备链,按序调用、
  遇错切下一个;它本身是 model.ToolCallingChatModel 故全路径透明。调用方主动取消
  (ctx.Err()!=nil) 不切;模型自身请求超时走内部 ctx、不污染父 ctx 故仍 failover。
  局限(v1):Stream 仅建流同步报错时切(已回流 token 的中途失败不切)。
- llm/pool.go: SetConfig 用激活配置(含 Fallbacks)重建——主+可用备用串成 failover 链,
  无备用则直接用主;备用单个构建失败跳过不影响主链。
- contract.ModelConfig: 加 Fallbacks 字段(骑在主配置里下发,不改任何 bus/ServeConfig 签名)。
- gateway store.ActiveConfig: chat 把"其它已登记 chat 模型"按序填进 Fallbacks;
  provide(main) + broadcast(admin) 共用 → 注册多个 chat 模型即自动成主备。
- bus.decryptConfig: 备用模型的 api_key(密文)一并解密。

测试:failover 单测(主可用不调备/主挂切备/全挂报错/取消不切/Stream 切备/WithTools 链)。
live 验证:active=死 ollama 主 + deepseek 备 → 任务连主拒连→自动切 deepseek→4s 完成。
DEPTH_ROADMAP T2.1(admin 注册多模型即主备,无需新 UI)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Blizzard
2026-06-30 09:26:18 +08:00
parent ef6f525a74
commit ecf4a80466
8 changed files with 312 additions and 22 deletions
+14 -3
View File
@@ -24,12 +24,23 @@ import (
// decryptConfig 在消费侧把配置里的 api_key 从密文还原为明文(控制面以密文过线缆,见 secrets 包)。
// 失败(密钥不匹配 / 密文损坏)时清空 api_key 并不再降级阻断——调用方据 Ready() 判定。
// 备用模型(Fallbacks)的 api_key 同样是密文,一并解密。
func decryptConfig(cfg *contract.ModelConfig) {
if cfg == nil || cfg.APIKey == "" {
if cfg == nil {
return
}
if plain, err := secrets.Decrypt(cfg.APIKey); err == nil {
cfg.APIKey = plain
if cfg.APIKey != "" {
if plain, err := secrets.Decrypt(cfg.APIKey); err == nil {
cfg.APIKey = plain
}
}
for i := range cfg.Fallbacks {
if cfg.Fallbacks[i].APIKey == "" {
continue
}
if plain, err := secrets.Decrypt(cfg.Fallbacks[i].APIKey); err == nil {
cfg.Fallbacks[i].APIKey = plain
}
}
}
+3
View File
@@ -201,6 +201,9 @@ type ModelConfig struct {
BaseURL string `json:"base_url"` // 如 https://api.deepseek.com
APIKey string `json:"api_key,omitempty"`
Model string `json:"model"` // 如 deepseek-chat / text-embedding-v3
// Fallbacks 是主模型调用失败/超时时按序切换的备用模型(仅 chat 用,骑在主配置里一并下发)。
// 网关把"其它 enabled chat 模型"填进来;dispatcher 据此把模型包成 failover 链。
Fallbacks []ModelConfig `json:"fallbacks,omitempty"`
}
// Ready 报告该配置是否足以发起真实推理。