opencodex 失败诊断字段的 usage.jsonl 持久化:让 5xx 事故可复盘、可检索、不丢失
2026/9/24 20:27:21 网站建设 项目流程

【免费下载链接】opencodex

Universal provider proxy for OpenAI Codex & Claude Code — use any LLM (Claude, Gemini, Grok, DeepSeek, Ollama…) with Codex CLI, App, SDK, and Claude Code

项目地址:https://gitcode.com/gh_mirrors/ope/opencodex
点击查看免费下载

导读

opencodex 是面向 OpenAI Codex CLI / App / SDK 与 Claude Code 的通用 Provider 代理(README.md)。本文聚焦260716_claudecode_hardening工作包 3(030_5xx_persistence.md)的核心改动:把请求失败诊断字段(errorCode/terminalStatus/closeReason/upstreamError)以additive(追加式)方式写入usage.jsonl持久化条目,解决"慢速 502 还是 SSE 中途response.failed无法判定"的事故复盘盲区。读完本文,你将理解这些字段的落盘语义、触发门槛、脱敏链路与验收断言方式,并能直接在自己的部署中通过usage.jsonl回溯任意一次上游失败。

背景:一次无法判定根因的事故

2026-07-16 的事故(即260716事件)暴露了一个根本性的可观测性缺陷:当一次请求以失败结束时,运维人员无法区分它到底是"上游缓慢返回的 502",还是"SSE 流中途收到了response.failed"。根因在于:

  • errorCodeterminalStatuscloseReasonupstreamError这四个诊断字段只存在于进程内存的 200 条环形缓冲区requestLog,见 src/server/request-log.ts);
  • 它们没有随持久化的usage.jsonl条目落盘,导致事故发生后内存缓冲被新请求冲掉,诊断线索永久丢失。

修复策略在 000_plan.md 中定义为三道防线:① 代理对 pre-stream 5xx 直接重试;② 重试耗尽后映射为 Claude Code 可自行重试的529 overloaded_error;③ 即本文——将失败条目以可判定形式持久化,为下一次事件提供依据。

设计原则:不新建文件,只在既有条目上做加法

该方案的三个关键约束(详见 030_5xx_persistence.md):

  1. 不新建日志文件:失败诊断直接搭载在既有usage.jsonl条目上,避免引入新的文件生命周期、轮转策略与消费端改造。
  2. Additive 兼容:所有新增字段均为可选字段,旧版读取器遇到未知字段会自然忽略,因此向后兼容
  3. 只写失败,不写成功:成功条目(2xx 且 terminal 为completed)保持原样,把对usage.jsonl文件增长速率的影响降到最低。

实现拆解一:PersistedUsageEntry新增可选诊断字段

持久化条目的类型定义位于 src/usage/log.ts,新增的四个可选字段(约 L318-L329):

export interface PersistedUsageEntry { // ...既有字段... errorCode?: string; /** 封闭枚举,来自 request-outcome.ts;作为分组键落盘 */ terminalStatus?: RequestTerminalStatus; closeReason?: RequestCloseReason; /** 捕获时刻已完成 redactSecretString + slice(0,500) */ upstreamError?: string; }

注意实际实现中terminalStatuscloseReason使用的是封闭联合类型RequestTerminalStatus/RequestCloseReason,而非文档初稿中的宽字符串字面量。这两个枚举定义在 src/usage/request-outcome.ts:

  • RequestTerminalStatus = "completed" | "failed" | "incomplete"(由结局分类派生,aborted排除在外,因为它是调用方主动离开而非上游终端);
  • RequestCloseReason = "terminal" | "client_cancel" | "non_stream" | "body_stall" | "body_overflow"

为什么必须是封闭枚举?注释给出了清晰理由:terminalStatus一旦成为分组键(grouping-key slot),其值来自上游 terminal frame,若类型开放,上游可控文本就可能进入分组键(request-outcome.ts 中isRequestTerminalStatus/isRequestCloseReason两个读回守卫正是为此而设)。normalizeUsageEntry(L814-L922)在归一化路径中同样对这四个字段做了展开,且terminalStatus/closeReason校验而非真值判断

...(entry.errorCode ? { errorCode: entry.errorCode } : {}), ...(isRequestTerminalStatus(entry.terminalStatus) ? { terminalStatus: entry.terminalStatus } : {}), ...(isRequestCloseReason(entry.closeReason) ? { closeReason: entry.closeReason } : {}), ...(entry.upstreamError ? { upstreamError: entry.upstreamError } : {}),

这一"白名单式归一化"与surfacetransportPhase等字段遵循同一纪律:手改过的损坏行携带未知值时会丢弃该字段而非污染枚举。

实现拆解二:addRequestLog的失败门槛与字段投影

写入侧的改动位于 src/server/request-log.ts 的addRequestLog。核心是仅在失败条目上构造failureDiagnostics并展开进appendUsageEntry

const failureDiagnostics = entry.status >= 400 || (entry.terminalStatus && entry.terminalStatus !== "completed") ? { ...(entry.errorCode ? { errorCode: entry.errorCode } : {}), ...(entry.terminalStatus ? { terminalStatus: entry.terminalStatus } : {}), ...(entry.closeReason ? { closeReason: entry.closeReason } : {}), ...(entry.upstreamError ? { upstreamError: entry.upstreamError } : {}), } : {}; appendUsageEntry({ /* 既有字段逐字段重建 */, ...failureDiagnostics });

这个"逐字段重建"而非整体 spread 的设计值得注意:addRequestLog注释明确指出,任何在函数体内漏写的字段都会"到达/api/logs却永远到不了usage.jsonl"(L590-L592),因为按 key 的 rollup 正是从usage.jsonl读取的。failureDiagnostics在重建序列的靠后位置展开(L634),紧随transportPhase/terminalSource之后。

门槛语义

