简介
今天,一套 ChatGPT / CC-Switch → Sub2API → TokenCenter 的链路突然出现了如下错误:
Error running remote compact task: Fatal error: remote compaction v2 expected exactly one compaction output item, got 0 from 0 output items
最终恢复方式很直接:在 Sub2API 的上游账号中,把 Responses API 支持从“自动”改为“强制 Responses”。恢复后,Codex 的远程压缩正常。
这不是一个普通的“模型没回复”错误,而是一次协议层的语义丢失:Codex 远程压缩依赖 Responses 协议中的特定输出项;一旦中转链路把请求导向 Chat Completions,或在 Responses 与 SSE/JSON 之间转换时丢掉该输出项,客户端就会收到“0 个 compaction output item”。
本文基于现场恢复结果,以及截至 2026 年 8 月 7 日 的 Sub2API 公开源码与历史修复记录,说明哪些结论已经证实、哪些仍是合理推断,以及如何把临时修复升级为稳定的工程方案。
文中不包含任何 Token、账号、域名或请求正文。TokenCenter 在本文中仅指该环境中的 OpenAI 兼容上游,不对其实现作超出证据范围的归因。
先给结论
- 现场已证实的根因:Sub2API 账号的“自动”模式把文本请求路由判定为 Chat Completions;强制使用 Responses 后恢复。因此,该账号在当时不能依赖自动判定。
- 为什么会触发 Codex 压缩错误:remote compaction v2 需要 Responses 的压缩输出项;Chat Completions 并不存在与之等价的一等结构。把该链路降级/改写到 Chat Completions,可能让压缩项在上游、网关转换或下游解析的任一处消失。
- “缩短探测时长”不是根治:它最多减少等待,不能修正“把一个不完整、异常或模型特定的 Responses 响应判成不支持”的逻辑。更糟的是,当前源码中网络超时会留下“未知”状态,而未知状态默认仍走 Responses,容易形成偶然恢复而不是可靠修复。
- Sub2API 已有高度相关的历史修复:2026 年 7 月的修复明确处理了同一错误文案、
output_item.done中 compaction 项在 SSE→JSON 桥接时被丢弃的问题;另一项修复要求保留remote_compaction_v2的原生 Responses 链路。若实例版本早于这些修复,升级是必要条件之一。

