package dispatch import ( "fmt" "slices" ) // RoutingPolicy 路由策略。 // 单一版本化对象,适配层从 sys_options 加载并做 Schema 校验,非法配置拒绝生效。 type RoutingPolicy struct { Version int `json:"version"` TierFactors map[Tier]float64 `json:"tierFactors"` Health struct { Alpha float64 `json:"alpha"` // 成功率指数,默认 2 LatRefMs float64 `json:"latRefMs"` // 延迟基准(毫秒),默认 3000 } `json:"health"` Load struct { Gamma float64 `json:"gamma"` // 余量指数,默认 2 LeaseSeconds int `json:"leaseSeconds"` // 租约时长,默认 90 } `json:"load"` Cost struct { Beta float64 `json:"beta"` // 成本指数,默认 0.5 Min float64 `json:"min"` // 因子下限,默认 0.5 Max float64 `json:"max"` // 因子上限,默认 2.0 } `json:"cost"` Ramp struct { WindowSeconds int `json:"windowSeconds"` // 爬坡窗口,默认 120 Floor float64 `json:"floor"` // 爬坡下限,默认 0.05 } `json:"ramp"` Binding struct { TTLSeconds int `json:"ttlSeconds"` // 绑定 TTL,默认 1800 KeepHealthMin float64 `json:"keepHealthMin"` // 守卫:健康因子下限,默认 0.5 KeepHeadroomMin float64 `json:"keepHeadroomMin"` // 守卫:余量下限,默认 0.1 } `json:"binding"` Retry RetryPolicy `json:"retry"` Breaker BreakerPolicy `json:"breaker"` Session SessionPolicy `json:"session"` Replay ReplayPolicy `json:"replay"` Degrade struct { MaxReplicas int `json:"maxReplicas"` // 实例级保守限额分母,默认 1 } `json:"degrade"` Proto struct { // ResponsesMismatch responses 入站打在不支持 Responses 协议渠道上的降权因子, // 默认 0.25(经 chat 转换丢失有状态 Responses 特性,降权较重)。 ResponsesMismatch float64 `json:"responsesMismatch"` // ChatBridgeMismatch chat 入站打在 responses-only 桥接渠道上的降权因子, // 默认 0.75(桥接基本无损,降权较轻)。 ChatBridgeMismatch float64 `json:"chatBridgeMismatch"` } `json:"proto"` } // ReplayPolicy 可重放性策略。 type ReplayPolicy struct { // UnsafeModes 归为 ReplayUnsafe 的 relay mode 字符串列表(与 handler 的 // relayModeString 取值一致)。重放可能产生重复任务/高额成本的端点应列入。 UnsafeModes []string `json:"unsafeModes"` // SafeModes 归为 ReplaySafe 的 relay mode 列表(可安全重放的查询类端点)。 SafeModes []string `json:"safeModes"` } // ReplayabilityForMode 按 relay mode 字符串查可重放性:Unsafe 优先,Safe 次之,默认 Costly。 func (p ReplayPolicy) ReplayabilityForMode(mode string) Replayability { if slices.Contains(p.UnsafeModes, mode) { return ReplayUnsafe } if slices.Contains(p.SafeModes, mode) { return ReplaySafe } return ReplayCostly } // RetryPolicy 重试策略。 type RetryPolicy struct { InPlaceBudget int `json:"inPlaceBudget"` // 原地重试预算(每渠道),默认 2 FailoverBudget int `json:"failoverBudget"` // 换渠道预算,默认 2 CredRotateBudget int `json:"credRotateBudget"` // 凭证轮换预算(每渠道),默认 1 CredCooldownSeconds int `json:"credCooldownSeconds"` // 凭证冷却时长,默认 300 TotalDeadlineSeconds int `json:"totalDeadlineSeconds"` // 总时限,默认 30 BackoffBaseMs int64 `json:"backoffBaseMs"` // 退避基数,默认 100 BackoffMaxMs int64 `json:"backoffMaxMs"` // 退避上限,默认 1000 RateLimitWaitMaxMs int64 `json:"rateLimitWaitMaxMs"` // 429 最大等待,默认 2000 // ReplayUnsafe 请求的收紧预算(默认 0/0,仅 NotSent 允许 failover) UnsafeInPlaceBudget int `json:"unsafeInPlaceBudget"` UnsafeFailoverBudget int `json:"unsafeFailoverBudget"` } // BreakerPolicy 熔断策略。 type BreakerPolicy struct { WindowSeconds int `json:"windowSeconds"` // 滑动窗口,默认 60 FailThreshold int `json:"failThreshold"` // 渠道级窗口失败阈值,默认 8 ModelFailThreshold int `json:"modelFailThreshold"` // 模型级窗口失败阈值,默认 4(低于渠道级,单模型故障先在模型级熔断隔离) CooldownSeconds int `json:"cooldownSeconds"` // OPEN 冷却起始,默认 30 CooldownMaxSeconds int `json:"cooldownMaxSeconds"` // 冷却上限,默认 300 ProbeWindowSeconds int `json:"probeWindowSeconds"` // HALF_OPEN 探测窗口,默认 10 AutoDisableAfterSeconds int `json:"autoDisableAfterSeconds"` // OPEN 持续自动禁用,默认 600 } // SessionPolicy 会话解析策略。 type SessionPolicy struct { HeaderName string `json:"headerName"` // 显式头名,默认 X-Session-Id ParseAnthropicMetadata bool `json:"parseAnthropicMetadata"` // 解析 metadata.user_id ParseOpenAIResponses bool `json:"parseOpenAIResponses"` // 解析 previous_response_id / conversation_id } // DefaultRoutingPolicy 返回默认策略。 func DefaultRoutingPolicy() *RoutingPolicy { p := &RoutingPolicy{ Version: 1, TierFactors: map[Tier]float64{ TierPrimary: 1.0, TierSecondary: 0.15, TierReserve: 0.02, }, } p.Health.Alpha = 2.0 p.Health.LatRefMs = 3000 p.Load.Gamma = 2.0 p.Load.LeaseSeconds = 90 p.Cost.Beta = 0.5 p.Cost.Min = 0.5 p.Cost.Max = 2.0 p.Ramp.WindowSeconds = 120 p.Ramp.Floor = 0.05 p.Binding.TTLSeconds = 1800 p.Binding.KeepHealthMin = 0.5 p.Binding.KeepHeadroomMin = 0.1 p.Retry = RetryPolicy{ InPlaceBudget: 2, FailoverBudget: 2, CredRotateBudget: 1, CredCooldownSeconds: 300, TotalDeadlineSeconds: 30, BackoffBaseMs: 100, BackoffMaxMs: 1000, RateLimitWaitMaxMs: 2000, UnsafeInPlaceBudget: 0, UnsafeFailoverBudget: 0, } p.Breaker = BreakerPolicy{ WindowSeconds: 60, FailThreshold: 8, ModelFailThreshold: 4, CooldownSeconds: 30, CooldownMaxSeconds: 300, ProbeWindowSeconds: 10, AutoDisableAfterSeconds: 600, } p.Session = SessionPolicy{ HeaderName: "X-Session-Id", ParseAnthropicMetadata: true, ParseOpenAIResponses: true, } p.Replay = ReplayPolicy{ UnsafeModes: []string{"images_generations", "images_edits", "video_generations"}, SafeModes: []string{"embeddings", "rerank"}, } p.Degrade.MaxReplicas = 1 p.Proto.ResponsesMismatch = 0.25 p.Proto.ChatBridgeMismatch = 0.75 return p } // Validate 策略校验。非法配置必须被拒绝生效(杜绝 O1 式静默降级)。 func (p *RoutingPolicy) Validate() error { if p == nil { return fmt.Errorf("routing policy 为空") } if len(p.TierFactors) == 0 { return fmt.Errorf("tierFactors 不能为空") } for t, f := range p.TierFactors { if t != TierPrimary && t != TierSecondary && t != TierReserve { return fmt.Errorf("未知层级: %s", t) } if f < 0 { return fmt.Errorf("tierFactors[%s] 不能为负: %v", t, f) } } if f, ok := p.TierFactors[TierPrimary]; !ok || f <= 0 { return fmt.Errorf("tierFactors.primary 必须 > 0") } if p.Health.Alpha <= 0 { return fmt.Errorf("health.alpha 必须 > 0") } if p.Load.Gamma < 0 { return fmt.Errorf("load.gamma 不能为负") } if p.Cost.Beta < 0 || p.Cost.Min <= 0 || p.Cost.Max < p.Cost.Min { return fmt.Errorf("cost 参数非法: beta=%v min=%v max=%v", p.Cost.Beta, p.Cost.Min, p.Cost.Max) } if p.Ramp.Floor < 0 || p.Ramp.Floor > 1 { return fmt.Errorf("ramp.floor 必须在 [0,1]") } if p.Binding.TTLSeconds <= 0 { return fmt.Errorf("binding.ttlSeconds 必须 > 0") } if p.Binding.KeepHealthMin < 0 || p.Binding.KeepHealthMin > 1 || p.Binding.KeepHeadroomMin < 0 || p.Binding.KeepHeadroomMin > 1 { return fmt.Errorf("binding 守卫阈值必须在 [0,1]") } r := p.Retry if r.InPlaceBudget < 0 || r.FailoverBudget < 0 || r.CredRotateBudget < 0 || r.UnsafeInPlaceBudget < 0 || r.UnsafeFailoverBudget < 0 { return fmt.Errorf("retry 预算不能为负") } if r.TotalDeadlineSeconds <= 0 || r.BackoffBaseMs <= 0 || r.BackoffMaxMs < r.BackoffBaseMs { return fmt.Errorf("retry 时间参数非法") } b := p.Breaker if b.WindowSeconds <= 0 || b.FailThreshold <= 0 || b.ModelFailThreshold <= 0 || b.CooldownSeconds <= 0 || b.CooldownMaxSeconds < b.CooldownSeconds || b.ProbeWindowSeconds <= 0 { return fmt.Errorf("breaker 参数非法") } if p.Degrade.MaxReplicas <= 0 { return fmt.Errorf("degrade.maxReplicas 必须 > 0") } // proto 偏好因子必须为 (0,1]:>1 会反向惩罚匹配渠道,=0 会把不匹配渠道硬排除 if p.Proto.ResponsesMismatch <= 0 || p.Proto.ResponsesMismatch > 1 || p.Proto.ChatBridgeMismatch <= 0 || p.Proto.ChatBridgeMismatch > 1 { return fmt.Errorf("proto 因子必须在 (0,1]: responsesMismatch=%v chatBridgeMismatch=%v", p.Proto.ResponsesMismatch, p.Proto.ChatBridgeMismatch) } return nil } // --------------------------------------------------------------------------- // 重试决策 FSM // --------------------------------------------------------------------------- // RetryDecision 重试决策。 type RetryDecision int const ( DecisionAbort RetryDecision = iota // 终止,返回错误 DecisionInPlaceRetry // 同渠道同 Key 退避重试 DecisionRotateCredential // 同渠道换下一个 Key DecisionFailover // 排除当前渠道重新选择 ) // String 实现 fmt.Stringer。 func (d RetryDecision) String() string { switch d { case DecisionInPlaceRetry: return "inplace" case DecisionRotateCredential: return "cred_rotate" case DecisionFailover: return "failover" default: return "abort" } } // AttemptState 请求级重试状态。原地与凭证轮换计数按当前渠道计,换渠道后由协调器清零。 type AttemptState struct { InPlaceUsed int // 当前渠道已用原地重试次数 CredRotationsUsed int // 当前渠道已用凭证轮换次数 FailoverUsed int // 已换渠道次数 ElapsedMs int64 // 请求已耗时(毫秒) HasAlternateKey bool // 当前渠道是否还有可用的其它 Key(未冷却、未在本请求排除) ResponseStarted bool // 是否已向客户端写出响应字节 } // Decide 重试决策 FSM(纯函数)。返回决策与建议退避毫秒数(不含 jitter,由协调器叠加)。 // // 硬规则优先级(高于错误分类): // 1. 已向客户端写出响应 → Abort; // 2. ReplayUnsafe + MaybeSent → Abort(状态码无关); // 3. 总时限耗尽 → Abort。 func Decide(class ErrorClass, delivery DeliveryState, replay Replayability, retryAfterMs int64, s AttemptState, p RetryPolicy) (RetryDecision, int64) { // 硬规则 if s.ResponseStarted || delivery == DeliveryResponseStarted { return DecisionAbort, 0 } if replay == ReplayUnsafe && delivery == DeliveryMaybeSent { return DecisionAbort, 0 } if class == ErrClassClient || class == ErrClassNone { return DecisionAbort, 0 } if s.ElapsedMs >= int64(p.TotalDeadlineSeconds)*1000 { return DecisionAbort, 0 } // ReplayUnsafe 使用收紧预算 inPlaceBudget, failoverBudget := p.InPlaceBudget, p.FailoverBudget if replay == ReplayUnsafe { inPlaceBudget, failoverBudget = p.UnsafeInPlaceBudget, p.UnsafeFailoverBudget } // failover 是否允许:预算内,且 ReplayUnsafe 仅在明确未送达时允许 failoverAllowed := s.FailoverUsed < failoverBudget && !(replay == ReplayUnsafe && delivery != DeliveryNotSent) failoverOrAbort := func() (RetryDecision, int64) { if failoverAllowed { return DecisionFailover, 0 } return DecisionAbort, 0 } switch class { case ErrClassCredential: // 先轮换同渠道 Key,耗尽后按渠道级致命处理 if s.CredRotationsUsed < p.CredRotateBudget && s.HasAlternateKey { return DecisionRotateCredential, 0 } return failoverOrAbort() case ErrClassRateLimit: // Retry-After 足够短则原地等待一次,否则立即 failover if retryAfterMs > 0 && retryAfterMs <= p.RateLimitWaitMaxMs && s.InPlaceUsed < inPlaceBudget { return DecisionInPlaceRetry, retryAfterMs } return failoverOrAbort() case ErrClassTimeout: // 超时(含 504)不原地重试 return failoverOrAbort() case ErrClassTransient: // MaybeSent 禁止原地重试(可能已送达,原地重发有重复风险),只允许 failover if delivery != DeliveryMaybeSent && s.InPlaceUsed < inPlaceBudget { return DecisionInPlaceRetry, backoffMs(s.InPlaceUsed, p) } return failoverOrAbort() case ErrClassChannelFatal, ErrClassModelFatal: return failoverOrAbort() } return DecisionAbort, 0 } // backoffMs 指数退避曲线:min(base × 2^attempt, max)。jitter 由协调器叠加。 func backoffMs(attempt int, p RetryPolicy) int64 { b := p.BackoffBaseMs for range attempt { b *= 2 if b >= p.BackoffMaxMs { return p.BackoffMaxMs } } if b > p.BackoffMaxMs { return p.BackoffMaxMs } return b }