一个 web_search 开关,账单涨了四五倍:复盘 Codex 在 Bedrock 上的提示词缓存事故

设想这样一个场景:你把自己常用的 Codex CLI 挂到了 AWS Bedrock 上,用 GPT-5.6 Sol 跑日常的 agentic 编码任务。一切看起来正常,命令照跑,代码照写,模型响应也很快。直到你打开 Cost Explorer,发现这周的账单比预期多出好几百美元。你翻来覆去检查自己的用量,请求数没有暴涨,代码量也没变多,可钱就是凭空多出来了。
这不是虚构。2026 年 8 月,一位开发者在 openai/codex 仓库提交了 issue #37674,记录的就是这样一起真实发生的事故。账单里多出来的那一大块,全是一个叫「cache-write」的东西。
账本里凭空多出的 85%#
issue 作者贴出的数据很直接:在 8 月 5 日到 8 日这四天里,他通过 Bedrock 跑了 3656 次请求,累计产生了 1.719 亿个 cache-write token。按照 Bedrock 的费率估算,光缓存写入这一项就烧掉了约 1182 美元,而同期总成本约 1386 美元。也就是说,85% 的钱都花在了「写缓存」上,而「读缓存」的 token 是零。
这里需要先解释一个概念。所谓提示词缓存(prompt caching),指的是模型服务商把请求里重复出现的那段「前缀」缓存下来,比如系统提示词、工具定义、项目约定这些每次都一样的部分。下一次请求时,模型直接读缓存,不用重新计算,成本能降到正常输入的十分之一甚至更低。对应地,缓存有「写」和「读」两个动作:写缓存按正常 token 甚至略高的价格计费,读缓存则便宜得多。
一个健康的 agentic 编码会话,应该是「写一次、读很多次」。第一次请求把稳定的前缀写进缓存,之后每一轮对话只处理新增的那一小段尾巴。可 issue 里的数据恰好反了过来:读是零,写却占了 85%。这意味着每一次请求,模型都在把整段前缀重新写一遍,从来没从缓存里读到过任何东西。
作者还给了个更直观的观察:本地一次 76 个请求的 Codex 会话里,cache_write_input_tokens 高达 670 万,cached_input_tokens 是零,平均每个请求要写约 8.8 万个 token。而 CloudWatch 里没有任何客户端错误。也就是说,这不是报错,而是模型在「正常地」反复重写同一段内容,钱就是这么无声无息流走的。
一次版本升级暴露的线索#
真正帮大家定位问题的人叫 kevmyung。他在评论里提供了一个决定性的对照实验:把 Codex CLI 从 0.146.0 升到 0.147.0 之后,缓存「写读比」从 0.08 一路飙到了 8.84,日成本至少翻了五倍。回滚到 0.146.0 之后,写读比立刻回到 0.025,缓存命中率恢复到 97%。
这个数字意味着问题几乎可以确定是 0.147.0 这个版本引入的。kevmyung 还敏锐地追问了一句:会不会是远程压缩(remote compaction)把 Bedrock 的提示词缓存给弄失效了?这个猜想方向是对的,只是真正的元凶比他想的更不起眼。
扒开源码:缓存键有了,缓存开关没带上#
jdcodes1 直接把 Codex 的源码翻了一遍,确认了请求结构的缺口。他给出的结论很干净:在 main 分支上,Responses 请求结构体里只带了一个 prompt_cache_key 字段,这个字段在 HTTP 和 WebSocket 两套请求构建器里都串了进去,但整份 codex-api 里既没有 prompt_cache_options,也没有按内容块标记的 breakpoint 字段。
这两者的区别,社区里 NetBr3ak 用一句话讲透了:promptCacheKey 只是告诉服务商「把请求路由到哪里」,而 cache_control 才是真正「把这段前缀标记为可缓存」的开关。把前者当成后者发出去,结果是每次请求都能正常返回,但什么东西都不会被缓存。
更关键的背景是:Bedrock 上 GPT-5.6 Sol 的缓存是「显式开启」的,不像直连 OpenAI 那样默认隐式缓存。你必须在请求里明确告诉它哪里是可缓存的前缀、哪里是断点。而 Codex 原生的 Bedrock provider 只暴露了传输和鉴权相关的配置,config.toml 里根本没法写这类请求体转换。于是这个缺口就成了死结:用户再想优化,也无从下手。
社区先动手:一个 fork 补丁把命中率拉回 97%#
在官方修复落地之前,有人等不及了。scottleibrand 让模型自己写了一个 workaround,思路是给请求显式加上缓存断点,断点设在「稳定的指令和历史」与「每条变化的内容」之间。他把补丁挂在自己的 fork 上,因为上游 PR 是邀请制。
这个补丁在真实生产负载上跑出来的数据很漂亮:35 次 Bedrock/Luna 部署、1486 次 provider 响应、30 个多轮会话全部缓存健康,聚合缓存命中率在种子请求之后达到 97.771%。这基本证实了根因判断:不是模型出了问题,而是请求里少了那一个标记。
真正的元凶:一个刚打开的 web search 开关#
谜底最终由 OpenAI 官方揭晓。celia-oai 在 8 月 20 日的回复里说得很简短:这很可能是「新打开的网页搜索工具」导致的缓存未命中,他们正在和 Bedrock 团队一起修,在此之前,可以在配置里加一句 web_search = "disabled" 临时规避。她还补充说,关掉这个工具不影响 Codex 用 shell 命令等方式继续搜网页。
kevmyung 当天就验证了:在 0.148.0 上,web_search = "cached" 时缓存读取依然是零,改成 web_search = "disabled" 后缓存立刻恢复正常。
到这里,整条因果链就完整了。0.147.0 为 Codex 打开了内置的网页搜索,而这个工具会在请求里注入一段动态内容,破坏了原本稳定、可缓存的前缀结构。Bedrock 又恰好是「显式缓存」模式,缺少缓存标记的前缀一旦被打散,模型就只能每轮从头重写一遍。用户的每一分钱,都花在了这个本不该发生的过程里。
账单之外,这件事留下了什么#
如果只看结果,这是一起「官方已修复」的计费 bug,8 月 21 日 Bedrock 侧的修复已经部署,celia-oai 在 us-east-1、us-west-2、us-east-2 三个区域验证通过,issue 也随之关闭。但它暴露出的几个问题,比这个 bug 本身更值得记住。
第一,成本异常往往没有报错。这次事故里,CloudWatch 没有任何客户端错误,请求全部「成功」返回。如果你不看账单或者不看 token 级遥测,根本不会发现钱在漏。几位受害者的经历很能说明问题:apexethdev 收到了 1200 多美元的账单,本该只要 300 美元;jakswa 也进了「上千美元」俱乐部。他们都庆幸自己只晚发现了一天。
第二,缓存键和缓存开关是两个概念,而「翻译层」最容易在这里翻车。NetBr3ak 在评论里点了一句很扎心的话:读一次缓存的成本只有正常输入 token 的十分之一,所以在稳定前缀上犯这种错,等于在每一个重复 token 上多付十倍的钱,直到有人去读原始请求体为止。他还提到,眼下 Anthropic 到 Bedrock 的翻译器、以及某个企业级编排层里,都还挂着同样形状的 open issue。这不是 Codex 一家的问题,而是一类系统性的坑。
第三,可观测性该做到什么程度。jdcodes1 和 tylerchen0123 都提到了同一个方向:把 cache-write 和 cache-read 作为独立的遥测字段暴露出来,而不是简单叠加到 input_tokens 里。因为 cached_input_tokens 其实是 input 的一个子集,直接相加会重复计费。只有把这三个桶分开,用户才能一眼看出「我是不是又在全量重写前缀了」。
第四,agent 工作流天然依赖缓存。长期稳定的指令、工具定义、环境上下文,这些是 agent 每一轮都要重复发送的固定前缀。缓存命中与否,直接决定了 agentic 任务的成本是正常水平还是十倍水平。这也解释了为什么 AWS 专门为 GPT-5.6 的 agentic 场景文档化了「显式缓存」模式:这类负载对缓存最敏感。
结尾#
一起 cache bug,从用户账单一笔笔多出来的钱,追到版本升级,再追到请求结构里缺失的一个字段,最后落到一个刚打开不久的网页搜索开关上。整个过程不到两周,社区的定位、复现、补丁、官方修复一条龙走完,堪称一次开源协作的高效样板。
但对普通开发者来说,这件事最实用的提醒其实很简单:当你把 AI 编码工具挂到新的服务商上、或者升级了版本之后,别只看代码能不能跑,顺手瞄一眼 token 级用量。缓存命中率这种平时看不见的指标,可能正在悄悄决定你月底要付多少钱。
参考来源#
- openai/codex issue #37674: Native Bedrock Codex GPT-5.6 Sol lacks explicit cache controls, producing high cache-write spend
- Hacker News 相关讨论帖(148 points)
- 相关 issue #35300(跨会话缓存键复用问题)