故障不是“端点名字不同”,而是协议能力不同
/v1/chat/completions 与 /v1/responses 都能完成普通文本对话,但并不表示它们在高级工作流上可互换。
Responses API 的响应由 output 项组成,可承载工具调用等结构化项;官方文档也将 Responses 的工具调用作为一等工作流来描述。OpenAI Responses API 文档
Codex 的远程压缩属于比普通“生成一段文字”更严格的场景:客户端期待返回恰好一个 compaction 输出项。因此,下列两种情况都可能触发同一个报错:
- 请求被路由到只适合普通对话的 Chat Completions 路径,压缩语义无法完整表达;
- 请求仍抵达 Responses 上游,但中转在 SSE 与非流式 JSON 间桥接时,没有保留原始
output_item.done中的 compaction 项。
错误中的 got 0 from 0 output items 很重要:它描述的是 Codex 最终收到的输出项集合为空,不等于已经证明 TokenCenter 从未产生过压缩项。项可能在上游没有产生,也可能在 CC-Switch、Sub2API 或其他反向代理的转换层中丢失。
Sub2API 的“自动”实际做了什么
在当前 Sub2API 源码中,OpenAI API Key 账号有三种模式:
| 模式 | 路由结果 |
|---|---|
| 自动 | 跟随探测结果 |
| 强制 Responses | 无视探测,使用 /v1/responses |
| 强制 Chat Completions | 无视探测,使用 /v1/chat/completions |
源码把手动模式保存在 openai_responses_mode,自动探测结果保存在 openai_responses_supported;手动模式优先级高于探测结果。能力判定源码
当自动探测结果为“不支持”时,Sub2API 会把 Chat Completions 请求直接转发到上游的 /v1/chat/completions,不再做 Chat Completions→Responses 的转换。原样 Chat Completions 转发实现
这正好解释了现场现象:同一个上游在“自动”状态被判为 Chat Completions 后,Codex 压缩失败;把模式钉死为 Responses 后,原生 Responses 链路恢复,压缩也恢复。
一个容易忽略的细节:它通常不是周期性自动扫描
当前源码显示,Responses 能力探测会在账号创建或更新且提交了凭据字段后异步触发;并非普通请求期间定时重新扫描。探测结果会持久化回账号的 extra 字段。触发位置 探测实现
所以“今天某次自动探测突然把端点改掉”是一个符合现象的描述,但还应继续追问:当天是否保存过账号、刷新/替换过 API Key 或 Base URL、做过批量配置同步、升级/迁移过实例,或有外部管理程序调用了账号更新接口。单凭错误本身,不能确认是周期性探测导致。
为什么自动探测会出现假阴性
当前探测并非只请求一次空的 /v1/responses。它会选择一个上游模型,发送带 tool_choice: "required" 的 Responses 请求,并要求响应 output 中出现 function_call;在 HTTP 2xx 但没有该项时,会把账号标记为“不支持”。探测超时当前为 15 秒。探测逻辑与判定条件
这比“404 即不支持”严格得多,但也引入了一个关键风险:它实际测量的是:
某一时刻、某一个被选中的模型、某一套请求头和工具调用策略下,是否能返回预期的 function_call。
它并不完全等价于:
这个账号是否能为 Codex 远程压缩稳定提供原生 Responses/compaction 语义。
可能产生假阴性的情形包括:
- 探测选择的映射模型与实际供 Codex 使用的模型不同,且前者工具调用不稳定;
- 上游在高负载、风控、灰度、区域路由或特定请求头下返回 2xx,但没有 function_call;
- 上游支持基本 Responses,却不完全支持探测使用的工具调用组合;
- 账号级请求头覆盖改变了探测的行为;
- 上游短暂返回了格式看似成功、实际不完整的响应。
这些都能把一个“业务上能跑 Codex Responses”的账号写成 false,继而触发 Chat Completions 路由。这个结论是基于源码逻辑的工程推断;没有当时的探测状态码、脱敏响应和日志,不能断言现场具体命中了哪一项。
同一报错在 Sub2API 历史中已经出现过
这次并非孤例。Sub2API 的公开提交记录中,2026 年 7 月 10 日的一项修复明确记录:某些 unary compact 请求的 SSE 流里,compaction 项只出现在原始 response.output_item.done 事件中;如果桥接层只按窄结构收集文本、函数调用和 reasoning 增量,就会把 compaction 项丢掉,最终导致 Codex 报:
expected exactly one compaction output item, got 0
该修复改为优先保留原始 output_item.done,并在缺失时从 output_item.added 回退;提交信息还直接引用了相关 issue #3777。 修复提交 #3906 相关 issue #3777
随后在 2026 年 7 月 13 日合入的修复又明确提出“保留 remote_compaction_v2 原生 Responses 请求链路”。修复提交 #3971
这带来两个实践含义:
- 若你的 Sub2API 版本不含这些修复:即使已强制 Responses,仍可能在 compact 的 SSE/JSON 桥接路径丢失 output item;应先升级到包含上述修复的版本,并在升级后做一次真实压缩验证。
- 若版本已经包含这些修复:本次“自动切到 Chat Completions”的路由误判仍是一个独立问题。历史 compact 修复不能替代把关键账号固定在正确端点、或改进探测模型。
对本次链路的因果判断
下面把结论按证据强度拆开。
已证实
- 手动将该 Sub2API 上游账号设置为“强制 Responses”后,故障恢复。
- 该设置按设计会覆盖自动探测结果。
- Codex 报错要求恰好一个 compaction 输出项,而失败响应中为 0。
高可信推断
- 自动判定为 Chat Completions 破坏了远程压缩所需的 Responses 语义,至少是本次故障的充分触发条件。
- 将 TokenCenter 这类已被现场验证可处理该工作流的账号降级为 Chat Completions,是不安全的路由决策。
尚待日志确认
- 自动判定变更的准确触发时间和操作来源;
- 探测时使用的具体模型、HTTP 状态码、脱敏响应以及
openai_responses_supported的前后值; - 请求是否经过 CC-Switch 的额外转换,或是否还存在 Nginx/CDN 等会影响 SSE 的中间层;
- 当前实例是否已包含 #3906 / #3971 及其后续修复。
立即可执行的处置方案
1. 保留当前临时修复,并把它当作生产配置
对于专门服务 Codex/ChatGPT 高级工作流、且已验证支持 Responses 的 TokenCenter 账号:
- 将 Responses API 支持设为 强制 Responses;
- 保留文本端点的 Responses 能力;不要把它作为只支持 Chat Completions 的普通兼容账号调度;
- 保存后重新发起一次小型 Codex 任务,并触发一次远程压缩或足够长的上下文测试;
- 在变更记录中写明:该覆盖是为了保护 Codex compaction,不是通用性能调优。
这不是“绕过问题”的脆弱操作。对于已经由生产行为验证过的上游,显式声明能力比一次通用探测更符合事实。
2. 做一次版本与链路核验
至少记录以下信息,均可脱敏:
| 检查项 | 目标证据 |
|---|---|
| Sub2API 版本 / 提交号 | 是否包含 #3906、#3971 和后续 compact 修复 |
账号 extra | openai_responses_mode、openai_responses_supported 的当前值和变更记录 |
| Sub2API 日志 | probe_done 的时间、状态码、supported 值、探测模型 |
| 上游访问日志 | compact 请求实际命中的路径:/v1/responses、/v1/responses/compact 或 /v1/chat/completions |
| CC-Switch / 反向代理 | 是否改写路径、Accept、SSE,是否存在空闲超时 |
| 验证请求 | 一次真实 remote compact 成功,且不只验证普通聊天成功 |
不要把“普通聊天能用”作为压缩恢复的验收标准;这两条协议路径的要求不同。
对 Sub2API 更合理的深度修复建议
“把探测时间从 15 秒缩短”可以降低管理操作等待感,但不能解决此次根因。更可靠的方案应是以下组合。
A. 将能力拆分,而不是用一个布尔值代表全部
至少区分:
- 基础
/v1/responses; - Responses 工具调用;
- Codex remote compaction /
/responses/compact; - Chat Completions。
一个上游可能支持基础 Responses 却不稳定支持工具调用;也可能普通 Responses 正常,但 compact 的 SSE 终态需要特殊保留。用单个 openai_responses_supported: true/false 同时决定这些能力,信息损失过大。
B. 对负面结论采用“保守降级”
建议把状态从二元布尔扩展为 supported / unsupported / inconclusive / stale:
- 只有稳定、可重复的明确“不支持”证据才把账号降级到 Chat Completions;
- 2xx 但响应不符合工具探测预期,应更适合写为
inconclusive,并保留既有的已验证 Responses 路由; - 已经在生产中成功处理过 compaction 的账号,不应被一次单模型探测直接降级;可要求连续失败、设置 TTL,或等待人工确认。
C. 让探测针对实际工作负载
探测至少应记录并展示:探测模型、上游路径、请求头摘要、状态码、响应类型、耗时和判定理由。对 Codex 专用账号,应提供独立的 compact 兼容性探测,而不是仅以 function_call 结果推断全部能力。
D. 保持原始 Responses 输出项的端到端透明
任何 Responses↔SSE↔JSON 桥接都应把 output_item.done 视为权威终态,避免只保留文本、reasoning、function_call 等已知字段。对未知 output item type 采取透传/保留策略,并为 compaction 增加回归测试。
E. 对关键路由提供可审计的人工覆盖
管理台已经提供“强制 Responses”,下一步应补足:最后一次探测、原始判定、人工覆盖人/时间、覆盖原因、回滚按钮和变更告警。对生产关键账号,这比“无感自动调整”更可控。
一个简短的排障决策树
Codex remote compact 报“got 0 output items”
│
├─ 当前账号是否被路由为 Chat Completions?
│ ├─ 是:强制 Responses;用真实 compact 验证
│ └─ 否:继续检查
│
├─ Sub2API 是否包含 2026-07 的 compact 输出项保留修复?
│ ├─ 否:升级后复测
│ └─ 是:继续检查
│
├─ 上游实际是否返回 compaction output item?
│ ├─ 是:排查 CC-Switch / Sub2API / 代理的 SSE-JSON 桥接
│ └─ 否:排查上游模型、权限、路径与 compact 能力
│
└─ 自动探测为何写成 false?
└─ 对照探测模型、状态码、响应和最近账号更新记录
结语
自动能力探测适合解决“绝大多数第三方 OpenAI 兼容上游只支持 Chat Completions”的兼容性问题;但 Codex remote compaction 属于协议要求更高的工作流。对已经验证可用的关键上游,让一次通用探测把它降级到 Chat Completions,会把“兼容性优化”变成“语义破坏”。
本次最正确的短期动作已经完成:强制 Responses。接下来的重点不是盲目缩短探测超时,而是:核验 Sub2API 是否包含 compact 相关修复、保留端到端请求证据,并推动能力模型从“一个自动布尔值”升级为“面向实际协议特性的可审计判定”。