You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(proxy): restore token-mode provider prefix-cache hit rate
Prompt-cache hit rate collapsed in headroom_mode=token. Four root causes:
- openai: pass the raw client messages as original_messages to
update_from_response at all three OpenAI-path recording sites (both
non-streaming branches and the streaming finalizer). They previously
omitted it, so the tracker recorded forwarded (compressed) bytes as
originals and the next turn's append-only check diverged immediately
and never replayed the cached prefix.
- session: fold a conversation discriminator (Anthropic metadata.user_id
/ OpenAI user, else first-user-message text) into compute_session_id's
fallback so two conversations sharing a model and system prompt no
longer collide onto one PrefixCacheTracker and overwrite each other's
recorded messages.
- ccr: skip proactive-expansion and memory-context injection when the
live-zone tail was already forwarded last turn (overlay_cached_prefix
had already replayed it, so re-injecting double-appends); dedup
expansions per session; scope compressed context by session so a
session started after Claude Code's /compact no longer resurfaces
pre-compact content.
- anthropic: route ccr_inject_system_instructions to the top-level
body["system"] field instead of prepending a role:"system" message
the Messages API rejects with 400, using a static hash-free block so
the system segment stays byte-stable; the messages-based path
(OpenAI format) uses the same hash-free text.
The ccr_inject_system_instructions flag is default off; savings
accounting paths are unchanged.
Copy file name to clipboardExpand all lines: CHANGELOG.md
+5Lines changed: 5 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -9,6 +9,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
9
9
## Unreleased
10
10
11
11
### Fixed
12
+
-**proxy/anthropic:** CCR system-instruction injection (`ccr_inject_system_instructions`, default off) now appends to the top-level `body["system"]` field instead of prepending a `role:"system"` message to `messages` — the Anthropic Messages API rejects that message shape outright, so every marker-bearing turn with the flag enabled previously got a 400 from upstream. The injected text is now a static, hash-free block (`create_stable_system_instructions`) appended idempotently (existing `cache_control` breakpoints on prior system blocks are left untouched), so the system segment stays byte-identical turn over turn instead of busting the cache via the old per-turn hash list.
13
+
-**proxy/ccr:** the messages-based system-instruction injection path (`CCRToolInjector.inject_into_system_message`, used by OpenAI-format clients when `ccr_inject_system_instructions` is enabled) now injects the same static, hash-free block via `create_stable_system_instructions` instead of `create_system_instructions`'s per-turn `Available hashes` list, so an OpenAI-format system prompt stays byte-identical turn over turn instead of busting the provider's prompt cache each time the enumerated hashes change.
14
+
-**proxy/ccr:** CCR proactive-expansion and memory-context injection now skip appending to the live-zone tail message when that exact position was already forwarded (and byte-replayed by `overlay_cached_prefix`) last turn. Previously both injections ran unconditionally in token mode, so a same-messages re-request appended a second copy on top of the one `overlay_cached_prefix` had just replayed — busting the final segment and growing it unboundedly turn over turn. Proactive-expansion recommendations are also deduped per session (a `SessionExpansionDedupTracker`, mirroring the existing sticky-CCR-tool session store) so the same compressed-content hash is never injected twice into one conversation, and `ContextTracker.analyze_query`/`track_compression` now accept an optional `session_id` so a fresh session started after Claude Code's `/compact` no longer has pre-compact compressed content surfaced back into it ([#2186](https://github.com/headroomlabs-ai/headroom/issues/2186)).
15
+
-**proxy/session:**`compute_session_id`'s fallback (used when no `x-headroom-session-id` header is sent) now folds in a conversation discriminator — the caller-supplied hint (Anthropic `metadata.user_id` / OpenAI `user`) or, failing that, the first user message's text — instead of hashing only `model + system prompt`. Two concurrent or successive conversations sharing a model and system prompt (two Claude Code tabs on one repo, a new session started after ending one) previously collapsed onto the same `PrefixCacheTracker`, so interleaved turns overwrote each other's recorded original/forwarded messages and busted the prompt cache for both ([#1808](https://github.com/headroomlabs-ai/headroom/issues/1808)).
16
+
-**proxy/openai:** the OpenAI-format chat-completions path (`handle_openai_chat`, both the LiteLLM/any-llm backend branch and the direct-backend branch) and its streaming finalizer now pass the raw client messages as `original_messages` to `PrefixCacheTracker.update_from_response`. All three call sites previously omitted it, so the tracker fell back to recording the FORWARDED (compressed) bytes as "originals" — the next turn's append-only cache-safety check then compared raw client bytes against compressed ones, diverged immediately, and never replayed the cached prefix, silently busting the provider's prompt cache on every turn for OpenAI-format clients in token mode.
12
17
-**proxy/savings:**`SavingsTracker.record_request()` only appended a history point when `tokens_saved > 0` (headroom's own lossy compression). In `--mode cache`, `tokens_saved` is near-always 0 by design, since the frozen prefix is byte-replayed rather than compressed to keep the provider's prompt cache warm. That silently dropped every history point on a cache-mode deployment even when `cache_read_tokens`/`cache_savings_usd` were large, making `headroom-monthly`-style tooling read as a total savings collapse. The guard now fires on `tokens_saved` OR `cache_read_tokens`, and the appended entry carries `cache_read_tokens`/`cache_savings_usd` so downstream consumers can show them; `_normalize_history_entry` defaults both fields to 0/0.0 for legacy entries that predate this change.
13
18
-**litellm:** vendor-specific top-level fields on `/v1/chat/completions`, including vLLM's `chat_template_kwargs` for per-request Qwen3 thinking-mode toggles, now reach OpenAI-compatible backends through LiteLLM `extra_body` instead of being dropped by the standard-parameter allowlist ([#2128](https://github.com/headroomlabs-ai/headroom/issues/2128)).
14
19
-**cache aligner:** hash the actual frozen Claude Code prefix instead of only system-message text, so `stable_prefix_hash` / `prefix_changed` now surface prompt-cache churn when a cached tool-result block changes without any system-prompt edit ([#2085](https://github.com/headroomlabs-ai/headroom/issues/2085)).
Copy file name to clipboardExpand all lines: docs/content/docs/configuration.mdx
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -317,7 +317,7 @@ For provider-only proxying, prefer `HEADROOM_HTTP_PROXY` over process-wide varia
317
317
318
318
### Session Beta Header Tracking
319
319
320
-
When running as a proxy, Headroom maintains a per-session union of `anthropic-beta` (and `OpenAI-Beta`) tokens via `SessionBetaTracker`. The session key is derived from the `x-headroom-session-id` header if present, otherwise from `md5(model + system_prompt[:500])[:16]` — stable across turns of the same conversation.
320
+
When running as a proxy, Headroom maintains a per-session union of `anthropic-beta` (and `OpenAI-Beta`) tokens via `SessionBetaTracker`. The session key is derived from the `x-headroom-session-id` header if present, otherwise from `md5(model + system_prompt + conversation_discriminator)[:16]`, where the discriminator is a client-supplied hint (Anthropic `metadata.user_id` / OpenAI `user`) or, failing that, the first user message's text. This keeps the key stable across turns of one conversation while staying distinct across concurrent conversations that share a model and system prompt.
321
321
322
322
**Why:** clients such as Claude Code and Codex CLI may drop a beta token between consecutive turns. Because `anthropic-beta` is part of the request bytes that determine the upstream prefix-cache key, a dropped token would bust the cache mid-conversation. The tracker re-injects any token seen earlier in the session so the cache key stays stable.
0 commit comments