diff --git a/src/agents/pi-embedded-runner/anthropic-cache-retention.ts b/src/agents/pi-embedded-runner/anthropic-cache-retention.ts index bed45f2cf2a..a22173e7f44 100644 --- a/src/agents/pi-embedded-runner/anthropic-cache-retention.ts +++ b/src/agents/pi-embedded-runner/anthropic-cache-retention.ts @@ -1,21 +1,23 @@ +import { resolveAnthropicCacheRetentionFamily } from "./anthropic-family-cache-semantics.js"; + type CacheRetention = "none" | "short" | "long"; export function resolveCacheRetention( extraParams: Record | 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; } diff --git a/src/agents/pi-embedded-runner/anthropic-family-cache-semantics.ts b/src/agents/pi-embedded-runner/anthropic-family-cache-semantics.ts new file mode 100644 index 00000000000..72f86668e06 --- /dev/null +++ b/src/agents/pi-embedded-runner/anthropic-family-cache-semantics.ts @@ -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; +} diff --git a/src/agents/pi-embedded-runner/bedrock-stream-wrappers.ts b/src/agents/pi-embedded-runner/bedrock-stream-wrappers.ts index 3e28ea9d860..97f20daaf2f 100644 --- a/src/agents/pi-embedded-runner/bedrock-stream-wrappers.ts +++ b/src/agents/pi-embedded-runner/bedrock-stream-wrappers.ts @@ -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 }; diff --git a/src/agents/pi-embedded-runner/extra-params.cache-retention-default.test.ts b/src/agents/pi-embedded-runner/extra-params.cache-retention-default.test.ts index b15ece98841..29c821c3cf6 100644 --- a/src/agents/pi-embedded-runner/extra-params.cache-retention-default.test.ts +++ b/src/agents/pi-embedded-runner/extra-params.cache-retention-default.test.ts @@ -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[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); + }); }); diff --git a/src/agents/pi-embedded-runner/extra-params.ts b/src/agents/pi-embedded-runner/extra-params.ts index 952e3089cc8..c8f92fbcb4f 100644 --- a/src/agents/pi-embedded-runner/extra-params.ts +++ b/src/agents/pi-embedded-runner/extra-params.ts @@ -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; diff --git a/src/agents/pi-embedded-runner/proxy-stream-wrappers.ts b/src/agents/pi-embedded-runner/proxy-stream-wrappers.ts index 740d72de3d3..3605993933b 100644 --- a/src/agents/pi-embedded-runner/proxy-stream-wrappers.ts +++ b/src/agents/pi-embedded-runner/proxy-stream-wrappers.ts @@ -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 { 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); }