重试次数和状态码范围,是 new-api 网关里最容易被误解的一对配置:RetryTimes 不是简单的"失败重试几次",它决定的是渠道档位能下探多深;而状态码范围决定哪些错误值得降级。搞清这两者的关系,才能配出"免费档逐级兜底、付费档最后接盘"的稳定链路,避免多配一个数字就多一次无效等待。

1. 重试的本质:次数是档位索引

先看一个反直觉的事实:new-api 的重试不是"同一个渠道再来一次",而是按优先级档位逐级换渠道。源码里 retry 序号直接当作档位索引(priorities[retry]),每次失败就跳到下一个 priority 档。

因此,一次性请求的实际尝试次数 = RetryTimes + 1(retry 从 0 数起)。假设你的渠道档位如下:

priority 110  → 渠道 A(免费)
priority 100  → 渠道 B(免费)
priority 90   → 渠道 C(免费)
priority 80   → 渠道 D(免费)
priority 50   → 渠道 E(中转)
priority 40   → 渠道 F(中转)
priority 30   → 渠道 G(付费兜底)

共 7 个 unique 档位,retry 序号 0..6 正好走完全部:

RetryTimes 能触达的最后一档 说明
5 40(渠道 F) 到不了付费兜底
6 30(渠道 G) 完整覆盖 7 档,刚好
7 30(渠道 G,重复) 兜底渠道被多试一次,无增益

结论:RetryTimes 的最小值 = 档位数量 - 1。档位多了、RetryTimes 没跟上,付费兜底永远等不到;比档位数更大,只是让最后一档多被尝试一次,纯属浪费等待时间。

还有一个细节:retry 序号超出档位总数时会钳制到最低档(retry = len(priorities) - 1),所以 RetryTimes=7 的效果是"30 档被试两遍",而不是报错。

2. 决定哪些错误值得重试:状态码范围

new-api 的全局设置 AutomaticRetryStatusCodes 定义了"哪些状态码算可重试错误"。默认等价于:

100-199, 300-399, 401-407, 409-499, 500-503, 505-523, 525-599
永跳:504 / 524

容易被忽略的边界:

  • 429 在范围内(409-499)——限流错误原生就会触发降级重试;
  • 400 不在范围内——客户端参数类错误默认不重试;
  • 504 / 524 无论设什么都不可重试——网关超时/上游断连,重试无意义。

判定时序:错误先可能经过渠道级 status_code_mapping 改写,再进入 shouldRetry 判断最终状态码是否落在上面的范围里。

3. 一个历史包袱:429 不需要映射

很多教程会让你在渠道高级设置里加状态码映射,把 429 映射成 503:

{"400": "500", "429": "503"}

429→503 这半段其实是历史包袱。早期资料认为 429 不可重试,所以需要先映射成 503 才能触发降级;但按上文的全局范围,429 本就在 409-499 里,原样透出照样降级。实测去掉映射后,日志里限流错误从"503"变回"429",降级链完全不受影响

channel error (channel #8, status code: 429): inference exceeds tpm/rpm limit
channel error (channel #1, status code: 429): ...
SUCCESS: 7  use_channel: ["8","1","2","3","7"]

全挂时客户端看到的最终错误码也从 503 变回 429——对很多 SDK 来说,429 反而会触发它自己的退避逻辑,体验更合理。

4. 400 怎么进重试:全局比渠道映射更干净

400 不在默认重试范围,但真实场景里上游偶发 400(缺某个请求头、格式瞬时异常)是值得降级到下一档的。两个做法:

做法一:渠道级映射,把 400 改成 500:

{"400": "500"}

做法二:全局状态码范围直接加 400(推荐):

AutomaticRetryStatusCodes = 100-199,300-407,409-503,505-523,525-599

注意这里的等价合并:300-399 + 400-407 可写作 300-407409-499 + 500-503 可写作 409-503,边界精确——408 依然不可重试(起点 409),504 依然不可重试(终点 503)。

推荐做法二的理由:重试判定收口到一处全局配置,渠道上不再残留状态码改写,渠道配置只剩 header/param 改写,心智负担小得多。

代价也要知道:400 进重试后,必现 400(参数写死错)的请求会拖着走完整条重试链、每档白试一遍。这类错误通常改一次就好,影响可接受。

5. 最佳实践配置

以"多档免费渠道 + 付费兜底"的典型架构为例:

渠道层(配置源统一维护):
  - 档位按 priority 降序,数量 = RetryTimes + 1 或更少
  - header_override / param_override 按需改写,不做状态码映射

全局 options:
  RetryTimes                = 档位数 - 1
  AutomaticRetryStatusCodes = 100-199,300-407,409-503,505-523,525-599

改动纪律三条:

  • options 直改数据库后必须重启 new-api 才加载(后台界面改则立即生效);
  • 双实例部署时 options 不随渠道同步,两端要分别改、分别重启;
  • 渠道配置改动后确认内存缓存已刷新(重启或走管理 API),SQL 直改不会自动同步缓存。

6. 验证与排错

验证降级链是否按预期工作,看日志两行就够:

channel error (channel #8, status code: 429): inference exceeds tpm/rpm limit
record consume log: ... use_channel:["8","1","2","3","7"]
  • channel error 行确认"命中哪个渠道 + 上游原话";
  • consume 日志的 use_channel 数组确认"这一请求实际经历了哪些档位";
  • 想验证 400 可重试,构造一个会被上游 400 的请求(比如临时缺头),看它是否会继续下探而不是直接失败。

排查"模型频繁报错"时,永远先看 channel error 的渠道 id 和状态码,再决定动 RetryTimes 还是状态码范围——这两者分别是"能下探多深"和"什么值得下探"的问题,改错一个都会让链路失衡。