From f972c69151ea689cf2183cdf1d6bdb2f15c773ff Mon Sep 17 00:00:00 2001
From: Peter Lee
Date: Tue, 14 Jul 2026 14:37:55 -0500
Subject: [PATCH] fix(agents): validate cacheRetention values (#94494)
* fix(agents): map cacheRetention 'standard' to 'short' for Bedrock Claude models
* fix(agents): scope standard cacheRetention alias to Anthropic cache family only
* fix(agents): validate prompt cache retention
* chore: keep changelog release-owned
---------
Co-authored-by: Peter Steinberger
---
docs/reference/prompt-caching.md | 1 +
...tra-params.cache-retention-default.test.ts | 26 +++++++++++++++++++
.../embedded-agent-runner/extra-params.ts | 11 +++++++-
.../prompt-cache-retention.test.ts | 11 ++++++++
.../prompt-cache-retention.ts | 8 ++++--
5 files changed, 54 insertions(+), 3 deletions(-)
diff --git a/docs/reference/prompt-caching.md b/docs/reference/prompt-caching.md
index f4c784bde731..f792298a0f8a 100644
--- a/docs/reference/prompt-caching.md
+++ b/docs/reference/prompt-caching.md
@@ -21,6 +21,7 @@ Provider references:
### `cacheRetention`
Values: `"none" | "short" | "long"`. Configurable as a global default, per model, and per agent.
+`"standard"` is not an alias; use `"short"` for the provider's default cache window. Invalid values are ignored with a warning.
```yaml
agents:
diff --git a/src/agents/embedded-agent-runner/extra-params.cache-retention-default.test.ts b/src/agents/embedded-agent-runner/extra-params.cache-retention-default.test.ts
index 93ec35d468cc..07d4e86894d6 100644
--- a/src/agents/embedded-agent-runner/extra-params.cache-retention-default.test.ts
+++ b/src/agents/embedded-agent-runner/extra-params.cache-retention-default.test.ts
@@ -3,6 +3,7 @@ import type { StreamFn } from "openclaw/plugin-sdk/agent-core";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { createLlmStreamSimpleMock } from "../../../test/helpers/agents/llm-stream-simple-mock.js";
import { testing as extraParamsTesting, applyExtraParamsToAgent } from "./extra-params.js";
+import { log } from "./logger.js";
import { resolveCacheRetention } from "./prompt-cache-retention.js";
function applyAndExpectWrapped(params: {
@@ -44,6 +45,7 @@ vi.mock("./logger.js", () => ({
vi.mock("../../llm/stream.js", () => createLlmStreamSimpleMock());
beforeEach(() => {
+ vi.mocked(log.warn).mockClear();
extraParamsTesting.setProviderRuntimeDepsForTest({
prepareProviderExtraParams: () => undefined,
resolveProviderExtraParamsForTransport: () => undefined,
@@ -257,6 +259,30 @@ describe("cacheRetention default behavior", () => {
).toBe("long");
});
+ it("warns instead of creating an undocumented cacheRetention alias", () => {
+ applyAndExpectWrapped({
+ cfg: {
+ agents: {
+ defaults: {
+ models: {
+ "amazon-bedrock/us.anthropic.claude-sonnet-4-6": {
+ params: { cacheRetention: "standard" },
+ },
+ },
+ },
+ },
+ },
+ modelId: "us.anthropic.claude-sonnet-4-6",
+ model: { api: "openai-completions" } as Parameters[8],
+ provider: "amazon-bedrock",
+ });
+
+ expect(log.warn).toHaveBeenCalledOnce();
+ expect(log.warn).toHaveBeenCalledWith(
+ 'ignoring invalid cacheRetention param; expected "none", "short", or "long"',
+ );
+ });
+
it("defaults to 'short' for anthropic-vertex without explicit config", () => {
expect(
resolveCacheRetention(
diff --git a/src/agents/embedded-agent-runner/extra-params.ts b/src/agents/embedded-agent-runner/extra-params.ts
index 0b217d82f15a..6b9dc44bd2d6 100644
--- a/src/agents/embedded-agent-runner/extra-params.ts
+++ b/src/agents/embedded-agent-runner/extra-params.ts
@@ -42,7 +42,7 @@ import type { AgentRuntimeTransport } from "../runtime-plan/types.js";
import type { StreamFn } from "../runtime/index.js";
import type { SettingsManager } from "../sessions/index.js";
import { log } from "./logger.js";
-import { resolveCacheRetention } from "./prompt-cache-retention.js";
+import { parseCacheRetention, resolveCacheRetention } from "./prompt-cache-retention.js";
import type { ProviderThinkLevel } from "./utils.js";
const defaultProviderRuntimeDeps = {
@@ -452,6 +452,15 @@ function createStreamFnWithExtraParams(
return undefined;
}
+ if (
+ Object.hasOwn(extraParams, "cacheRetention") &&
+ parseCacheRetention(extraParams.cacheRetention) === undefined
+ ) {
+ // Provider params stay open-ended, so validate this shared knob at its consumer boundary.
+ // Never echo the authored value: model params can contain sensitive custom data.
+ log.warn('ignoring invalid cacheRetention param; expected "none", "short", or "long"');
+ }
+
const streamParams: CacheRetentionStreamOptions = {};
if (typeof extraParams.temperature === "number") {
streamParams.temperature = extraParams.temperature;
diff --git a/src/agents/embedded-agent-runner/prompt-cache-retention.test.ts b/src/agents/embedded-agent-runner/prompt-cache-retention.test.ts
index 11d1490525bb..dde8018ff7f2 100644
--- a/src/agents/embedded-agent-runner/prompt-cache-retention.test.ts
+++ b/src/agents/embedded-agent-runner/prompt-cache-retention.test.ts
@@ -63,6 +63,17 @@ describe("prompt cache retention", () => {
).toBe("none");
});
+ it("keeps undocumented cacheRetention values outside the Bedrock runtime contract", () => {
+ expect(
+ resolveCacheRetention(
+ { cacheRetention: "standard" },
+ "amazon-bedrock",
+ "openai-completions",
+ "us.anthropic.claude-sonnet-4-6",
+ ),
+ ).toBeUndefined();
+ });
+
it("does not honor explicit cacheRetention for openai-completions without supportsPromptCacheKey", () => {
// Providers that route via openai-completions but do not advertise prompt
// caching must keep retention out of outgoing payloads.
diff --git a/src/agents/embedded-agent-runner/prompt-cache-retention.ts b/src/agents/embedded-agent-runner/prompt-cache-retention.ts
index 1bcbbc246a49..760f43347e40 100644
--- a/src/agents/embedded-agent-runner/prompt-cache-retention.ts
+++ b/src/agents/embedded-agent-runner/prompt-cache-retention.ts
@@ -6,6 +6,10 @@ import { resolveAnthropicCacheRetentionFamily } from "../../llm/providers/stream
type CacheRetention = "none" | "short" | "long";
+export function parseCacheRetention(value: unknown): CacheRetention | undefined {
+ return value === "none" || value === "short" || value === "long" ? value : undefined;
+}
+
export function isGooglePromptCacheEligible(params: {
modelApi?: string;
modelId?: string;
@@ -46,8 +50,8 @@ export function resolveCacheRetention(
return undefined;
}
- const newVal = extraParams?.cacheRetention;
- if (newVal === "none" || newVal === "short" || newVal === "long") {
+ const newVal = parseCacheRetention(extraParams?.cacheRetention);
+ if (newVal) {
return newVal;
}