  • status >= 400:覆盖所有 HTTP 失败;其中499 client-cancel 被刻意纳入——客户端主动取消同样具有诊断价值(见 030_5xx_persistence.md 的说明)。
  • terminalStatus !== "completed":捕获"HTTP 200 但语义上未完成"的情况,例如max_output_tokens截断导致的incomplete,或 SSE 中途failed。这正是 request-outcome.ts 中classifyRequestOutcome强调"先读语义终端、后读数字状态"的原因:一个 200 携带 incomplete terminal 的行是失败,只有数字状态会把它误判为成功。
  • 成功条目(2xx + completed)failureDiagnostics为空对象{},落盘形态与历史完全一致,文件增长速率影响最小。

脱敏与截断:上游错误文本的落盘边界

upstreamError不会原样落盘。它的清洗发生在捕获时刻而非持久化时刻,位于captureUpstreamError/captureUpstreamErrorParsed(src/server/request-log.ts):

  • 优先取response.failedSSE 负载error.message或非流式 JSON 错误体中的第一条非空原因(保留原始失败,避免被后续重试覆盖);
  • 统一经过redactSecretString(...)脱敏(来自 src/lib/redact.ts),再slice(0, 500)截断,保证密钥永不进入/api/logsusage.jsonl
  • 无人类可读错误消息时,回退到 bridge 发出的response.incomplete结构化原因(如max_output_tokensupstream_stall_timeoutadapter_eof),映射为面向读者的标签(incompleteReasonLabel)。

因此持久化侧无需再做任何额外处理,PersistedUsageEntry.upstreamError的注释"already redacted + capped at capture"就是这条链路的契约声明。

四个诊断字段速查

字段类型触发条件语义与来源
errorCodestring(可选)status >= 400或非completed终端HTTP 映射错误码(如 502 →upstream_server_error
terminalStatus"completed" \| "failed" \| "incomplete"同上语义终端,来自上游 terminal frame,作为分组键
closeReason"terminal" \| "client_cancel" \| "non_stream" \| "body_stall" \| "body_overflow"同上响应体为何停止被读取
upstreamErrorstring(可选)同上已脱敏 + 500 字符截断的上游错误原因

验收标准与断言路径

030_5xx_persistence.md 定义的验收标准(Accept criteria)要求新增tests/usage-failure-persistence.test.ts

  1. 失败条目四字段齐备:以OPENCODEX_HOME=<tmpdir>隔离环境(依赖 src/config.ts 中 per-call 的环境变量解析,可在进程中途覆盖),通过addRequestLog写入status: 502terminalStatus: "failed"closeReason: "terminal"upstreamError的条目,然后断言usage.jsonl最后一行 JSON 中四个字段全部存在。
  2. 成功条目形态不变status: 200terminalStatus: "completed"的条目,诊断字段缺席,保持既有形状。

之所以只能走 env-var 隔离路径,是因为addRequestLog直接调用appendUsageEntry(无注入 seam),测试中另有一条断言路径:observeRequestLogsForTests(src/server/request-log.ts)可在内存环上观察落库前的条目,配合 tests/claude-integration/claude-messages-endpoint.test.ts 中已有的terminalStatus: "failed"+closeReason: "terminal"断言模式交叉验证(如该文件 L773-L774)。完整验证矩阵见 000_plan.md 的 C-ACTIVATION-GROUNDING-01 一节,其中"失败持久化"一行正是本文所述行为。

显式排除项(Out of scope)

为避免范围蔓延,030_5xx_persistence.md 明确列出:

  • 不新建日志文件、不改轮转策略、不做 GUI 暴露;
  • 不修改 200 条环形缓冲区大小——持久化已达成目的,缓冲区扩缩容无必要。

复盘价值:从"看到 502"到"解释 502"

该改动让 opencodex 的每次上游失败都留下一条可解释、可检索、可聚合的持久化记录:usage.jsonl中的失败行现在同时携带 HTTP 状态、语义终端、关闭原因与脱敏错误文本,运维既可以用terminalStatus: "failed"精确圈出 SSE 中途失败,也可以用status >= 400圈出 pre-stream 拒绝,还能用closeReason: "client_cancel"区分用户主动放弃。它没有发明新的协议,只是把已经在内存里存在的事实,以兼容旧读取器的形式搬到了磁盘上——这正是 260716 事件教给项目的最小代价教训:诊断字段若只活在内存里,就等于不存在

【免费下载链接】opencodex

Universal provider proxy for OpenAI Codex & Claude Code — use any LLM (Claude, Gemini, Grok, DeepSeek, Ollama…) with Codex CLI, App, SDK, and Claude Code

项目地址:https://gitcode.com/gh_mirrors/ope/opencodex
点击查看免费下载

相关推荐

上一篇:Jellyfin API 实战指南:从认证到查库的 4 个场景
下一篇:彻底告别PS1画面撕裂!ScePSX模拟器高精度渲染技术完全指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询