|
|
|
@@ -0,0 +1,648 @@ |
|
|
|
# 重试机制技术文档 |
|
|
|
|
|
|
|
> 本文档详细描述 new-api 的请求重试机制,包括重试流程、优先级降级、权重选择、状态码判断、跨组重试、渠道亲和性、计费退款、自动禁用等完整链路。 |
|
|
|
|
|
|
|
--- |
|
|
|
|
|
|
|
## 目录 |
|
|
|
|
|
|
|
1. [整体流程概述](#1-整体流程概述) |
|
|
|
2. [重试次数与配置](#2-重试次数与配置) |
|
|
|
3. [优先级降级机制](#3-优先级降级机制) |
|
|
|
4. [权重选择机制](#4-权重选择机制) |
|
|
|
5. [重试判断规则](#5-重试判断规则) |
|
|
|
6. [可重试状态码范围](#6-可重试状态码范围) |
|
|
|
7. [Auto-Group 跨组重试](#7-auto-group-跨组重试) |
|
|
|
8. [渠道亲和性与重试](#8-渠道亲和性与重试) |
|
|
|
9. [计费与退款](#9-计费与退款) |
|
|
|
10. [自动禁用渠道](#10-自动禁用渠道) |
|
|
|
11. [Task Relay 重试](#11-task-relay-重试) |
|
|
|
12. [RetryParam 状态管理](#12-retryparam-状态管理) |
|
|
|
|
|
|
|
--- |
|
|
|
|
|
|
|
## 1. 整体流程概述 |
|
|
|
|
|
|
|
### 请求完整链路 |
|
|
|
|
|
|
|
``` |
|
|
|
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`: |
|
|
|
|
|
|
|
```go |
|
|
|
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 执行退款 |
|
|
|
``` |
|
|
|
|
|
|
|
--- |
|
|
|
|
|
|
|
## 2. 重试次数与配置 |
|
|
|
|
|
|
|
### RetryTimes 配置 |
|
|
|
|
|
|
|
| 属性 | 值 | |
|
|
|
|------|---| |
|
|
|
| 变量 | `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` 时不会重试。 |
|
|
|
|
|
|
|
--- |
|
|
|
|
|
|
|
## 3. 优先级降级机制 |
|
|
|
|
|
|
|
### 核心算法 |
|
|
|
|
|
|
|
`model/channel_cache.go:125-142`: |
|
|
|
|
|
|
|
```go |
|
|
|
// 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 | |
|
|
|
|
|
|
|
--- |
|
|
|
|
|
|
|
## 4. 权重选择机制 |
|
|
|
|
|
|
|
### 加权随机算法 |
|
|
|
|
|
|
|
`model/channel_cache.go:145-188`: |
|
|
|
|
|
|
|
在同一优先级的候选渠道中,使用加权随机选择: |
|
|
|
|
|
|
|
```go |
|
|
|
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)在同一优先级: |
|
|
|
|
|
|
|
- totalWeight = 100 |
|
|
|
- randomWeight ∈ [0, 100) |
|
|
|
- A 被选中的概率 = 70% |
|
|
|
- B 被选中的概率 = 30% |
|
|
|
|
|
|
|
E2E 测试验证:30 次请求中,21 次到 A(70.0%),9 次到 B(30.0%),与配置完全一致。 |
|
|
|
|
|
|
|
--- |
|
|
|
|
|
|
|
## 5. 重试判断规则 |
|
|
|
|
|
|
|
### shouldRetry() 完整判断链 |
|
|
|
|
|
|
|
`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 Error(必重试) |
|
|
|
|
|
|
|
以下错误码**始终触发重试**,无论其他条件: |
|
|
|
|
|
|
|
| 错误码 | 说明 | |
|
|
|
|--------|------| |
|
|
|
| `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` | 响应超时 | |
|
|
|
|
|
|
|
### Skip-Retry Error(不重试) |
|
|
|
|
|
|
|
以下场景标记为 `skipRetry=true`,不触发重试: |
|
|
|
|
|
|
|
- 请求体读取失败 |
|
|
|
- 渠道未找到 |
|
|
|
- 计费/预扣费失败 |
|
|
|
- 敏感词检测 |
|
|
|
- 各种内部错误 |
|
|
|
|
|
|
|
--- |
|
|
|
|
|
|
|
## 6. 可重试状态码范围 |
|
|
|
|
|
|
|
### 默认配置 |
|
|
|
|
|
|
|
`setting/operation_setting/status_code_ranges.go:19-32`: |
|
|
|
|
|
|
|
```go |
|
|
|
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()` 会自动排序、合并重叠范围。 |
|
|
|
|
|
|
|
--- |
|
|
|
|
|
|
|
## 7. Auto-Group 跨组重试 |
|
|
|
|
|
|
|
### 触发条件 |
|
|
|
|
|
|
|
当 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`: |
|
|
|
|
|
|
|
```go |
|
|
|
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 |
|
|
|
} |
|
|
|
``` |
|
|
|
|
|
|
|
--- |
|
|
|
|
|
|
|
## 8. 渠道亲和性与重试 |
|
|
|
|
|
|
|
### 亲和性原理 |
|
|
|
|
|
|
|
渠道亲和性(Channel Affinity)是一种缓存机制:对于相同用户+模型的请求,优先使用上次成功的渠道,以保持会话连续性(如 Claude Code 的对话上下文)。 |
|
|
|
|
|
|
|
### 对重试的影响 |
|
|
|
|
|
|
|
`service/channel_affinity.go:493-506`: |
|
|
|
|
|
|
|
当亲和性选择的渠道请求失败时,可能**跳过重试**: |
|
|
|
|
|
|
|
``` |
|
|
|
ShouldSkipRetryAfterChannelAffinityFailure(c) |
|
|
|
→ 检查 ginKeyChannelAffinitySkipRetry 标记 |
|
|
|
→ true = 不重试(直接返回错误) |
|
|
|
→ false = 正常重试(默认行为) |
|
|
|
``` |
|
|
|
|
|
|
|
### 亲和性标记设置 |
|
|
|
|
|
|
|
`service/channel_affinity.go:508-532`: |
|
|
|
|
|
|
|
```go |
|
|
|
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` 的规则时,亲和性失败才会跳过重试。 |
|
|
|
|
|
|
|
### 亲和性记录时机 |
|
|
|
|
|
|
|
- **记录**:请求成功(status < 400)后,`middleware/distributor.go:135` 调用 `RecordChannelAffinity()` |
|
|
|
- **查询**:初始渠道选择时,`distributor.go:86-107` 调用 `GetPreferredChannelByAffinity()` |
|
|
|
|
|
|
|
--- |
|
|
|
|
|
|
|
## 9. 计费与退款 |
|
|
|
|
|
|
|
### 计费生命周期 |
|
|
|
|
|
|
|
``` |
|
|
|
PreConsumeBilling() ← 预扣费(retry 循环之前) |
|
|
|
│ |
|
|
|
├─ Retry 0: 尝试请求 ─── 成功 → Settle() 结算 → 结束 |
|
|
|
│ └─ 失败 → 继续重试 |
|
|
|
├─ Retry 1: 尝试请求 ─── 成功 → Settle() 结算 → 结束 |
|
|
|
│ └─ 失败 → 继续重试 |
|
|
|
├─ ... |
|
|
|
│ |
|
|
|
└─ 全部失败 → BillingSession.Refund() ← defer 函数,只执行一次 |
|
|
|
``` |
|
|
|
|
|
|
|
### 关键特性 |
|
|
|
|
|
|
|
1. **预扣费只一次**:在 retry 循环开始前执行 `PreConsumeBilling()`(`controller/relay.go:164`) |
|
|
|
2. **多次重试复用同一 BillingSession**:不重复扣费 |
|
|
|
3. **退款只一次**:通过 `defer` 函数(`controller/relay.go:170-179`),仅在 `newAPIError != nil` 时触发 |
|
|
|
4. **退款幂等**:`BillingSession.Refund()` 检查 `settled`、`refunded`、`fundingSettled` 标记 |
|
|
|
5. **异步执行**:退款操作通过 `gopool.Go()` 异步执行 |
|
|
|
|
|
|
|
### 退款流程代码 |
|
|
|
|
|
|
|
```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 时扣费。 |
|
|
|
|
|
|
|
--- |
|
|
|
|
|
|
|
## 10. 自动禁用渠道 |
|
|
|
|
|
|
|
### 触发时机 |
|
|
|
|
|
|
|
每次 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` 中的关键词 | |
|
|
|
|
|
|
|
### 执行方式 |
|
|
|
|
|
|
|
```go |
|
|
|
if service.ShouldDisableChannel(channelType, err) && channelError.AutoBan { |
|
|
|
gopool.Go(func() { |
|
|
|
service.DisableChannel(channelError, err.ErrorWithStatusCode()) |
|
|
|
}) |
|
|
|
} |
|
|
|
``` |
|
|
|
|
|
|
|
- **异步执行**:通过 `gopool.Go()` 不阻塞请求处理 |
|
|
|
- **AutoBan 控制**:每个渠道可配置是否允许自动禁用(`auto_ban` 字段) |
|
|
|
- **自动恢复**:当 `AutomaticEnableChannelEnabled=true` 时,渠道后续请求成功会自动重新启用 |
|
|
|
|
|
|
|
### 自动禁用状态码范围 |
|
|
|
|
|
|
|
```go |
|
|
|
AutomaticDisableStatusCodeRanges = []StatusCodeRange{{Start: 401, End: 401}} |
|
|
|
``` |
|
|
|
|
|
|
|
默认只有 401(未授权)触发自动禁用。可自定义。 |
|
|
|
|
|
|
|
--- |
|
|
|
|
|
|
|
## 11. Task Relay 重试 |
|
|
|
|
|
|
|
### 适用场景 |
|
|
|
|
|
|
|
异步任务提交(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 | **重试**(兜底) | |
|
|
|
|
|
|
|
### Locked Channel |
|
|
|
|
|
|
|
Task Relay 支持 `LockedChannel`:如果 relayInfo 中有锁定的渠道,所有重试都使用该渠道(不切换)。 |
|
|
|
|
|
|
|
--- |
|
|
|
|
|
|
|
## 12. RetryParam 状态管理 |
|
|
|
|
|
|
|
### 结构体定义 |
|
|
|
|
|
|
|
`service/channel_select.go:14-20`: |
|
|
|
|
|
|
|
```go |
|
|
|
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` | |
|
|
|
|
|
|
|
### resetNextTry 机制 |
|
|
|
|
|
|
|
用于 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 直接从次高优先级开始,跳过了最高优先级的渠道。 |
|
|
|
|
|
|
|
### Context Key |
|
|
|
|
|
|
|
`constant/context_key.go`: |
|
|
|
|
|
|
|
```go |
|
|
|
ContextKeyAutoGroup // 当前 auto-group 名称 |
|
|
|
ContextKeyAutoGroupIndex // 当前组索引 |
|
|
|
ContextKeyAutoGroupRetryIndex // 当前组开始时的全局 retry 数 |
|
|
|
ContextKeyTokenCrossGroupRetry // 是否启用跨组重试 |
|
|
|
``` |
|
|
|
|
|
|
|
--- |
|
|
|
|
|
|
|
## 附录:重试链路追踪 |
|
|
|
|
|
|
|
### 日志 |
|
|
|
|
|
|
|
重试过程中使用了多个渠道时,循环结束后会打印重试链路: |
|
|
|
|
|
|
|
```go |
|
|
|
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`),包含: |
|
|
|
|
|
|
|
- 渠道 ID、名称、类型 |
|
|
|
- 状态码、错误类型、错误码 |
|
|
|
- 上游请求 ID |
|
|
|
- `use_channel` 数组(重试链路) |
|
|
|
- 渠道亲和性信息 |