Skip to content

feat(models): a caller can keep the cache breakpoint off per-request rows - #50

Merged
rezaho merged 1 commit into
mainfrom
s236-durable-row-cache-breakpoint
Aug 19, 2026
Merged

feat(models): a caller can keep the cache breakpoint off per-request rows#50
rezaho merged 1 commit into
mainfrom
s236-durable-row-cache-breakpoint

Conversation

@rezaho

@rezaho rezaho commented Aug 2, 2026

Copy link
Copy Markdown
Owner

Why

A prompt-cache entry is only worth writing if the next request can read it, which requires its hashed prefix to consist of bytes that request still contains.

mark_conversation_tail_for_cache marks the last content block of the last message, unconditionally. When a caller appends per-request content after the durable conversation — a clock line, a budget figure, anything derived from "now" — the marker lands on exactly the row the next request cannot reproduce. Every request then writes a fresh entry and reads none, silently, forever.

Measured on Bedrock / anthropic.claude-opus-5, single-step turns, tools present:

marker position turn 1 turn 2
on the volatile row (behaviour before this PR) write=8211 write=8226 read=0
on the last durable row (this PR) write=8425 write=15 read=8425

Note the tools array is load-bearing in any probe of this: without one, a messages-tier marker writes entries that never become readable, which produces false negatives.

What

CACHE_EXEMPT_KEY — a neutral per-item key riding the caller's own message dict, following the defer_loading precedent (the framework's nearest analogous feature also took no new request parameter). A caller marks its per-request rows; the marker moves back to the last durable row, which the next request contains by construction since durable rows only ever grow by append.

Semantics:

  • Counted from the END, stopping at the first unmarked row — the exemption is about position, so a marked row with durable rows after it is not a tail and does not move the marker.
  • An all-exempt list writes no marker at all (nothing durable to anchor to; a marker there would cost an entry per request and read none).
  • The key never reaches the wire — both payload builders already rebuild rows with only the wire-legal fields.

Compatibility

volatile_tail defaults to 0, so with no exempt row the payload is byte-identical to before. A test pins that explicitly. No existing caller changes behaviour.

Twinned on AnthropicOAuthAdapter, which is not a subclass of AnthropicAdapter and has its own format_request_payload. Bedrock inherits unchanged.

Tests

10 new tests in tests/models/test_prompt_cache_breakpoint.py covering placement, the position semantics, the all-exempt case, wire-cleanliness, byte-identity of the no-key path, both legs, and idempotence. Full file: 39 passed. Suite: 4 failures, all pre-existing on main and none in adapters (3 reproduce at the merge base; the 4th is a live smoke test that skips without a key).

…rows

A prompt-cache entry is only worth writing if the NEXT request can read it, which
requires its hashed prefix to be bytes that request still contains. When a caller
appends per-request content after the durable conversation — a clock, a budget
figure, anything derived from "now" — the unconditional last-message breakpoint
lands on exactly the row the next request cannot reproduce, so every request
writes a fresh entry and reads none.

`CACHE_EXEMPT_KEY` is a neutral per-item key on the caller's own message dict
(the `defer_loading` shape) marking a row as per-request. The marker then lands
on the last durable row, which the next request contains by construction since
durable rows only ever grow by append. Counted from the end and stopping at the
first unmarked row: the exemption is about position, so a marked row with durable
rows after it is not a tail. An all-exempt list writes no marker at all.

Measured on Bedrock/Opus 5, single-step turns with tools present:
  marker on the volatile row: turn 1 write=8211 -> turn 2 write=8226 read=0
  marker on the last durable row: turn 1 write=8425 -> turn 2 write=15 read=8425

With no exempt row the payload is byte-identical to before, so no existing caller
changes behaviour. Twinned on the OAuth leg, which is not a subclass and has its
own payload builder. The key never reaches the wire — both builders rebuild rows
with only the wire-legal fields.
@gemini-code-assist

Copy link
Copy Markdown

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@rezaho
rezaho merged commit 76a944c into main Aug 19, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant