概览
在部分 Codex 会话中,API 会返回 HTTP 400,并指出某个输入项的 ID 以 fc_ 开头,但该位置要求使用 ctc 前缀。错误通常出现在包含 custom_tool_call 的连续工具调用链里:前一个工具调用已经完成,下一次请求携带历史输入时,上游接口在参数校验阶段拒绝了请求。
本文给出当前排查结论、临时恢复方法和永久修复判定标准。核心判断是:这是一类 Sub2API 转发层的兼容性故障。在没有修复版本和最小复现验证之前,不应把模型、思考强度、LeanCTX 压缩或 Shell/路径权限当作这类错误的根因。
如果当前目标只是恢复工作流,最短路径是:在报错前一阶段对应的分支中新开会话,先压缩文本,再继续任务;不要在原异常会话中反复重试。

现象:400 发生在输入项 ID 校验阶段
典型报错如下:
Invalid 'input[216].id': 'fc_09f77ac43cf7db36016a8920e7934487d0a9fd08e9db431d47'. Expected an ID that begins with 'ctc'.
Invalid 'input[9].id': 'fc_0911234de5598a11016a897cb7fba887d1ad603d5c403f8249'. Expected an ID that begins with 'ctc'.
这里有三个重要信号:
- 返回码是 HTTP 400,说明请求在服务端参数校验阶段被拒绝,而不是模型生成质量下降。
- 错误点明确指向
input[n].id,问题位于历史输入项的标识符,而不是工具参数内容本身。 - 同一类错误集中出现在
custom_tool_call链路,说明触发条件与工具调用记录的序列化、还原或转发有关。
因此,这不是“多试几次就能过去”的普通瞬时错误。只要异常会话继续携带同一个不符合约束的历史输入,重试很可能只是重复提交同一个坏请求。
把事实、诊断和未知项分开
排障时最容易犯的错误,是把不同层级的问题混在一起。当前证据可以分成三类:
| 层级 | 当前可确认内容 | 不能据此推出的结论 |
|---|---|---|
| 可观察事实 | 上游返回 400;某些输入项为 fc_;该位置要求 ctc;常见于 custom_tool_call 链路 | 不能仅凭一次错误判断所有会话都受影响 |
| 当前诊断 | Sub2API 在转发或还原 Codex 工具调用时保留、生成或遗漏了应转换的 ID 前缀 | 不能把诊断写成已由官方修复的事实 |
| 尚待验证 | 哪个具体版本引入问题、哪些请求组合会触发、修复版本何时发布 | 不能用 issue 关闭、模型切换或一次成功重试代替回归验证 |
这个区分也决定了后续动作:先隔离异常会话,再等待代理层修复,最后用最小复现确认修复是否真正生效。
根因链:代理层重建了不符合上游契约的输入项
可以把请求路径简化为:
Codex 会话历史
-> Sub2API 接收并重建 custom_tool_call 输入项
-> 转发到上游接口
-> 上游校验 input[n].id 前缀
-> fc_ 出现在要求 ctc 的位置,返回 HTTP 400
关键不在于 fc_ 这个字符串本身“是否有效”,而在于它被放到了一个具有不同 ID 契约的位置。只要代理层在还原历史消息时没有完成类型对应的转换,上游就会在进入模型处理前拒绝整个请求。
这也解释了为什么错误往往在连续工具调用的下一轮才出现:第一次调用可能成功,问题在于调用记录被保存、重建并作为下一次请求的上下文发送时才暴露。
为什么不应优先怀疑模型或 LeanCTX
在这类错误中,下面几类因素可能同时存在于环境里,但目前没有证据表明它们是 fc_ / ctc 报错的核心根因:
- 模型或思考强度:它们会影响生成内容和调用路径,但不能解释代理层为何把某一输入项以错误前缀提交给上游。
- LeanCTX 压缩:压缩改变的是上下文呈现和传输规模;本错误明确落在输入项 ID 的格式契约上。
- Shell allowlist、
allow_paths或路径隔离:这些设置影响本地工具是否能访问或执行,不能修复已经发往上游的 ID 校验失败。
如果同时存在上述独立问题,应分别记录、分别验证。把它们混成一个“环境不稳定”结论,会导致无效的扩大权限、切换模型和重复重试。
关联 GitHub issue
目前应重点跟踪 Sub2API 的相关讨论:
其中,#6146 的公开标题直接指向 custom 工具请求还原时 item id 转换遗漏导致上游 400 的问题;#6071 是同一兼容性方向上的关联跟踪项。issue 能帮助确认修复进展,但在看到明确的发布版本和本地回归结果前,不能把“已讨论”当作“已修复”。
临时恢复:新会话加压缩文本
在修复版本发布前,建议按以下顺序恢复工作:
- 保留原会话作为故障记录,不再在其中连续重试。
- 回到报错前一阶段对应的分支或任务状态。
- 新开一个 Codex 会话,将必要背景压缩成短摘要,只保留目标、已完成工作、待办事项和关键约束。
- 在新会话中继续后续操作;如果再次出现同样错误,记录触发它的最小工具调用序列。
“压缩文本”在这里是为了减少异常历史输入被再次带入请求,并不是修复 ID 转换逻辑。它只能降低复现概率或绕开已污染的会话状态,因此应明确标注为临时方案。
永久修复:升级后做最小复现
永久解决依赖 Sub2API 发布包含兼容性修复的新版本。升级后,不要只看版本号或 issue 状态,而应执行一次最小回归:
- 新建会话,触发一个包含
custom_tool_call的调用。 - 让会话继续发送下一轮请求,使历史调用记录被重新带入上下文。
- 检查请求或代理日志中的输入项类型与 ID 前缀是否匹配;必要时对敏感字段脱敏后保存证据。
- 确认同一序列不再返回
Expected an ID that begins with 'ctc'。 - 再用普通文本请求和不含工具调用的会话做一次回归,避免修复只覆盖单一路径。
只有当上述检查在修复版本上稳定通过,才可以把故障标记为已解决。若仍然失败,应回退到“新会话 + 压缩文本”的临时流程,并把新的最小复现提交给 Sub2API 维护者。
排障清单
遇到相同错误时,可以快速核对:
- 是否出现
Invalid 'input[n].id'和Expected an ID that begins with 'ctc'? - 请求历史中是否包含
custom_tool_call? - 是否经由 Sub2API 或其他兼容层转发?
- 是否在同一异常会话中反复重试?如果是,先停止并新开会话。
- 是否已经确认代理版本包含修复,并完成了下一轮工具调用回归?
前两项用于确认症状,第三项用于定位边界,第四项用于立即恢复,第五项用于避免过早宣布修复。
结语
fc_ / ctc 报错的价值不只在于指出一个错误前缀,更在于揭示了兼容层最脆弱的地方:工具调用历史不是普通文本,任何重建、映射或序列化差异都可能在下一轮请求中变成上游契约错误。
当前最稳妥的处理是把它当作 Sub2API 的兼容性故障管理:隔离异常会话、使用新会话临时恢复、跟踪 #6071 和 #6146,并在修复版本发布后用最小复现验证。模型、思考强度、LeanCTX 和本地权限问题应保留为独立排查线,不能用来替代对代理层的修复。