fix(providers): align cache-ttl anthropic semantics (#60375)

This commit is contained in:
Vincent Koc 2026-04-04 00:22:32 +09:00 committed by GitHub
parent af835acd00
commit ed297eb8b9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 75 additions and 8 deletions

View File

@ -16,6 +16,21 @@ export function isOpenRouterAnthropicModelRef(provider: string, modelId: string)
return provider.trim().toLowerCase() === "openrouter" && isAnthropicModelRef(modelId);
}
export function isAnthropicFamilyCacheTtlEligible(params: {
provider: string;
modelApi?: string;
modelId: string;
}): boolean {
const normalizedProvider = params.provider.trim().toLowerCase();
if (normalizedProvider === "anthropic") {
return true;
}
if (normalizedProvider === "amazon-bedrock") {
return isAnthropicBedrockModel(params.modelId);
}
return params.modelApi === "anthropic-messages";
}
export function resolveAnthropicCacheRetentionFamily(params: {
provider: string;
modelApi?: string;
@ -35,7 +50,6 @@ export function resolveAnthropicCacheRetentionFamily(params: {
return "anthropic-bedrock";
}
if (
normalizedProvider !== "anthropic" &&
normalizedProvider !== "amazon-bedrock" &&
params.hasExplicitCacheConfig &&
params.modelApi === "anthropic-messages"

View File

@ -2,7 +2,7 @@ import { describe, expect, it, vi } from "vitest";
vi.mock("../../plugins/provider-runtime.js", () => ({
resolveProviderCacheTtlEligibility: (params: {
context: { provider: string; modelId: string };
context: { provider: string; modelId: string; modelApi?: string };
}) => {
if (params.context.provider === "anthropic") {
return true;
@ -47,4 +47,20 @@ describe("isCacheTtlEligibleProvider", () => {
expect(isCacheTtlEligibleProvider("openai", "gpt-4o")).toBe(false);
expect(isCacheTtlEligibleProvider("openrouter", "openai/gpt-4o")).toBe(false);
});
it("allows custom anthropic-messages providers", () => {
expect(isCacheTtlEligibleProvider("litellm", "claude-sonnet-4-6", "anthropic-messages")).toBe(
true,
);
});
it("allows anthropic Bedrock models", () => {
expect(
isCacheTtlEligibleProvider(
"amazon-bedrock",
"us.anthropic.claude-sonnet-4-20250514-v1:0",
"anthropic-messages",
),
).toBe(true);
});
});

View File

@ -1,4 +1,5 @@
import { resolveProviderCacheTtlEligibility } from "../../plugins/provider-runtime.js";
import { isAnthropicFamilyCacheTtlEligible } from "./anthropic-family-cache-semantics.js";
type CustomEntryLike = { type?: unknown; customType?: unknown; data?: unknown };
@ -10,7 +11,11 @@ export type CacheTtlEntryData = {
modelId?: string;
};
export function isCacheTtlEligibleProvider(provider: string, modelId: string): boolean {
export function isCacheTtlEligibleProvider(
provider: string,
modelId: string,
modelApi?: string,
): boolean {
const normalizedProvider = provider.toLowerCase();
const normalizedModelId = modelId.toLowerCase();
const pluginEligibility = resolveProviderCacheTtlEligibility({
@ -18,12 +23,17 @@ export function isCacheTtlEligibleProvider(provider: string, modelId: string): b
context: {
provider: normalizedProvider,
modelId: normalizedModelId,
modelApi,
},
});
if (pluginEligibility !== undefined) {
return pluginEligibility;
}
return false;
return isAnthropicFamilyCacheTtlEligible({
provider: normalizedProvider,
modelId: normalizedModelId,
modelApi,
});
}
export function readLastCacheTtlTimestamp(sessionManager: unknown): number | null {

View File

@ -4,6 +4,7 @@ import { describe, expect, it } from "vitest";
import type { OpenClawConfig } from "../../config/config.js";
import { getCompactionSafeguardRuntime } from "../pi-hooks/compaction-safeguard-runtime.js";
import compactionSafeguardExtension from "../pi-hooks/compaction-safeguard.js";
import contextPruningExtension from "../pi-hooks/context-pruning.js";
import { buildEmbeddedExtensionFactories } from "./extensions.js";
function buildSafeguardFactories(cfg: OpenClawConfig) {
@ -69,4 +70,24 @@ describe("buildEmbeddedExtensionFactories", () => {
qualityGuardMaxRetries: 2,
});
});
it("enables cache-ttl pruning for custom anthropic-messages providers", () => {
const factories = buildEmbeddedExtensionFactories({
cfg: {
agents: {
defaults: {
contextPruning: {
mode: "cache-ttl",
},
},
},
} as OpenClawConfig,
sessionManager: {} as SessionManager,
provider: "litellm",
modelId: "claude-sonnet-4-6",
model: { api: "anthropic-messages", contextWindow: 200_000 } as Model<Api>,
});
expect(factories).toContain(contextPruningExtension);
});
});

View File

@ -39,7 +39,7 @@ function buildContextPruningFactory(params: {
if (raw?.mode !== "cache-ttl") {
return undefined;
}
if (!isCacheTtlEligibleProvider(params.provider, params.modelId)) {
if (!isCacheTtlEligibleProvider(params.provider, params.modelId, params.model?.api)) {
return undefined;
}

View File

@ -24,6 +24,7 @@ describe("runEmbeddedAttempt cache-ttl tracking after compaction", () => {
},
provider: "anthropic",
modelId: "claude-sonnet-4-20250514",
modelApi: "anthropic-messages",
isCacheTtlEligibleProvider: () => true,
now: 123,
});
@ -54,6 +55,7 @@ describe("runEmbeddedAttempt cache-ttl tracking after compaction", () => {
},
provider: "anthropic",
modelId: "claude-sonnet-4-20250514",
modelApi: "anthropic-messages",
isCacheTtlEligibleProvider: () => true,
now: 123,
});

View File

@ -47,14 +47,15 @@ export function shouldAppendAttemptCacheTtl(params: {
config?: OpenClawConfig;
provider: string;
modelId: string;
isCacheTtlEligibleProvider: (provider: string, modelId: string) => boolean;
modelApi?: string;
isCacheTtlEligibleProvider: (provider: string, modelId: string, modelApi?: string) => boolean;
}): boolean {
if (params.timedOutDuringCompaction || params.compactionOccurredThisAttempt) {
return false;
}
return (
params.config?.agents?.defaults?.contextPruning?.mode === "cache-ttl" &&
params.isCacheTtlEligibleProvider(params.provider, params.modelId)
params.isCacheTtlEligibleProvider(params.provider, params.modelId, params.modelApi)
);
}
@ -67,7 +68,8 @@ export function appendAttemptCacheTtlIfNeeded(params: {
config?: OpenClawConfig;
provider: string;
modelId: string;
isCacheTtlEligibleProvider: (provider: string, modelId: string) => boolean;
modelApi?: string;
isCacheTtlEligibleProvider: (provider: string, modelId: string, modelApi?: string) => boolean;
now?: number;
}): boolean {
if (!shouldAppendAttemptCacheTtl(params)) {

View File

@ -1722,6 +1722,7 @@ export async function runEmbeddedAttempt(
config: params.config,
provider: params.provider,
modelId: params.modelId,
modelApi: params.model.api,
isCacheTtlEligibleProvider,
});

View File

@ -730,6 +730,7 @@ export type ProviderCreateEmbeddingProviderContext = {
export type ProviderCacheTtlEligibilityContext = {
provider: string;
modelId: string;
modelApi?: string;
};
/**