Skip to content

Commit c04c2b7

Browse files
fix(google): omit cached content request conflicts
1 parent 982230f commit c04c2b7

4 files changed

Lines changed: 103 additions & 8 deletions

File tree

extensions/google/transport-stream.test.ts

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -175,16 +175,13 @@ describe("google transport stream", () => {
175175
throw new Error("Expected Google transport request body to be serialized JSON");
176176
}
177177
const payload = JSON.parse(requestBody) as Record<string, unknown>;
178-
expect(payload.systemInstruction).toEqual({
179-
parts: [{ text: "Follow policy." }],
180-
});
181178
expect(payload.cachedContent).toBe("cachedContents/request-cache");
179+
expect(payload.systemInstruction).toBeUndefined();
182180
expect(payload.generationConfig).toMatchObject({
183181
thinkingConfig: { includeThoughts: true, thinkingLevel: "HIGH" },
184182
});
185-
expect(payload.toolConfig).toMatchObject({
186-
functionCallingConfig: { mode: "AUTO" },
187-
});
183+
expect(payload.tools).toBeUndefined();
184+
expect(payload.toolConfig).toBeUndefined();
188185
expect(result).toMatchObject({
189186
api: "google-generative-ai",
190187
provider: "google",
@@ -514,4 +511,43 @@ describe("google transport stream", () => {
514511

515512
expect(params.cachedContent).toBe("cachedContents/prebuilt-context");
516513
});
514+
515+
it("omits prompt and tool request settings when cachedContent is used", () => {
516+
const model = {
517+
id: "gemini-2.5-pro",
518+
name: "Gemini 2.5 Pro",
519+
api: "google-generative-ai",
520+
provider: "google",
521+
baseUrl: "https://generativelanguage.googleapis.com/v1beta",
522+
reasoning: true,
523+
input: ["text"],
524+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
525+
contextWindow: 128000,
526+
maxTokens: 8192,
527+
} satisfies Model<"google-generative-ai">;
528+
529+
const params = buildGoogleGenerativeAiParams(
530+
model,
531+
{
532+
systemPrompt: "Follow policy.",
533+
messages: [{ role: "user", content: "hello", timestamp: 0 }],
534+
tools: [
535+
{
536+
name: "lookup",
537+
description: "Look up a value",
538+
parameters: { type: "object" },
539+
},
540+
],
541+
} as never,
542+
{
543+
cachedContent: "cachedContents/prebuilt-context",
544+
toolChoice: "auto",
545+
},
546+
);
547+
548+
expect(params.cachedContent).toBe("cachedContents/prebuilt-context");
549+
expect(params.systemInstruction).toBeUndefined();
550+
expect(params.tools).toBeUndefined();
551+
expect(params.toolConfig).toBeUndefined();
552+
});
517553
});

extensions/google/transport-stream.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -481,7 +481,9 @@ export function buildGoogleGenerativeAiParams(
481481
if (Object.keys(generationConfig).length > 0) {
482482
params.generationConfig = generationConfig;
483483
}
484-
if (context.systemPrompt) {
484+
const usesCachedContent =
485+
typeof params.cachedContent === "string" && params.cachedContent.length > 0;
486+
if (!usesCachedContent && context.systemPrompt) {
485487
params.systemInstruction = {
486488
parts: [
487489
{
@@ -490,7 +492,7 @@ export function buildGoogleGenerativeAiParams(
490492
],
491493
};
492494
}
493-
if (context.tools?.length) {
495+
if (!usesCachedContent && context.tools?.length) {
494496
params.tools = convertGoogleTools(context.tools);
495497
const toolChoice = mapToolChoice(options?.toolChoice);
496498
if (toolChoice) {

src/agents/pi-embedded-runner/google-prompt-cache.test.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,30 @@ function createCapturingStreamFn(result = "stream") {
8080
};
8181
}
8282

83+
function createConflictingPayloadStreamFn(result = "stream") {
84+
let capturedPayload: Record<string, unknown> | undefined;
85+
const streamFn = vi.fn(
86+
(
87+
model: Parameters<StreamFn>[0],
88+
_context: Parameters<StreamFn>[1],
89+
options: Parameters<StreamFn>[2],
90+
) => {
91+
const payload: Record<string, unknown> = {
92+
systemInstruction: { parts: [{ text: "Follow policy." }] },
93+
tools: [{ functionDeclarations: [{ name: "lookup" }] }],
94+
toolConfig: { functionCallingConfig: { mode: "AUTO" } },
95+
};
96+
void options?.onPayload?.(payload, model);
97+
capturedPayload = payload;
98+
return result as never;
99+
},
100+
);
101+
return {
102+
streamFn,
103+
getCapturedPayload: () => capturedPayload,
104+
};
105+
}
106+
83107
function preparePromptCacheStream(params: {
84108
fetchMock: ReturnType<typeof vi.fn>;
85109
now: number;
@@ -172,11 +196,41 @@ describe("google prompt cache", () => {
172196
expect(getCapturedPayload()).toMatchObject({
173197
cachedContent: "cachedContents/system-cache-1",
174198
});
199+
expect(getCapturedPayload()?.systemInstruction).toBeUndefined();
200+
expect(getCapturedPayload()?.tools).toBeUndefined();
201+
expect(getCapturedPayload()?.toolConfig).toBeUndefined();
175202
expect(entries).toHaveLength(1);
176203
expect(entries[0]?.customType).toBe("openclaw.google-prompt-cache");
177204
expect((entries[0]?.data as { status?: string; cachedContent?: string })?.status).toBe("ready");
178205
});
179206

207+
it("removes GenerateContent fields that conflict with managed cachedContent", async () => {
208+
const now = 1_500_000;
209+
const sessionManager = makeSessionManager();
210+
const fetchMock = createCacheFetchMock({
211+
name: "cachedContents/system-cache-conflict",
212+
expireTime: new Date(now + 3_600_000).toISOString(),
213+
});
214+
const { streamFn: innerStreamFn, getCapturedPayload } = createConflictingPayloadStreamFn();
215+
216+
const wrapped = await preparePromptCacheStream({
217+
fetchMock,
218+
now,
219+
sessionManager,
220+
streamFn: innerStreamFn,
221+
});
222+
223+
void wrapped?.(
224+
makeGoogleModel(),
225+
{ systemPrompt: "Follow policy.", messages: [] } as never,
226+
{} as never,
227+
);
228+
229+
expect(getCapturedPayload()).toEqual({
230+
cachedContent: "cachedContents/system-cache-conflict",
231+
});
232+
});
233+
180234
it("reuses a persisted cache entry without creating a second cache", async () => {
181235
const now = 2_000_000;
182236
const entries: SessionCustomEntry[] = [];

src/agents/pi-embedded-runner/google-prompt-cache.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,9 @@ export async function prepareGooglePromptCacheStreamFn(
407407
options,
408408
(payload) => {
409409
payload.cachedContent = cachedContent;
410+
delete payload.systemInstruction;
411+
delete payload.tools;
412+
delete payload.toolConfig;
410413
},
411414
);
412415
}

0 commit comments

Comments
 (0)