Skip to content

Commit f08a1c3

Browse files
authored
fix(providers): scope anthropic-family cache semantics (#60370)
1 parent b50b85a commit f08a1c3

6 files changed

Lines changed: 93 additions & 18 deletions
Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,23 @@
1+
import { resolveAnthropicCacheRetentionFamily } from "./anthropic-family-cache-semantics.js";
2+
13
type CacheRetention = "none" | "short" | "long";
24

35
export function resolveCacheRetention(
46
extraParams: Record<string, unknown> | undefined,
57
provider: string,
68
modelApi?: string,
9+
modelId?: string,
710
): CacheRetention | undefined {
8-
const isAnthropicDirect = provider === "anthropic";
911
const hasExplicitCacheConfig =
1012
extraParams?.cacheRetention !== undefined || extraParams?.cacheControlTtl !== undefined;
11-
const isAnthropicBedrock = provider === "amazon-bedrock" && hasExplicitCacheConfig;
12-
const isCustomAnthropicApi =
13-
!isAnthropicDirect &&
14-
!isAnthropicBedrock &&
15-
modelApi === "anthropic-messages" &&
16-
hasExplicitCacheConfig;
13+
const family = resolveAnthropicCacheRetentionFamily({
14+
provider,
15+
modelApi,
16+
modelId,
17+
hasExplicitCacheConfig,
18+
});
1719

18-
if (!isAnthropicDirect && !isAnthropicBedrock && !isCustomAnthropicApi) {
20+
if (!family) {
1921
return undefined;
2022
}
2123

@@ -32,5 +34,5 @@ export function resolveCacheRetention(
3234
return "long";
3335
}
3436

35-
return isAnthropicDirect ? "short" : undefined;
37+
return family === "anthropic-direct" ? "short" : undefined;
3638
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
type AnthropicCacheRetentionFamily =
2+
| "anthropic-direct"
3+
| "anthropic-bedrock"
4+
| "custom-anthropic-api";
5+
6+
export function isAnthropicModelRef(modelId: string): boolean {
7+
return modelId.trim().toLowerCase().startsWith("anthropic/");
8+
}
9+
10+
export function isAnthropicBedrockModel(modelId: string): boolean {
11+
const normalized = modelId.trim().toLowerCase();
12+
return normalized.includes("anthropic.claude") || normalized.includes("anthropic/claude");
13+
}
14+
15+
export function isOpenRouterAnthropicModelRef(provider: string, modelId: string): boolean {
16+
return provider.trim().toLowerCase() === "openrouter" && isAnthropicModelRef(modelId);
17+
}
18+
19+
export function resolveAnthropicCacheRetentionFamily(params: {
20+
provider: string;
21+
modelApi?: string;
22+
modelId?: string;
23+
hasExplicitCacheConfig: boolean;
24+
}): AnthropicCacheRetentionFamily | undefined {
25+
const normalizedProvider = params.provider.trim().toLowerCase();
26+
if (normalizedProvider === "anthropic") {
27+
return "anthropic-direct";
28+
}
29+
if (
30+
normalizedProvider === "amazon-bedrock" &&
31+
params.hasExplicitCacheConfig &&
32+
typeof params.modelId === "string" &&
33+
isAnthropicBedrockModel(params.modelId)
34+
) {
35+
return "anthropic-bedrock";
36+
}
37+
if (
38+
normalizedProvider !== "anthropic" &&
39+
normalizedProvider !== "amazon-bedrock" &&
40+
params.hasExplicitCacheConfig &&
41+
params.modelApi === "anthropic-messages"
42+
) {
43+
return "custom-anthropic-api";
44+
}
45+
return undefined;
46+
}
Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { StreamFn } from "@mariozechner/pi-agent-core";
22
import { streamSimple } from "@mariozechner/pi-ai";
3+
import { isAnthropicBedrockModel } from "./anthropic-family-cache-semantics.js";
34

45
export function createBedrockNoCacheWrapper(baseStreamFn: StreamFn | undefined): StreamFn {
56
const underlying = baseStreamFn ?? streamSimple;
@@ -10,7 +11,4 @@ export function createBedrockNoCacheWrapper(baseStreamFn: StreamFn | undefined):
1011
});
1112
}
1213

13-
export function isAnthropicBedrockModel(modelId: string): boolean {
14-
const normalized = modelId.toLowerCase();
15-
return normalized.includes("anthropic.claude") || normalized.includes("anthropic/claude");
16-
}
14+
export { isAnthropicBedrockModel };

src/agents/pi-embedded-runner/extra-params.cache-retention-default.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { StreamFn } from "@mariozechner/pi-agent-core";
22
import { describe, expect, it, vi } from "vitest";
33
import { applyExtraParamsToAgent } from "../pi-embedded-runner.js";
44
import { resolveCacheRetention } from "./anthropic-cache-retention.js";
5+
import { isOpenRouterAnthropicModelRef } from "./anthropic-family-cache-semantics.js";
56

67
function applyAndExpectWrapped(params: {
78
cfg?: Parameters<typeof applyExtraParamsToAgent>[1];
@@ -214,4 +215,34 @@ describe("cacheRetention default behavior", () => {
214215
resolveCacheRetention({ cacheRetention: "short" }, "litellm", "anthropic-messages"),
215216
).toBe("short");
216217
});
218+
219+
it("does not treat non-Anthropic Bedrock models as cache-retention eligible", () => {
220+
expect(
221+
resolveCacheRetention(
222+
{ cacheRetention: "long" },
223+
"amazon-bedrock",
224+
"openai-completions",
225+
"amazon.nova-micro-v1:0",
226+
),
227+
).toBeUndefined();
228+
});
229+
230+
it("keeps explicit cacheRetention for Anthropic Bedrock models", () => {
231+
expect(
232+
resolveCacheRetention(
233+
{ cacheRetention: "long" },
234+
"amazon-bedrock",
235+
"openai-completions",
236+
"us.anthropic.claude-sonnet-4-5",
237+
),
238+
).toBe("long");
239+
});
240+
});
241+
242+
describe("anthropic-family cache semantics", () => {
243+
it("classifies OpenRouter Anthropic model refs centrally", () => {
244+
expect(isOpenRouterAnthropicModelRef("openrouter", "anthropic/claude-opus-4-6")).toBe(true);
245+
expect(isOpenRouterAnthropicModelRef("openrouter", "google/gemini-2.5-pro")).toBe(false);
246+
expect(isOpenRouterAnthropicModelRef("OpenRouter", "Anthropic/Claude-Sonnet-4")).toBe(true);
247+
});
217248
});

