mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-21 22:52:19 +03:00
fix(sse): advance Claude cache breakpoints on growing tails (#10684)
Merged — locally validated (110/110 focused tests, typecheck:core clean, gates green) after resolving base-drift against #10683 (both landed today, same test file — combined). Nice, well-tested cache-breakpoint fix. Thanks!
This commit is contained in:
@@ -130,27 +130,34 @@ export function ensureCacheControlOnLastUserMessage(body: Record<string, unknown
|
||||
if (!Array.isArray(messages) || messages.length === 0) return;
|
||||
|
||||
const system = body.system as Array<Record<string, unknown>> | 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<string, unknown> | undefined)?.ttl === "5m"
|
||||
)
|
||||
: false;
|
||||
|
||||
for (const message of messages) {
|
||||
const content = message.content as Array<Record<string, unknown>> | 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<string, unknown> | 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<string, unknown>;
|
||||
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<string, unknown
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Real Claude Code (and CC-protocol-compatible clients) commonly send
|
||||
* `cache_control: { type: "ephemeral" }` with no `ttl`. On the native Claude
|
||||
* OAuth path the outbound anthropic-beta set always includes
|
||||
* extended-cache-ttl-2025-04-11 (see ANTHROPIC_BETA_BASE /
|
||||
* ANTHROPIC_BETA_CLAUDE_OAUTH in anthropicHeaders.ts), so requesting the 1h
|
||||
* TTL is always valid here — but Anthropic only honors it when `ttl` is
|
||||
* explicitly set; an absent `ttl` silently falls back to the platform
|
||||
* default of 5 minutes even though the 1h beta was negotiated. Any pause
|
||||
* longer than 5 minutes between turns then forces a full prefix rewrite
|
||||
* instead of a cache hit. Default the ttl to "1h" wherever it's missing;
|
||||
* never touch a cache_control that already specifies one (explicit client
|
||||
* choice is preserved).
|
||||
*/
|
||||
/** Defaults missing TTLs to 1h until a 5m breakpoint; later defaults stay at 5m. */
|
||||
export function normalizeCacheControlTtl(body: Record<string, unknown>): void {
|
||||
let hasFiveMinuteCacheControl = false;
|
||||
|
||||
const defaultMissingTtl = (block: Record<string, unknown> | null | undefined) => {
|
||||
const cc = block?.cache_control as Record<string, unknown> | 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<Record<string, unknown>> | undefined;
|
||||
if (Array.isArray(system)) {
|
||||
for (const block of system) defaultMissingTtl(block);
|
||||
}
|
||||
|
||||
const tools = body.tools as Array<Record<string, unknown>> | undefined;
|
||||
if (Array.isArray(tools)) {
|
||||
for (const tool of tools) defaultMissingTtl(tool);
|
||||
}
|
||||
|
||||
const system = body.system as Array<Record<string, unknown>> | undefined;
|
||||
if (Array.isArray(system)) {
|
||||
for (const block of system) defaultMissingTtl(block);
|
||||
}
|
||||
|
||||
const messages = body.messages as Array<Record<string, unknown>> | undefined;
|
||||
if (Array.isArray(messages)) {
|
||||
for (const message of messages) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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" }],
|
||||
|
||||
Reference in New Issue
Block a user