Skip to content

fix(sse): stop thrashing the provider prompt cache for caching-aware clients - #8705

Merged
diegosouzapw merged 2 commits into
release/v3.8.49from
fix/prompt-cache-preservation
Jul 27, 2026
Merged

diegosouzapw merged 2 commits into
release/v3.8.49from
fix/prompt-cache-preservation

Conversation

@diegosouzapw

Copy link
Copy Markdown
Collaborator

Problem

A Claude Code (or any caching-aware) client routed through a combo had its own
cache_control markers stripped and rewritten unless the combo used one of the
two "deterministic" strategies (priority, cost-optimized). The rewritten
breakpoint positions are recomputed per request and are not stable
turn-over-turn, which invalidates the provider prompt cache on every turn.

Observed in production on quota-share (qtSd/) combos: cacheRead stuck at the
tools+system prefix while cacheWrite re-wrote ~200k tokens per turn — the
account quota drained at a multiple of the expected rate.

Fix (three coordinated changes)

  1. shouldPreserveCacheControl now preserves client markers for every combo
    strategy. Preserving is never worse than rewriting: on a stable target the
    client's breakpoints advance deterministically (the per-request heuristic's
    do not), and on a target switch both approaches miss equally. The
    alwaysPreserveClientCache: "always" | "never" overrides are unchanged, as
    are the caching-aware-client and caching-provider gates.
  2. prepareClaudeRequest gains a translator-path opt-in fallback: when
    preserve-mode has nothing to preserve (the client sent no cache_control
    anywhere), the standard heuristic still applies so a request never ships with
    zero cache breakpoints. The claude-code-compatible relay path keeps its
    long-standing no-supplement contract (cc-compatible-provider.test.ts
    unchanged and green).
  3. FORWARDABLE_CLIENT_BETAS now forwards a client-negotiated
    context-1m-2025-08-07. Claude Code with /model <id>[1m] sends that beta
    itself; the allowlist merge silently dropped it for non-Opus models. It is
    still never forced when the client did not send it (guarded by test).

Tests

  • tests/unit/cache-control-policy.test.ts — contract updated: preservation for
    all strategies incl. quota-share; regressions kept (non-caching provider,
    non-caching-aware client, always/never overrides).
  • tests/unit/claude-prepare-request-preserve-fallback.test.ts (new) — the
    fallback + the no-supplement contract without the opt-in flag.
  • tests/unit/claude-context-1m-client-beta-forward.test.ts (new) — forward
    when negotiated, never forced otherwise; [BUG] tool-search-tool-2025-10-19 beta dropped on Claude OAuth path → 400 "Tool reference not found" with ENABLE_TOOL_SEARCH #3974 tool-search regression kept.
  • Local validation: typecheck:core clean; lint clean on touched files;
    translator suite 688/688; touched suites 115/115; full npm run test:unit
    (26k+ tests) with the only failures reproducing identically on the clean base
    branch in this environment (env-doc-sync / i18n parity / monaco /
    ServiceSupervisor port-adoption — pre-existing, unrelated files).

…clients

- shouldPreserveCacheControl: preserve client cache_control markers for every
  combo strategy. The deterministic-strategy gate forced marker rewrites whose
  per-request breakpoint positions are not stable turn-over-turn, thrashing the
  upstream prompt cache (observed in production as ~200k cache_write tokens per
  turn on quota-share combos). Preserving is never worse: on a stable target
  the client's breakpoints advance deterministically; on a target switch both
  approaches miss equally.
- prepareClaudeRequest: translator-path opt-in fallback — when preserve-mode
  has nothing to preserve (client sent no cache_control anywhere), apply the
  standard heuristic so requests never ship with zero cache breakpoints. The
  claude-code-compatible relay path keeps its no-supplement contract.
- anthropic-beta: forward the client-negotiated context-1m-2025-08-07 through
  the allowlist merge so a /model <id>[1m] client keeps its long-context
  negotiation behind the proxy (never forced when the client did not send it).
…ze pending on quota-share block

