Commit 4820134
authored
fix(proxy): keep cache_control bounded + stable so the freeze overlay stops busting (headroomlabs-ai#1852)
Follow-up to headroomlabs-ai#1850. Two residual cache-bust sources, both
`cache_control`-related:
1. **Guard too strict.** `overlay_cached_prefix` decided "is this turn
an append-only extension?" by comparing whole message dicts — including
`cache_control`. Clients (Claude Code, litellm) move the cache
breakpoint to the newest message every call, so a marker landing in the
frozen prefix made the guard fail, the overlay skip its replay, and the
raw freeze forward ORIGINAL bytes over the cached COMPRESSED prefix →
partial bust (the ~42% residual on the a10 run, `prefix_change=0`). Fix:
run the append-only guard on **content only** (strip `cache_control`
before comparing) — content is what the provider's cache keys on.
2. **Marker accumulation.** The overlay replays the markers that rode on
each turn's then-newest message, so `cache_control` blocks pile up
~1/turn; Anthropic hard-errors at >4 total. Fix:
`normalize_message_cache_control` strips every message-level marker and
re-places a single ephemeral breakpoint on the last block (one
breakpoint caches the whole prefix; cache is content-keyed so re-placing
never busts). Wired into the Anthropic handler after the overlay.
**Per-provider (deliberately scoped):**
- **Anthropic**: `cache_control` markers → both fixes apply.
- **OpenAI**: AUTOMATIC prefix caching, no markers → overlay
(byte-identity) only; normalize is NOT applied (Anthropic markers on an
OpenAI request would be wrong).
- **Bedrock**: serves Claude via the pipeline but has no
cachePoint/freeze-replay path → not affected; a cachePoint analog would
be needed if caching is expanded.
- **Gemini**: explicit Cache API (`cachedContent`), no inline
markers/freeze → N/A.
> Stacked on headroomlabs-ai#1850 — review that first; the diff against `main` includes
its overlay + `has_new_ccr_markers` work.
## Description
Keeps the freeze overlay's cache-safety intact against real clients that
relocate the `cache_control` breakpoint each turn, and prevents
`cache_control` blocks from accumulating past Anthropic's 4-marker
limit. See the two fixes above.
Closes #<!-- none --> — follow-up to headroomlabs-ai#1850 (no separate issue).
## Type of Change
- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)
## Changes Made
- `headroom/cache/prefix_tracker.py`: append-only guard in
`overlay_cached_prefix` now compares **content only** (ignores
`cache_control`); new `normalize_message_cache_control()` collapses
message-level markers to a single ephemeral breakpoint on the last
block.
- `headroom/proxy/handlers/anthropic.py`: apply
`normalize_message_cache_control` after the overlay (Anthropic only).
- `tests/test_cache_control_move_bust.py`: reproduces the moved-marker
bust + proves both fixes.
## Testing
- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`)
- [x] Type checking passes (`mypy headroom`)
- [x] New tests added for new functionality
- [x] Manual testing performed (local, see below)
### Test Output
```text
$ pytest tests/test_cache_control_move_bust.py -q
....... [100%]
7 passed in 0.19s
# broader cache-safety suite (overlay + cross-turn + CCR deferred + openai/anthropic cache-stability + helpers)
$ pytest tests/test_cache_control_move_bust.py tests/test_cache_prefix_overlay.py \
tests/test_cross_turn_cache_safety.py tests/test_proxy/test_anthropic_ccr_deferred_injection.py \
tests/test_proxy_handler_helpers.py tests/test_proxy_openai_cache_stability.py \
tests/test_proxy_anthropic_cache_stability.py -q
91 passed, 2 warnings in 29.98s
$ ruff check . # ruff 0.15.17 (CI-pinned)
All checks passed!
$ ruff format --check . # ruff 0.15.17
1057 files already formatted
$ mypy headroom --ignore-missing-imports
Success: no issues found (changed modules: prefix_tracker, anthropic, openai, helpers)
```
## Real Behavior Proof
- **Environment:** local (`.venv`, Python 3.12), ruff 0.15.17 / mypy
pinned to CI versions.
- **Exact command / steps:** `tests/test_cache_control_move_bust.py`
drives the REAL tracker + freeze + `overlay_cached_prefix` +
`normalize_message_cache_control` across multiple append-only turns
where the client moves the `cache_control` breakpoint each turn.
- **Observed result:** with a moved marker in the frozen prefix, the
content-only guard keeps the overlay replaying (forwarded prefix stays
byte-identical → no bust); `cache_control` blocks stay ≤4 across many
turns and content is never altered. The reproduction test fails without
the fix and passes with it.
- **Not tested (this PR):** the end-to-end a10 SWE-bench run is the
field observation motivating fix headroomlabs-ai#1 (~42% residual, `prefix_change=0`);
not re-run here.
## Review Readiness
- [x] I have performed a self-review
- [x] This PR is ready for human review
## Checklist
- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] I have updated the CHANGELOG.md if applicable
## Additional Notes
Stacked on headroomlabs-ai#1850; land that first. Docs/CHANGELOG untouched (behavioral
cache-safety fix; no user-facing surface change). N/A: no screenshots
(no UI).1 parent 5d14080 commit 4820134
3 files changed
Lines changed: 278 additions & 4 deletions
File tree
- headroom
- cache
- proxy/handlers
- tests
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
112 | 112 | | |
113 | 113 | | |
114 | 114 | | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
115 | 131 | | |
116 | 132 | | |
117 | 133 | | |
| |||
151 | 167 | | |
152 | 168 | | |
153 | 169 | | |
154 | | - | |
155 | | - | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
156 | 177 | | |
157 | | - | |
| 178 | + | |
158 | 179 | | |
159 | 180 | | |
160 | 181 | | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
161 | 232 | | |
162 | 233 | | |
163 | 234 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1348 | 1348 | | |
1349 | 1349 | | |
1350 | 1350 | | |
1351 | | - | |
| 1351 | + | |
| 1352 | + | |
| 1353 | + | |
| 1354 | + | |
1352 | 1355 | | |
1353 | 1356 | | |
1354 | 1357 | | |
| |||
1360 | 1363 | | |
1361 | 1364 | | |
1362 | 1365 | | |
| 1366 | + | |
| 1367 | + | |
| 1368 | + | |
| 1369 | + | |
| 1370 | + | |
| 1371 | + | |
| 1372 | + | |
| 1373 | + | |
| 1374 | + | |
| 1375 | + | |
1363 | 1376 | | |
1364 | 1377 | | |
1365 | 1378 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
0 commit comments