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.
This commit is contained in:
Julius Smith
2026-04-18 06:13:27 -07:00
committed by Peter Steinberger
parent ca1aa08709
commit 8c5a4eb866
3 changed files with 76 additions and 0 deletions

View File

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

View File

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

View File

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