本文档详细描述 new-api 的请求重试机制,包括重试流程、优先级降级、权重选择、状态码判断、跨组重试、渠道亲和性、计费退款、自动禁用等完整链路。
HTTP Request
→ router (SetApiRouter)
→ middleware/auth.go (认证)
→ middleware/distributor.go (初始渠道选择,retry=0)
→ middleware/rate_limit.go (限流)
→ controller/relay.go Relay()
→ PreConsumeBilling() (预扣费)
→ retry loop (retry=0 ~ RetryTimes):
→ getChannel() (retry=0 用中间件选好的,retry≥1 重新选择)
→ restore request body
→ relayHandler() (发送到上游)
→ 成功则 return
→ 失败则 processChannelError() + shouldRetry() 判断
→ 全部失败 → BillingSession.Refund() (退款)
| 文件 | 函数/逻辑 | 行号 |
|---|---|---|
controller/relay.go |
Relay() 重试循环 |
181-234 |
controller/relay.go |
getChannel() 渠道获取 |
283-313 |
controller/relay.go |
shouldRetry() 重试判断 |
315-339 |
controller/relay.go |
processChannelError() 错误处理 |
341-392 |
controller/relay.go |
shouldRetryTaskRelay() 任务重试判断 |
604-641 |
model/channel_cache.go |
GetRandomSatisfiedChannel() 优先级+权重选择 |
96-191 |
service/channel_select.go |
CacheGetRandomSatisfiedChannel() 跨组重试编排 |
83-162 |
service/channel_select.go |
RetryParam 结构体 |
14-46 |
middleware/distributor.go |
初始渠道选择 | 60-137 |
setting/operation_setting/status_code_ranges.go |
状态码范围配置 | 1-197 |
service/channel_affinity.go |
渠道亲和性重试判断 | 493-532 |
service/billing_session.go |
计费会话 | 1-115 |
service/channel.go |
ShouldDisableChannel() 自动禁用 |
47-102 |
controller/relay.go:181-234:
retryParam := &service.RetryParam{
Ctx: c,
TokenGroup: relayInfo.TokenGroup,
ModelName: relayInfo.OriginModelName,
Retry: common.GetPointer(0),
}
for ; retryParam.GetRetry() <= common.RetryTimes; retryParam.IncreaseRetry() {
channel, channelErr := getChannel(c, relayInfo, retryParam)
// ... 渠道选择、body 恢复、请求转发 ...
if newAPIError == nil {
return // 成功,直接返回
}
processChannelError(c, ...) // 记录错误 + 可能自动禁用
if !shouldRetry(c, newAPIError, common.RetryTimes-retryParam.GetRetry()) {
break // 不可重试,退出循环
}
}
// 循环结束,defer 执行退款
| 属性 | 值 |
|---|---|
| 变量 | common.RetryTimes |
| 定义 | common/constants.go:122 |
| 默认值 | 0(不重试,只做 1 次初始尝试) |
| 存储位置 | 数据库 options 表,key=RetryTimes |
| 加载方式 | model/option.go:496-497,strconv.Atoi(value) |
| 配置入口 | 管理后台 → 运营设置 |
总尝试次数 = RetryTimes + 1
循环条件 retryParam.GetRetry() <= common.RetryTimes:
| RetryTimes 值 | 循环范围 | 总尝试次数 |
|---|---|---|
| 0 | retry=0 | 1 次(仅初始尝试) |
| 1 | retry=0, 1 | 2 次 |
| 3 | retry=0, 1, 2, 3 | 4 次 |
| 5 | retry=0, 1, 2, 3, 4, 5 | 6 次 |
shouldRetry() 的 retryTimes 参数 = common.RetryTimes - retryParam.GetRetry(),表示剩余重试次数。当 retryTimes <= 0 时不会重试。
model/channel_cache.go:125-142:
// 1. 收集所有唯一优先级值
uniquePriorities := make(map[int]bool)
for _, channelId := range channels {
uniquePriorities[int(channel.GetPriority())] = true
}
// 2. 降序排序(最高优先级在前)
sort.Sort(sort.Reverse(sort.IntSlice(sortedUniquePriorities)))
// 3. retry index 映射到优先级层级
if retry >= len(uniquePriorities) {
retry = len(uniquePriorities) - 1 // 限制为最低优先级
}
targetPriority := int64(sortedUniquePriorities[retry])
retry=0 → 最高优先级(sortedUniquePriorities[0])
retry=1 → 次高优先级(sortedUniquePriorities[1])
...
retry ≥ 层级数 → 降为最低优先级(不再降级)
假设某模型有 5 个渠道:
| 渠道 | 优先级 | 权重 |
|---|---|---|
| A | 100 | 50 |
| B | 100 | 30 |
| C | 50 | 80 |
| D | 10 | 20 |
| E | 10 | 10 |
唯一优先级(降序):[100, 50, 10]
| 尝试 | retry 值 | 选择优先级 | 候选渠道 |
|---|---|---|---|
| 第 1 次 | 0 | 100 | A, B |
| 第 2 次 | 1 | 50 | C |
| 第 3 次 | 2 | 10 | D, E |
| 第 4 次+ | 超出范围,降为 2(最低优先级) | 10 | D, E |
model/channel_cache.go:145-188:
在同一优先级的候选渠道中,使用加权随机选择:
totalWeight := sumWeight * smoothingFactor
randomWeight := rand.Intn(totalWeight)
for _, channel := range targetChannels {
randomWeight -= channel.GetWeight()*smoothingFactor + smoothingAdjustment
if randomWeight < 0 {
return channel, nil
}
}
model/channel_cache.go:162-174:
| 场景 | 处理 |
|---|---|
| 所有渠道 weight=0 | 每个 channel 的 effective weight = 100(均等选择) |
| 平均 weight < 10 | 乘以 smoothingFactor=100,提高区分度 |
| 正常 weight | 直接使用配置的 weight 值 |
渠道 A(weight=70)和 渠道 B(weight=30)在同一优先级:
E2E 测试验证:30 次请求中,21 次到 A(70.0%),9 次到 B(30.0%),与配置完全一致。
controller/relay.go:315-339:
shouldRetry(c, openaiErr, retryTimes)
│
├─ openaiErr == nil? → 不重试 (false)
│
├─ 渠道亲和性跳过? → 不重试 (false)
│ ShouldSkipRetryAfterChannelAffinityFailure(c)
│
├─ 是 channel error? → 必须重试 (true) ← 优先级最高
│ IsChannelError(openaiErr) 错误码以 "channel:" 开头
│
├─ 是 skip-retry error? → 不重试 (false)
│ IsSkipRetryError(openaiErr) skipRetry=true 标记
│
├─ retryTimes <= 0? → 不重试 (false)
│ 剩余重试次数用完
│
├─ 2xx 状态码? → 不重试 (false)
│ code >= 200 && code < 300
│
├─ 非 HTTP 状态码? → 重试 (true)
│ code < 100 || code > 599 连接/传输层错误
│
└─ 按状态码范围判断 → ShouldRetryByStatusCode(code)
以下错误码始终触发重试,无论其他条件:
| 错误码 | 说明 |
|---|---|
channel:no_available_key |
渠道无可用 API Key |
channel:param_override_invalid |
参数覆写无效 |
channel:header_override_invalid |
Header 覆写无效 |
channel:model_mapped_error |
模型映射错误 |
channel:aws_client_error |
AWS 客户端错误 |
channel:invalid_key |
无效的 API Key |
channel:response_time_exceeded |
响应超时 |
以下场景标记为 skipRetry=true,不触发重试:
setting/operation_setting/status_code_ranges.go:19-32:
AutomaticRetryStatusCodeRanges = []StatusCodeRange{
{Start: 100, End: 199}, // 1xx 信息响应
{Start: 300, End: 399}, // 3xx 重定向
{Start: 401, End: 407}, // 认证错误
{Start: 409, End: 499}, // 409-499 客户端错误
{Start: 500, End: 503}, // 500-503 服务端错误
{Start: 505, End: 523}, // 505-523 服务端错误
{Start: 525, End: 599}, // 525-599 服务端错误
}
alwaysSkipRetryStatusCodes = {504, 524} // 超时不重试
| 状态码范围 | 是否重试 | 说明 |
|---|---|---|
| 100-199 | 重试 | 信息响应 |
| 200-299 | 不重试 | 成功响应 |
| 300-399 | 重试 | 重定向 |
| 400 | 不重试 | 不在范围内 |
| 401-407 | 重试 | 认证错误 |
| 408 | 不重试 | Request Timeout(不在范围内) |
| 409-499 | 重试 | 各种客户端错误 |
| 500-503 | 重试 | 服务端错误 |
| 504 | 不重试 | Gateway Timeout(always-skip) |
| 505-523 | 重试 | 服务端错误 |
| 524 | 不重试 | Cloudflare Timeout(always-skip) |
| 525-599 | 重试 | 服务端错误 |
| <100 或 >599 | 重试 | 非 HTTP 状态(传输层错误) |
管理员可通过运营设置修改 AutomaticRetryStatusCodeRanges,格式为逗号分隔的范围:
"500-503,525-599" # 范围表示
"429" # 单个状态码
"500-503,429,525-599" # 混合使用
解析函数 ParseHTTPStatusCodeRanges() 会自动排序、合并重叠范围。
当 Token 的 group 字段为 "auto" 时启用。系统会在多个渠道组之间依次尝试,每个组内耗尽所有优先级后才切换到下一个组。
需要在 Token 上启用 cross_group_retry,存储在 context key ContextKeyTokenCrossGroupRetry 中。未启用时只在第一个匹配的组内重试。
通过 Gin Context 中的三个 key 跟踪状态:
| Context Key | 说明 |
|---|---|
ContextKeyAutoGroupIndex |
当前正在尝试的组索引 |
ContextKeyAutoGroupRetryIndex |
切换到当前组时的全局 retry 计数 |
ContextKeyAutoGroup |
当前选中的组名 |
2 个组,每组 2 个优先级,RetryTimes=3:
Retry=0: GroupA, priority0 (startRetryIndex=0, priorityRetry=0)
→ GroupA 最高优先级渠道
Retry=1: GroupA, priority1 (startRetryIndex=0, priorityRetry=1)
→ GroupA 次高优先级渠道
Retry=2: GroupA 优先级用完 → GroupB, priority0 (startRetryIndex=2, priorityRetry=0)
→ GroupB 最高优先级渠道
Retry=3: GroupB, priority1 (startRetryIndex=2, priorityRetry=1)
→ GroupB 次高优先级渠道
service/channel_select.go:106-154:
for i := startGroupIndex; i < len(autoGroups); i++ {
priorityRetry := param.GetRetry()
if i > startGroupIndex {
priorityRetry = 0 // 新组从最高优先级开始
}
channel, _ = model.GetRandomSatisfiedChannel(autoGroup, param.ModelName, priorityRetry)
if channel == nil {
// 当前组没有该模型/优先级的可用渠道,切换到下一组
common.SetContextKey(param.Ctx, constant.ContextKeyAutoGroupIndex, i+1)
param.SetRetry(0)
continue
}
// 跨组重试:当前组重试次数耗尽,准备切换
if crossGroupRetry && priorityRetry >= common.RetryTimes {
common.SetContextKey(param.Ctx, constant.ContextKeyAutoGroupIndex, i+1)
param.SetRetry(0)
param.ResetRetryNextTry() // 防止外层循环再 +1
}
break
}
渠道亲和性(Channel Affinity)是一种缓存机制:对于相同用户+模型的请求,优先使用上次成功的渠道,以保持会话连续性(如 Claude Code 的对话上下文)。
service/channel_affinity.go:493-506:
当亲和性选择的渠道请求失败时,可能跳过重试:
ShouldSkipRetryAfterChannelAffinityFailure(c)
→ 检查 ginKeyChannelAffinitySkipRetry 标记
→ true = 不重试(直接返回错误)
→ false = 正常重试(默认行为)
service/channel_affinity.go:508-532:
func MarkChannelAffinityUsed(c *gin.Context, selectedGroup string, channelID int) {
// ...
c.Set(ginKeyChannelAffinitySkipRetry, meta.SkipRetry)
// ...
}
标记值来自亲和性规则的 SkipRetryOnFailure 字段。
| 规则名 | 匹配模型 | 端点 | 亲和性 Key | SkipRetryOnFailure |
|---|---|---|---|---|
| codex trace | ^gpt-.*$ |
/v1/responses |
prompt_cache_key |
false |
| claude code trace | ^claude-.*$ |
/v1/messages |
metadata.user_id |
false |
默认两条规则都不阻止重试。只有管理员创建了 SkipRetryOnFailure=true 的规则时,亲和性失败才会跳过重试。
middleware/distributor.go:135 调用 RecordChannelAffinity()distributor.go:86-107 调用 GetPreferredChannelByAffinity()PreConsumeBilling() ← 预扣费(retry 循环之前)
│
├─ Retry 0: 尝试请求 ─── 成功 → Settle() 结算 → 结束
│ └─ 失败 → 继续重试
├─ Retry 1: 尝试请求 ─── 成功 → Settle() 结算 → 结束
│ └─ 失败 → 继续重试
├─ ...
│
└─ 全部失败 → BillingSession.Refund() ← defer 函数,只执行一次
PreConsumeBilling()(controller/relay.go:164)defer 函数(controller/relay.go:170-179),仅在 newAPIError != nil 时触发BillingSession.Refund() 检查 settled、refunded、fundingSettled 标记gopool.Go() 异步执行defer func() {
if newAPIError != nil {
newAPIError = service.NormalizeViolationFeeError(newAPIError)
if relayInfo.Billing != nil {
relayInfo.Billing.Refund(c) // 退还预扣额度
}
service.ChargeViolationFeeIfNeeded(c, relayInfo, newAPIError)
}
}()
高余额用户(Trust Quota Bypass)跳过预扣费,preConsumedQuota 为 0,直接在 Settle 时扣费。
每次 relay 失败后调用 processChannelError()(controller/relay.go:229),其中包含自动禁用逻辑。
service/channel.go:47-102 ShouldDisableChannel():
| 条件 | 说明 |
|---|---|
AutomaticDisableChannelEnabled = false |
全局开关关闭则不触发 |
IsChannelError(err) |
channel:* 错误码,始终触发 |
ShouldDisableByStatusCode(code) |
默认 401 触发 |
| 403 + Gemini 渠道 | Gemini 特有 |
| 错误码匹配 | invalid_api_key、account_deactivated、billing_not_active、Arrearage 等 |
| 错误类型匹配 | insufficient_quota、authentication_error、permission_error、forbidden |
| 关键词匹配 | AutomaticDisableKeywords 中的关键词 |
if service.ShouldDisableChannel(channelType, err) && channelError.AutoBan {
gopool.Go(func() {
service.DisableChannel(channelError, err.ErrorWithStatusCode())
})
}
gopool.Go() 不阻塞请求处理auto_ban 字段)AutomaticEnableChannelEnabled=true 时,渠道后续请求成功会自动重新启用AutomaticDisableStatusCodeRanges = []StatusCodeRange{{Start: 401, End: 401}}
默认只有 401(未授权)触发自动禁用。可自定义。
异步任务提交(Midjourney、Suno 等非实时请求),使用 RelayTask() 函数(controller/relay.go:476)。
shouldRetryTaskRelay()(controller/relay.go:604-641)与主 shouldRetry() 的区别:
| 条件 | shouldRetry() | shouldRetryTaskRelay() |
|---|---|---|
| channel error | 必重试 | 不检查 |
| skip-retry 标记 | 不重试 | 不检查 |
| 429 | 按状态码范围判断 | 必重试 |
| 307 | 按状态码范围判断 | 必重试 |
| 5xx | 按状态码范围判断 | 重试(排除 504/524) |
| 400 | 不在默认范围,不重试 | 明确不重试 |
| 408 | 不在默认范围,不重试 | 明确不重试(Azure 超时) |
| LocalError | 不适用 | 不重试 |
| 渠道亲和性 | 检查 | 检查 |
| 2xx | 不重试 | 不重试 |
| 其他 | 委托 ShouldRetryByStatusCode | 重试(兜底) |
Task Relay 支持 LockedChannel:如果 relayInfo 中有锁定的渠道,所有重试都使用该渠道(不切换)。
service/channel_select.go:14-20:
type RetryParam struct {
Ctx *gin.Context
TokenGroup string
ModelName string
Retry *int // 当前重试计数
resetNextTry bool // 标记下一次 IncreaseRetry 是否跳过
}
| 方法 | 说明 |
|---|---|
GetRetry() int |
返回当前 retry 值,nil 时返回 0 |
SetRetry(retry int) |
直接设置 retry 值 |
IncreaseRetry() |
retry + 1;若 resetNextTry=true 则跳过并重置标记 |
ResetRetryNextTry() |
设置 resetNextTry=true |
用于 Auto-Group 跨组重试时的状态管理,防止 retry 计数器被双重递增:
场景:GroupA 重试用完,准备切换到 GroupB
1. CacheGetRandomSatisfiedChannel 检测到 priorityRetry >= RetryTimes
2. 调用 param.SetRetry(0) ← 重置计数器
3. 调用 param.ResetRetryNextTry() ← 标记下次跳过
4. 外层 for 循环调用 IncreaseRetry()
5. 检测到 resetNextTry=true → 跳过递增,重置标记
6. 结果:retry 保持为 0,GroupB 从最高优先级开始
如果不跳过,retry 会从 0 变成 1,导致 GroupB 直接从次高优先级开始,跳过了最高优先级的渠道。
constant/context_key.go:
ContextKeyAutoGroup // 当前 auto-group 名称
ContextKeyAutoGroupIndex // 当前组索引
ContextKeyAutoGroupRetryIndex // 当前组开始时的全局 retry 数
ContextKeyTokenCrossGroupRetry // 是否启用跨组重试
重试过程中使用了多个渠道时,循环结束后会打印重试链路:
useChannel := c.GetStringSlice("use_channel")
if len(useChannel) > 1 {
retryLogStr := fmt.Sprintf("重试:%s", strings.Trim(...))
logger.LogInfo(c, retryLogStr)
}
输出示例:重试:5->12->8(渠道 ID 5 失败后重试到 12,再到 8)。
每次失败的渠道信息都会记录到数据库 logs 表(需开启 ErrorLogEnabled),包含:
use_channel 数组(重试链路)