限流快照和重试延迟是两条不同数据流:前者持续告诉 UI 额度窗口,后者只控制一次失败后的等待。Codex 优先采用错误携带的 delay,否则使用带抖动的指数退避。
具体问题与启用条件
本节拆开 rate-limit telemetry、服务端等待提示和外层重试计数;429 用量耗尽等不可重试错误的分类在 5.22。
| 条件来源 | 决定字段或状态 | 对本机制的影响 |
|---|---|---|
| HTTP Header | x--primary/secondary-、credits | 解析一个或多个 RateLimitSnapshot |
| WS 事件 | codex.rate_limits | 流中更新快照 |
| failed event | code=rate_limit_exceeded 且 message 含 seconds/ms | 解析 retry delay |
协议、类型与状态所有权
| 状态或协议 | 所有者 | 生命周期 | 关键不变量 |
|---|---|---|---|
| RateLimitSnapshot | Session state | 跨请求更新 | 按 limit_id 保存额度视图 |
| should_emit_token_count | 一次采样 | 到工具 drain 后 | 合并 usage 与 rate limit UI 更新 |
| retry counter | run_sampling_request | 一次逻辑请求 | 最多 stream_max_retries |
| retry delay | CodexErr | 单次错误 | 服务端值优先 |
机制调用链如下:
响应 Header 或 codex.rate_limits
→ parse snapshot
→ Session.record_rate_limits_info
→ 延后 TokenCount/limit UI 事件
失败 rate_limit_exceeded
→ 从 message 解析 seconds/ms delay
→ CodexErr.with_retry_delay
→ 外层检查 retryable/budget
→ delay 或 jittered backoff
→ 重建 Prompt 并重试
机制怎样工作
Header parser支持默认 codex 限额族,也扫描其他 x-<limit>-primary-used-percent 前缀并规范化为 limit_id;每个快照可含 primary、secondary、credits、窗口分钟和 reset timestamp。WebSocket 的 codex.rate_limits 事件走专用解析器。Core 收到快照先更新状态,不立刻发 TokenCount,因为同一 Completed 还会带 usage;等 pending tools 结束后统一发,避免重复或在等待用户输入时错误显示进度。
response.failed 只有 code 为 rate_limit_exceeded 时尝试从 message 解析 s/second/ms;它产生 Retryable error 并保留 delay。重试函数先检查预算,再用 err.retry_delay(),没有才调用带抖动 backoff。注意 HTTP 429 若 body 表示 usage_limit_reached 会映射成不可重试 UsageLimitReached;一般 429 在底层请求重试耗尽后变成 RetryLimit,也不是无限外层重试。
Python 风格伪代码
这段伪代码保留生产实现中会改变结果的状态、分支和异步边界;认证 SDK、遥测字段和 Rust 所有权样板被折叠为明确的领域对象。
async def handle_rate_limit_event(snapshot: RateLimitSnapshot, sample: SampleState):
await session.rate_limits.update(snapshot.limit_id or "codex", snapshot)
sample.emit_token_count_later = True
async def retry_stream(error: CodexError, retries: int, maximum: int) -> int:
if not error.retryable or retries >= maximum:
raise error
next_retry = retries + 1
delay = error.retry_delay
if delay is None:
delay = jittered_exponential_backoff(next_retry)
await ui.maybe_emit_reconnecting(next_retry, maximum)
await asyncio.sleep(delay)
return next_retry
def parse_wire_retry_delay(code: str | None, message: str) -> float | None:
if code != "rate_limit_exceeded":
return None
return parse_seconds_or_milliseconds(message)
失败、取消与恢复
| 故障或边界 | 已发生的状态 | 对上层的结果 | 能否直接重试 | 恢复动作 |
|---|---|---|---|---|
| 限流 Header 缺失/非法 | 其他响应仍有效 | 忽略对应字段 | 不适用 | 等待后续快照 |
| Retry message 格式不认识 | 错误仍 Retryable | 使用本地 backoff | 是 | 抖动退避 |
| usage_limit_reached | 账户额度已耗尽 | UsageLimitReached + reset 信息 | 否 | 等待额度恢复/用户处理 |
| 重试预算耗尽 | 已等待和重发多次 | 回退 WS 或返回错误 | 否 | 由 5.12/5.22 决定 |
设计取舍与验证
服务端 delay 能减少无效流量,本地抖动避免大量客户端同步重试。把限流展示延迟到 usage 合并点减少事件噪声,却让 UI 不是在 Header 到达的瞬间更新。多 limit_id 支持增加解析复杂度,但能表达不同模型或产品额度窗口。
| 可验证契约 | 证据方式 | 预期结果 |
|---|---|---|
| 秒和毫秒 delay 都可解析 | try_parse_retry_after 单元测试 | Duration 精确匹配 |
| 多个 Header family 生成多个快照 | rate_limits parser 测试 | limit_id 稳定规范化 |
| 服务端 delay 覆盖 backoff | responses retry 测试 | 等待采用错误携带值 |
Mini Codex 对照
Mini Codex 至少应把 RetryPolicy 和 RateLimitStore 分成两个对象。测试时注入 sleep 和随机源,避免真实等待;服务端 delay 要设上限,防止异常值永久挂起。
本节边界
本节只决定何时等和怎样展示额度。5.22 将 Transport、API、Provider 与模型语义错误归一为上层可判断的 CodexErr。
评论
登录后即可评论