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