GPT-5.6 prompt caching: Codex cannot emit `prompt_cache_breakpoint`, preventing reuse of stable startup prefixes
Summary
GPT-5.6 supports an explicit prompt_cache_breakpoint on the content block that ends a reusable prefix. Codex's own vendored migration guide (codex-rs/skills/src/assets/samples/openai-docs/references/upgrading-to-gpt-5p6-sol.md:210) describes this exact failure mode:
GPT-5.6 implicit caching places a managed breakpoint near the latest user or tool message and no longer relies on 128-token rounding. A prompt with a large stable prefix followed by a changing suffix can therefore lose cache hits even when the stable prefix itself has not changed.
Its remedy, at line 242: "Place explicit breakpoints at the actual stable rendered boundary using prompt_cache_breakpoint." And at line 244, why this now costs money: "Cache writes cost more than ordinary uncached input, so a lower hit rate can be both slower and more expensive."
Codex cannot emit the field. ContentItem::InputText and InputImage have no prompt_cache_breakpoint member or extension map, so the documented field is unrepresentable in both HTTP and WebSocket request construction.
In a controlled replay of real Codex request bodies with the same ~9k-token developer prefix, a fixed prompt_cache_key, and only the final user message changed, unmodified requests produced 0% cache hits and rewrote ~9,060 tokens each. Adding only prompt_cache_breakpoint: {"mode":"explicit"} to the final stable input_text block produced 98.6% hits and reduced warm-request writes to 123–128 tokens.
This does not affect every turn. Append-only continuation turns within a single session already reached ~98% hits. The demonstrated failure is reuse of a stable startup prefix when independent requests diverge at the first volatile user turn.
The source-level defect is the missing content-block field. Session-scoped cache keys are a separate compounding issue on the measured backend, automatic breakpoint placement is a policy decision, and prompt_cache_options is optional API completeness.
Environment
| | |
|---|---|
| Codex CLI | 0.145.0 and 0.146.0-alpha.10, tested as of 2026-07-24 |
| Source refs | rust-v0.145.0 (25af12f7e615), rust-v0.146.0-alpha.10 (ae83c6df6ffd), and main @ 4c43465133428898aa84f0bfc02c306ed65fb66a |
| Model measured | gpt-5.6-sol |
| Also affected by the wire-type gap | gpt-5.6-terra, gpt-5.6-luna |
| Backend measured | AWS Bedrock Mantle, OpenAI-compatible Responses API (/openai/v1/responses) |
| Wire API | responses, streaming |
| Platform | macOS 27.0 arm64 |
The measurements below are from Bedrock Mantle. The serialization defect is backend-independent and directly verifiable from the Codex source, but absolute cache behavior and billing should also be reproduced against api.openai.com before being treated as universal.
Expected behavior
OpenAI's prompt-caching guide documents that GPT-5.6 and later models can mark the exact end of a reusable prompt prefix by adding:
"prompt_cache_breakpoint": {"mode": "explicit"}
to a supported content block. Content after that block may then change without invalidating the earlier cached prefix.
Codex should at minimum be able to represent and serialize this field on supported content blocks. Whether Codex places breakpoints automatically, exposes them through configuration, or both is a separate default-policy decision. The vendored migration guide explicitly advises using explicit breakpoints only for measured stable boundaries and not converting every prompt globally.
Actual behavior
Codex always relies on the implicit managed breakpoint because it has no wire representation for an explicit one. When requests share a large startup prefix but differ at the first volatile user turn, Codex cannot mark the actual stable boundary.
On the measured workload, those requests missed and rewrote essentially the entire eligible prefix. In contrast, ordinary append-only turns within one session cached normally because each prior turn became stable prefix for the next.
Minimal source proof
The content-block field is unrepresentable
codex-rs/protocol/src/models.rs:702 in rust-v0.145.0 and :706 on the cited main revision:
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, TS)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ContentItem {
InputText {
text: String,
},
InputImage {
image_url: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
detail: Option<ImageDetail>,
},
InputAudio {
audio_url: String,
},
OutputText {
text: String,
},
}
There is no prompt_cache_breakpoint field and no #[serde(flatten)] or extra-fields map. The API supports breakpoints on input_text, input_image, and input_file, but Codex cannot serialize one on any of those blocks.
The top-level cache options are also absent
ResponsesApiRequest and ResponseCreateWsRequest both contain prompt_cache_key, but neither contains prompt_cache_options or an arbitrary request-body extension map:
pub struct ResponsesApiRequest {
// ...
pub prompt_cache_key: Option<String>,
pub text: Option<TextControls>,
pub client_metadata: Option<HashMap<String, String>>,
}
This is not required for the measured hit-rate recovery: the A/B below used a content-block breakpoint without sending prompt_cache_options. It is listed separately because the same GPT-5.6 API surface is incomplete in both the HTTP and WebSocket request structs.
Configuration cannot work around the missing nested field
ModelProviderInfo accepts connection-level customization such as base_url, wire_api, headers, query parameters, and retry controls, but no request-body transformation. The schema uses additionalProperties: false, so a user cannot add a nested prompt_cache_breakpoint through config.toml.
A top-level request-body passthrough such as #34569 could expose prompt_cache_options, but it would not solve the core bug unless it also supported structured transformation inside input.
Controlled reproduction
Procedure
- Configure Codex with a recording custom
model_providerusingwire_api = "responses". - Capture a real
codex execrequest body. - Replay the body several times with:
- one fixed
prompt_cache_key; - an identical developer prefix;
- only the final user message changed between requests.
- Record
usage.input_tokens_details.cached_tokensandcache_write_tokens. - Repeat with the same bodies, adding only the following field to the last
input_textblock in the stable developer prefix:
"prompt_cache_breakpoint": {"mode": "explicit"}
The measured bodies used the non-lite request shape because the production gateway rejects the proprietary additional_tools item. The serialization gap is the same in the Responses Lite path; only the correct placement differs.
Result
The first request in each arm was a cold write. The table below compares the three subsequent requests.
| request form | input tokens | cached tokens | cache-write tokens | hit rate |
|---|---:|---:|---:|---:|
| Unmodified Codex #1 | 9,067 | 0 | 9,065 | 0% |
| Unmodified Codex #2 | 9,062 | 0 | 9,060 | 0% |
| Unmodified Codex #3 | 9,065 | 0 | 9,063 | 0% |
| Breakpoint #1 | 9,067 | 8,937 | 128 | 98.6% |
| Breakpoint #2 | 9,062 | 8,937 | 123 | 98.6% |
| Breakpoint #3 | 9,065 | 8,937 | 126 | 98.6% |
No prompt_cache_options were sent. The only request-body change was the content-block breakpoint.
Scope and non-claims
- This report does not claim every Codex request misses cache. Five sequential turns in one unmodified session reached approximately 98% hits after the cold turn.
- The controlled A/B establishes causality for a stable prefix followed by a changing tail. It does not establish that every cache write on the provider invoice was caused by this defect.
- The source-level bug is inability to represent the field. Automatic insertion, stable-key policy, and top-level cache options may be fixed together or handled separately.
- The measured requirement for a stable cross-session key is specific evidence from Bedrock Mantle. OpenAI documents
prompt_cache_keyas a routing and matching hint, not a guaranteed hard cache partition.
Suggested minimum fix
- Add an optional breakpoint field to supported content variants, at least
ContentItem::InputTextandInputImage, withskip_serializing_if = "Option::is_none". AddInputFilesupport when that content variant exists. - Add serialization tests proving that the field is emitted only on supported content blocks and omitted by default.
- Gate use through an explicit model/provider capability such as
supports_prompt_cache_breakpoints. Do not infer support solely from a model-name string, because custom providers may expose different request capabilities. - Provide a code path that can mark the end of a measured stable rendered prefix. Automatic placement can remain conservative or opt-in; the minimum requirement is that the documented field be representable.
Follow-up improvements that are useful but not required to establish or fix the core serialization defect:
- derive or expose a cache key that can remain stable across independent requests with the same startup context, while respecting the documented request-rate guidance per key;
- add
prompt_cache_options: Option<PromptCacheOptions>to both HTTP and WebSocket request structs; - audit
CompactionInput, which also carriesprompt_cache_keywithoutprompt_cache_options, after confirming whether the specialized compact endpoint accepts the field.
<details>
<summary><strong>Additional measurements</strong></summary>
Intra-session continuation already caches correctly
Five sequential turns from one unmodified Codex session:
| turn | input | cached | writes | hit rate |
|---|---:|---:|---:|---:|
| 1 | 9,093 | 0 | 9,091 | 0% |
| 2 | 9,244 | 9,091 | 151 | 98% |
| 3 | 9,385 | 9,242 | 141 | 98% |
| 4 | 9,559 | 9,383 | 174 | 98% |
| 5 | 9,771 | 9,557 | 212 | 98% |
This is why the issue is easy to miss when inspecting a long interactive session: the implicit tail breakpoint works for monotonically growing history.
Cross-session key interaction on Bedrock Mantle
Three captured codex exec bodies contained 5,621 / 5,641 / 5,617 input tokens and differed in their volatile tails. Each arm replayed those same fixed bodies.
| configuration | per-request hit rate | total cache-write tokens |
|---|---|---:|
| Unmodified Codex | 0%, 0%, 0% | 16,873 |
| Explicit breakpoint only | 0%, 0%, 0% | 16,873 |
| Stable prompt_cache_key only | 0%, 0%, 0% | 16,873 |
| Breakpoint and stable key | 0%, 96%, 97% | 5,973 (−65%) |
The inputs total 16,879 while the full-miss writes total 16,873 because the provider reported two fewer write tokens than total input on each request.
This establishes that, on the measured backend, a stable key was also needed for independent sessions to realize reuse. It is a compounding key-policy issue, not evidence that the missing breakpoint field itself is backend-specific.
Other causes tested
- A 101,448-token prefix cached at 99% with a breakpoint, so this was not a prefix-size ceiling.
- Six byte-identical concurrent requests on a warm key all hit 99%, distinguishing this test from #33821.
stream: trueandstream: falsebehaved the same.- Reasoning efforts from
lowthroughxhighshowed the same baseline miss pattern. - Carrying reasoning items and requesting
reasoning.encrypted_contenthad no measurable effect after the arms were pre-warmed identically.
</details>
<details>
<summary><strong>Request shapes and breakpoint placement</strong></summary>
All three shipped GPT-5.6 catalog entries have use_responses_lite: true, but custom providers may use the non-lite path.
Responses Lite
Codex prepends an additional_tools developer item and a developer message containing base instructions to input; top-level instructions and tools are omitted.
{
"model": "gpt-5.6-sol",
"prompt_cache_key": "<session UUID>",
"input": [
{"type": "additional_tools", "role": "developer", "tools": ["..."]},
{"type": "message", "role": "developer", "content": [
{"type": "input_text", "text": "..."}
]},
{"type": "message", "role": "developer", "content": [
{"type": "input_text", "text": "..."},
{"type": "input_text", "text": "..."}
]},
{"type": "message", "role": "user", "content": ["..."]}
]
}
additional_tools has no content block on which to place a breakpoint. A placement algorithm would need to mark the last supported content block in the measured stable developer portion, not the additional_tools item itself.
Non-lite Responses
instructions and tools remain top-level, while leading developer messages are in input:
{
"model": "gpt-5.6-sol",
"prompt_cache_key": "<session UUID>",
"instructions": "...",
"input": [
{"type": "message", "role": "developer", "content": [
{"type": "input_text", "text": "..."},
{"type": "input_text", "text": "..."}
]},
{"type": "message", "role": "user", "content": ["..."]}
],
"tools": ["..."]
}
The controlled A/B marked the last input_text block in the stable developer item. An implementation must not assume every developer-role item is stable: current time, workspace data, permissions, tool availability, and project instructions can vary. The boundary should be based on the rendered stable portion, splitting content blocks where necessary.
</details>
<details>
<summary><strong>Cost impact and provider billing evidence</strong></summary>
GPT-5.6 cache writes are billed at 1.25× uncached input, while cache reads are billed at 0.10×. A write therefore costs 12.5× a read. Earlier model families did not expose the same cache-write line-item cost.
AWS Cost Explorer for OpenAI GPT-5.6 Sol (Amazon Bedrock Edition), 2026-07-01 through 2026-07-23 complete days, filtered to RECORD_TYPE = Usage:
| usage type | tokens | cost | share of model spend |
|---|---:|---:|---:|
| cache_write_tokens_30m_standard | 258.8M | $1,780.33 | 90.0% |
| output_tokens_standard | 3.9M | $127.56 | 6.5% |
| input_tokens_standard | 9.8M | $53.98 | 2.7% |
| cache_read_tokens_standard | 28.6M | $15.71 | 0.8% |
| Total | | $1,977.58 | |
Cache-write tokens were 9.1× cache-read tokens. Counting uncached input, cache writes, and cache reads together gives a 9.6% aggregate cached-token share.
For comparison, the same client and gateway against OpenAI GPT-5.5 (Amazon Bedrock Edition) billed 9.17M input tokens and zero cache-write tokens over the same period.
What this evidence establishes:
- cache-write volume is a material real-world cost on this GPT-5.6 workload;
- the provider's implied unit prices reproduce the documented write/read multipliers;
- the controlled replay shows that a correctly placed breakpoint can eliminate most warm writes for the tested request pattern.
What it does not establish:
- that this defect caused every one of the 258.8M cache-write tokens;
- the share attributable to cold starts, genuinely distinct prefixes, compaction, or other prefix churn;
- universal savings on other providers or workloads.
Applying the controlled reductions to all invoice writes would imply approximately $1,157 at the 65% cross-session reduction or $1,745 at the 98% same-prefix A/B reduction. Those are illustrative upper bounds, not attributable savings.
</details>
<details>
<summary><strong>Relationship to existing issues</strong></summary>
| issue | relationship |
|---|---|
| #34569 | Confirms that ModelProviderInfo has no generic top-level request-body extension. It could expose prompt_cache_options, but not a nested content-block breakpoint without structured input transformation. |
| #31882 | use_responses_lite changes the rendered prefix shape and therefore where an automatically inserted breakpoint would belong. |
| #32479 / PR #33454 | Cache-write usage is now carried through Codex for API-key/provider responses. The issue remains open because subscription-backed responses may still report zero. |
| #24704 | Closest prior art for preserving cache lineage in forked subagents; discussion mentions an inherited key and explicit inherited-prefix boundary, but not the missing serialization field. |
| #29377 / #21796 / #26283 | Overlap with the separate request for stable or configurable cache keys. |
| #20301 | Similar low-hit symptom on GPT-5.5, but this report does not establish its cause and the GPT-5.6 breakpoint API does not apply there. |
| #30425 | Reports intermittent misses on repeated bodies. A truly byte-identical body also has an identical implicit breakpoint, so the missing explicit field does not by itself explain that issue. |
| #33821 | Concurrent identical requests can split between hit and miss. Concurrency reproduced cleanly in this test, so it appears separate. |
| #32613 | Image-triggered invalidation may be adjacent because GPT-5.6 supports image-block breakpoints, but no causal link is established here. |
No issue found in the searched set reports that prompt_cache_breakpoint is structurally unserializable in Codex, and no open PR found in that search addresses it.
</details>
<details>
<summary><strong>Verification notes</strong></summary>
- The cache-relevant source was checked at
rust-v0.145.0,rust-v0.146.0-alpha.9,rust-v0.146.0-alpha.10, and the citedmainrevision. Relevant line numbers move, but the missing fields remain. git grep -l -E 'prompt_cache_options|prompt_cache_breakpoint' <ref>returned only the vendored migration-guide markdown asset and no Rust implementation at the checked refs.ResponsesApiRequestchanged between stable and prerelease for unrelated tool serialization, confirming the request struct has been edited recently without adding these cache fields.- The replay fixtures, proxy transform, and raw provider responses are not embedded in this issue body. The source-level defect is independently verifiable without them; the controlled measurements should be treated as author-supplied evidence unless a sanitized bundle is attached.
- The AWS figures can be reproduced with Cost Explorer by filtering the model service to
RECORD_TYPE = Usageand grouping byUSAGE_TYPE.UsageQuantityis reported in millions of tokens.
</details>
6 Comments
I reproduced the cross-thread failure on the ChatGPT Codex subscription backend, using a custom Codex build based on
rust-v0.146.0andgpt-5.6-sol.Controlled setup:
prompt_cache_key;WebSocket result:
| branch | input | cached | ancestor coverage |
|---|---:|---:|---:|
| A | 41,488 | 5,888 | 14.2% |
| B | 41,488 | 5,888 | 14.2% |
I repeated the same experiment with WebSockets disabled, forcing full HTTP Responses requests. The stable ancestor was 42,387 tokens, both forks again used the same root cache key, and all cache-relevant request fingerprints plus the full ancestor item sequence matched:
| branch | input | cached |
|---|---:|---:|
| A | 42,423 | 0 |
| B | 42,423 | 0 |
The root's ordinary append-only turns did cache within the same run (18,176 cached on turn 2 and 31,488 on turn 3), so the account/backend was caching. The failure begins when the stable history is replayed into independent forked sessions.
This supports the issue's distinction between append-only intra-session caching and independent requests that diverge after a stable ancestor. A shared
prompt_cache_keyalone did not preserve the ancestor cache on the ChatGPT subscription backend.I also manually probed explicit
prompt_cache_breakpointagainst the ChatGPT Codex backend with both Sol and Luna routes; the backend rejected the parameter as unsupported. That leaves a subscription client unable to apply the documented GPT-5.6 remedy even after adding the missing Codex wire representation.Questions for maintainers:
prompt_cache_breakpointintended to be supported bychatgpt.com/backend-api/codex/responsesfor GPT-5.6?prompt_cache_keyand the rendered prompt prefix?These results were collected within one cache-TTL window. The fork requests contained no sibling content.
Follow-up with the missing orthogonal control: I reran the same 41,452-token ancestor on the same
gpt-5.6-solbuild, still dispatching A first and B only after A completed.prompt_cache_key: both cached only 5,888 tokens (14.20% ancestor coverage).The model, stable ancestor, cache key, order, and branch prompts were held constant. Splitting the execution into independent thread/session/connection state is the variable that caused the cache loss; concurrency is not required to reproduce it.
I hit this exact failure in session
019fd916-3d94-7b01-b3ff-cbf95071f34bon 2026-08-06 withgpt-5.6-luna.The final five Responses API calls were:
| timestamp | uncached input | cached input | output | cost |
|---|---:|---:|---:|---:|
| 22:23:14.319Z | 206 | 135,936 | 31 | $0.002797 |
| 22:28:04.511Z | 143,985 | 0 | 67 | $0.028877 |
| 22:28:17.715Z | 976 | 143,104 | 31 | $0.003094 |
| 22:33:08.523Z | 144,134 | 0 | 70 | $0.028911 |
| 22:33:21.719Z | 1,128 | 143,104 | 31 | $0.003125 |
The two full misses occurred immediately after completed long-running tool calls (about 287 seconds each). In both cases, the next continuation roughly 13 seconds later reused 143K cached tokens. The request context was otherwise append-only and about 144K tokens, so this does not look like genuine context churn or cache expiry.
This is a concrete billing impact: the two misses cost about 9x more than their immediately-following cached continuations. It appears consistent with GPT-5.6's latest-tool implicit breakpoint invalidating the full prefix when the completed tool output changes, exactly as described in the issue.
ChatGPT subscription backend reproduction: explicit GPT-5.6 cache controls are rejected, and same-key append-only turns still miss intermittently
We reproduced the subscription-backend half of this issue under sustained, naturally initiated Claude Code traffic routed to
https://chatgpt.com/backend-api/codex/responses.Capability result
We enabled a deterministic treatment that changed only one field: it added
to a historical stable
input_textblock while preserving the physical model, account, endpoint, request ordering, tools, instructions, and existing conversation-scopedprompt_cache_key.Results:
| model | treatment requests | HTTP 400 | successful cache measurements |
|---|---:|---:|---:|
|
gpt-5.6-sol| 18 | 18 | 0 ||
gpt-5.6-terra| 8 | 8 | 0 || total | 26 | 26 | 0 |
Claude Code surfaced:
prompt_cache_breakpoint is not supported on this model.The treatment was rolled back immediately. The first 16 post-rollback requests were 16/16 HTTP 200, confirming the errors were treatment-specific.
Same-key miss result without explicit fields
On ordinary append-only traffic with conversation-scoped keys, we observed a 304,776-token Sol follow-up with:
prompt_cache_keyas adjacent turns;This rules out cache expiry, concurrent publication, key rotation, tool/instruction drift, and an in-flight leader for that event. It matches the intermittent large mid-loop misses discussed here and in #33821/#35925.
Transport and key controls
We also tested two alternatives under natural traffic:
These results support the existing evidence that a client key or WebSocket connection alone cannot recover the stable ancestor on the ChatGPT subscription backend.
Backend asks
prompt_cache_breakpointintended to be supported onchatgpt.com/backend-api/codex/responsesfor Sol/Terra/Luna?prompt_cache_breakpointand top-levelprompt_cache_optionsover HTTP and WebSocket.No scripted traffic was sent to Anthropic-backed accounts. The aggregate evidence above contains no prompt text, raw cache keys, session IDs, account IDs, or credentials.
I prepared the public client-representation patch against current
mainafter the subscription-backend reproduction above.Branch: https://github.com/StartupBros/codex/tree/feat/gpt56-prompt-cache-controls
Commit: https://github.com/StartupBros/codex/commit/8271903d0a5179db9b3f220acf8231eea4cc150c
OpenAI's collaborator-only PR policy rejected external PR creation, so a maintainer can fetch/cherry-pick it directly:
Patch scope:
prompt_cache_options(implicit|explicit,30m){"prompt_cache_breakpoint":{"mode":"explicit"}}injection for message content and structured function/custom-tool outputsValidation on rebased current main:
just test -p codex-api: 176 passedcodex-apiandcodex-coretest targets compileThis intentionally does not auto-place breakpoints or change cache keys. It makes the documented public GPT-5.6 wire contract representable; the private ChatGPT subscription endpoint still needs backend acceptance before Codex can enable it there.
These are two different failures and they don't cost the same.
Missing the cache on a prefix that still exists means you pay 1.0 for input
instead of 0.1. Losing the prefix is worse, because rebuilding it is a write,
and writes cost 1.25 times input on the five minute cache and 2x on the one
hour one.
Which is why raising the TTL to survive forking backfires. At 2x you need
1.11 reads per write to break even instead of 0.28. Forking already wrecked
the hit rate, and the longer cache raises the bar.
The number that settles it is reads over writes, per model, with the cache
key and the model held still. The 98.20 against 14.20 above is the only
measurement here that actually does that.