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 }