diff --git a/open-sse/services/claudeCodeConstraints.ts b/open-sse/services/claudeCodeConstraints.ts index 42ed52f996..094727e11b 100644 --- a/open-sse/services/claudeCodeConstraints.ts +++ b/open-sse/services/claudeCodeConstraints.ts @@ -130,27 +130,34 @@ export function ensureCacheControlOnLastUserMessage(body: Record> | undefined; - const systemCacheControlCount = Array.isArray(system) + let cacheControlCount = Array.isArray(system) ? system.filter((block) => block.cache_control).length : 0; + let hasFiveMinuteCacheControl = Array.isArray(system) + ? system.some( + (block) => (block.cache_control as Record | undefined)?.ttl === "5m" + ) + : false; for (const message of messages) { const content = message.content as Array> | undefined; - if (Array.isArray(content) && content.some((block) => block.cache_control)) { - return; - } + if (!Array.isArray(content)) continue; + cacheControlCount += content.filter((block) => block.cache_control).length; + hasFiveMinuteCacheControl ||= content.some( + (block) => (block.cache_control as Record | undefined)?.ttl === "5m" + ); } - if (systemCacheControlCount >= MAX_CACHE_CONTROL_BLOCKS) return; - // Find the last user message for (let i = messages.length - 1; i >= 0; i--) { if (String(messages[i].role) === "user") { const content = messages[i].content; if (Array.isArray(content) && content.length > 0) { const lastBlock = content[content.length - 1] as Record; - if (!lastBlock.cache_control) { - lastBlock.cache_control = { type: "ephemeral" }; + if (!lastBlock.cache_control && cacheControlCount < MAX_CACHE_CONTROL_BLOCKS) { + lastBlock.cache_control = hasFiveMinuteCacheControl + ? { type: "ephemeral", ttl: "5m" } + : { type: "ephemeral" }; } } break; @@ -158,38 +165,31 @@ export function ensureCacheControlOnLastUserMessage(body: Record): void { + let hasFiveMinuteCacheControl = false; + const defaultMissingTtl = (block: Record | null | undefined) => { const cc = block?.cache_control as Record | undefined; - if (cc && cc.type === "ephemeral" && cc.ttl === undefined) { - cc.ttl = "1h"; + if (!cc || cc.type !== "ephemeral") return; + + if (cc.ttl === "5m") { + hasFiveMinuteCacheControl = true; + } else if (cc.ttl === undefined) { + cc.ttl = hasFiveMinuteCacheControl ? "5m" : "1h"; } }; - const system = body.system as Array> | undefined; - if (Array.isArray(system)) { - for (const block of system) defaultMissingTtl(block); - } - const tools = body.tools as Array> | undefined; if (Array.isArray(tools)) { for (const tool of tools) defaultMissingTtl(tool); } + const system = body.system as Array> | undefined; + if (Array.isArray(system)) { + for (const block of system) defaultMissingTtl(block); + } + const messages = body.messages as Array> | undefined; if (Array.isArray(messages)) { for (const message of messages) { diff --git a/tests/unit/chatcore-translation-paths.test.ts b/tests/unit/chatcore-translation-paths.test.ts index db081c9345..8ae3d4d36d 100644 --- a/tests/unit/chatcore-translation-paths.test.ts +++ b/tests/unit/chatcore-translation-paths.test.ts @@ -1085,13 +1085,16 @@ test("chatCore preserves Opus 5 mid-conversation system cache breakpoints", asyn ); assert.deepEqual(call.body.messages[2].content[0].cache_control, { type: "ephemeral", - ttl: "1h", + ttl: "5m", }); assert.equal( call.body.system.some((block: { text?: string }) => block.text === "compact continuation"), false ); - assert.equal(call.body.messages[3].content[0].cache_control, undefined); + assert.deepEqual(call.body.messages[3].content[0].cache_control, { + type: "ephemeral", + ttl: "5m", + }); }); test("chatCore keeps Claude normalization for non-Claude-Code Claude passthrough", async () => { const { call, result } = await invokeChatCore({ @@ -1264,12 +1267,12 @@ test("chatCore preserves cache_control automatically for Claude Code single-mode assert.deepEqual(call.body.system[2].cache_control, { type: "ephemeral", ttl: "5m" }); assert.deepEqual(call.body.messages[0].content[0].cache_control, { type: "ephemeral", - ttl: "1h", + ttl: "5m", }); // base.ts executor explicitly strips cache_control from tools for Claude Code clients assert.equal(call.body.tools[0].cache_control, undefined); }); -test("chatCore supplements a missing message cache breakpoint for native Claude Code requests", async () => { +test("chatCore advances a message cache breakpoint for native Claude Code requests", async () => { await settingsDb.updateSettings({ alwaysPreserveClientCache: "auto" }); invalidateCacheControlSettingsCache(); @@ -1294,7 +1297,16 @@ test("chatCore supplements a missing message cache breakpoint for native Claude }, ], messages: [ - { role: "user", content: [{ type: "text", text: "first turn" }] }, + { + role: "user", + content: [ + { + type: "text", + text: "first turn", + cache_control: { type: "ephemeral" }, + }, + ], + }, { role: "assistant", content: [{ type: "text", text: "first response" }] }, { role: "user", content: [{ type: "text", text: "latest turn" }] }, ], @@ -1313,7 +1325,7 @@ test("chatCore supplements a missing message cache breakpoint for native Claude assert.deepEqual(call.body.messages[2].content[0].cache_control, { type: "ephemeral", - ttl: "1h", + ttl: "5m", }); assert.equal(call.body.tools[0].cache_control, undefined); }); @@ -1401,10 +1413,12 @@ test("chatCore disables raw Claude passthrough when cache preservation is off an ), true ); - // Cache preservation is on for native Claude, so cache markers are intact + // Cache preservation is on for native Claude, so cache markers are intact. This PR: + // an omitted TTL now defaults to "5m" once a "5m" boundary breakpoint (the system + // block above) has already appeared, instead of always defaulting to "1h". assert.deepEqual(call.body.messages[0].content[0].cache_control, { type: "ephemeral", - ttl: "1h", + ttl: "5m", }); // Tools disable flag is applied assert.equal("_disableToolPrefix" in call.body, false); diff --git a/tests/unit/claude-code-parity.test.ts b/tests/unit/claude-code-parity.test.ts index 0e58e9a3f3..07a1e5e2d1 100644 --- a/tests/unit/claude-code-parity.test.ts +++ b/tests/unit/claude-code-parity.test.ts @@ -360,7 +360,7 @@ describe("ensureCacheControlOnLastUserMessage", () => { assert.deepEqual(body.messages[2].content[0].cache_control, { type: "ephemeral" }); }); - it("keeps an existing message breakpoint without adding another", () => { + it("keeps an existing message breakpoint and advances one to the last user message", () => { const body = { messages: [ { @@ -377,9 +377,33 @@ describe("ensureCacheControlOnLastUserMessage", () => { ], }; + ensureCacheControlOnLastUserMessage(body); ensureCacheControlOnLastUserMessage(body); - assert.equal(body.messages[1].content[0].cache_control, undefined); + assert.deepEqual(body.messages[0].content[0].cache_control, { type: "ephemeral" }); + assert.deepEqual(body.messages[1].content[0].cache_control, { type: "ephemeral" }); + assert.equal( + body.messages.flatMap((message) => message.content).filter((block) => block.cache_control) + .length, + 2 + ); + }); + + it("keeps a new tail breakpoint at 5m after an existing 5m breakpoint", () => { + const body = { + system: [ + { type: "text", text: "long", cache_control: { type: "ephemeral", ttl: "1h" } }, + { type: "text", text: "short", cache_control: { type: "ephemeral", ttl: "5m" } }, + ], + messages: [{ role: "user", content: [{ type: "text", text: "Follow up" }] }], + }; + + ensureCacheControlOnLastUserMessage(body); + + assert.deepEqual(body.messages[0].content[0].cache_control, { + type: "ephemeral", + ttl: "5m", + }); }); it("does not exceed four surviving system and message breakpoints", () => { @@ -452,6 +476,25 @@ describe("normalizeCacheControlTtl", () => { }); }); + it("defaults missing ttl to 5m after a 5m breakpoint", () => { + const body = { + system: [{ type: "text", text: "stable", cache_control: { type: "ephemeral", ttl: "5m" } }], + messages: [ + { + role: "user", + content: [{ type: "text", text: "follow up", cache_control: { type: "ephemeral" } }], + }, + ], + }; + + normalizeCacheControlTtl(body); + + assert.deepEqual(body.messages[0].content[0].cache_control, { + type: "ephemeral", + ttl: "5m", + }); + }); + it("leaves blocks without cache_control untouched", () => { const body = { system: [{ type: "text", text: "no cache_control here" }],