错误处理
依据 retry.strategy、retry.retryable 与 billing.status 决定下一步,而不是为每个错误码写一个分支。
每个 RouteMux 错误都用三个结构化字段回答「我的代码接下来该怎么办」。请按这些字段判断, 不要为每个错误码各写一个分支——错误码会随产品增加,而按这些字段写的代码不需要跟着改。
一段能直接抄的处理逻辑
async function callWithRouteMuxHandling(send) {
for (let attempt = 0; ; attempt++) {
const res = await send();
if (res.ok) return res;
const { error } = await res.json();
switch (error.retry.strategy) {
case 'BACKOFF': {
if (attempt >= 5) throw new Error(error.code);
// 服务端给了延迟就用它的
const base = error.retry.retry_after_seconds ?? Math.min(2 ** attempt, 30);
await sleep((base + Math.random()) * 1000); // 加抖动
continue;
}
case 'RETRY':
if (error.retry.retryable && attempt < 3) { await sleep(500); continue; }
throw new Error(error.code);
case 'TOP_UP': return promptUserToAddCredit(error);
case 'SWITCH_MODEL': return retryWithAnotherModel(error);
case 'AUTHENTICATE': return fixApiKey(error);
case 'FIX_REQUEST': throw new Error(error.message); // 重试不会有任何变化
case 'CONTACT_SUPPORT':
throw new Error(`${error.reference} · ${error.request_id}`);
case 'NONE':
return null; // 没有需要你处理的失败
}
}
}两个 retry 字段回答的是不同问题
| 字段 | 它回答什么 |
|---|---|
retry.retryable | 同一个请求原样再发一次,有没有可能成功? |
retry.strategy | 要真的成功,该改哪里? |
两者相互独立。看几个"取值不一致"的例子就明白了:
| 错误 | retryable | strategy | 为什么两个都对 |
|---|---|---|---|
MODEL_UNAVAILABLE | true | SWITCH_MODEL | 模型可能会回来,重试不算白费;但换个模型更快。 |
JOB_ARTIFACT_NOT_FOUND | false | RETRY | 那个产物已经永久删除,重查拿不到;有意义的是重新发起一个任务。 |
PLATFORM_INTERNAL_ERROR | true | CONTACT_SUPPORT | 值得重试一次;但反复出现就是我们的问题,不是你的。 |
八个 strategy 分别是什么意思
retry.strategy | 含义 | 你该做什么 | 例 |
|---|---|---|---|
FIX_REQUEST | 请求本身不被接受 | 改请求。重试没有任何意义 | REQUEST_INVALID、MODEL_VISION_UNSUPPORTED |
AUTHENTICATE | 凭据有问题 | 修或更换 API key,不要重试 | AUTH_INVALID_API_KEY、AUTH_API_KEY_EXPIRED |
BACKOFF | 暂时拥堵 | 等待后重试,遵守 Retry-After | MODEL_BUSY、LIMIT_RATE_EXCEEDED |
RETRY | 瞬时失败 | 可以重试,无强制等待 | REQUEST_TIMEOUT、JOB_FAILED |
SWITCH_MODEL | 这个模型处理不了 | 换一个模型 | MODEL_NOT_FOUND、MODEL_ACCESS_DENIED |
TOP_UP | 余额不足 | 充值后重新发送 | BILLING_INSUFFICIENT_CREDITS |
CONTACT_SUPPORT | 你这边改不了 | 带上 reference 和 request_id 联系客服 | AUTH_ACCOUNT_SUSPENDED、PLATFORM_PRICING_INCOMPLETE |
NONE | 无需处理 | 继续 | JOB_NOT_CANCELLABLE |
退避要怎么退
当一个错误带了等待时长,RouteMux 会同时放在两个位置,用你客户端好取的那个:
- 响应头
Retry-After,单位秒 - 响应体里的
error.retry.retry_after_seconds
例如 MODEL_BUSY 给的是 5。没有给时长时,请用指数退避,并且一定要加抖动——
否则同一时刻失败的所有客户端会在同一时刻重试,把你正要躲开的那个尖峰原样重建一次。
这次扣钱了吗
error.billing.status 告诉你这次失败的请求有没有花钱。
| 取值 | 含义 | 能安全重试吗 |
|---|---|---|
NOT_BILLED | 没有扣费 | 可以,不存在重复扣费风险 |
BILLED | 失败前已经消耗了额度 | 重试会再次计费,请自行判断 |
目前目录里的每一个错误都是 NOT_BILLED:被这些错误码拒绝的请求不收费,所以按
strategy 重试不会造成重复扣费。BILLED 只会出现在请求日志里——那是失败发生前用量
已经结算的情况。
涉及钱的问题,以请求日志为准。唯一可能出现"响应体与最终计费不一致"的情形,是流式 请求在已经产出 token 之后失败。
两件不要做的事
不要按 message 分支。 它会随 Accept-Language 本地化,也会随产品迭代改写。
请按 code 分支,message 只拿来展示给人看。
不要重试 FIX_REQUEST 类错误。 请求没有任何变化,结果也不会变。目录里大约一半的
错误码属于这一类——一个无脑「任何错误都重试三次」的封装,会把重试预算全花在根本不可能
成功的请求上。
查具体某个错误
全部错误码与 RMX-… 编号都在错误参考页,可搜索,每条都有永久锚点,
可以直接把链接发给客服。