Skip to content

fix: stabilize cache inputs - #634

Closed
jgautheron wants to merge 1 commit into
lidge-jun:devfrom
jgautheron:fix/cache-input-stability-v2
Closed

fix: stabilize cache inputs#634
jgautheron wants to merge 1 commit into
lidge-jun:devfrom
jgautheron:fix/cache-input-stability-v2

Conversation

@jgautheron

@jgautheron jgautheron commented Jul 28, 2026

Copy link
Copy Markdown

Fixes an issue with cache inputs where I had claude code forward subagent work to GPT 5.6 and ended up with a surprise 7k bill in the morning due to repeated cache writes.

I see both fixes were already in the dev branch (I initially started off main), so closing this PR as this additional change isn't worth it.

5819b107  prompt_cache_key from metadata.user_id
751e473c  session_id header from same session identity

Summary by CodeRabbit

  • New Features

    • Improved prompt-cache keying for native OpenAI/ChatGPT routing based on model, normalized system content, and tool definitions.
    • Added deterministic ordering for tools, skills, and deferred-tool listings to improve cache consistency.
    • Added session affinity handling based on request metadata.
  • Documentation

    • Updated Claude Code caching guidance across English, Japanese, Korean, Russian, and Simplified Chinese documentation.
  • Bug Fixes

    • Prevented cache mismatches caused by nondeterministic tool discovery and system-reminder ordering.

@github-actions github-actions Bot added the bug Something isn't working label Jul 28, 2026
@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Claude prompt-cache inputs are now canonicalized by sorting tool definitions and recognized system-reminder listings. Native passthrough applies the same stabilization, routed requests derive session_id from metadata.user_id, and documentation and tests describe and verify the updated behavior.

Changes

Claude cache stabilization

Layer / File(s) Summary
Canonical prompt inputs
src/adapters/anthropic-sort-stabilize.ts, src/claude/inbound.ts, tests/claude-inbound.test.ts
Recognized system-reminder listings and tool definitions are sorted deterministically before instruction generation and prompt-cache key construction, with coverage for equivalent inputs and tool permutations.
Native passthrough and session affinity
src/server/claude-messages.ts, tests/claude-messages-endpoint.test.ts, tests/claude-native-passthrough.test.ts, docs-site/src/content/docs/*/guides/claude-code.md
Native Anthropic requests stabilize system and tool ordering; routed requests derive missing session_id values from metadata.user_id, with updated endpoint tests and localized documentation.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ClaudeClient
  participant ClaudeRouting
  participant PromptCache
  participant NativeUpstream
  ClaudeClient->>ClaudeRouting: Send system content, tools, and metadata
  ClaudeRouting->>ClaudeRouting: Normalize reminders and sort tools
  ClaudeRouting->>PromptCache: Build canonical cache inputs
  ClaudeRouting->>NativeUpstream: Forward stabilized request with derived session_id
  NativeUpstream-->>ClaudeRouting: Return routed response
Loading

Possibly related PRs

Suggested reviewers: wibias, lidge-jun, ingwannu

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately captures the main change: stabilizing cache inputs.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: be453849c9

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

}
}

if (Array.isArray(body.tools)) sortToolsByName(body.tools as unknown[]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve tool cache breakpoints after sorting

When a native Anthropic request includes a tool-level cache_control breakpoint on the last tool, this unconditional sort can move that annotated tool away from the end, e.g. [Zeta, Alpha(cache_control)] becomes [Alpha(cache_control), Zeta]. Anthropic cache breakpoints are positional, and the repo’s own applyPromptCaching strategy places the breakpoint on the last tool so the whole tool prefix is cached; after this change the prefix can omit later tools and defeat the cache stabilization this patch is trying to provide. Reapply or move the tool-level breakpoint after sorting, or avoid sorting arrays that already carry explicit tool breakpoints.

Useful? React with 👍 / 👎.


/** Sort `body.system` skills/deferred-tools listings and `body.tools` by name, in place. */
export function stabilizeSystemAndToolOrder(body: Rec): void {
if (Array.isArray(body.system)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Normalize native string system prompts too

When native passthrough receives the valid Anthropic string form of system, this guard skips it entirely, so a string system reminder such as User-invocable skills remains in the client’s nondeterministic order while the array form is normalized. That leaves native Anthropic /v1/messages and /count_tokens cache inputs unstable for one of the accepted request shapes; handle typeof body.system === "string" with normalizeSystemReminderText before the array branch.

Useful? React with 👍 / 👎.

Comment on lines +55 to +56
if (isSkillsBlockText(text)) return sortSkillsBlockText(text);
if (isDeferredToolsBlockText(text)) return sortDeferredToolsBlockText(text);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Normalize every reminder block in a system string

When one system text contains both a skills reminder and a later deferred-tools reminder, this early return normalizes only the skills list and leaves the deferred-tools list in its original discovery order. The code already preserves following reminder blocks, so this combined-block shape is accepted, but routed instructions/prompt_cache_key and native passthrough bodies still vary whenever the later deferred-tools listing arrives in a different order; run both normalizers over the same text or scan each reminder block independently.

Useful? React with 👍 / 👎.

Comment on lines +381 to +383
**Native OpenAI/ChatGPT routing:** when system content exists, derives a content-scoped
`prompt_cache_key` from the resolved model, normalized system content, and full tool schemas.
Without system content, it falls back to `metadata.user_id`. When metadata is present, it

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Correct the documented cache-key precedence

This section now says system content takes precedence for prompt_cache_key, but anthropicToResponsesTranslation still enters the metadata.user_id branch first whenever Claude Code sends metadata, so normal sessions with both metadata and system content remain session-scoped and do not use the normalized system/tool hash described here. Users following these docs will expect cross-session content cache affinity that the implementation does not provide; either change the precedence in code or keep the docs aligned with metadata-first behavior.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs-site/src/content/docs/guides/claude-code.md`:
- Around line 381-385: Correct the prompt-cache-key precedence description in
docs-site/src/content/docs/guides/claude-code.md:381-385,
docs-site/src/content/docs/ja/guides/claude-code.md:296-300,
docs-site/src/content/docs/ko/guides/claude-code.md:333-336,
docs-site/src/content/docs/ru/guides/claude-code.md:313-318, and
docs-site/src/content/docs/zh-cn/guides/claude-code.md:291-294. In every locale,
state that metadata.user_id takes precedence and produces the session-scoped
prompt_cache_key plus separate session_id; only when metadata.user_id is absent
should system content enable the content-scoped key based on the resolved model,
normalized system content, and full tool schemas, preserving each locale’s
language and existing canonicalization details.

In `@src/adapters/anthropic-sort-stabilize.ts`:
- Around line 70-82: Update stabilizeSystemAndToolOrder to normalize
string-valued body.system using normalizeSystemReminderText, while preserving
the existing per-block handling for array-valued system content. Ensure
native-passthrough requests with a plain string system receive the same
stabilization as array requests, and add coverage for the string shape in the
relevant native passthrough tests.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 7c8f5f1e-2385-4a72-b020-3b0314bae6a4

📥 Commits

Reviewing files that changed from the base of the PR and between ca7b104 and be45384.

📒 Files selected for processing (11)
  • docs-site/src/content/docs/guides/claude-code.md
  • docs-site/src/content/docs/ja/guides/claude-code.md
  • docs-site/src/content/docs/ko/guides/claude-code.md
  • docs-site/src/content/docs/ru/guides/claude-code.md
  • docs-site/src/content/docs/zh-cn/guides/claude-code.md
  • src/adapters/anthropic-sort-stabilize.ts
  • src/claude/inbound.ts
  • src/server/claude-messages.ts
  • tests/claude-inbound.test.ts
  • tests/claude-messages-endpoint.test.ts
  • tests/claude-native-passthrough.test.ts

Comment on lines +381 to +385
**Native OpenAI/ChatGPT routing:** when system content exists, derives a content-scoped
`prompt_cache_key` from the resolved model, normalized system content, and full tool schemas.
Without system content, it falls back to `metadata.user_id`. When metadata is present, it
separately derives a per-session `session_id` header for backend affinity. Tool and
system-reminder listings are canonicalized before either cache input is built.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Native OpenAI/ChatGPT prompt-cache-key precedence is described backward in all 5 locales. src/claude/inbound.ts (Lines 457-489) checks metadata.user_id FIRST — metadata always wins the session-scoped prompt_cache_key when present, and the content-scoped hash (model + normalized system + tool schemas) is only an else if fallback used when metadata is absent. All five doc pages instead present system-content-presence as the primary discriminator ("when system content exists → content-scoped key; without it → falls back to metadata.user_id"), which inverts the real precedence and misleads readers about when each caching mode actually applies (materially, for Claude Code CLI traffic, which per the code's own comments always sends metadata.user_id, so it never reaches the content-scoped path these docs describe as primary).

  • docs-site/src/content/docs/guides/claude-code.md#L381-L385: rewrite to state metadata-first precedence, e.g. "when metadata.user_id is present, derives a session-scoped prompt_cache_key from it plus a separate session_id header; otherwise, when system content exists, falls back to a content-scoped key from the resolved model, normalized system content, and full tool schemas."
  • docs-site/src/content/docs/ja/guides/claude-code.md#L296-L300: apply the equivalent corrected precedence in Japanese.
  • docs-site/src/content/docs/ko/guides/claude-code.md#L333-L336: apply the equivalent corrected precedence in Korean.
  • docs-site/src/content/docs/ru/guides/claude-code.md#L313-L318: apply the equivalent corrected precedence in Russian.
  • docs-site/src/content/docs/zh-cn/guides/claude-code.md#L291-L294: apply the equivalent corrected precedence in Simplified Chinese.
📍 Affects 5 files
  • docs-site/src/content/docs/guides/claude-code.md#L381-L385 (this comment)
  • docs-site/src/content/docs/ja/guides/claude-code.md#L296-L300
  • docs-site/src/content/docs/ko/guides/claude-code.md#L333-L336
  • docs-site/src/content/docs/ru/guides/claude-code.md#L313-L318
  • docs-site/src/content/docs/zh-cn/guides/claude-code.md#L291-L294
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs-site/src/content/docs/guides/claude-code.md` around lines 381 - 385,
Correct the prompt-cache-key precedence description in
docs-site/src/content/docs/guides/claude-code.md:381-385,
docs-site/src/content/docs/ja/guides/claude-code.md:296-300,
docs-site/src/content/docs/ko/guides/claude-code.md:333-336,
docs-site/src/content/docs/ru/guides/claude-code.md:313-318, and
docs-site/src/content/docs/zh-cn/guides/claude-code.md:291-294. In every locale,
state that metadata.user_id takes precedence and produces the session-scoped
prompt_cache_key plus separate session_id; only when metadata.user_id is absent
should system content enable the content-scoped key based on the resolved model,
normalized system content, and full tool schemas, preserving each locale’s
language and existing canonicalization details.

Comment on lines +70 to +82
export function stabilizeSystemAndToolOrder(body: Rec): void {
if (Array.isArray(body.system)) {
const system = body.system as unknown[];
for (let i = 0; i < system.length; i++) {
const block = system[i];
if (!isRec(block) || block.type !== "text" || typeof block.text !== "string") continue;
const normalized = normalizeSystemReminderText(block.text);
if (normalized !== block.text) system[i] = { ...block, text: normalized };
}
}

if (Array.isArray(body.tools)) sortToolsByName(body.tools as unknown[]);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

stabilizeSystemAndToolOrder skips string-typed body.system.

Only Array.isArray(body.system) is handled (Line 71). Anthropic's /v1/messages API also accepts system as a plain string, and src/claude/inbound.ts's systemToInstructions/systemMessageText explicitly handle both shapes. Here, a string system containing the skills/deferred-tools listing silently skips normalization for native-passthrough requests, leaving that shape's cache prefix unstabilized — the exact bug this PR sets out to fix.

🐛 Proposed fix
 export function stabilizeSystemAndToolOrder(body: Rec): void {
-  if (Array.isArray(body.system)) {
+  if (typeof body.system === "string") {
+    body.system = normalizeSystemReminderText(body.system);
+  } else if (Array.isArray(body.system)) {
     const system = body.system as unknown[];
     for (let i = 0; i < system.length; i++) {
       const block = system[i];
       if (!isRec(block) || block.type !== "text" || typeof block.text !== "string") continue;
       const normalized = normalizeSystemReminderText(block.text);
       if (normalized !== block.text) system[i] = { ...block, text: normalized };
     }
   }
 
   if (Array.isArray(body.tools)) sortToolsByName(body.tools as unknown[]);
 }

tests/claude-native-passthrough.test.ts only exercises the array-system shape (Lines 194-197), so this gap isn't currently covered by tests either.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function stabilizeSystemAndToolOrder(body: Rec): void {
if (Array.isArray(body.system)) {
const system = body.system as unknown[];
for (let i = 0; i < system.length; i++) {
const block = system[i];
if (!isRec(block) || block.type !== "text" || typeof block.text !== "string") continue;
const normalized = normalizeSystemReminderText(block.text);
if (normalized !== block.text) system[i] = { ...block, text: normalized };
}
}
if (Array.isArray(body.tools)) sortToolsByName(body.tools as unknown[]);
}
export function stabilizeSystemAndToolOrder(body: Rec): void {
if (typeof body.system === "string") {
body.system = normalizeSystemReminderText(body.system);
} else if (Array.isArray(body.system)) {
const system = body.system as unknown[];
for (let i = 0; i < system.length; i++) {
const block = system[i];
if (!isRec(block) || block.type !== "text" || typeof block.text !== "string") continue;
const normalized = normalizeSystemReminderText(block.text);
if (normalized !== block.text) system[i] = { ...block, text: normalized };
}
}
if (Array.isArray(body.tools)) sortToolsByName(body.tools as unknown[]);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/adapters/anthropic-sort-stabilize.ts` around lines 70 - 82, Update
stabilizeSystemAndToolOrder to normalize string-valued body.system using
normalizeSystemReminderText, while preserving the existing per-block handling
for array-valued system content. Ensure native-passthrough requests with a plain
string system receive the same stabilization as array requests, and add coverage
for the string shape in the relevant native passthrough tests.

@jgautheron
jgautheron marked this pull request as draft July 28, 2026 15:48
@jgautheron jgautheron closed this Jul 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant