From 04b5e3118f584754ca4e2185dcec672cf8ebf8e9 Mon Sep 17 00:00:00 2001 From: JUN Date: Wed, 7 Oct 2026 22:49:39 +0900 Subject: [PATCH 1/2] fix(claude): stop re-attaching the token footer as a user turn --- .../src/content/docs/guides/claude-code.md | 2 +- scripts/test-layout/layout.json | 2 +- src/claude/inbound-cache-stabilize.ts | 13 ++-- src/claude/inbound.ts | 1 + structure/clients/claude-desktop.md | 2 +- structure/data-planes/inbound-compat.md | 6 +- .../claude-inbound-cache-stabilize.test.ts | 72 +++++++++---------- .../claude-inbound-token-footer.test.ts | 67 +++++++++++++++++ .../claude-native-passthrough.test.ts | 25 +++++++ tests/fixtures/test-layout-expected.json | 2 +- 10 files changed, 138 insertions(+), 54 deletions(-) create mode 100644 tests/claude-integration/claude-inbound-token-footer.test.ts diff --git a/docs-site/src/content/docs/guides/claude-code.md b/docs-site/src/content/docs/guides/claude-code.md index 945a0c56834..613c85fc0c9 100644 --- a/docs-site/src/content/docs/guides/claude-code.md +++ b/docs-site/src/content/docs/guides/claude-code.md @@ -1016,7 +1016,7 @@ and `"force"`. A value you export yourself always wins over the injected one. directives, not the Agent tool's `model` argument. Make sure the directive matches the intended route. Pass `"haiku"` as the model placeholder. -Set `claudeCode.stabilizePromptCache` to `true` in `config.json` to relocate supported trailing Claude harness notices from system instructions to a trailing user message on translated routes. The default is `false`. Enable it only when this role change is appropriate for your clients. It preserves fenced examples and unmatched text; native Anthropic passthrough is unchanged. The metadata-less prompt-cache key then follows stabilized instructions. This does not create conversation identity or guarantee upstream cache hits. +Set `claudeCode.stabilizePromptCache` to `true` in `config.json` to peel supported trailing Claude harness notices from system instructions on translated routes. Recognized `N tokens left` footers are dropped, including repeated footers. TaskCreate reminders still move to a trailing user message; if only token footers were peeled, no input message is added. The default is `false`. Enable it only when this role change is appropriate for your clients. It preserves fenced examples and unmatched text; native Anthropic passthrough is unchanged. The metadata-less prompt-cache key then follows stabilized instructions. This does not create conversation identity or guarantee upstream cache hits. On every translated Chat route, timeline reminders keep their position in the conversation, after any pending tool results. This prevents a newly appended diff --git a/scripts/test-layout/layout.json b/scripts/test-layout/layout.json index 5a6f6c0f21f..7cfb9cad88c 100644 --- a/scripts/test-layout/layout.json +++ b/scripts/test-layout/layout.json @@ -385,7 +385,7 @@ "claude-first-party-agent-sync.test.ts": "claude-integration", "claude-first-party-union.test.ts": "claude-integration", "claude-gateway-cache.test.ts": "claude-integration", - "claude-inbound-cache-stabilize.test.ts": "claude-integration", + "claude-inbound-cache-stabilize.test.ts": "claude-integration", "claude-inbound-token-footer.test.ts": "claude-integration", "claude-inbound-debug.test.ts": "claude-integration", "claude-inbound.test.ts": "claude-integration", "claude-inbound-tool-reference.test.ts": "claude-integration", "claude-output-schema-strict.test.ts": "claude-integration", diff --git a/src/claude/inbound-cache-stabilize.ts b/src/claude/inbound-cache-stabilize.ts index e085dd7c4cc..b9406c73284 100644 --- a/src/claude/inbound-cache-stabilize.ts +++ b/src/claude/inbound-cache-stabilize.ts @@ -3,10 +3,10 @@ * occasional TaskCreate nudges) into system text that becomes Responses * `instructions`. That churn breaks Muse/Go prefix cache on the instructions * prefix even when tools stay stable. Strip dynamics from instructions; - * surface the latest notice on `input` instead. + * drop token footers and surface the latest TaskCreate notice on `input` instead. * - * Relocation is identified by harness shape: only a trailing, unfenced, - * canonical notice at the end of instructions is moved. An unmatched fence + * Peeling is identified by harness shape: only a trailing, unfenced, + * canonical notice at the end of instructions is removed. An unmatched fence * opener covers through EOF. No match → the original string is returned * byte-for-byte. The matcher is content identity only; `translateAnthropicRequest` * requires `claudeCode.stabilizePromptCache: true` before this helper runs. Claude Code @@ -86,7 +86,6 @@ export function stabilizeClaudeInstructionsForPromptCache( const ranges = fencedRanges(instructions); let fenceIndex = ranges.length - 1; let end = instructions.length; - let latestTotal: string | null = null; let latestNudge: string | null = null; let peeled = false; @@ -110,7 +109,6 @@ export function stabilizeClaudeInstructionsForPromptCache( while (fenceIndex >= 0 && ranges[fenceIndex]!.start > contentStart) fenceIndex--; if (fenceIndex >= 0 && contentStart < ranges[fenceIndex]!.end) break; - if (total && latestTotal === null) latestTotal = notice; if (nudge && latestNudge === null) latestNudge = notice; peeled = true; @@ -123,8 +121,5 @@ export function stabilizeClaudeInstructionsForPromptCache( } if (!peeled) return { instructions, dynamicNotice: null }; - const noticeParts: string[] = []; - if (latestTotal) noticeParts.push(latestTotal); - if (latestNudge) noticeParts.push(latestNudge); - return { instructions: instructions.slice(0, end), dynamicNotice: noticeParts.join("\n\n") }; + return { instructions: instructions.slice(0, end), dynamicNotice: latestNudge }; } diff --git a/src/claude/inbound.ts b/src/claude/inbound.ts index 243473beae6..53047a35941 100644 --- a/src/claude/inbound.ts +++ b/src/claude/inbound.ts @@ -445,6 +445,7 @@ function translateAnthropicRequest( // a matching suffix or metadata.user_id. const stabilized = stabilizeClaudeInstructionsForPromptCache(joinedSystem); if (stabilized.instructions) body.instructions = stabilized.instructions; + // Token footers are dropped; append nothing if no other notice remains. if (stabilized.dynamicNotice) { input.push({ type: "message", diff --git a/structure/clients/claude-desktop.md b/structure/clients/claude-desktop.md index bab6350ed3d..fb864035d15 100644 --- a/structure/clients/claude-desktop.md +++ b/structure/clients/claude-desktop.md @@ -483,7 +483,7 @@ The explicit sync coordinator also accepts Cline CLI as a separate file integrat `claudeCode.stabilizePromptCache` is a default-off operator setting for [translated instruction stabilization](../data-planes/inbound-compat.md#opt-in-claude-instruction-stabilization). -Config JSON preserves the boolean; only literal true activates the role-changing transform. +Config JSON preserves the boolean; only literal true activates the transform, which drops recognized token footers and retains TaskCreate nudges as trailing user messages. The lightweight top-level CLI help counts Cline CLI among the fifteen registered export clients; registry parity remains covered by the client help and integration tests. Native Chat applies qualifying effort ceilings independently of model pins; pin selection precedes the cap and only pins or cap rewrites enter wire mapping. The [catalog effort contract](../catalog.md#ultra-reasoning-level) records the V1/compaction exemptions and caller-preservation boundary. diff --git a/structure/data-planes/inbound-compat.md b/structure/data-planes/inbound-compat.md index 5a4a514b8f9..bdf54363405 100644 --- a/structure/data-planes/inbound-compat.md +++ b/structure/data-planes/inbound-compat.md @@ -344,8 +344,10 @@ final native affinity, and shared-system keys do not provide either conversation `src/claude/inbound.ts` reads only literal `claudeCode.stabilizePromptCache: true` from its existing configuration argument. The default is off for every translated Messages caller. -`src/claude/inbound-cache-stabilize.ts` relocates only exact single-line trailing unfenced harness notices -into a trailing user input message; unmatched and fenced text is preserved, including an open +`src/claude/inbound-cache-stabilize.ts` peels only exact single-line trailing unfenced harness notices. +Recognized `N tokens left` footers are dropped, including repeated footers; +TaskCreate nudges retain their trailing user input message. A footer-only suffix adds no input item, +so token footers do not introduce a user turn after a function output. Unmatched and fenced text is preserved, including an open fence through EOF. Native passthrough never enters this translator. Without opt-in the original system-parts cache-key derivation remains unchanged; with opt-in the metadata-less key uses stabilized instructions. Metadata-derived keys retain their existing derivation. This configuration diff --git a/tests/claude-integration/claude-inbound-cache-stabilize.test.ts b/tests/claude-integration/claude-inbound-cache-stabilize.test.ts index 09b55cc3290..71c66d98b66 100644 --- a/tests/claude-integration/claude-inbound-cache-stabilize.test.ts +++ b/tests/claude-integration/claude-inbound-cache-stabilize.test.ts @@ -95,7 +95,7 @@ describe("stabilizeClaudeInstructionsForPromptCache", () => { }); }); - test("three trailing total_tokens footers keep only the latest in the notice", () => { + test("three trailing total_tokens footers are all dropped", () => { const stable = "You are Claude Code."; const first = footer(1000); const second = footer(4000); @@ -105,7 +105,7 @@ describe("stabilizeClaudeInstructionsForPromptCache", () => { ); expect(result.instructions).toBe(stable); expect(result.instructions).not.toContain(""); - expect(result.dynamicNotice).toBe(third); + expect(result.dynamicNotice).toBeNull(); }); test("real Claude Code 15000000 tokens left trailing footer peels", () => { @@ -113,7 +113,7 @@ describe("stabilizeClaudeInstructionsForPromptCache", () => { const harness = footer(15_000_000); const result = stabilizeClaudeInstructionsForPromptCache(`${stable}\n\n${harness}`); expect(result.instructions).toBe(stable); - expect(result.dynamicNotice).toBe("15000000 tokens left"); + expect(result.dynamicNotice).toBeNull(); }); test("bare numeric total_tokens without tokens left is not a harness footer", () => { @@ -123,12 +123,12 @@ describe("stabilizeClaudeInstructionsForPromptCache", () => { expect(result.dynamicNotice).toBeNull(); }); - test("mid-document total_tokens stays; only the trailing harness footer relocates", () => { + test("mid-document total_tokens stays; only the trailing harness footer is dropped", () => { const result = stabilizeClaudeInstructionsForPromptCache( `System.\n${footer(1)}\nMore system.\n${footer(3)}`, ); expect(result.instructions).toBe(`System.\n${footer(1)}\nMore system.`); - expect(result.dynamicNotice).toBe(footer(3)); + expect(result.dynamicNotice).toBeNull(); }); test("TaskCreate nudge is stripped from instructions and kept in the notice", () => { @@ -152,10 +152,10 @@ describe("stabilizeClaudeInstructionsForPromptCache", () => { expect(result.instructions).toBe(stable); expect(result.instructions).not.toContain(""); expect(result.instructions).not.toContain("TaskCreate"); - expect(result.dynamicNotice).toBe(`${latest}\n\n${TASKCREATE_NUDGE_CC_2_1_263}`); + expect(result.dynamicNotice).toBe(TASKCREATE_NUDGE_CC_2_1_263); }); - test("latest footer and latest nudge both surface in the notice", () => { + test("latest nudge surfaces in the notice without the footer", () => { const stable = "Stay stable."; const older = footer(10); const latest = footer(50); @@ -163,7 +163,7 @@ describe("stabilizeClaudeInstructionsForPromptCache", () => { [stable, older, TASKCREATE_NUDGE, latest].join("\n\n"), ); expect(result.instructions).toBe(stable); - expect(result.dynamicNotice).toBe(`${latest}\n\n${TASKCREATE_NUDGE}`); + expect(result.dynamicNotice).toBe(TASKCREATE_NUDGE); }); test("legacy and 2.1.263 nudges both leave the same stable instructions prefix", () => { @@ -199,22 +199,22 @@ describe("stabilizeClaudeInstructionsForPromptCache", () => { expect(result.dynamicNotice).toBeNull(); }); - test("docs plus a real trailing footer keep the docs and move only the latest footer", () => { + test("docs plus a real trailing footer keep the docs and drop all trailing footers", () => { const docs = "Describe 0 in the protocol guide."; const latest = footer(8000); const result = stabilizeClaudeInstructionsForPromptCache( [docs, footer(1), latest].join("\n\n"), ); expect(result.instructions).toBe(docs); - expect(result.dynamicNotice).toBe(latest); + expect(result.dynamicNotice).toBeNull(); }); - test("fenced example plus a trailing harness footer moves only the footer", () => { + test("fenced example plus a trailing harness footer drops only the footer", () => { const docs = ["Docs:", "```", footer(123), "```"].join("\n"); const latest = footer(8000); const result = stabilizeClaudeInstructionsForPromptCache(`${docs}\n\n${latest}`); expect(result.instructions).toBe(docs); - expect(result.dynamicNotice).toBe(latest); + expect(result.dynamicNotice).toBeNull(); }); test("unclosed fence through EOF is not a harness suffix", () => { @@ -245,18 +245,18 @@ describe("linear canonical notice extraction", () => { test("retains prefix trailing spaces after a successful peel and failed next candidate", () => { expect(stabilizeClaudeInstructionsForPromptCache(`System. \n\n${footer(1)}`)).toEqual({ - instructions: "System. ", dynamicNotice: footer(1), + instructions: "System. ", dynamicNotice: null, }); }); - test("CRLF separators and horizontal padding retain the canonical notice", () => { + test("CRLF separators and horizontal padding still peel the canonical footer", () => { expect(stabilizeClaudeInstructionsForPromptCache(`System.\r\n\r\n \t${footer(1)}\t \r\n`)).toEqual({ - instructions: "System.", dynamicNotice: footer(1), + instructions: "System.", dynamicNotice: null, }); }); test("malformed notice before a valid suffix remains byte-for-byte", () => { const prefix = `System. \n123\ntokens left `; expect(stabilizeClaudeInstructionsForPromptCache(`${prefix}\n${footer(2)}`)).toEqual({ - instructions: prefix, dynamicNotice: footer(2), + instructions: prefix, dynamicNotice: null, }); }); test("a whitespace-only trailing line and lone CR are not canonical separators", () => { @@ -264,11 +264,11 @@ describe("linear canonical notice extraction", () => { expect(stabilizeClaudeInstructionsForPromptCache(system)).toEqual({ instructions: system, dynamicNotice: null }); } }); - test("twenty thousand notices after many fences keep only the latest without repeated prefix scans", () => { + test("twenty thousand footers after many fences are dropped without repeated prefix scans", () => { const prefix = "System.\n" + ("```xml\nexample\n```\n").repeat(2_000) + "Stable. "; const notices = Array.from({ length: 20_000 }, (_, index) => footer(index)); expect(stabilizeClaudeInstructionsForPromptCache(`${prefix}\n\n${notices.join("\n")}`)).toEqual({ - instructions: prefix, dynamicNotice: footer(19_999), + instructions: prefix, dynamicNotice: null, }); }); }); @@ -293,20 +293,16 @@ describe("anthropicToResponsesTranslation cache-stabilize wire-in", () => { ]); }); - test("opted-in harness relocates the latest total_tokens footer onto a trailing input user message", () => { + test("opted-in harness drops all total_tokens footers without adding a user message", () => { const first = footer(1000); const latest = footer(8000); const { body } = translateHarness(["You are Claude Code.", first, latest].join("\n\n")); expect(body.instructions).toBe("You are Claude Code."); expect(String(body.instructions)).not.toContain(""); const input = userTurns(body); - const last = input[input.length - 1]!; - expect(last).toEqual({ - type: "message", - role: "user", - content: [{ type: "input_text", text: latest }], - }); - expect(input.some(item => item.role === "user" && item !== last)).toBe(true); + expect(input).toEqual([ + { type: "message", role: "user", content: [{ type: "input_text", text: "hi" }] }, + ]); }); test("opted-in peel does not require metadata.user_id", () => { @@ -314,11 +310,9 @@ describe("anthropicToResponsesTranslation cache-stabilize wire-in", () => { const { body } = translateHarness(["You are Claude Code.", latest].join("\n\n")); expect(body.instructions).toBe("You are Claude Code."); const input = userTurns(body); - expect(input[input.length - 1]).toEqual({ - type: "message", - role: "user", - content: [{ type: "input_text", text: latest }], - }); + expect(input).toEqual([ + { type: "message", role: "user", content: [{ type: "input_text", text: "hi" }] }, + ]); }); test("fenced standalone total_tokens example is a translator no-op when opted in", () => { @@ -339,17 +333,15 @@ describe("anthropicToResponsesTranslation cache-stabilize wire-in", () => { ]); }); - test("real footer after a closed fence still relocates when opted in", () => { + test("real footer after a closed fence is dropped when opted in", () => { const docs = ["Docs:", "```", footer(123), "```"].join("\n"); const latest = footer(8000); const { body } = translateHarness(`${docs}\n\n${latest}`); expect(body.instructions).toBe(docs); const input = userTurns(body); - expect(input[input.length - 1]).toEqual({ - type: "message", - role: "user", - content: [{ type: "input_text", text: latest }], - }); + expect(input).toEqual([ + { type: "message", role: "user", content: [{ type: "input_text", text: "hi" }] }, + ]); }); test("whitespace-only system without a footer is preserved byte-for-byte", () => { @@ -465,12 +457,14 @@ describe("Messages operator opt-in at the outbound boundary", () => { } for (const notice of [footer(15_000_000), TASKCREATE_NUDGE, TASKCREATE_NUDGE_CC_2_1_263]) { - test(`explicit HTTP opt-in relocates supported notice ${notice.slice(0, 35)}`, async () => { + test(`explicit HTTP opt-in drops token footers and retains supported nudges ${notice.slice(0, 35)}`, async () => { const wire = await outbound(`System.\n\n${notice}`, true); expect(wire.instructions).toBe("System."); expect(wire.input).toEqual([ { type: "message", role: "user", content: [{ type: "input_text", text: "hi" }] }, - { type: "message", role: "user", content: [{ type: "input_text", text: notice }] }, + ...(notice.startsWith("") ? [] : [ + { type: "message", role: "user", content: [{ type: "input_text", text: notice }] }, + ]), ]); }); } diff --git a/tests/claude-integration/claude-inbound-token-footer.test.ts b/tests/claude-integration/claude-inbound-token-footer.test.ts new file mode 100644 index 00000000000..6c4ec918f64 --- /dev/null +++ b/tests/claude-integration/claude-inbound-token-footer.test.ts @@ -0,0 +1,67 @@ +import { describe, expect, test } from "bun:test"; +import { anthropicToResponsesTranslation } from "../../src/claude/inbound"; + +const TASKCREATE_NUDGE = "The task tools haven't been used recently. If you're working on tasks that would benefit from tracking, consider using TaskCreate to add them. Only use these if relevant to the current work. This is just a gentle reminder - ignore if not applicable."; +const footer = (remaining: number) => `${remaining} tokens left`; +const userItem = { type: "message", role: "user", content: [{ type: "input_text", text: "hi" }] }; + +function translate(system: string, messages: unknown[] = [{ role: "user", content: "hi" }]) { + return anthropicToResponsesTranslation({ model: "m", max_tokens: 32, system, messages }, { + stabilizePromptCache: true, + }).body; +} + +describe("Claude token footer does not introduce a user turn", () => { + test("footer-only notices are peeled without adding an input item", () => { + for (const system of [`System.\n\n${footer(1000)}`, footer(1000)]) { + const body = translate(system); + expect(body.instructions).toBe(system.startsWith("System.") ? "System." : undefined); + expect(body.input).toEqual([userItem]); + } + }); + + test("repeated footers preserve the stabilized instructions and historical cache key", () => { + const baseline = translate("System."); + for (const count of [1, 3]) { + const body = translate(["System.", ...Array.from({ length: count }, (_, i) => footer(i + 1))].join("\n\n")); + expect(body.instructions).toBe(baseline.instructions); + expect(body.prompt_cache_key).toBe(baseline.prompt_cache_key); + expect(body.input).toEqual(baseline.input); + } + }); + + test("mixed notices retain TaskCreate as the same trailing user message", () => { + const baseline = translate(`System.\n\n${TASKCREATE_NUDGE}`); + for (const notices of [ + [footer(1), TASKCREATE_NUDGE, footer(2)], + [TASKCREATE_NUDGE, footer(1), footer(2)], + [footer(1), footer(2), TASKCREATE_NUDGE], + ]) { + const body = translate(["System.", ...notices].join("\n\n")); + expect(body.instructions).toBe(baseline.instructions); + expect(body.prompt_cache_key).toBe(baseline.prompt_cache_key); + expect(body.input).toEqual([ + userItem, + { type: "message", role: "user", content: [{ type: "input_text", text: TASKCREATE_NUDGE }] }, + ]); + } + }); + + test("a tool-result turn ends with its function output without a synthetic footer user item", () => { + const messages = [ + { role: "user", content: "hi" }, + { role: "assistant", content: [{ type: "tool_use", id: "call_read", name: "Read", input: { path: "example.txt" } }] }, + { role: "user", content: [{ type: "tool_result", tool_use_id: "call_read", content: "file contents" }] }, + ]; + const baseline = translate("System.", messages); + const body = translate(`System.\n\n${footer(1000)}\n\n${footer(999)}`, messages); + expect(body.instructions).toBe(baseline.instructions); + expect(body.prompt_cache_key).toBe(baseline.prompt_cache_key); + expect(body.input).toEqual(baseline.input); + expect(body.input).toEqual([ + userItem, + { type: "function_call", call_id: "call_read", name: "Read", arguments: '{"path":"example.txt"}' }, + { type: "function_call_output", call_id: "call_read", output: "file contents" }, + ]); + }); +}); diff --git a/tests/claude-integration/claude-native-passthrough.test.ts b/tests/claude-integration/claude-native-passthrough.test.ts index bc7af93b0da..1b7bf39fbc5 100644 --- a/tests/claude-integration/claude-native-passthrough.test.ts +++ b/tests/claude-integration/claude-native-passthrough.test.ts @@ -98,6 +98,31 @@ function claudeBody(): Record { }; } +test("prompt-cache opt-in leaves native token footers and tool-result turns verbatim", async () => { + const captured: Captured[] = []; + const upstream = mockAnthropicUpstream(captured); + saveConfig(cfg(upstream.url.toString().replace(/\/$/, ""), { stabilizePromptCache: true })); + const server = startServer(0); + const body = claudeBody(); + body.system = "System.\n\n1000 tokens left"; + body.messages = [ + { role: "assistant", content: [{ type: "tool_use", id: "call_read", name: "Read", input: {} }] }, + { role: "user", content: [{ type: "tool_result", tool_use_id: "call_read", content: "file contents" }] }, + ]; + try { + const res = await fetch(new URL("/v1/messages", server.url), { + method: "POST", headers: OAUTH_HEADERS, body: JSON.stringify(body), + }); + expect(res.status).toBe(200); + await res.text(); + expect(captured).toHaveLength(1); + expect(captured[0]!.body).toEqual(body); + } finally { + await server.stop(true); + await upstream.stop(true); + } +}); + test("unmapped claude model + sk-ant credential passes through verbatim", async () => { const captured: Captured[] = []; const upstream = mockAnthropicUpstream(captured); diff --git a/tests/fixtures/test-layout-expected.json b/tests/fixtures/test-layout-expected.json index 700841acd0a..12c97b86825 100644 --- a/tests/fixtures/test-layout-expected.json +++ b/tests/fixtures/test-layout-expected.json @@ -259,7 +259,7 @@ "claude-estimate-projection.test.ts": "claude-integration", "claude-first-party-agent-sync.test.ts": "claude-integration", "claude-first-party-union.test.ts": "claude-integration", "claude-desktop-system-proxy.test.ts": "claude-integration", - "claude-gateway-cache.test.ts": "claude-integration", "claude-inbound-cache-stabilize.test.ts": "claude-integration", + "claude-gateway-cache.test.ts": "claude-integration", "claude-inbound-cache-stabilize.test.ts": "claude-integration", "claude-inbound-token-footer.test.ts": "claude-integration", "claude-inbound-debug.test.ts": "claude-integration", "claude-inbound.test.ts": "claude-integration", "claude-inbound-tool-reference.test.ts": "claude-integration", "claude-output-schema-strict.test.ts": "claude-integration", "claude-intercept-integration.test.ts": "server", From 6095abb78f6ee3bd1d4e1296a8f72702b3da4da4 Mon Sep 17 00:00:00 2001 From: JUN Date: Wed, 7 Oct 2026 23:06:54 +0900 Subject: [PATCH 2/2] docs: align translated Claude token footer guidance --- docs-site/src/content/docs/fr/guides/claude-code.md | 2 +- docs-site/src/content/docs/ja/guides/claude-code.md | 2 +- docs-site/src/content/docs/ko/guides/claude-code.md | 2 +- docs-site/src/content/docs/ru/guides/claude-code.md | 2 +- docs-site/src/content/docs/tr/guides/claude-code.md | 2 +- docs-site/src/content/docs/zh-cn/guides/claude-code.md | 2 +- docs-site/src/content/docs/zh-tw/guides/claude-code.md | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs-site/src/content/docs/fr/guides/claude-code.md b/docs-site/src/content/docs/fr/guides/claude-code.md index 0de2c391c4f..58e92f8e0ad 100644 --- a/docs-site/src/content/docs/fr/guides/claude-code.md +++ b/docs-site/src/content/docs/fr/guides/claude-code.md @@ -752,7 +752,7 @@ par défaut par un contenu minimal (`blockedSkills: ["claude-api"]`). ``, et non l'argument `model` de l'outil Agent. Vérifiez que la directive désigne la route voulue. Utilisez `"haiku"` comme valeur de remplacement pour le modèle. -Dans `config.json`, `claudeCode.stabilizePromptCache: true` déplace les notices Claude reconnues en fin des instructions système vers un dernier message utilisateur sur les routes traduites. La valeur par défaut est `false`. Activez cette option seulement si ce changement de rôle convient à vos clients. Les exemples dans des blocs de code et le texte non reconnu sont conservés ; le transfert Anthropic natif reste inchangé. Sans métadonnées, la clé de cache suit les instructions stabilisées. Cette option ne crée pas une identité de conversation et ne garantit aucun succès du cache amont. +Dans `config.json`, `claudeCode.stabilizePromptCache: true` retire les notices Claude reconnues à la fin des instructions système sur les routes traduites. Les pieds de page reconnus de la forme `N tokens left` sont supprimés, même lorsqu’ils sont répétés. Les rappels TaskCreate sont toujours déplacés vers un dernier message utilisateur ; si seuls des pieds de page de tokens ont été retirés, aucun message d’entrée n’est ajouté. La valeur par défaut est `false`. Activez cette option seulement si ce changement de rôle convient à vos clients. Les exemples dans des blocs de code et le texte non reconnu sont conservés ; le transfert Anthropic natif reste inchangé. Sans métadonnées, la clé de cache suit les instructions stabilisées. Cette option ne crée pas une identité de conversation et ne garantit aucun succès du cache amont. Sur toutes les routes Chat traduites, les rappels de l’historique conservent leur position dans la conversation, après les résultats d’outils encore attendus. L’ajout d’un rappel ne réécrit donc pas le prompt système initial, et une instruction placée au milieu de la conversation n’arrive plus avant les tours qu’elle était censée suivre. Le rôle porté par cet emplacement se décide séparément : un rappel part en `system`, sauf si le fournisseur enregistre `foldDeveloperRoleToSystem: false`, ce qui indique que le service en amont accepte le rôle `developer` et le transmet à la même position. Un service qui ne l’accepte pas répond `400 role 'developer' is not allowed` et le tour ne démarre pas, d’où le repli d’une destination non enregistrée. Ce comportement s’applique avec ou sans `stabilizePromptCache` ; le transfert Anthropic natif reste inchangé. La réutilisation du cache exige toujours une identité de session stable et un cache disponible en amont. Les changements des instructions ou outils antérieurs et la compaction de la conversation peuvent aussi affecter les succès du cache ; préserver l’ordre des rappels ne suffit pas à garantir sa réutilisation. diff --git a/docs-site/src/content/docs/ja/guides/claude-code.md b/docs-site/src/content/docs/ja/guides/claude-code.md index d208366d07f..a859acaa62d 100644 --- a/docs-site/src/content/docs/ja/guides/claude-code.md +++ b/docs-site/src/content/docs/ja/guides/claude-code.md @@ -611,7 +611,7 @@ Anthropic バックエンドを明示すると意図的に失敗後停止しま 引数ではなく `` ディレクティブを使います。ディレクティブが希望ルートと一致するか確認し、 モデルプレースホルダとして `"haiku"` を渡してください。 -`config.json` の `claudeCode.stabilizePromptCache` を `true` にすると、変換ルートのシステム指示末尾にある対応済み Claude 通知を最後のユーザーメッセージへ移します。既定値は `false` です。このロール変更が適切なクライアントでのみ有効にしてください。コードフェンス内の例と一致しない本文は保持され、Anthropic のネイティブ転送は変わりません。メタデータがない場合のキャッシュキーは安定化した指示から計算されます。会話 ID の生成やキャッシュヒットの保証は行いません。 +`config.json` の `claudeCode.stabilizePromptCache` を `true` にすると、変換ルートのシステム指示末尾にある対応済み Claude 通知を切り離します。認識された `N tokens left` 形式のトークンフッターは、繰り返されている場合もすべて削除します。TaskCreate のリマインダーは従来どおり末尾のユーザーメッセージへ移します。トークンフッターだけを切り離した場合、入力メッセージは追加しません。既定値は `false` です。このロール変更が適切なクライアントでのみ有効にしてください。コードフェンス内の例と一致しない本文は保持され、Anthropic のネイティブ転送は変わりません。メタデータがない場合のキャッシュキーは安定化した指示から計算されます。会話 ID の生成やキャッシュヒットの保証は行いません。 変換されたすべての Chat ルートで、タイムライン上のリマインダーは保留中のツール結果の後、会話内の元の位置を保ちます。これにより、新しいリマインダーを追加しても先頭のシステムプロンプトが書き換わらず、会話の途中に置かれた指示がそれより前のターンの前に移動することもありません。そのスロットが運ぶロールは別に決まります。プロバイダーが `foldDeveloperRoleToSystem: false` を記録していないかぎり、リマインダーは `system` として送られます。この記録は上流が `developer` ロールを受け付けることを表し、その場合は同じ位置のまま転送します。受け付けない上流は `400 role 'developer' is not allowed` を返してターンが始まらないため、記録のない宛先は畳む側になります。`stabilizePromptCache` の設定にかかわらず適用され、Anthropic のネイティブ転送は変わりません。キャッシュの再利用には、安定したセッション ID と上流キャッシュの利用可能性が引き続き必要です。過去の指示やツールの変更、会話の圧縮もキャッシュヒットに影響します。リマインダーの順序を保つだけで再利用が保証されるわけではありません。 diff --git a/docs-site/src/content/docs/ko/guides/claude-code.md b/docs-site/src/content/docs/ko/guides/claude-code.md index b545d4ca0e0..1b9947eebb9 100644 --- a/docs-site/src/content/docs/ko/guides/claude-code.md +++ b/docs-site/src/content/docs/ko/guides/claude-code.md @@ -674,7 +674,7 @@ Anthropic 백엔드를 명시하면 의도적으로 실패 후 중단해요. 인자가 아니라 `` 지시문을 사용해요. 지시문이 원하는 라우트와 일치하는지 확인하고, 모델 자리 표시자로 `"haiku"`를 전달하세요. -`config.json`에서 `claudeCode.stabilizePromptCache`를 `true`로 설정하면 번역 경로의 시스템 지시 끝에 붙은 지원 대상 Claude 알림을 마지막 사용자 메시지로 옮깁니다. 기본값은 `false`입니다. 사용하는 클라이언트에서 이 역할 변경을 허용할 때만 켜세요. 코드 펜스 안의 예제와 일치하지 않는 원문은 보존하며, Anthropic 원본 전달 경로는 바꾸지 않습니다. 메타데이터가 없는 요청의 캐시 키는 정리된 지시문을 기준으로 계산합니다. 대화 식별자를 만들거나 상위 서비스의 캐시 적중을 보장하는 기능은 아닙니다. +`config.json`에서 `claudeCode.stabilizePromptCache`를 `true`로 설정하면 변환 경로의 시스템 지시 끝에 붙은 지원 대상 Claude 알림을 떼어냅니다. 인식된 `N tokens left` 토큰 푸터는 반복된 경우에도 모두 제거합니다. TaskCreate 알림은 기존처럼 마지막 사용자 메시지로 옮기며, 토큰 푸터만 떼어낸 경우에는 입력 메시지를 추가하지 않습니다. 기본값은 `false`입니다. 사용하는 클라이언트에서 이 역할 변경을 허용할 때만 켜세요. 코드 펜스 안의 예제와 일치하지 않는 원문은 보존하며, Anthropic 원본 전달 경로는 바꾸지 않습니다. 메타데이터가 없는 요청의 캐시 키는 정리된 지시문을 기준으로 계산합니다. 대화 식별자를 만들거나 상위 서비스의 캐시 적중을 보장하는 기능은 아닙니다. 변환된 모든 Chat 경로에서 타임라인 알림은 대기 중인 도구 결과 뒤, 대화 안의 원래 위치를 그대로 유지합니다. 덕분에 새 알림을 추가해도 맨 앞의 시스템 프롬프트를 다시 쓰지 않고, 대화 중간의 지시가 그 지시보다 앞선 턴으로 끌려가지도 않습니다. 그 자리가 어떤 역할을 싣는지는 따로 정합니다. 공급자가 `foldDeveloperRoleToSystem: false`를 기록하지 않는 한 알림은 `system`으로 보내며, 이 기록은 상위 서비스가 `developer` 역할을 받아들인다는 뜻이라 같은 위치에서 그대로 전달합니다. 받아들이지 않는 상위 서비스는 `400 role 'developer' is not allowed`로 응답해 턴이 시작조차 못 하므로, 기록이 없는 목적지는 접는 쪽을 씁니다. `stabilizePromptCache` 설정과 관계없이 적용되며 Anthropic 네이티브 전달은 기존 동작을 유지합니다. 캐시 재사용에는 안정적인 세션 식별자와 사용 가능한 상위 서비스 캐시가 여전히 필요합니다. 이전 지시나 도구의 변경, 대화 압축도 캐시 적중에 영향을 줄 수 있으며, 알림 순서를 유지하는 것만으로 재사용을 보장하지는 않습니다. diff --git a/docs-site/src/content/docs/ru/guides/claude-code.md b/docs-site/src/content/docs/ru/guides/claude-code.md index d96d99cd490..2248e9647fa 100644 --- a/docs-site/src/content/docs/ru/guides/claude-code.md +++ b/docs-site/src/content/docs/ru/guides/claude-code.md @@ -646,7 +646,7 @@ Responses `web_search_call` в парные блоки Anthropic `server_tool_us ``, а не аргумент `model` инструмента Agent. Убедитесь, что директива соответствует нужному маршруту. В качестве плейсхолдера модели передавайте `"haiku"`. -Параметр `claudeCode.stabilizePromptCache: true` в `config.json` переносит поддерживаемые уведомления Claude в конце системных инструкций в последнее пользовательское сообщение на маршрутах с преобразованием. По умолчанию он выключен (`false`). Включайте его только когда такое изменение роли допустимо для ваших клиентов. Примеры в блоках кода и нераспознанный текст сохраняются; нативная передача Anthropic не меняется. Без метаданных ключ кэша рассчитывается по стабилизированным инструкциям. Идентификатор разговора не создаётся, попадания в кэш не гарантируются. +Параметр `claudeCode.stabilizePromptCache: true` в `config.json` отделяет поддерживаемые уведомления Claude от конца системных инструкций на маршрутах с преобразованием. Распознанные блоки вида `N tokens left` удаляются, в том числе при повторении. Напоминания TaskCreate по-прежнему переносятся в пользовательское сообщение в конце; если отделены только блоки с остатком токенов, входное сообщение не добавляется. По умолчанию он выключен (`false`). Включайте его только когда такое изменение роли допустимо для ваших клиентов. Примеры в блоках кода и нераспознанный текст сохраняются; нативная передача Anthropic не меняется. Без метаданных ключ кэша рассчитывается по стабилизированным инструкциям. Идентификатор разговора не создаётся, попадания в кэш не гарантируются. На всех преобразованных Chat-маршрутах напоминания в истории сохраняют свою позицию в разговоре — после ожидаемых результатов инструментов. Поэтому добавление нового напоминания не переписывает начальный системный промпт, а инструкция из середины разговора не оказывается раньше тех ходов, после которых она была написана. Роль этой позиции определяется отдельно: напоминание отправляется как `system`, если провайдер не записал `foldDeveloperRoleToSystem: false` — эта запись означает, что вышестоящий сервис принимает роль `developer`, и тогда она передаётся в той же позиции. Сервис, который её не принимает, отвечает `400 role 'developer' is not allowed`, и ход не начинается, поэтому незаписанное назначение сворачивается. Это работает независимо от `stabilizePromptCache`; нативная передача Anthropic остаётся прежней. Для повторного использования кэша по-прежнему нужны стабильный идентификатор сессии и доступный кэш провайдера. Изменения прежних инструкций или инструментов и сжатие разговора также могут влиять на попадания в кэш; само сохранение порядка напоминаний не гарантирует повторного использования. diff --git a/docs-site/src/content/docs/tr/guides/claude-code.md b/docs-site/src/content/docs/tr/guides/claude-code.md index 04e4d331fb2..607937289a1 100644 --- a/docs-site/src/content/docs/tr/guides/claude-code.md +++ b/docs-site/src/content/docs/tr/guides/claude-code.md @@ -864,7 +864,7 @@ aracının `model` argümanını değil, `` yönergelerin kullanır. Yönergenin hedeflenen rotayla eşleştiğinden emin olun. Model yer tutucusu olarak `"haiku"` iletin. -`config.json` içindeki `claudeCode.stabilizePromptCache: true`, dönüştürülen rotalarda sistem talimatlarının sonundaki desteklenen Claude bildirimlerini son kullanıcı mesajına taşır. Varsayılan değer `false` olur. Yalnızca bu rol değişikliği istemcileriniz için uygunsa etkinleştirin. Kod bloklarındaki örnekler ve eşleşmeyen metin korunur; yerel Anthropic aktarımı değişmez. Meta veri yoksa önbellek anahtarı kararlı talimatlardan hesaplanır. Bu seçenek konuşma kimliği oluşturmaz veya üst hizmette önbellek isabeti garanti etmez. +`config.json` içindeki `claudeCode.stabilizePromptCache: true`, dönüştürülen rotalarda sistem talimatlarının sonundaki desteklenen Claude bildirimlerini ayırır. Tanınan `N tokens left` biçimindeki token altbilgileri, yinelenenler de dahil olmak üzere silinir. TaskCreate hatırlatmaları eskisi gibi sona eklenen bir kullanıcı mesajına taşınır; yalnızca token altbilgileri ayrılmışsa girişe yeni bir mesaj eklenmez. Varsayılan değer `false` olur. Yalnızca bu rol değişikliği istemcileriniz için uygunsa etkinleştirin. Kod bloklarındaki örnekler ve eşleşmeyen metin korunur; yerel Anthropic aktarımı değişmez. Meta veri yoksa önbellek anahtarı kararlı talimatlardan hesaplanır. Bu seçenek konuşma kimliği oluşturmaz veya üst hizmette önbellek isabeti garanti etmez. Dönüştürülen tüm Chat rotalarında zaman çizelgesi hatırlatmaları, bekleyen araç sonuçlarından sonra konuşmadaki konumlarını korur. Böylece yeni bir hatırlatma eklenmesi baştaki sistem istemini yeniden yazmaz ve konuşmanın ortasındaki bir yönerge, izlemesi gereken turların önüne geçmez. O konumun hangi rolü taşıdığı ayrı bir karardır: sağlayıcı `foldDeveloperRoleToSystem: false` kaydetmedikçe hatırlatma `system` olarak gönderilir; bu kayıt, üst hizmetin `developer` rolünü kabul ettiğini belirtir ve rol aynı konumda iletilir. Kabul etmeyen bir üst hizmet `400 role 'developer' is not allowed` yanıtı verir ve tur hiç başlamaz; kaydı olmayan hedefin katlanmasının nedeni budur. Bu davranış `stabilizePromptCache` açık veya kapalıyken geçerlidir; yerel Anthropic aktarımı değişmez. Önbelleğin yeniden kullanımı için kararlı bir oturum kimliği ve kullanılabilir üst hizmet önbelleği hâlâ gereklidir. Önceki talimatların veya araçların değişmesi ve konuşmanın sıkıştırılması da önbellek isabetini etkileyebilir; hatırlatma sırasını korumak tek başına yeniden kullanımı garanti etmez. diff --git a/docs-site/src/content/docs/zh-cn/guides/claude-code.md b/docs-site/src/content/docs/zh-cn/guides/claude-code.md index 5fbf851d5d2..1f0ed34b9ce 100644 --- a/docs-site/src/content/docs/zh-cn/guides/claude-code.md +++ b/docs-site/src/content/docs/zh-cn/guides/claude-code.md @@ -573,7 +573,7 @@ Claude 模型时自动加载。对于原生透传,这是正常现象;对于 **子代理派发到错误模型**——名册代理(`ocx-*`)使用 `` 指令, 而不是 Agent 工具的 `model` 参数。请确保指令与预期路由一致。传入 `"haiku"` 作为模型占位符。 -在 `config.json` 中设置 `claudeCode.stabilizePromptCache: true`,可在转换路由上将系统指令末尾受支持的 Claude 提示移到最后一条用户消息。默认值为 `false`。仅在客户端允许这种角色变化时启用。代码围栏内的示例和不匹配的文本会保留,Anthropic 原生透传不变。没有元数据时,缓存键按稳定后的指令计算。该选项不会生成会话标识,也不保证上游缓存命中。 +在 `config.json` 中设置 `claudeCode.stabilizePromptCache: true`,可在转换路由上剥离系统指令末尾受支持的 Claude 提示。识别出的 `N tokens left` 形式的 token 页脚会被删除,包括重复出现的页脚。TaskCreate 提醒仍按原有方式移到末尾的用户消息;如果剥离的只有 token 页脚,则不会添加输入消息。默认值为 `false`。仅在客户端允许这种角色变化时启用。代码围栏内的示例和不匹配的文本会保留,Anthropic 原生透传不变。没有元数据时,缓存键按稳定后的指令计算。该选项不会生成会话标识,也不保证上游缓存命中。 在所有转换后的 Chat 路由上,时间线提醒都会保留在对话中的原有位置(排在尚待返回的工具结果之后)。因此,追加提醒不会重写开头的系统提示,对话中途的指令也不会被挪到它本应跟随的轮次之前。该位置携带哪个角色是单独决定的:除非提供方记录了 `foldDeveloperRoleToSystem: false`,否则提醒以 `system` 发送;该记录表示上游接受 `developer` 角色,此时提醒在同一位置按原样转发。不接受该角色的上游会返回 `400 role 'developer' is not allowed`,这一轮根本无法开始,所以未记录的目的地采用折叠。无论 `stabilizePromptCache` 是否启用,该行为都会生效;Anthropic 原生透传保持不变。缓存复用仍需要稳定的会话标识和可用的上游缓存。修改较早的指令或工具、压缩对话也可能影响缓存命中;仅保留提醒顺序并不保证缓存复用。 diff --git a/docs-site/src/content/docs/zh-tw/guides/claude-code.md b/docs-site/src/content/docs/zh-tw/guides/claude-code.md index ceac4790726..3d6bc538a02 100644 --- a/docs-site/src/content/docs/zh-tw/guides/claude-code.md +++ b/docs-site/src/content/docs/zh-tw/guides/claude-code.md @@ -649,7 +649,7 @@ Claude 模型時自動載入。對於原生透傳,這是正常現象;對於 **子代理派發到錯誤模型**——名冊代理(`ocx-*`)使用 `` 指令, 而不是 Agent 工具的 `model` 引數。請確保指令與預期路由一致。傳入 `"haiku"` 作為模型佔位符。 -在 `config.json` 中設定 `claudeCode.stabilizePromptCache: true`,可在轉換路由上將系統指令末尾支援的 Claude 提示移到最後一則使用者訊息。預設值為 `false`。僅在用戶端允許這種角色變更時啟用。程式碼圍欄中的範例和不符合的文字會保留,Anthropic 原生轉送不變。沒有中繼資料時,快取鍵依穩定後的指令計算。此選項不會產生對話識別碼,也不保證上游快取命中。 +在 `config.json` 中設定 `claudeCode.stabilizePromptCache: true`,可在轉換路由上移除系統指令末尾支援的 Claude 提示。辨識出的 `N tokens left` 格式的 token 頁尾會被刪除,包括重複出現的頁尾。TaskCreate 提醒仍依原有方式移到末尾的使用者訊息;如果移除的只有 token 頁尾,就不會新增輸入訊息。預設值為 `false`。僅在用戶端允許這種角色變更時啟用。程式碼圍欄中的範例和不符合的文字會保留,Anthropic 原生轉送不變。沒有中繼資料時,快取鍵依穩定後的指令計算。此選項不會產生對話識別碼,也不保證上游快取命中。 在所有轉換後的 Chat 路由上,時間線提醒都會保留在對話中的原有位置(排在尚待傳回的工具結果之後)。因此,新增提醒不會重寫開頭的系統提示,對話中途的指令也不會被移到它原本應跟隨的輪次之前。該位置攜帶哪個角色是另外決定的:除非提供者記錄了 `foldDeveloperRoleToSystem: false`,否則提醒以 `system` 傳送;該記錄表示上游接受 `developer` 角色,此時提醒在相同位置照原樣轉送。不接受該角色的上游會回應 `400 role 'developer' is not allowed`,該回合根本無法開始,所以未記錄的目的地採用摺疊。無論 `stabilizePromptCache` 是否啟用,此行為都會生效;Anthropic 原生轉送維持不變。快取重用仍需要穩定的工作階段識別碼和可用的上游快取。修改較早的指令或工具、壓縮對話也可能影響快取命中;僅保留提醒順序並不保證快取重用。