src/agents/pi-embedded-runner/extra-params.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,7 @@ function createStreamFnWithExtraParams(
210210
extraParams,
211211
provider,
212212
typeof model?.api === "string" ? model.api : undefined,
213+
typeof model?.id === "string" ? model.id : undefined,
213214
);
214215
if (cacheRetention) {
215216
streamParams.cacheRetention = cacheRetention;

src/agents/pi-embedded-runner/proxy-stream-wrappers.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { StreamFn } from "@mariozechner/pi-agent-core";
22
import { streamSimple } from "@mariozechner/pi-ai";
33
import type { ThinkLevel } from "../../auto-reply/thinking.js";
44
import { resolveProviderRequestPolicyConfig } from "../provider-request-config.js";
5+
import { isOpenRouterAnthropicModelRef } from "./anthropic-family-cache-semantics.js";
56
import { streamWithPayloadPatch } from "./stream-payload-utils.js";
67
const KILOCODE_FEATURE_HEADER = "X-KILOCODE-FEATURE";
78
const KILOCODE_FEATURE_DEFAULT = "openclaw";
@@ -12,10 +13,6 @@ function resolveKilocodeAppHeaders(): Record<string, string> {
1213
return { [KILOCODE_FEATURE_HEADER]: feature };
1314
}
1415

15-
function isOpenRouterAnthropicModel(provider: string, modelId: string): boolean {
16-
return provider.toLowerCase() === "openrouter" && modelId.toLowerCase().startsWith("anthropic/");
17-
}
18-
1916
function mapThinkingLevelToOpenRouterReasoningEffort(
2017
thinkingLevel: ThinkLevel,
2118
): "none" | "minimal" | "low" | "medium" | "high" | "xhigh" {
@@ -62,7 +59,7 @@ export function createOpenRouterSystemCacheWrapper(baseStreamFn: StreamFn | unde
6259
if (
6360
typeof model.provider !== "string" ||
6461
typeof model.id !== "string" ||
65-
!isOpenRouterAnthropicModel(model.provider, model.id)
62+
!isOpenRouterAnthropicModelRef(model.provider, model.id)
6663
) {
6764
return underlying(model, context, options);
6865
}

0 commit comments

Comments
 (0)