网络断开、HTTP 429、服务端 failed event 和 Provider 特有认证错误起初形状完全不同。Codex 先在 codex-api 保留传输细节,再由 Provider 映射成统一 CodexErr;上层只依据语义类别决定重试、回退、提示或终止。
具体问题与启用条件
本节建立错误分层与可恢复性矩阵。ContextWindowExceeded 单独在 5.23,401 token 刷新作为认证特例在此说明。
| 条件来源 | 决定字段或状态 | 对本机制的影响 |
|---|---|---|
| 传输 | HTTP/WS 建连、发送、读取 | 产生 Transport/Stream ApiError |
| Responses 事件 | failed/incomplete | 产生模型/服务端 ApiError |
| Provider | 默认 OpenAI-compatible 或 Bedrock override | 映射用户可见语义 |
协议、类型与状态所有权
| 状态或协议 | 所有者 | 生命周期 | 关键不变量 |
|---|---|---|---|
| TransportError/ApiError | codex-client/codex-api | 单次尝试 | 保留状态、Header、body、URL |
| Provider map_api_error | ModelProvider | 错误边界 | 允许后端特有解释 |
| CodexErr | Core | 请求/Turn 控制流 | 携带 retryable 与 retry_delay |
| Error/StreamError EventMsg | Session | 客户端事件 | 不进入模型 History |
机制调用链如下:
socket/HTTP/Responses failed
→ ApiError(Transport|Stream|Context|Quota|Retryable|Invalid...)
→ ModelProvider.map_api_error
→ CodexErr semantic category
→ 401:一次 auth recovery/rebuild request
→ retryable:stream retry budget
→ non-retryable:Error event + Turn 结束
→ WS budget exhausted:HTTP fallback
机制怎样工作
SSE failed event按 code 直接区分 context、quota、usage-not-included、cyber policy、invalid prompt/bio policy、server overloaded 与普通 Retryable。HTTP/WS wrapped error还保留 status、headers 和 body。默认 bridge 把 400 映射 InvalidRequest(并识别无效图片/安全策略),500 映射 InternalServerError,结构化 429 usage limit 映射不可重试 UsageLimitReached,其余状态保留为 UnexpectedStatus;Bedrock 等 Provider 可以覆盖映射,例如把过期签名转成明确用户提示。
CodexErr.is_retryable() 是上层权威:Stream、timeout、部分 unexpected status、connection/internal 错误可重试;invalid request、quota、usage limit、cyber policy、server overloaded、context error等不走普通流重试。这里 server overloaded 虽可能稍后恢复,当前采样重试策略仍将其视为非重试,避免在已知容量信号上自动打满。401 是连接建立/请求前的专门恢复路径:若有 AuthManager,最多刷新并重建认证尝试;永久刷新失败变成 RefreshTokenFailed。
Python 风格伪代码
这段伪代码保留生产实现中会改变结果的状态、分支和异步边界;认证 SDK、遥测字段和 Rust 所有权样板被折叠为明确的领域对象。
async def request_with_auth_recovery(make_request, auth: AuthManager | None):
recovery_used = False
while True:
try:
return await make_request(await current_auth_headers())
except HttpUnauthorized as error:
if auth is None or recovery_used:
raise provider.map_error(error)
recovery_used = True
await auth.refresh_or_raise()
def classify_api_error(error: ApiError, provider: ModelProvider) -> CodexError:
mapped = provider.map_error(error)
return CodexError(
category=mapped.category,
retryable=mapped.category in RETRYABLE_CATEGORIES,
retry_delay=mapped.retry_delay,
diagnostics=mapped.safe_diagnostics,
)
async def handle_sampling_error(error: CodexError):
if error.retryable:
await retry_or_fallback(error)
else:
await emit_turn_error(error)
raise error
失败、取消与恢复
| 故障或边界 | 已发生的状态 | 对上层的结果 | 能否直接重试 | 恢复动作 |
|---|---|---|---|---|
| Network/early stream close | 请求结果未知或不完整 | Stream/Connection error | 是 | 退避重试 |
| 401 且刷新成功 | 原请求未完成 | 透明重建一次尝试 | 一次 | 使用新认证 |
| 400 invalid request | 服务端拒绝语义 | InvalidRequest | 否 | 修改输入/模型能力 |
| 429 usage limit | 额度状态可解析 | UsageLimitReached | 否 | 更新 Session 限流并提示 |
| Provider 特有错误 | 底层细节保留 | Provider 定制 CodexErr | 依映射 | 例如刷新 AWS 凭据 |
设计取舍与验证
分两次映射让通用协议层不依赖具体 Provider,同时保留后端定制错误消息。代价是同一错误要穿过多个枚举,新增类别需同步测试 is_retryable 和客户端协议映射。把可重试性集中到 CodexErr 避免各调用点凭字符串判断。
| 可验证契约 | 证据方式 | 预期结果 |
|---|---|---|
| failed code 映射到具体 ApiError | SSE parser 参数化测试 | context/quota/policy 等不混为 Stream |
| 503 overloaded body 特判 | api bridge 单元测试 | 得到 ServerOverloaded |
| Provider 可覆写错误消息 | Bedrock error tests | 签名过期有专用提示 |
Mini Codex 对照
Mini Codex 应至少有 TransportError、ProtocolError、ModelRequestError 三层,并让 Provider Adapter 提供 map_error。不要用 except Exception: retry;可重试集合必须穷举测试。
本节边界
错误分类已经到达 Turn 控制流。5.23 单独分析 ContextWindowExceeded,因为它不是普通重试,而会影响下一次压缩决策。
评论
登录后即可评论