-
Notifications
You must be signed in to change notification settings - Fork 1.2k
fix: stabilize cache inputs #634
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -378,9 +378,11 @@ other 5xx `api_error`. `Retry-After` is preserved. | |
| and the penultimate user message, plus top-level automatic `cache_control`. Stable turns normally | ||
| produce about a 99.9% cache hit rate. | ||
|
|
||
| **Native OpenAI/ChatGPT routing:** derives a session-scoped `prompt_cache_key` (from | ||
| `metadata.user_id` when present, falling back to a system-content hash) and `session_id` header | ||
| for cache affinity. The cache key includes model and full tool schemas. | ||
| **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. | ||
|
Comment on lines
+381
to
+385
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
📍 Affects 5 files
🤖 Prompt for AI Agents |
||
|
|
||
| **Token math:** Anthropic output subtracts `cached_tokens` and `cache_write_tokens` from | ||
| `input_tokens`, exposing them as `cache_read_input_tokens` and `cache_creation_input_tokens`. | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,82 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Deterministic ordering of Claude Code's own tool/skills/deferred-tools listings on | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * the native Anthropic passthrough (`anthropicNativePassthrough`, `claude-messages.ts`). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Claude Code enumerates MCP tools/skills in whatever order its own reconnect/discovery | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * race resolves them, so byte-identical conversations can arrive with different array | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * order turn to turn — busting Anthropic's prompt-cache prefix for no reason. Sorting is | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * safe: `tool_choice` targets tools by name, not position, and the two system-reminder | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * blocks below are pure listings with no inherent order the model depends on. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Ported (algorithm only, re-implemented in TypeScript) from `sort-stabilization.mjs`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * MIT licensed, github.com/cnighswonger/claude-code-cache-fix. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type Rec = Record<string, unknown>; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function isRec(v: unknown): v is Rec { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return !!v && typeof v === "object" && !Array.isArray(v); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const SKILLS_BLOCK_RE = /^([\s\S]*?\n\n)(- [\s\S]+?)(\n<\/system-reminder>)([\s\S]*)$/; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const DEFERRED_TOOLS_BLOCK_RE = /^(<system-reminder>\nThe following deferred tools are now available[^\n]*\n)([\s\S]+?)(\n<\/system-reminder>)([\s\S]*)$/; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function codePointCompare(a: string, b: string): number { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return a < b ? -1 : a > b ? 1 : 0; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export function isSkillsBlockText(text: unknown): text is string { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return typeof text === "string" && text.includes("User-invocable skills"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export function isDeferredToolsBlockText(text: unknown): text is string { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return typeof text === "string" && text.includes("deferred tools are now available"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export function sortSkillsBlockText(text: string): string { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const match = text.match(SKILLS_BLOCK_RE); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!match) return text; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [, header, entriesText, footer, suffix] = match; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const entries = entriesText.split(/\n(?=- )/); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| entries.sort(codePointCompare); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return header + entries.join("\n") + footer + suffix; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export function sortDeferredToolsBlockText(text: string): string { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const match = text.match(DEFERRED_TOOLS_BLOCK_RE); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!match) return text; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [, header, toolsList, footer, suffix] = match; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const tools = toolsList.split("\n").map(t => t.trim()).filter(Boolean); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tools.sort(codePointCompare); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return header + tools.join("\n") + footer + suffix; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** Normalize known system-reminder listings; leave all other instructions untouched. */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export function normalizeSystemReminderText(text: string): string { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (isSkillsBlockText(text)) return sortSkillsBlockText(text); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (isDeferredToolsBlockText(text)) return sortDeferredToolsBlockText(text); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+55
to
+56
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
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 Useful? React with 👍 / 👎. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return text; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** Sort tool definitions by name; unnamed server tools sort first. */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export function sortToolsByName(tools: unknown[]): void { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tools.sort((a, b) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const nameA = isRec(a) && typeof a.name === "string" ? a.name : ""; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const nameB = isRec(b) && typeof b.name === "string" ? b.name : ""; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return codePointCompare(nameA, nameB); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** 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)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When native passthrough receives the valid Anthropic string form of Useful? React with 👍 / 👎. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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[]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a native Anthropic request includes a tool-level Useful? React with 👍 / 👎. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+70
to
+82
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Only 🐛 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[]);
}
📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This section now says system content takes precedence for
prompt_cache_key, butanthropicToResponsesTranslationstill enters themetadata.user_idbranch 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 👍 / 👎.