fix(providers): scope anthropic-family cache semantics (#60370)

This commit is contained in:
Vincent Koc
2026-04-04 00:11:57 +09:00
committed by GitHub
parent b50b85a5db
commit f08a1c34dd
6 changed files with 93 additions and 18 deletions

View File

@@ -1,21 +1,23 @@
import { resolveAnthropicCacheRetentionFamily } from "./anthropic-family-cache-semantics.js";
type CacheRetention = "none" | "short" | "long";
export function resolveCacheRetention(
extraParams: Record<string, unknown> | undefined,
provider: string,
modelApi?: string,
modelId?: string,
): CacheRetention | undefined {
const isAnthropicDirect = provider === "anthropic";
const hasExplicitCacheConfig =
extraParams?.cacheRetention !== undefined || extraParams?.cacheControlTtl !== undefined;
const isAnthropicBedrock = provider === "amazon-bedrock" && hasExplicitCacheConfig;
const isCustomAnthropicApi =
!isAnthropicDirect &&
!isAnthropicBedrock &&
modelApi === "anthropic-messages" &&
hasExplicitCacheConfig;
const family = resolveAnthropicCacheRetentionFamily({
provider,
modelApi,
modelId,
hasExplicitCacheConfig,
});
if (!isAnthropicDirect && !isAnthropicBedrock && !isCustomAnthropicApi) {
if (!family) {
return undefined;
}
@@ -32,5 +34,5 @@ export function resolveCacheRetention(
return "long";
}
return isAnthropicDirect ? "short" : undefined;
return family === "anthropic-direct" ? "short" : undefined;
}

View File

@@ -0,0 +1,46 @@
type AnthropicCacheRetentionFamily =
| "anthropic-direct"
| "anthropic-bedrock"
| "custom-anthropic-api";
export function isAnthropicModelRef(modelId: string): boolean {
return modelId.trim().toLowerCase().startsWith("anthropic/");
}
export function isAnthropicBedrockModel(modelId: string): boolean {
const normalized = modelId.trim().toLowerCase();
return normalized.includes("anthropic.claude") || normalized.includes("anthropic/claude");
}
export function isOpenRouterAnthropicModelRef(provider: string, modelId: string): boolean {
return provider.trim().toLowerCase() === "openrouter" && isAnthropicModelRef(modelId);
}
export function resolveAnthropicCacheRetentionFamily(params: {
provider: string;
modelApi?: string;
modelId?: string;
hasExplicitCacheConfig: boolean;
}): AnthropicCacheRetentionFamily | undefined {
const normalizedProvider = params.provider.trim().toLowerCase();
if (normalizedProvider === "anthropic") {
return "anthropic-direct";
}
if (
normalizedProvider === "amazon-bedrock" &&
params.hasExplicitCacheConfig &&
typeof params.modelId === "string" &&
isAnthropicBedrockModel(params.modelId)
) {
return "anthropic-bedrock";
}
if (
normalizedProvider !== "anthropic" &&
normalizedProvider !== "amazon-bedrock" &&
params.hasExplicitCacheConfig &&
params.modelApi === "anthropic-messages"
) {
return "custom-anthropic-api";
}
return undefined;
}

View File

@@ -1,5 +1,6 @@
import type { StreamFn } from "@mariozechner/pi-agent-core";
import { streamSimple } from "@mariozechner/pi-ai";
import { isAnthropicBedrockModel } from "./anthropic-family-cache-semantics.js";
export function createBedrockNoCacheWrapper(baseStreamFn: StreamFn | undefined): StreamFn {
const underlying = baseStreamFn ?? streamSimple;
@@ -10,7 +11,4 @@ export function createBedrockNoCacheWrapper(baseStreamFn: StreamFn | undefined):
});
}
export function isAnthropicBedrockModel(modelId: string): boolean {
const normalized = modelId.toLowerCase();
return normalized.includes("anthropic.claude") || normalized.includes("anthropic/claude");
}
export { isAnthropicBedrockModel };

View File

@@ -2,6 +2,7 @@ import type { StreamFn } from "@mariozechner/pi-agent-core";
import { describe, expect, it, vi } from "vitest";
import { applyExtraParamsToAgent } from "../pi-embedded-runner.js";
import { resolveCacheRetention } from "./anthropic-cache-retention.js";
import { isOpenRouterAnthropicModelRef } from "./anthropic-family-cache-semantics.js";
function applyAndExpectWrapped(params: {
cfg?: Parameters<typeof applyExtraParamsToAgent>[1];
@@ -214,4 +215,34 @@ describe("cacheRetention default behavior", () => {
resolveCacheRetention({ cacheRetention: "short" }, "litellm", "anthropic-messages"),
).toBe("short");
});
it("does not treat non-Anthropic Bedrock models as cache-retention eligible", () => {
expect(
resolveCacheRetention(
{ cacheRetention: "long" },
"amazon-bedrock",
"openai-completions",
"amazon.nova-micro-v1:0",
),
).toBeUndefined();
});
it("keeps explicit cacheRetention for Anthropic Bedrock models", () => {
expect(
resolveCacheRetention(
{ cacheRetention: "long" },
"amazon-bedrock",
"openai-completions",
"us.anthropic.claude-sonnet-4-5",
),
).toBe("long");
});
});
describe("anthropic-family cache semantics", () => {
it("classifies OpenRouter Anthropic model refs centrally", () => {
expect(isOpenRouterAnthropicModelRef("openrouter", "anthropic/claude-opus-4-6")).toBe(true);
expect(isOpenRouterAnthropicModelRef("openrouter", "google/gemini-2.5-pro")).toBe(false);
expect(isOpenRouterAnthropicModelRef("OpenRouter", "Anthropic/Claude-Sonnet-4")).toBe(true);
});
});

View File

@@ -210,6 +210,7 @@ function createStreamFnWithExtraParams(
extraParams,
provider,
typeof model?.api === "string" ? model.api : undefined,
typeof model?.id === "string" ? model.id : undefined,
);
if (cacheRetention) {
streamParams.cacheRetention = cacheRetention;

View File

@@ -2,6 +2,7 @@ import type { StreamFn } from "@mariozechner/pi-agent-core";
import { streamSimple } from "@mariozechner/pi-ai";
import type { ThinkLevel } from "../../auto-reply/thinking.js";
import { resolveProviderRequestPolicyConfig } from "../provider-request-config.js";
import { isOpenRouterAnthropicModelRef } from "./anthropic-family-cache-semantics.js";
import { streamWithPayloadPatch } from "./stream-payload-utils.js";
const KILOCODE_FEATURE_HEADER = "X-KILOCODE-FEATURE";
const KILOCODE_FEATURE_DEFAULT = "openclaw";
@@ -12,10 +13,6 @@ function resolveKilocodeAppHeaders(): Record<string, string> {
return { [KILOCODE_FEATURE_HEADER]: feature };
}
function isOpenRouterAnthropicModel(provider: string, modelId: string): boolean {
return provider.toLowerCase() === "openrouter" && modelId.toLowerCase().startsWith("anthropic/");
}
function mapThinkingLevelToOpenRouterReasoningEffort(
thinkingLevel: ThinkLevel,
): "none" | "minimal" | "low" | "medium" | "high" | "xhigh" {
@@ -62,7 +59,7 @@ export function createOpenRouterSystemCacheWrapper(baseStreamFn: StreamFn | unde
if (
typeof model.provider !== "string" ||
typeof model.id !== "string" ||
!isOpenRouterAnthropicModel(model.provider, model.id)
!isOpenRouterAnthropicModelRef(model.provider, model.id)
) {
return underlying(model, context, options);
}