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 <steipete@gmail.com>
This commit is contained in:
Peter Lee
2026-07-14 14:37:55 -05:00
committed by GitHub
parent 60f0a83db0
commit f972c69151
5 changed files with 54 additions and 3 deletions

View File

@@ -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:

View File

@@ -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<typeof applyExtraParamsToAgent>[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(

View File

@@ -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;

View File

@@ -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.

View File

@@ -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;
}