diff --git a/CHANGELOG.md b/CHANGELOG.md index c017b32938b..58e5a0055c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -75,6 +75,7 @@ Docs: https://docs.openclaw.ai - Plugins/marketplace: block remote marketplace symlink escapes without breaking ordinary local marketplace install paths. (#60556) Thanks @eleqtrizit. - Plugins/install: preserve unsafe override flags across linked plugin and hook-pack probes so local `--link` installs honor the documented override behavior. (#60624) Thanks @JerrettDavis. - Config/All Settings: keep the raw config view intact when sensitive fields are blank instead of corrupting or dropping the rendered snapshot. (#28214) Thanks @solodmd. +- Google/cache: pass explicit `cachedContent` handles through direct Google transport params and agent extra params so prebuilt Gemini context caches can be targeted again. Thanks @vincentkoc. - Security: preserve restrictive plugin-only tool allowlists, require owner access for `/allowlist add` and `/allowlist remove`, fail closed when `before_tool_call` hooks crash, block browser SSRF redirect bypasses earlier, and keep non-interactive auth-choice inference scoped to bundled and already-trusted plugins. (#58476, #59836, #59822, #58771, #59120) - Exec approvals: reuse durable exact-command `allow-always` approvals in allowlist mode so identical reruns stop prompting, and tighten Windows interpreter/path approval handling so wrapper and malformed-path cases fail closed more consistently. (#59880, #59780, #58040, #59182) - Agents/runtime: make default subagent allowlists, inherited skills/workspaces, and duplicate session-id resolution behave more predictably, and include value-shape hints in missing-parameter tool errors. (#59944, #59992, #59858, #55317) diff --git a/src/agents/google-transport-stream.test.ts b/src/agents/google-transport-stream.test.ts index 14cb9869004..e4db42278f5 100644 --- a/src/agents/google-transport-stream.test.ts +++ b/src/agents/google-transport-stream.test.ts @@ -112,6 +112,7 @@ describe("google transport stream", () => { } as unknown as Parameters[1], { apiKey: "gemini-api-key", + cachedContent: "cachedContents/request-cache", reasoning: "medium", toolChoice: "auto", } as Parameters[2], @@ -142,6 +143,7 @@ describe("google transport stream", () => { expect(payload.systemInstruction).toEqual({ parts: [{ text: "Follow policy." }], }); + expect(payload.cachedContent).toBe("cachedContents/request-cache"); expect(payload.generationConfig).toMatchObject({ thinkingConfig: { includeThoughts: true, thinkingLevel: "HIGH" }, }); @@ -246,4 +248,31 @@ describe("google transport stream", () => { thinkingConfig: { thinkingBudget: -1 }, }); }); + + it("includes cachedContent in direct Gemini payloads when requested", () => { + const model = { + id: "gemini-2.5-pro", + name: "Gemini 2.5 Pro", + api: "google-generative-ai", + provider: "google", + baseUrl: "https://generativelanguage.googleapis.com/v1beta", + reasoning: true, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 128000, + maxTokens: 8192, + } satisfies Model<"google-generative-ai">; + + const params = buildGoogleGenerativeAiParams( + model, + { + messages: [{ role: "user", content: "hello", timestamp: 0 }], + } as never, + { + cachedContent: "cachedContents/prebuilt-context", + }, + ); + + expect(params.cachedContent).toBe("cachedContents/prebuilt-context"); + }); }); diff --git a/src/agents/google-transport-stream.ts b/src/agents/google-transport-stream.ts index 20ed9712e77..5c66b722ba8 100644 --- a/src/agents/google-transport-stream.ts +++ b/src/agents/google-transport-stream.ts @@ -30,6 +30,7 @@ type GoogleTransportModel = Model<"google-generative-ai"> & { type GoogleThinkingLevel = "MINIMAL" | "LOW" | "MEDIUM" | "HIGH"; type GoogleTransportOptions = SimpleStreamOptions & { + cachedContent?: string; toolChoice?: | "auto" | "none" @@ -49,6 +50,7 @@ type GoogleTransportOptions = SimpleStreamOptions & { }; type GoogleGenerateContentRequest = { + cachedContent?: string; contents: Array>; generationConfig?: Record; systemInstruction?: Record; @@ -441,6 +443,9 @@ export function buildGoogleGenerativeAiParams( const params: GoogleGenerateContentRequest = { contents: convertGoogleMessages(model, context), }; + if (typeof options?.cachedContent === "string" && options.cachedContent.trim()) { + params.cachedContent = options.cachedContent.trim(); + } if (Object.keys(generationConfig).length > 0) { params.generationConfig = generationConfig; } diff --git a/src/agents/pi-embedded-runner/extra-params.google.test.ts b/src/agents/pi-embedded-runner/extra-params.google.test.ts index 87bcf37ba41..e37ed4a872a 100644 --- a/src/agents/pi-embedded-runner/extra-params.google.test.ts +++ b/src/agents/pi-embedded-runner/extra-params.google.test.ts @@ -37,4 +37,71 @@ describe("extra-params: Google thinking payload compatibility", () => { expect(payload.config?.thinkingConfig?.thinkingBudget).toBeUndefined(); expect(payload.config?.thinkingConfig?.thinkingLevel).toBe("HIGH"); }); + + it("passes cachedContent through Google extra params", () => { + const { options } = runExtraParamsCase({ + cfg: { + agents: { + defaults: { + models: { + "google/gemini-2.5-pro": { + params: { + cachedContent: "cachedContents/test-cache", + }, + }, + }, + }, + }, + } as never, + applyProvider: "google", + applyModelId: "gemini-2.5-pro", + model: { + api: "google-generative-ai", + provider: "google", + id: "gemini-2.5-pro", + } as unknown as Model<"openai-completions">, + payload: { + contents: [], + }, + }); + + expect((options as { cachedContent?: string } | undefined)?.cachedContent).toBe( + "cachedContents/test-cache", + ); + }); + + it("lets higher-precedence cachedContent override lower-precedence cached_content", () => { + const { options } = runExtraParamsCase({ + cfg: { + agents: { + defaults: { + params: { + cached_content: "cachedContents/default-cache", + }, + models: { + "google/gemini-2.5-pro": { + params: { + cachedContent: "cachedContents/model-cache", + }, + }, + }, + }, + }, + } as never, + applyProvider: "google", + applyModelId: "gemini-2.5-pro", + model: { + api: "google-generative-ai", + provider: "google", + id: "gemini-2.5-pro", + } as unknown as Model<"openai-completions">, + payload: { + contents: [], + }, + }); + + expect((options as { cachedContent?: string } | undefined)?.cachedContent).toBe( + "cachedContents/model-cache", + ); + }); }); diff --git a/src/agents/pi-embedded-runner/extra-params.test-support.ts b/src/agents/pi-embedded-runner/extra-params.test-support.ts index 36077789777..3e1dfd1ffbb 100644 --- a/src/agents/pi-embedded-runner/extra-params.test-support.ts +++ b/src/agents/pi-embedded-runner/extra-params.test-support.ts @@ -6,6 +6,7 @@ import { applyExtraParamsToAgent } from "./extra-params.js"; export type ExtraParamsCapture> = { headers?: Record; + options?: SimpleStreamOptions; payload: TPayload; }; @@ -45,6 +46,7 @@ export function runExtraParamsCase< const baseStreamFn: StreamFn = (model, _context, options) => { captured.headers = options?.headers; + captured.options = options; options?.onPayload?.(params.payload, model); return createMockStream(); }; diff --git a/src/agents/pi-embedded-runner/extra-params.ts b/src/agents/pi-embedded-runner/extra-params.ts index 681f231ff6c..f44ed001081 100644 --- a/src/agents/pi-embedded-runner/extra-params.ts +++ b/src/agents/pi-embedded-runner/extra-params.ts @@ -91,11 +91,22 @@ export function resolveExtraParams(params: { delete merged.textVerbosity; } + const resolvedCachedContent = resolveAliasedParamValue( + [defaultParams, globalParams, agentParams], + "cached_content", + "cachedContent", + ); + if (resolvedCachedContent !== undefined) { + merged.cachedContent = resolvedCachedContent; + delete merged.cached_content; + } + return merged; } type CacheRetentionStreamOptions = Partial & { cacheRetention?: "none" | "short" | "long"; + cachedContent?: string; openaiWsWarmup?: boolean; }; type SupportedTransport = Exclude; @@ -137,6 +148,15 @@ export function resolvePreparedExtraParams(params: { ...sanitizeExtraParamsRecord(resolvedExtraParams), ...override, }; + const resolvedCachedContent = resolveAliasedParamValue( + [resolvedExtraParams, override], + "cached_content", + "cachedContent", + ); + if (resolvedCachedContent !== undefined) { + merged.cachedContent = resolvedCachedContent; + delete merged.cached_content; + } return ( providerRuntimeDeps.prepareProviderExtraParams({ provider: params.provider, @@ -207,6 +227,15 @@ function createStreamFnWithExtraParams( if (typeof extraParams.openaiWsWarmup === "boolean") { streamParams.openaiWsWarmup = extraParams.openaiWsWarmup; } + const cachedContent = + typeof extraParams.cachedContent === "string" + ? extraParams.cachedContent + : typeof extraParams.cached_content === "string" + ? extraParams.cached_content + : undefined; + if (typeof cachedContent === "string" && cachedContent.trim()) { + streamParams.cachedContent = cachedContent.trim(); + } const initialCacheRetention = resolveCacheRetention( extraParams, provider,