From ed297eb8b970c96ffdb983c2c954172e84a32f04 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sat, 4 Apr 2026 00:22:32 +0900 Subject: [PATCH] fix(providers): align cache-ttl anthropic semantics (#60375) --- .../anthropic-family-cache-semantics.ts | 16 +++++++++++++- .../pi-embedded-runner/cache-ttl.test.ts | 18 +++++++++++++++- src/agents/pi-embedded-runner/cache-ttl.ts | 14 +++++++++++-- .../pi-embedded-runner/extensions.test.ts | 21 +++++++++++++++++++ src/agents/pi-embedded-runner/extensions.ts | 2 +- .../attempt.spawn-workspace.cache-ttl.test.ts | 2 ++ .../run/attempt.thread-helpers.ts | 8 ++++--- src/agents/pi-embedded-runner/run/attempt.ts | 1 + src/plugins/types.ts | 1 + 9 files changed, 75 insertions(+), 8 deletions(-) diff --git a/src/agents/pi-embedded-runner/anthropic-family-cache-semantics.ts b/src/agents/pi-embedded-runner/anthropic-family-cache-semantics.ts index 72f86668e06..fe2def4e102 100644 --- a/src/agents/pi-embedded-runner/anthropic-family-cache-semantics.ts +++ b/src/agents/pi-embedded-runner/anthropic-family-cache-semantics.ts @@ -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" diff --git a/src/agents/pi-embedded-runner/cache-ttl.test.ts b/src/agents/pi-embedded-runner/cache-ttl.test.ts index f5ff8be2827..ad00348ba27 100644 --- a/src/agents/pi-embedded-runner/cache-ttl.test.ts +++ b/src/agents/pi-embedded-runner/cache-ttl.test.ts @@ -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); + }); }); diff --git a/src/agents/pi-embedded-runner/cache-ttl.ts b/src/agents/pi-embedded-runner/cache-ttl.ts index dfea1c714b9..1db3f9dd7f9 100644 --- a/src/agents/pi-embedded-runner/cache-ttl.ts +++ b/src/agents/pi-embedded-runner/cache-ttl.ts @@ -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 { diff --git a/src/agents/pi-embedded-runner/extensions.test.ts b/src/agents/pi-embedded-runner/extensions.test.ts index 605816ae56b..f0288de911b 100644 --- a/src/agents/pi-embedded-runner/extensions.test.ts +++ b/src/agents/pi-embedded-runner/extensions.test.ts @@ -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, + }); + + expect(factories).toContain(contextPruningExtension); + }); }); diff --git a/src/agents/pi-embedded-runner/extensions.ts b/src/agents/pi-embedded-runner/extensions.ts index cc8e1caadc8..1b671b36b19 100644 --- a/src/agents/pi-embedded-runner/extensions.ts +++ b/src/agents/pi-embedded-runner/extensions.ts @@ -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; } diff --git a/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.cache-ttl.test.ts b/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.cache-ttl.test.ts index 0447b766fac..37cc74ae66f 100644 --- a/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.cache-ttl.test.ts +++ b/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.cache-ttl.test.ts @@ -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, }); diff --git a/src/agents/pi-embedded-runner/run/attempt.thread-helpers.ts b/src/agents/pi-embedded-runner/run/attempt.thread-helpers.ts index 5cba64734fd..94e76ad9d66 100644 --- a/src/agents/pi-embedded-runner/run/attempt.thread-helpers.ts +++ b/src/agents/pi-embedded-runner/run/attempt.thread-helpers.ts @@ -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)) { diff --git a/src/agents/pi-embedded-runner/run/attempt.ts b/src/agents/pi-embedded-runner/run/attempt.ts index 4543aac0947..bcbc9b0c66d 100644 --- a/src/agents/pi-embedded-runner/run/attempt.ts +++ b/src/agents/pi-embedded-runner/run/attempt.ts @@ -1722,6 +1722,7 @@ export async function runEmbeddedAttempt( config: params.config, provider: params.provider, modelId: params.modelId, + modelApi: params.model.api, isCacheTtlEligibleProvider, }); diff --git a/src/plugins/types.ts b/src/plugins/types.ts index 3073fc781cb..41d13a40c52 100644 --- a/src/plugins/types.ts +++ b/src/plugins/types.ts @@ -730,6 +730,7 @@ export type ProviderCreateEmbeddingProviderContext = { export type ProviderCacheTtlEligibilityContext = { provider: string; modelId: string; + modelApi?: string; }; /**