feat(memory): P1 长期记忆升级 —— 异步攒批 Consolidate + 软删 + importance/last_seen #1
@@ -0,0 +1,82 @@
|
|||||||
|
package llm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/cloudwego/eino/components/model"
|
||||||
|
"github.com/cloudwego/eino/schema"
|
||||||
|
)
|
||||||
|
|
||||||
|
// failoverModel 把多个 ToolCallingChatModel 串成主备链:按序调用,主模型遇错即切下一个,
|
||||||
|
// 直到成功或全部失败。它本身就是个 model.ToolCallingChatModel,故 compose / ReAct / Chat
|
||||||
|
// 全路径透明白嫖 failover —— 单 provider 抖动/挂掉时平台不整体宕。
|
||||||
|
//
|
||||||
|
// 局限(v1):Stream 仅在「建流(Stream() 调用)同步报错」时切备;已开始回流 token 的中途失败不切
|
||||||
|
// (输出已半出,无法干净重来)。连接级失败(拒连/立即 5xx)由 openai 客户端在 Stream() 同步返回,
|
||||||
|
// 已覆盖"provider 整体挂"的主场景。
|
||||||
|
type failoverModel struct {
|
||||||
|
models []model.ToolCallingChatModel // 主模型在前,其余为按序备用
|
||||||
|
onFailover func(idx int, err error) // 切换回调(日志/观测;可空)
|
||||||
|
}
|
||||||
|
|
||||||
|
// newFailoverModel 建主备链。models 至少 1 个;只有 1 个时调用方应直接用该模型而非本包装。
|
||||||
|
func newFailoverModel(models []model.ToolCallingChatModel, onFailover func(int, error)) model.ToolCallingChatModel {
|
||||||
|
return &failoverModel{models: models, onFailover: onFailover}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *failoverModel) Generate(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.Message, error) {
|
||||||
|
var lastErr error
|
||||||
|
for i, m := range f.models {
|
||||||
|
out, err := m.Generate(ctx, input, opts...)
|
||||||
|
if err == nil {
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
lastErr = err
|
||||||
|
if !f.shouldFailover(ctx, i) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if f.onFailover != nil {
|
||||||
|
f.onFailover(i, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, lastErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *failoverModel) Stream(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.StreamReader[*schema.Message], error) {
|
||||||
|
var lastErr error
|
||||||
|
for i, m := range f.models {
|
||||||
|
sr, err := m.Stream(ctx, input, opts...)
|
||||||
|
if err == nil {
|
||||||
|
return sr, nil
|
||||||
|
}
|
||||||
|
lastErr = err
|
||||||
|
if !f.shouldFailover(ctx, i) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if f.onFailover != nil {
|
||||||
|
f.onFailover(i, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, lastErr
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithTools 给链上每个模型绑定工具,返回新的 failover 链(不可变,并发安全;ReAct 用)。
|
||||||
|
func (f *failoverModel) WithTools(tools []*schema.ToolInfo) (model.ToolCallingChatModel, error) {
|
||||||
|
bound := make([]model.ToolCallingChatModel, len(f.models))
|
||||||
|
for i, m := range f.models {
|
||||||
|
b, err := m.WithTools(tools)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failover WithTools[%d]: %w", i, err)
|
||||||
|
}
|
||||||
|
bound[i] = b
|
||||||
|
}
|
||||||
|
return &failoverModel{models: bound, onFailover: f.onFailover}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// shouldFailover:还有备用模型 且 调用方未主动取消/截止 → 切。
|
||||||
|
// 调用方取消(ctx.Err()!=nil) 不切——切了也没用,且违背用户意图(注:模型自身的请求超时走的是
|
||||||
|
// 内部派生 ctx,不会污染父 ctx,故仍会正常 failover)。
|
||||||
|
func (f *failoverModel) shouldFailover(ctx context.Context, idx int) bool {
|
||||||
|
return idx < len(f.models)-1 && ctx.Err() == nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
package llm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/cloudwego/eino/components/model"
|
||||||
|
"github.com/cloudwego/eino/schema"
|
||||||
|
)
|
||||||
|
|
||||||
|
// fakeModel 是 model.ToolCallingChatModel 测试替身:fail=true 则 Generate/Stream 报错。
|
||||||
|
type fakeModel struct {
|
||||||
|
name string
|
||||||
|
reply string
|
||||||
|
fail bool
|
||||||
|
calls *int // 共享计数器,记录被调次数(验证 failover 没多调/早停)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeModel) Generate(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.Message, error) {
|
||||||
|
if f.calls != nil {
|
||||||
|
*f.calls++
|
||||||
|
}
|
||||||
|
if f.fail {
|
||||||
|
return nil, fmt.Errorf("%s down", f.name)
|
||||||
|
}
|
||||||
|
return schema.AssistantMessage(f.reply, nil), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeModel) Stream(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.StreamReader[*schema.Message], error) {
|
||||||
|
if f.calls != nil {
|
||||||
|
*f.calls++
|
||||||
|
}
|
||||||
|
if f.fail {
|
||||||
|
return nil, fmt.Errorf("%s down", f.name)
|
||||||
|
}
|
||||||
|
sr, sw := schema.Pipe[*schema.Message](1)
|
||||||
|
go func() { sw.Send(schema.AssistantMessage(f.reply, nil), nil); sw.Close() }()
|
||||||
|
return sr, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeModel) WithTools(_ []*schema.ToolInfo) (model.ToolCallingChatModel, error) { return f, nil }
|
||||||
|
|
||||||
|
func genText(t *testing.T, ctx context.Context, m model.ToolCallingChatModel) (string, error) {
|
||||||
|
t.Helper()
|
||||||
|
out, err := m.Generate(ctx, []*schema.Message{schema.UserMessage("hi")})
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return out.Content, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestFailover_PrimaryWorks 主可用 → 直接用主,备用不被调用。
|
||||||
|
func TestFailover_PrimaryWorks(t *testing.T) {
|
||||||
|
var pc, fc int
|
||||||
|
m := newFailoverModel([]model.ToolCallingChatModel{
|
||||||
|
&fakeModel{name: "primary", reply: "主回答", calls: &pc},
|
||||||
|
&fakeModel{name: "fb", reply: "备回答", calls: &fc},
|
||||||
|
}, nil)
|
||||||
|
ans, err := genText(t, context.Background(), m)
|
||||||
|
if err != nil || ans != "主回答" {
|
||||||
|
t.Fatalf("应返回主回答, got %q err=%v", ans, err)
|
||||||
|
}
|
||||||
|
if pc != 1 || fc != 0 {
|
||||||
|
t.Fatalf("主可用时不应调备用: primary=%d fb=%d", pc, fc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestFailover_PrimaryDownUseFallback 主挂 → 切备用并成功。
|
||||||
|
func TestFailover_PrimaryDownUseFallback(t *testing.T) {
|
||||||
|
var pc, fc int
|
||||||
|
switched := false
|
||||||
|
m := newFailoverModel([]model.ToolCallingChatModel{
|
||||||
|
&fakeModel{name: "primary", fail: true, calls: &pc},
|
||||||
|
&fakeModel{name: "fb", reply: "备回答", calls: &fc},
|
||||||
|
}, func(int, error) { switched = true })
|
||||||
|
ans, err := genText(t, context.Background(), m)
|
||||||
|
if err != nil || ans != "备回答" {
|
||||||
|
t.Fatalf("主挂应切备用, got %q err=%v", ans, err)
|
||||||
|
}
|
||||||
|
if pc != 1 || fc != 1 || !switched {
|
||||||
|
t.Fatalf("主备各调一次且触发回调: primary=%d fb=%d switched=%v", pc, fc, switched)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestFailover_AllDown 全挂 → 返回最后一个错误。
|
||||||
|
func TestFailover_AllDown(t *testing.T) {
|
||||||
|
m := newFailoverModel([]model.ToolCallingChatModel{
|
||||||
|
&fakeModel{name: "p", fail: true},
|
||||||
|
&fakeModel{name: "fb", fail: true},
|
||||||
|
}, nil)
|
||||||
|
if _, err := genText(t, context.Background(), m); err == nil {
|
||||||
|
t.Fatal("全挂应返回错误")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestFailover_CtxCanceledNoSwitch 调用方已取消 → 不切备用(切了也没用,且违背意图)。
|
||||||
|
func TestFailover_CtxCanceledNoSwitch(t *testing.T) {
|
||||||
|
var pc, fc int
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
cancel() // 立即取消
|
||||||
|
m := newFailoverModel([]model.ToolCallingChatModel{
|
||||||
|
&fakeModel{name: "p", fail: true, calls: &pc},
|
||||||
|
&fakeModel{name: "fb", reply: "备", calls: &fc},
|
||||||
|
}, nil)
|
||||||
|
if _, err := genText(t, ctx, m); err == nil {
|
||||||
|
t.Fatal("已取消仍应返回主错误")
|
||||||
|
}
|
||||||
|
if fc != 0 {
|
||||||
|
t.Fatalf("取消时不应切备用: fb 被调 %d 次", fc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestFailover_StreamFailover Stream 建流失败也切备用。
|
||||||
|
func TestFailover_StreamFailover(t *testing.T) {
|
||||||
|
m := newFailoverModel([]model.ToolCallingChatModel{
|
||||||
|
&fakeModel{name: "p", fail: true},
|
||||||
|
&fakeModel{name: "fb", reply: "流备"},
|
||||||
|
}, nil)
|
||||||
|
sr, err := m.Stream(context.Background(), []*schema.Message{schema.UserMessage("hi")})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Stream 应切到备用: %v", err)
|
||||||
|
}
|
||||||
|
defer sr.Close()
|
||||||
|
chunk, _ := sr.Recv()
|
||||||
|
if chunk == nil || chunk.Content != "流备" {
|
||||||
|
t.Fatalf("应收到备用的流, got %+v", chunk)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestFailover_WithToolsBindsAll WithTools 给链上每个模型绑定,返回仍是 failover 链。
|
||||||
|
func TestFailover_WithToolsBindsAll(t *testing.T) {
|
||||||
|
m := newFailoverModel([]model.ToolCallingChatModel{
|
||||||
|
&fakeModel{name: "p", fail: true},
|
||||||
|
&fakeModel{name: "fb", reply: "带工具的备"},
|
||||||
|
}, nil)
|
||||||
|
bound, err := m.WithTools(nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
ans, err := genText(t, context.Background(), bound)
|
||||||
|
if err != nil || ans != "带工具的备" {
|
||||||
|
t.Fatalf("绑定工具后 failover 仍生效, got %q err=%v", ans, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -45,16 +45,11 @@ func NewPool() *Pool { return &Pool{} }
|
|||||||
// 绕开真实 LLM 推理与计费,只压全链路 plumbing(网关→NATS→调度→工具RTT→回流)。
|
// 绕开真实 LLM 推理与计费,只压全链路 plumbing(网关→NATS→调度→工具RTT→回流)。
|
||||||
func forceStub() bool { return os.Getenv("LLM_FORCE_STUB") == "1" }
|
func forceStub() bool { return os.Getenv("LLM_FORCE_STUB") == "1" }
|
||||||
|
|
||||||
// SetConfig 热更新后端配置:重建 ChatModel 实例(控制面变更时调用)。
|
// SetConfig 热更新后端配置:用激活配置(含备用模型)重建 ChatModel(控制面变更时调用)。
|
||||||
func (p *Pool) SetConfig(cfg *contract.ModelConfig) {
|
func (p *Pool) SetConfig(cfg *contract.ModelConfig) {
|
||||||
var cm model.BaseChatModel
|
var cm model.BaseChatModel
|
||||||
if cfg != nil && cfg.Ready() && !forceStub() {
|
if cfg != nil && cfg.Ready() && !forceStub() {
|
||||||
built, err := buildChatModel(cfg)
|
cm = buildWithFallbacks(cfg)
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("[llm] 构建 ChatModel 失败(降级桩运行): %v\n", err)
|
|
||||||
} else {
|
|
||||||
cm = built
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
p.mu.Lock()
|
p.mu.Lock()
|
||||||
p.cfg = cfg
|
p.cfg = cfg
|
||||||
@@ -62,10 +57,47 @@ func (p *Pool) SetConfig(cfg *contract.ModelConfig) {
|
|||||||
p.mu.Unlock()
|
p.mu.Unlock()
|
||||||
if cfg != nil {
|
if cfg != nil {
|
||||||
// 不打印 api_key。
|
// 不打印 api_key。
|
||||||
fmt.Printf("[llm] model config set: provider=%s base=%s model=%s\n", cfg.Provider, normalizeBaseURL(cfg), cfg.Model)
|
fmt.Printf("[llm] model config set: provider=%s base=%s model=%s fallbacks=%d\n",
|
||||||
|
cfg.Provider, normalizeBaseURL(cfg), cfg.Model, len(cfg.Fallbacks))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// buildWithFallbacks 构建主模型,并把可用的备用模型串成 failover 链(无备用则直接返回主模型)。
|
||||||
|
// 主模型构建失败 → 返回 nil(降级桩);备用单个失败 → 跳过该备用,不影响主链。
|
||||||
|
func buildWithFallbacks(cfg *contract.ModelConfig) model.BaseChatModel {
|
||||||
|
primary, err := buildChatModel(cfg)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("[llm] 构建主 ChatModel 失败(降级桩运行): %v\n", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
ptcm, ok := primary.(model.ToolCallingChatModel)
|
||||||
|
if !ok || len(cfg.Fallbacks) == 0 {
|
||||||
|
return primary // 不支持 WithTools 或无备用 → 直接用主模型
|
||||||
|
}
|
||||||
|
models := []model.ToolCallingChatModel{ptcm}
|
||||||
|
for i := range cfg.Fallbacks {
|
||||||
|
fb := cfg.Fallbacks[i]
|
||||||
|
if !fb.Ready() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fbm, ferr := buildChatModel(&fb)
|
||||||
|
if ferr != nil {
|
||||||
|
fmt.Printf("[llm] 构建备用模型 %s 失败,跳过: %v\n", fb.Model, ferr)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if t, ok := fbm.(model.ToolCallingChatModel); ok {
|
||||||
|
models = append(models, t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(models) == 1 {
|
||||||
|
return primary // 无有效备用
|
||||||
|
}
|
||||||
|
fmt.Printf("[llm] 启用模型 failover:主 %s + %d 个备用\n", cfg.Model, len(models)-1)
|
||||||
|
return newFailoverModel(models, func(idx int, ferr error) {
|
||||||
|
fmt.Printf("[llm] 模型 failover:第 %d 个模型失败(%v),切下一个\n", idx, ferr)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// buildChatModel 据 provider 归一化连接参数后构建 OpenAI 兼容 ChatModel。
|
// buildChatModel 据 provider 归一化连接参数后构建 OpenAI 兼容 ChatModel。
|
||||||
// vLLM 与 Ollama 都暴露 OpenAI 兼容 API(底层 go-openai 请求 {base}/chat/completions),
|
// vLLM 与 Ollama 都暴露 OpenAI 兼容 API(底层 go-openai 请求 {base}/chat/completions),
|
||||||
// 故统一走 openai 客户端,仅差在 BaseURL 是否带 /v1 与是否需要占位 key:
|
// 故统一走 openai 客户端,仅差在 BaseURL 是否带 /v1 与是否需要占位 key:
|
||||||
|
|||||||
@@ -53,11 +53,7 @@ func main() {
|
|||||||
for _, kind := range []string{contract.ConfigKindChat, contract.ConfigKindEmbedding} {
|
for _, kind := range []string{contract.ConfigKindChat, contract.ConfigKindEmbedding} {
|
||||||
k := kind
|
k := kind
|
||||||
if _, err := bus.ServeConfig(k, func() *contract.ModelConfig {
|
if _, err := bus.ServeConfig(k, func() *contract.ModelConfig {
|
||||||
row, _ := db.GetActiveModel(context.Background(), k)
|
return db.ActiveConfig(context.Background(), k) // chat 含 Fallbacks(其它模型作备用)
|
||||||
if row == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return &contract.ModelConfig{Provider: row.Provider, BaseURL: row.BaseURL, APIKey: row.APIKey, Model: row.Model}
|
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
log.Printf("[gateway] serve %s config: %v", k, err)
|
log.Printf("[gateway] serve %s config: %v", k, err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -202,15 +202,12 @@ func (h *Handler) TestModel(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// broadcastActive 重新广播各 kind 当前激活配置,触发对应消费方热更新。
|
// broadcastActive 重新广播各 kind 当前激活配置,触发对应消费方热更新。
|
||||||
|
// chat 配置带 Fallbacks(其它已登记 chat 模型作备用),dispatcher 据此重建 failover 链。
|
||||||
func (h *Handler) broadcastActive(ctx context.Context) {
|
func (h *Handler) broadcastActive(ctx context.Context) {
|
||||||
for _, kind := range []string{contract.ConfigKindChat, contract.ConfigKindEmbedding} {
|
for _, kind := range []string{contract.ConfigKindChat, contract.ConfigKindEmbedding} {
|
||||||
row, _ := h.db.GetActiveModel(ctx, kind)
|
if cfg := h.db.ActiveConfig(ctx, kind); cfg != nil {
|
||||||
if row == nil {
|
_ = h.bus.PublishConfigUpdated(kind, cfg)
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
_ = h.bus.PublishConfigUpdated(kind, &contract.ModelConfig{
|
|
||||||
Provider: row.Provider, BaseURL: row.BaseURL, APIKey: row.APIKey, Model: row.Model,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import (
|
|||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"gorm.io/gorm/clause"
|
"gorm.io/gorm/clause"
|
||||||
|
|
||||||
|
"github.com/sundynix/sundynix-shared/contract"
|
||||||
)
|
)
|
||||||
|
|
||||||
// KB 是一个知识库(按 owner 隔离 + 按 kind 组织:文件夹/项目/案件/通用)。
|
// KB 是一个知识库(按 owner 隔离 + 按 kind 组织:文件夹/项目/案件/通用)。
|
||||||
@@ -321,6 +323,28 @@ func (p *Postgres) GetActiveModel(ctx context.Context, kind string) (*LLMModel,
|
|||||||
return &m, nil
|
return &m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ActiveConfig 取某 kind 的激活模型配置;chat 还把"其它已登记 chat 模型"按序填进 Fallbacks,
|
||||||
|
// 供 dispatcher 串成 failover 链(主 provider 抖动/挂掉时自动切备,平台不整体宕)。
|
||||||
|
func (p *Postgres) ActiveConfig(ctx context.Context, kind string) *contract.ModelConfig {
|
||||||
|
row, _ := p.GetActiveModel(ctx, kind)
|
||||||
|
if row == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
cfg := &contract.ModelConfig{Provider: row.Provider, BaseURL: row.BaseURL, APIKey: row.APIKey, Model: row.Model}
|
||||||
|
if kind == contract.ConfigKindChat {
|
||||||
|
all, _ := p.ListModels(ctx, kind)
|
||||||
|
for _, m := range all {
|
||||||
|
if m.ID == row.ID {
|
||||||
|
continue // 跳过激活模型(它已是主)
|
||||||
|
}
|
||||||
|
cfg.Fallbacks = append(cfg.Fallbacks, contract.ModelConfig{
|
||||||
|
Provider: m.Provider, BaseURL: m.BaseURL, APIKey: m.APIKey, Model: m.Model,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cfg
|
||||||
|
}
|
||||||
|
|
||||||
// DeleteModel 删除一条模型配置。
|
// DeleteModel 删除一条模型配置。
|
||||||
func (p *Postgres) DeleteModel(ctx context.Context, id string) error {
|
func (p *Postgres) DeleteModel(ctx context.Context, id string) error {
|
||||||
if p.db == nil {
|
if p.db == nil {
|
||||||
|
|||||||
@@ -24,13 +24,24 @@ import (
|
|||||||
|
|
||||||
// decryptConfig 在消费侧把配置里的 api_key 从密文还原为明文(控制面以密文过线缆,见 secrets 包)。
|
// decryptConfig 在消费侧把配置里的 api_key 从密文还原为明文(控制面以密文过线缆,见 secrets 包)。
|
||||||
// 失败(密钥不匹配 / 密文损坏)时清空 api_key 并不再降级阻断——调用方据 Ready() 判定。
|
// 失败(密钥不匹配 / 密文损坏)时清空 api_key 并不再降级阻断——调用方据 Ready() 判定。
|
||||||
|
// 备用模型(Fallbacks)的 api_key 同样是密文,一并解密。
|
||||||
func decryptConfig(cfg *contract.ModelConfig) {
|
func decryptConfig(cfg *contract.ModelConfig) {
|
||||||
if cfg == nil || cfg.APIKey == "" {
|
if cfg == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if cfg.APIKey != "" {
|
||||||
if plain, err := secrets.Decrypt(cfg.APIKey); err == nil {
|
if plain, err := secrets.Decrypt(cfg.APIKey); err == nil {
|
||||||
cfg.APIKey = plain
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bus 持有 NATS 连接与 JetStream 上下文。
|
// Bus 持有 NATS 连接与 JetStream 上下文。
|
||||||
|
|||||||
@@ -201,6 +201,9 @@ type ModelConfig struct {
|
|||||||
BaseURL string `json:"base_url"` // 如 https://api.deepseek.com
|
BaseURL string `json:"base_url"` // 如 https://api.deepseek.com
|
||||||
APIKey string `json:"api_key,omitempty"`
|
APIKey string `json:"api_key,omitempty"`
|
||||||
Model string `json:"model"` // 如 deepseek-chat / text-embedding-v3
|
Model string `json:"model"` // 如 deepseek-chat / text-embedding-v3
|
||||||
|
// Fallbacks 是主模型调用失败/超时时按序切换的备用模型(仅 chat 用,骑在主配置里一并下发)。
|
||||||
|
// 网关把"其它 enabled chat 模型"填进来;dispatcher 据此把模型包成 failover 链。
|
||||||
|
Fallbacks []ModelConfig `json:"fallbacks,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ready 报告该配置是否足以发起真实推理。
|
// Ready 报告该配置是否足以发起真实推理。
|
||||||
|
|||||||
Reference in New Issue
Block a user