fix(agents): honor cacheRetention for custom anthropic providers (#59049)

* fix(agents): honor cacheRetention for custom anthropic providers

* docs(changelog): add cache retention entry

* Update CHANGELOG.md

* test(agents): add direct cache retention assertions
This commit is contained in:
Vincent Koc 2026-04-02 14:34:01 +09:00 committed by GitHub
parent 41aac73590
commit ed6012eb5b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 83 additions and 4 deletions

View File

@ -100,6 +100,7 @@ Docs: https://docs.openclaw.ai
- ACPX/runtime: repair `queue owner unavailable` session recovery by replacing dead named sessions and resuming the backend session when ACPX exposes a stable session id, so the first ACP prompt no longer inherits a dead handle. (#58669) Thanks @neeravmakwana
- ACPX/runtime: retry dead-session queue-owner repair without `--resume-session` when the reported ACPX session id is stale, so recovery still creates a fresh named session instead of failing session init. Thanks @obviyus.
- Auth/OpenAI Codex: persist plugin-refreshed OAuth credentials to `auth-profiles.json` before returning them, so rotated Codex refresh tokens survive restart and stop falling into `refresh_token_reused` loops. (#53082)
- Agents/Anthropic: honor explicit `cacheRetention` for custom providers using `anthropic-messages`, so Anthropic-compatible proxy providers can reuse prompt caching when they opt in. (#59049) Thanks @wwerst and @vincentkoc.
- Discord/gateway: hand reconnect ownership back to Carbon, keep runtime status aligned with close/reconnect state, and force-stop sockets that open without reaching READY so Discord monitors recover promptly instead of waiting on stale health timeouts. (#59019) Thanks @obviyus
- Config/Telegram: migrate removed `channels.telegram.groupMentionsOnly` into `channels.telegram.groups["*"].requireMention` on load so legacy configs no longer crash at startup. (#55336) thanks @jameslcowan.

View File

@ -3,13 +3,19 @@ type CacheRetention = "none" | "short" | "long";
export function resolveCacheRetention(
extraParams: Record<string, unknown> | undefined,
provider: string,
modelApi?: string,
): CacheRetention | undefined {
const isAnthropicDirect = provider === "anthropic";
const hasBedrockOverride =
const hasExplicitCacheConfig =
extraParams?.cacheRetention !== undefined || extraParams?.cacheControlTtl !== undefined;
const isAnthropicBedrock = provider === "amazon-bedrock" && hasBedrockOverride;
const isAnthropicBedrock = provider === "amazon-bedrock" && hasExplicitCacheConfig;
const isCustomAnthropicApi =
!isAnthropicDirect &&
!isAnthropicBedrock &&
modelApi === "anthropic-messages" &&
hasExplicitCacheConfig;
if (!isAnthropicDirect && !isAnthropicBedrock) {
if (!isAnthropicDirect && !isAnthropicBedrock && !isCustomAnthropicApi) {
return undefined;
}

View File

@ -1,11 +1,13 @@
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";
function applyAndExpectWrapped(params: {
cfg?: Parameters<typeof applyExtraParamsToAgent>[1];
extraParamsOverride?: Parameters<typeof applyExtraParamsToAgent>[4];
modelId: string;
model?: Parameters<typeof applyExtraParamsToAgent>[8];
provider: string;
}) {
const agent: { streamFn?: StreamFn } = {};
@ -16,6 +18,10 @@ function applyAndExpectWrapped(params: {
params.provider,
params.modelId,
params.extraParamsOverride,
undefined,
undefined,
undefined,
params.model,
);
expect(agent.streamFn).toBeDefined();
@ -144,4 +150,68 @@ describe("cacheRetention default behavior", () => {
provider: "anthropic",
});
});
it("respects cacheRetention for custom provider with anthropic-messages API", () => {
applyAndExpectWrapped({
cfg: {
agents: {
defaults: {
models: {
"litellm/claude-sonnet-4-6": {
params: {
cacheRetention: "long" as const,
},
},
},
},
},
},
modelId: "claude-sonnet-4-6",
model: { api: "anthropic-messages" } as Parameters<typeof applyExtraParamsToAgent>[8],
provider: "litellm",
});
});
it("passes cacheRetention 'long' through for custom anthropic-messages provider", () => {
expect(resolveCacheRetention({ cacheRetention: "long" }, "litellm", "anthropic-messages")).toBe(
"long",
);
});
it("does not default to caching for custom provider without explicit config", () => {
expect(resolveCacheRetention(undefined, "litellm", "anthropic-messages")).toBeUndefined();
});
it("passes cacheRetention 'none' through for custom anthropic-messages provider", () => {
expect(resolveCacheRetention({ cacheRetention: "none" }, "litellm", "anthropic-messages")).toBe(
"none",
);
});
it("respects cacheRetention 'short' for custom anthropic-messages provider", () => {
applyAndExpectWrapped({
cfg: {
agents: {
defaults: {
models: {
"litellm/claude-opus-4-6": {
params: {
cacheRetention: "short" as const,
},
},
},
},
},
},
modelId: "claude-opus-4-6",
model: { api: "anthropic-messages" } as Parameters<typeof applyExtraParamsToAgent>[8],
provider: "litellm",
});
});
it("passes cacheRetention 'short' through for custom anthropic-messages provider", () => {
expect(
resolveCacheRetention({ cacheRetention: "short" }, "litellm", "anthropic-messages"),
).toBe("short");
});
});

View File

@ -198,6 +198,7 @@ function createStreamFnWithExtraParams(
baseStreamFn: StreamFn | undefined,
extraParams: Record<string, unknown> | undefined,
provider: string,
modelApi?: string,
): StreamFn | undefined {
if (!extraParams || Object.keys(extraParams).length === 0) {
return undefined;
@ -223,7 +224,7 @@ function createStreamFnWithExtraParams(
if (typeof extraParams.openaiWsWarmup === "boolean") {
streamParams.openaiWsWarmup = extraParams.openaiWsWarmup;
}
const cacheRetention = resolveCacheRetention(extraParams, provider);
const cacheRetention = resolveCacheRetention(extraParams, provider, modelApi);
if (cacheRetention) {
streamParams.cacheRetention = cacheRetention;
}
@ -316,6 +317,7 @@ function applyPrePluginStreamWrappers(ctx: ApplyExtraParamsContext): void {
ctx.agent.streamFn,
ctx.effectiveExtraParams,
ctx.provider,
ctx.model?.api,
);
if (wrappedStreamFn) {