mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
fix cc-compatible cache markers
This commit is contained in:
@@ -166,12 +166,18 @@ export function buildClaudeCodeCompatibleRequest({
|
||||
systemBlocks: preparedClaudeBody?.system as Record<string, unknown>[] | undefined,
|
||||
cwd,
|
||||
now,
|
||||
preserveCacheControl,
|
||||
});
|
||||
const resolvedSessionId = sessionId || randomUUID();
|
||||
const effort = resolveClaudeCodeCompatibleEffort(sourceBody, normalizedBody, model);
|
||||
const maxTokens = resolveClaudeCodeCompatibleMaxTokens(sourceBody, normalizedBody);
|
||||
const tools = preparedClaudeBody?.tools
|
||||
? (preparedClaudeBody.tools as Record<string, unknown>[])
|
||||
? applyClaudeCodeCompatibleToolCacheStrategy(
|
||||
preparedClaudeBody.tools as Record<string, unknown>[],
|
||||
{
|
||||
preserveExisting: preserveCacheControl,
|
||||
}
|
||||
)
|
||||
: buildClaudeCodeCompatibleTools(normalizedBody, sourceBody);
|
||||
const toolChoice =
|
||||
tools.length > 0
|
||||
@@ -351,8 +357,10 @@ function buildClaudeCodeCompatibleMessagesFromClaude(
|
||||
for (const message of merged) {
|
||||
stripCacheControlFromContentBlocks(message.content);
|
||||
}
|
||||
applyClaudeCodeCompatibleMessageCacheStrategy(merged);
|
||||
}
|
||||
applyClaudeCodeCompatibleMessageCacheStrategy(merged, {
|
||||
preserveExisting: preserveCacheControl,
|
||||
});
|
||||
|
||||
if (merged.length === 0) {
|
||||
const fallbackText = converted
|
||||
@@ -379,11 +387,13 @@ function buildClaudeCodeCompatibleSystemBlocks({
|
||||
systemBlocks,
|
||||
cwd,
|
||||
now,
|
||||
preserveCacheControl,
|
||||
}: {
|
||||
messages: MessageLike[] | undefined;
|
||||
systemBlocks?: Array<Record<string, unknown>> | undefined;
|
||||
cwd: string;
|
||||
now: Date;
|
||||
preserveCacheControl: boolean;
|
||||
}) {
|
||||
const customSystemBlocks =
|
||||
Array.isArray(systemBlocks) && systemBlocks.length > 0
|
||||
@@ -412,6 +422,15 @@ function buildClaudeCodeCompatibleSystemBlocks({
|
||||
blocks.push(systemBlock);
|
||||
}
|
||||
|
||||
if (
|
||||
preserveCacheControl &&
|
||||
customSystemBlocks.length > 0 &&
|
||||
!customSystemBlocks.some((block) => hasCacheControl(block))
|
||||
) {
|
||||
const lastCustomSystemBlock = customSystemBlocks[customSystemBlocks.length - 1];
|
||||
lastCustomSystemBlock.cache_control = { type: "ephemeral", ttl: "1h" };
|
||||
}
|
||||
|
||||
return blocks;
|
||||
}
|
||||
|
||||
@@ -448,12 +467,10 @@ function buildClaudeCodeCompatibleTools(
|
||||
return rawTools
|
||||
.map((tool) => convertClaudeCodeCompatibleTool(tool))
|
||||
.filter((tool): tool is Record<string, unknown> => !!tool)
|
||||
.map((tool) => ({ ...tool }))
|
||||
.map((tool, index, tools) => {
|
||||
if (index !== findLastCacheableToolIndex(tools)) return tool;
|
||||
return {
|
||||
...tool,
|
||||
cache_control: { type: "ephemeral", ttl: "1h" },
|
||||
};
|
||||
return { ...tool, cache_control: { type: "ephemeral", ttl: "1h" } };
|
||||
});
|
||||
}
|
||||
|
||||
@@ -633,7 +650,7 @@ function convertClaudeCodeCompatibleClaudeMessage(
|
||||
function extractCustomSystemBlocks(messages: MessageLike[] | undefined) {
|
||||
if (!Array.isArray(messages)) return [];
|
||||
|
||||
return messages
|
||||
const blocks = messages
|
||||
.filter((message) => {
|
||||
const role = String(message?.role || "").toLowerCase();
|
||||
return role === "system" || role === "developer";
|
||||
@@ -645,11 +662,21 @@ function extractCustomSystemBlocks(messages: MessageLike[] | undefined) {
|
||||
text,
|
||||
cache_control: { type: "ephemeral" },
|
||||
}));
|
||||
|
||||
if (blocks.length > 0) {
|
||||
blocks[blocks.length - 1].cache_control = { type: "ephemeral", ttl: "1h" };
|
||||
}
|
||||
|
||||
return blocks;
|
||||
}
|
||||
|
||||
function applyClaudeCodeCompatibleMessageCacheStrategy(
|
||||
messages: Array<{ role: "user" | "assistant"; content: Array<Record<string, unknown>> }>
|
||||
messages: Array<{ role: "user" | "assistant"; content: Array<Record<string, unknown>> }>,
|
||||
options: {
|
||||
preserveExisting?: boolean;
|
||||
} = {}
|
||||
) {
|
||||
const preserveExisting = options.preserveExisting === true;
|
||||
const userIndexes = messages.reduce((indexes, message, index) => {
|
||||
if (message.role === "user") indexes.push(index);
|
||||
return indexes;
|
||||
@@ -658,14 +685,28 @@ function applyClaudeCodeCompatibleMessageCacheStrategy(
|
||||
const secondToLastUserIndex = userIndexes.length >= 2 ? userIndexes[userIndexes.length - 2] : -1;
|
||||
|
||||
if (secondToLastUserIndex >= 0) {
|
||||
markLastContentCacheControl(messages[secondToLastUserIndex].content);
|
||||
markLastContentCacheControl(
|
||||
messages[secondToLastUserIndex].content,
|
||||
undefined,
|
||||
preserveExisting
|
||||
);
|
||||
} else if (!hasAssistant && userIndexes.length > 0) {
|
||||
markLastContentCacheControl(messages[userIndexes[userIndexes.length - 1]].content);
|
||||
markLastContentCacheControl(
|
||||
messages[userIndexes[userIndexes.length - 1]].content,
|
||||
undefined,
|
||||
preserveExisting
|
||||
);
|
||||
}
|
||||
|
||||
const lastUserIndex = userIndexes.length > 0 ? userIndexes[userIndexes.length - 1] : -1;
|
||||
const endsOnUser = lastUserIndex === messages.length - 1;
|
||||
if (endsOnUser && lastUserIndex >= 0) {
|
||||
markLastContentCacheControl(messages[lastUserIndex].content, undefined, preserveExisting);
|
||||
}
|
||||
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
if (messages[i].role !== "assistant") continue;
|
||||
if (markLastContentCacheControl(messages[i].content)) break;
|
||||
if (markLastContentCacheControl(messages[i].content, undefined, preserveExisting)) break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -675,14 +716,52 @@ function stripCacheControlFromContentBlocks(content: Array<Record<string, unknow
|
||||
}
|
||||
}
|
||||
|
||||
function markLastContentCacheControl(content: Array<Record<string, unknown>>, ttl?: string) {
|
||||
function applyClaudeCodeCompatibleToolCacheStrategy(
|
||||
tools: Array<Record<string, unknown>>,
|
||||
options: {
|
||||
preserveExisting?: boolean;
|
||||
} = {}
|
||||
) {
|
||||
const preparedTools = tools.map((tool) => ({ ...tool }));
|
||||
const lastCacheableToolIndex = findLastCacheableToolIndex(preparedTools);
|
||||
if (lastCacheableToolIndex < 0) return preparedTools;
|
||||
|
||||
if (options.preserveExisting && preparedTools.some((tool) => hasCacheControl(tool))) {
|
||||
return preparedTools;
|
||||
}
|
||||
|
||||
preparedTools[lastCacheableToolIndex].cache_control = { type: "ephemeral", ttl: "1h" };
|
||||
return preparedTools;
|
||||
}
|
||||
|
||||
function markLastContentCacheControl(
|
||||
content: Array<Record<string, unknown>>,
|
||||
ttl?: string,
|
||||
preserveExisting = false
|
||||
) {
|
||||
if (!Array.isArray(content) || content.length === 0) return false;
|
||||
const lastBlock = content[content.length - 1];
|
||||
if (!lastBlock) return false;
|
||||
if (
|
||||
preserveExisting &&
|
||||
content.some(
|
||||
(block) =>
|
||||
!!block &&
|
||||
typeof block === "object" &&
|
||||
!!readRecord(block)?.cache_control &&
|
||||
typeof readRecord(block)?.cache_control === "object"
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
lastBlock.cache_control = ttl ? { type: "ephemeral", ttl } : { type: "ephemeral" };
|
||||
return true;
|
||||
}
|
||||
|
||||
function hasCacheControl(value: unknown) {
|
||||
return !!readRecord(value)?.cache_control && typeof readRecord(value)?.cache_control === "object";
|
||||
}
|
||||
|
||||
function findLastCacheableToolIndex(tools: Array<Record<string, unknown>>) {
|
||||
for (let i = tools.length - 1; i >= 0; i--) {
|
||||
if (!tools[i].defer_loading) {
|
||||
|
||||
@@ -148,6 +148,13 @@ export function shouldPreserveCacheControl({
|
||||
return false;
|
||||
}
|
||||
|
||||
// CC-compatible bridges should default to OmniRoute-managed cache markers.
|
||||
// Their request shape differs from native Claude Messages payloads, so
|
||||
// preserving client markers in auto mode weakens cache coverage.
|
||||
if (typeof targetProvider === "string" && targetProvider.startsWith("anthropic-compatible-cc-")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Auto mode: use automatic detection (existing logic)
|
||||
// Must be a caching-aware client
|
||||
if (!isClaudeCodeClient(userAgent)) {
|
||||
|
||||
@@ -277,7 +277,7 @@ export default function RoutingTab() {
|
||||
{
|
||||
value: "auto",
|
||||
label: "Auto (Recommended)",
|
||||
desc: "Preserve cache_control only for caching-aware clients (Claude Code) with deterministic routing",
|
||||
desc: "Preserve cache_control for native Claude-compatible flows with deterministic routing; CC-compatible bridges use OmniRoute-managed markers",
|
||||
},
|
||||
{
|
||||
value: "always",
|
||||
|
||||
@@ -138,4 +138,19 @@ describe("Cache Control Policy - Claude Protocol Providers", () => {
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
test("shouldPreserveCacheControl defaults CC-compatible providers to OmniRoute-managed cache in auto mode", () => {
|
||||
const claudeCodeUA = "Claude-Code/1.0.0";
|
||||
|
||||
assert.equal(
|
||||
shouldPreserveCacheControl({
|
||||
userAgent: claudeCodeUA,
|
||||
isCombo: false,
|
||||
targetProvider: "anthropic-compatible-cc-cm",
|
||||
targetFormat: "claude",
|
||||
settings: { alwaysPreserveClientCache: "auto" },
|
||||
}),
|
||||
false
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -106,7 +106,7 @@ test("buildClaudeCodeCompatibleRequest keeps prior role history while dropping t
|
||||
);
|
||||
assert.deepEqual(payload.messages[0].content.at(-1).cache_control, { type: "ephemeral" });
|
||||
assert.deepEqual(payload.messages[1].content.at(-1).cache_control, { type: "ephemeral" });
|
||||
assert.equal(payload.messages[2].content.at(-1).cache_control, undefined);
|
||||
assert.deepEqual(payload.messages[2].content.at(-1).cache_control, { type: "ephemeral" });
|
||||
assert.equal(payload.system.length, 4);
|
||||
assert.equal(payload.system.at(-1).text, "sys");
|
||||
assert.equal(payload.tools.length, 1);
|
||||
@@ -182,10 +182,90 @@ test("buildClaudeCodeCompatibleRequest preserves Claude cache markers when reque
|
||||
type: "ephemeral",
|
||||
ttl: "10m",
|
||||
});
|
||||
assert.equal(payload.messages[2].content[0].cache_control, undefined);
|
||||
assert.deepEqual(payload.messages[2].content[0].cache_control, { type: "ephemeral" });
|
||||
assert.deepEqual(payload.tools[0].cache_control, { type: "ephemeral", ttl: "30m" });
|
||||
});
|
||||
|
||||
test("buildClaudeCodeCompatibleRequest supplements missing Claude cache markers in preserve mode", () => {
|
||||
const payload = buildClaudeCodeCompatibleRequest({
|
||||
sourceBody: {
|
||||
max_tokens: 64,
|
||||
},
|
||||
normalizedBody: {
|
||||
max_tokens: 64,
|
||||
messages: [{ role: "user", content: "fallback" }],
|
||||
},
|
||||
claudeBody: {
|
||||
system: [{ type: "text", text: "sys" }],
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: [{ type: "text", text: "u1" }],
|
||||
},
|
||||
{
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: "a1" }],
|
||||
},
|
||||
{ role: "user", content: [{ type: "text", text: "u2" }] },
|
||||
],
|
||||
tools: [
|
||||
{
|
||||
name: "lookup_weather",
|
||||
description: "Fetch weather",
|
||||
input_schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
city: { type: "string" },
|
||||
},
|
||||
required: ["city"],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
model: "claude-sonnet-4-6",
|
||||
sessionId: "session-preserve-defaults",
|
||||
preserveCacheControl: true,
|
||||
});
|
||||
|
||||
assert.deepEqual(payload.messages[0].content[0].cache_control, { type: "ephemeral" });
|
||||
assert.deepEqual(payload.messages[1].content[0].cache_control, { type: "ephemeral" });
|
||||
assert.deepEqual(payload.messages[2].content[0].cache_control, { type: "ephemeral" });
|
||||
assert.deepEqual(payload.tools[0].cache_control, { type: "ephemeral", ttl: "1h" });
|
||||
});
|
||||
|
||||
test("buildClaudeCodeCompatibleRequest marks final user turn and 1h system cache in non-preserve mode", () => {
|
||||
const largeUserPrompt = Array.from(
|
||||
{ length: 200 },
|
||||
(_, index) => `Context line ${index + 1}: repeated stable context for cache testing.`
|
||||
).join("\n");
|
||||
|
||||
const payload = buildClaudeCodeCompatibleRequest({
|
||||
sourceBody: {
|
||||
max_tokens: 64,
|
||||
},
|
||||
normalizedBody: {
|
||||
max_tokens: 64,
|
||||
messages: [
|
||||
{ role: "system", content: "Follow the house style exactly." },
|
||||
{ role: "user", content: "[Start a new chat]" },
|
||||
{ role: "assistant", content: "Hello short ack" },
|
||||
{ role: "user", content: largeUserPrompt },
|
||||
],
|
||||
},
|
||||
model: "claude-sonnet-4-6",
|
||||
sessionId: "session-last-user-cache",
|
||||
preserveCacheControl: false,
|
||||
});
|
||||
|
||||
assert.deepEqual(payload.system.at(-1).cache_control, {
|
||||
type: "ephemeral",
|
||||
ttl: "1h",
|
||||
});
|
||||
assert.deepEqual(payload.messages[0].content[0].cache_control, { type: "ephemeral" });
|
||||
assert.deepEqual(payload.messages[1].content[0].cache_control, { type: "ephemeral" });
|
||||
assert.deepEqual(payload.messages[2].content[0].cache_control, { type: "ephemeral" });
|
||||
});
|
||||
|
||||
test("buildClaudeCodeCompatibleRequest falls back to a user turn when the source only has assistant/model text", () => {
|
||||
const payload = buildClaudeCodeCompatibleRequest({
|
||||
sourceBody: {
|
||||
@@ -419,7 +499,7 @@ test("handleChatCore forces upstream streaming for CC compatible while returning
|
||||
assert.equal(payload.usage.completion_tokens, 5);
|
||||
});
|
||||
|
||||
test("handleChatCore preserves Claude cache_control when CC-compatible requests originate from Claude", async () => {
|
||||
test("handleChatCore applies OmniRoute-managed cache strategy for CC-compatible requests in auto mode", async () => {
|
||||
const calls = [];
|
||||
globalThis.fetch = async (url, init = {}) => {
|
||||
calls.push({
|
||||
@@ -524,18 +604,20 @@ test("handleChatCore preserves Claude cache_control when CC-compatible requests
|
||||
assert.equal(calls.length, 1);
|
||||
assert.deepEqual(calls[0].body.system.at(-1).cache_control, {
|
||||
type: "ephemeral",
|
||||
ttl: "5m",
|
||||
ttl: "1h",
|
||||
});
|
||||
assert.deepEqual(calls[0].body.messages[0].content[0].cache_control, {
|
||||
type: "ephemeral",
|
||||
});
|
||||
assert.deepEqual(calls[0].body.messages[1].content[0].cache_control, {
|
||||
type: "ephemeral",
|
||||
ttl: "10m",
|
||||
});
|
||||
assert.deepEqual(calls[0].body.messages[2].content[0].cache_control, {
|
||||
type: "ephemeral",
|
||||
});
|
||||
assert.deepEqual(calls[0].body.tools[0].cache_control, {
|
||||
type: "ephemeral",
|
||||
ttl: "30m",
|
||||
ttl: "1h",
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user