fix(claude): never anchor cache breakpoint on defer_loading tools (#3567)

This commit is contained in:
Federico Liva
2026-09-03 10:05:24 +07:00
committed by decolua
parent 6efb97904b
commit 6ab9ca9eb1
2 changed files with 97 additions and 2 deletions

View File

@@ -12,6 +12,18 @@ import { DEFAULT_MAX_TOKENS } from "../../config/runtimeConfig.js";
const CACHE_CONTROL_5M = { type: "ephemeral" };
const CACHE_CONTROL_1H = { type: "ephemeral", ttl: "1h" };
// Anthropic rejects a tool carrying BOTH defer_loading:true and cache_control
// ("Tools defer_loading cannot use prompt caching", #3567). MCP clients put
// deferred tools at the tail, which is exactly where the cache anchor lands.
// Anchor on the last tool that CAN be cached instead of dropping caching.
export function lastCacheableToolIndex(tools) {
if (!Array.isArray(tools)) return -1;
for (let i = tools.length - 1; i >= 0; i--) {
if (tools[i]?.defer_loading !== true) return i;
}
return -1;
}
// Check if message has valid non-empty content
export function hasValidContent(msg) {
if (typeof msg.content === "string" && msg.content.trim()) return true;
@@ -270,7 +282,7 @@ export function anchorClaudeCache(body) {
}
if (Array.isArray(body.tools)) {
const last = body.tools.length - 1;
const last = lastCacheableToolIndex(body.tools);
body.tools.forEach((tool, i) => {
if (i === last) tool.cache_control = { ...CACHE_CONTROL_1H };
else delete tool.cache_control;
@@ -464,9 +476,10 @@ export function prepareClaudeRequest(body, provider = null, apiKey = null, conne
});
}
const lastCacheable = lastCacheableToolIndex(body.tools);
body.tools = body.tools.map((tool, i) => {
const { cache_control, ...rest } = tool;
if (i === body.tools.length - 1) {
if (i === lastCacheable) {
return { ...rest, cache_control: { type: "ephemeral", ttl: "1h" } };
}
return rest;