diff --git a/CHANGELOG.md b/CHANGELOG.md index c349b2e76f3..108a7264867 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/src/agents/pi-embedded-runner/anthropic-cache-retention.ts b/src/agents/pi-embedded-runner/anthropic-cache-retention.ts index 8468cfac0e2..bed45f2cf2a 100644 --- a/src/agents/pi-embedded-runner/anthropic-cache-retention.ts +++ b/src/agents/pi-embedded-runner/anthropic-cache-retention.ts @@ -3,13 +3,19 @@ type CacheRetention = "none" | "short" | "long"; export function resolveCacheRetention( extraParams: Record | 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; } 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 b988a8c3c59..b15ece98841 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 @@ -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[1]; extraParamsOverride?: Parameters[4]; modelId: string; + model?: Parameters[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[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[8], + provider: "litellm", + }); + }); + + it("passes cacheRetention 'short' through for custom anthropic-messages provider", () => { + expect( + resolveCacheRetention({ cacheRetention: "short" }, "litellm", "anthropic-messages"), + ).toBe("short"); + }); }); diff --git a/src/agents/pi-embedded-runner/extra-params.ts b/src/agents/pi-embedded-runner/extra-params.ts index 4e268cc6abe..c61ccc0db24 100644 --- a/src/agents/pi-embedded-runner/extra-params.ts +++ b/src/agents/pi-embedded-runner/extra-params.ts @@ -198,6 +198,7 @@ function createStreamFnWithExtraParams( baseStreamFn: StreamFn | undefined, extraParams: Record | 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) {