- translateNonStreamingResponse (claude→openai): fold cache_read into
  prompt_tokens and expose prompt_tokens_details.cached_tokens /
  cache_creation_tokens, mirroring the streaming contract (#1426/#2215).
  Non-streaming OpenAI clients behind a cached Claude upstream previously saw
  prompt_tokens=<uncached remainder> (e.g. 23 for a ~9k request) with no cache
  visibility.
- chatCore quota-share block: finalize the pending-request slot before
  returning the policy 429 — the path never reaches upstream and the orphaned
  pending lingered as a status-0 call-log row until the reaper swept it.
  Validated live on the staging box (repro: blocked key → orphan row; after:
  clean).
@diegosouzapw
diegosouzapw merged commit 7db3ba6 into release/v3.8.49 Jul 27, 2026
20 checks passed
@diegosouzapw
diegosouzapw deleted the fix/prompt-cache-preservation branch July 27, 2026 20:24
HouMinXi pushed a commit to HouMinXi/OmniRoute that referenced this pull request Aug 2, 2026
…clients (cheaper-inference#8705)

* fix(sse): stop thrashing the provider prompt cache for caching-aware clients

- shouldPreserveCacheControl: preserve client cache_control markers for every
  combo strategy. The deterministic-strategy gate forced marker rewrites whose
  per-request breakpoint positions are not stable turn-over-turn, thrashing the
  upstream prompt cache (observed in production as ~200k cache_write tokens per
  turn on quota-share combos). Preserving is never worse: on a stable target
  the client's breakpoints advance deterministically; on a target switch both
  approaches miss equally.
- prepareClaudeRequest: translator-path opt-in fallback — when preserve-mode
  has nothing to preserve (client sent no cache_control anywhere), apply the
  standard heuristic so requests never ship with zero cache breakpoints. The
  claude-code-compatible relay path keeps its no-supplement contract.
- anthropic-beta: forward the client-negotiated context-1m-2025-08-07 through
  the allowlist merge so a /model <id>[1m] client keeps its long-context
  negotiation behind the proxy (never forced when the client did not send it).

* fix(sse): surface cache tokens in non-streaming OpenAI usage + finalize pending on quota-share block

- translateNonStreamingResponse (claude→openai): fold cache_read into
  prompt_tokens and expose prompt_tokens_details.cached_tokens /
  cache_creation_tokens, mirroring the streaming contract (cheaper-inference#1426/cheaper-inference#2215).
  Non-streaming OpenAI clients behind a cached Claude upstream previously saw
  prompt_tokens=<uncached remainder> (e.g. 23 for a ~9k request) with no cache
  visibility.
- chatCore quota-share block: finalize the pending-request slot before
  returning the policy 429 — the path never reaches upstream and the orphaned
  pending lingered as a status-0 call-log row until the reaper swept it.
  Validated live on the staging box (repro: blocked key → orphan row; after:
  clean).

---------

Co-authored-by: diegosouzapw <diegosouzapw24@gmail.com>
muhamadgalihsaputra pushed a commit to niyatna/NiyatnaRoute that referenced this pull request Sep 27, 2026
…clients (cheaper-inference#8705)

* fix(sse): stop thrashing the provider prompt cache for caching-aware clients

- shouldPreserveCacheControl: preserve client cache_control markers for every
  combo strategy. The deterministic-strategy gate forced marker rewrites whose
  per-request breakpoint positions are not stable turn-over-turn, thrashing the
  upstream prompt cache (observed in production as ~200k cache_write tokens per
  turn on quota-share combos). Preserving is never worse: on a stable target
  the client's breakpoints advance deterministically; on a target switch both
  approaches miss equally.
- prepareClaudeRequest: translator-path opt-in fallback — when preserve-mode
  has nothing to preserve (client sent no cache_control anywhere), apply the
  standard heuristic so requests never ship with zero cache breakpoints. The
  claude-code-compatible relay path keeps its no-supplement contract.
- anthropic-beta: forward the client-negotiated context-1m-2025-08-07 through
  the allowlist merge so a /model <id>[1m] client keeps its long-context
  negotiation behind the proxy (never forced when the client did not send it).

* fix(sse): surface cache tokens in non-streaming OpenAI usage + finalize pending on quota-share block

- translateNonStreamingResponse (claude→openai): fold cache_read into
  prompt_tokens and expose prompt_tokens_details.cached_tokens /
  cache_creation_tokens, mirroring the streaming contract (cheaper-inference#1426/cheaper-inference#2215).
  Non-streaming OpenAI clients behind a cached Claude upstream previously saw
  prompt_tokens=<uncached remainder> (e.g. 23 for a ~9k request) with no cache
  visibility.
- chatCore quota-share block: finalize the pending-request slot before
  returning the policy 429 — the path never reaches upstream and the orphaned
  pending lingered as a status-0 call-log row until the reaper swept it.
  Validated live on the staging box (repro: blocked key → orphan row; after:
  clean).

---------

Co-authored-by: diegosouzapw <diegosouzapw24@gmail.com>
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