From 8c5a4eb866e7584cd07278466007c94324c08304 Mon Sep 17 00:00:00 2001 From: Julius Smith <2200853+josmithiii@users.noreply.github.com> Date: Sat, 18 Apr 2026 06:13:27 -0700 Subject: [PATCH] fix(google): strip thinkingBudget=0 for gemini-2.5-pro thinking-required model Gemini 2.5 Pro only works in thinking mode and rejects thinkingBudget=0 with 'Budget 0 is invalid. This model only works in thinking mode.' The existing sanitizer in the embedded runner only handled negative budgets; now it also removes zero budgets for the thinking-required model so the API uses its default thinking behavior. When thinkingBudget was the only key in thinkingConfig, the empty object is also removed to match the Gemma 4 cleanup path. --- CHANGELOG.md | 1 + .../google-stream-wrappers.test.ts | 55 +++++++++++++++++++ .../google-stream-wrappers.ts | 20 +++++++ 3 files changed, 76 insertions(+) create mode 100644 src/agents/pi-embedded-runner/google-stream-wrappers.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 66fc6672260..701e5ebd39f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -64,6 +64,7 @@ Docs: https://docs.openclaw.ai - Agents/failover: avoid treating bare leading `402 ...` prose as billing errors while still recognizing proxy subscription failures. (#45827) Thanks @junyuc25. - Config/$schema: preserve root-authored `$schema` during partial config rewrites without injecting include-only schema URLs into the root config. (#47322) Thanks @EfeDurmaz16. - Agents/CLI delivery: run the same reply-media path normalizer the auto-reply flow uses before shipping `openclaw agent --deliver` payloads, so relative `MEDIA:./out/photo.png` tokens resolve against the agent workspace instead of being rejected downstream with `LocalMediaAccessError: Local media path is not under an allowed directory`. Thanks @frankekn. +- Agents/Google: strip `thinkingBudget=0` for the thinking-required `gemini-2.5-pro` model in the embedded runner Google sanitizer, so requests no longer fail with `Budget 0 is invalid. This model only works in thinking mode.` and the API uses its default thinking behavior instead. Thanks @josmithiii. ## 2026.4.15 diff --git a/src/agents/pi-embedded-runner/google-stream-wrappers.test.ts b/src/agents/pi-embedded-runner/google-stream-wrappers.test.ts new file mode 100644 index 00000000000..31ca20a23e7 --- /dev/null +++ b/src/agents/pi-embedded-runner/google-stream-wrappers.test.ts @@ -0,0 +1,55 @@ +import { describe, expect, it } from "vitest"; +import { sanitizeGoogleThinkingPayload } from "./google-stream-wrappers.js"; + +describe("sanitizeGoogleThinkingPayload — gemini-2.5-pro zero budget", () => { + it("removes thinkingBudget=0 for gemini-2.5-pro", () => { + const payload = { + config: { + thinkingConfig: { thinkingBudget: 0 }, + }, + }; + sanitizeGoogleThinkingPayload({ payload, modelId: "gemini-2.5-pro" }); + expect(payload.config).not.toHaveProperty("thinkingConfig"); + }); + + it("removes thinkingBudget=0 for gemini-2.5-pro with provider prefix", () => { + const payload = { + config: { + thinkingConfig: { thinkingBudget: 0 }, + }, + }; + sanitizeGoogleThinkingPayload({ payload, modelId: "google/gemini-2.5-pro-preview" }); + expect(payload.config).not.toHaveProperty("thinkingConfig"); + }); + + it("removes only thinkingBudget and preserves other thinkingConfig keys", () => { + const payload = { + config: { + thinkingConfig: { thinkingBudget: 0, includeThoughts: true }, + }, + }; + sanitizeGoogleThinkingPayload({ payload, modelId: "gemini-2.5-pro" }); + expect(payload.config.thinkingConfig).not.toHaveProperty("thinkingBudget"); + expect(payload.config.thinkingConfig).toHaveProperty("includeThoughts", true); + }); + + it("keeps thinkingBudget=0 for gemini-2.5-flash (not thinking-required)", () => { + const payload = { + config: { + thinkingConfig: { thinkingBudget: 0 }, + }, + }; + sanitizeGoogleThinkingPayload({ payload, modelId: "gemini-2.5-flash" }); + expect(payload.config.thinkingConfig).toHaveProperty("thinkingBudget", 0); + }); + + it("keeps positive thinkingBudget for gemini-2.5-pro", () => { + const payload = { + config: { + thinkingConfig: { thinkingBudget: 1000 }, + }, + }; + sanitizeGoogleThinkingPayload({ payload, modelId: "gemini-2.5-pro" }); + expect(payload.config.thinkingConfig).toHaveProperty("thinkingBudget", 1000); + }); +}); diff --git a/src/agents/pi-embedded-runner/google-stream-wrappers.ts b/src/agents/pi-embedded-runner/google-stream-wrappers.ts index be7893f4c60..5d781cfa7b3 100644 --- a/src/agents/pi-embedded-runner/google-stream-wrappers.ts +++ b/src/agents/pi-embedded-runner/google-stream-wrappers.ts @@ -13,6 +13,12 @@ function isGemma4Model(modelId: string): boolean { return normalizeLowercaseStringOrEmpty(modelId).startsWith("gemma-4"); } +// Gemini 2.5 Pro only works in thinking mode and rejects thinkingBudget=0 with +// "Budget 0 is invalid. This model only works in thinking mode." +function isThinkingRequiredModel(modelId: string): boolean { + return normalizeLowercaseStringOrEmpty(modelId).includes("gemini-2.5-pro"); +} + function mapThinkLevelToGoogleThinkingLevel( thinkingLevel: ThinkLevel, ): "MINIMAL" | "LOW" | "MEDIUM" | "HIGH" | undefined { @@ -116,6 +122,20 @@ export function sanitizeGoogleThinkingPayload(params: { } const thinkingBudget = thinkingConfigObj.thinkingBudget; + + // Gemini 2.5 Pro rejects thinkingBudget=0; remove it so the API uses its default. + if ( + thinkingBudget === 0 && + typeof params.modelId === "string" && + isThinkingRequiredModel(params.modelId) + ) { + delete thinkingConfigObj.thinkingBudget; + if (Object.keys(thinkingConfigObj).length === 0) { + delete configObj.thinkingConfig; + } + return; + } + if (typeof thinkingBudget !== "number" || thinkingBudget >= 0) { return; }