mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 09:50:42 +00:00
fix(google): cover gemini pro zero thinking budget (#68607) (thanks @josmithiii)
This commit is contained in:
@@ -64,7 +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.
|
||||
- Agents/Google: strip `thinkingBudget=0` for the thinking-required `gemini-2.5-pro` model in embedded-runner and native Google payloads, 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. (#68607) Thanks @josmithiii.
|
||||
|
||||
## 2026.4.15
|
||||
|
||||
|
||||
22
src/agents/google-thinking-compat.ts
Normal file
22
src/agents/google-thinking-compat.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
|
||||
|
||||
// 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."
|
||||
export function isGoogleThinkingRequiredModel(modelId: string): boolean {
|
||||
return normalizeLowercaseStringOrEmpty(modelId).includes("gemini-2.5-pro");
|
||||
}
|
||||
|
||||
export function stripInvalidGoogleThinkingBudget(params: {
|
||||
thinkingConfig: Record<string, unknown>;
|
||||
modelId?: string;
|
||||
}): boolean {
|
||||
if (
|
||||
params.thinkingConfig.thinkingBudget !== 0 ||
|
||||
typeof params.modelId !== "string" ||
|
||||
!isGoogleThinkingRequiredModel(params.modelId)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
delete params.thinkingConfig.thinkingBudget;
|
||||
return true;
|
||||
}
|
||||
@@ -295,6 +295,45 @@ describe("google transport stream", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("omits disabled thinkingBudget=0 for Gemini 2.5 Pro direct payloads", () => {
|
||||
const params = buildGoogleGenerativeAiParams(
|
||||
buildGeminiModel(),
|
||||
{
|
||||
messages: [{ role: "user", content: "hello", timestamp: 0 }],
|
||||
} as never,
|
||||
{
|
||||
maxTokens: 128,
|
||||
} as never,
|
||||
);
|
||||
|
||||
expect(params.generationConfig).toMatchObject({
|
||||
maxOutputTokens: 128,
|
||||
});
|
||||
expect(params.generationConfig).not.toHaveProperty("thinkingConfig");
|
||||
});
|
||||
|
||||
it("strips explicit thinkingBudget=0 but preserves includeThoughts for Gemini 2.5 Pro", () => {
|
||||
const params = buildGoogleGenerativeAiParams(
|
||||
buildGeminiModel(),
|
||||
{
|
||||
messages: [{ role: "user", content: "hello", timestamp: 0 }],
|
||||
} as never,
|
||||
{
|
||||
thinking: {
|
||||
enabled: true,
|
||||
budgetTokens: 0,
|
||||
},
|
||||
} as never,
|
||||
);
|
||||
|
||||
expect(params.generationConfig).toMatchObject({
|
||||
thinkingConfig: { includeThoughts: true },
|
||||
});
|
||||
expect(params.generationConfig).not.toMatchObject({
|
||||
thinkingConfig: { thinkingBudget: 0 },
|
||||
});
|
||||
});
|
||||
|
||||
it("includes cachedContent in direct Gemini payloads when requested", () => {
|
||||
const params = buildGoogleGenerativeAiParams(
|
||||
buildGeminiModel(),
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
import { parseGeminiAuth } from "../infra/gemini-auth.js";
|
||||
import { normalizeGoogleApiBaseUrl } from "../infra/google-api-base-url.js";
|
||||
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
|
||||
import { stripInvalidGoogleThinkingBudget } from "./google-thinking-compat.js";
|
||||
import { buildGuardedModelFetch } from "./provider-transport-fetch.js";
|
||||
import { stripSystemPromptCacheBoundary } from "./system-prompt-cache-boundary.js";
|
||||
import { transformTransportMessages } from "./transport-message-transform.js";
|
||||
@@ -213,14 +214,14 @@ function resolveThinkingLevel(level: ThinkingLevel, modelId: string): GoogleThin
|
||||
throw new Error("Unsupported thinking level");
|
||||
}
|
||||
|
||||
function getDisabledThinkingConfig(modelId: string): Record<string, unknown> {
|
||||
function getDisabledThinkingConfig(modelId: string): Record<string, unknown> | undefined {
|
||||
if (isGemini3ProModel(modelId)) {
|
||||
return { thinkingLevel: "LOW" };
|
||||
}
|
||||
if (isGemini3FlashModel(modelId)) {
|
||||
return { thinkingLevel: "MINIMAL" };
|
||||
}
|
||||
return { thinkingBudget: 0 };
|
||||
return normalizeGoogleThinkingConfig(modelId, { thinkingBudget: 0 });
|
||||
}
|
||||
|
||||
function getGoogleThinkingBudget(
|
||||
@@ -258,7 +259,7 @@ function resolveGoogleThinkingConfig(
|
||||
} else if (typeof options.thinking.budgetTokens === "number") {
|
||||
config.thinkingBudget = options.thinking.budgetTokens;
|
||||
}
|
||||
return config;
|
||||
return normalizeGoogleThinkingConfig(model.id, config);
|
||||
}
|
||||
if (!options?.reasoning) {
|
||||
return getDisabledThinkingConfig(model.id);
|
||||
@@ -270,10 +271,18 @@ function resolveGoogleThinkingConfig(
|
||||
};
|
||||
}
|
||||
const budget = getGoogleThinkingBudget(model.id, options.reasoning, options.thinkingBudgets);
|
||||
return {
|
||||
return normalizeGoogleThinkingConfig(model.id, {
|
||||
includeThoughts: true,
|
||||
...(typeof budget === "number" ? { thinkingBudget: budget } : {}),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeGoogleThinkingConfig(
|
||||
modelId: string,
|
||||
thinkingConfig: Record<string, unknown>,
|
||||
): Record<string, unknown> | undefined {
|
||||
stripInvalidGoogleThinkingBudget({ thinkingConfig, modelId });
|
||||
return Object.keys(thinkingConfig).length > 0 ? thinkingConfig : undefined;
|
||||
}
|
||||
|
||||
function convertGoogleMessages(model: GoogleTransportModel, context: Context) {
|
||||
|
||||
@@ -33,6 +33,17 @@ describe("sanitizeGoogleThinkingPayload — gemini-2.5-pro zero budget", () => {
|
||||
expect(payload.config.thinkingConfig).toHaveProperty("includeThoughts", true);
|
||||
});
|
||||
|
||||
it("removes thinkingBudget=0 from native Google generationConfig payloads", () => {
|
||||
const payload = {
|
||||
generationConfig: {
|
||||
thinkingConfig: { thinkingBudget: 0, includeThoughts: true },
|
||||
},
|
||||
};
|
||||
sanitizeGoogleThinkingPayload({ payload, modelId: "gemini-2.5-pro" });
|
||||
expect(payload.generationConfig.thinkingConfig).not.toHaveProperty("thinkingBudget");
|
||||
expect(payload.generationConfig.thinkingConfig).toHaveProperty("includeThoughts", true);
|
||||
});
|
||||
|
||||
it("keeps thinkingBudget=0 for gemini-2.5-flash (not thinking-required)", () => {
|
||||
const payload = {
|
||||
config: {
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { StreamFn } from "@mariozechner/pi-agent-core";
|
||||
import { streamSimple } from "@mariozechner/pi-ai";
|
||||
import type { ThinkLevel } from "../../auto-reply/thinking.js";
|
||||
import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js";
|
||||
import { stripInvalidGoogleThinkingBudget } from "../google-thinking-compat.js";
|
||||
import { streamWithPayloadPatch } from "./stream-payload-utils.js";
|
||||
|
||||
function isGemini31Model(modelId: string): boolean {
|
||||
@@ -13,12 +14,6 @@ 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 {
|
||||
@@ -82,11 +77,27 @@ export function sanitizeGoogleThinkingPayload(params: {
|
||||
return;
|
||||
}
|
||||
const payloadObj = params.payload as Record<string, unknown>;
|
||||
const config = payloadObj.config;
|
||||
if (!config || typeof config !== "object") {
|
||||
sanitizeGoogleThinkingConfigContainer({
|
||||
container: payloadObj.config,
|
||||
modelId: params.modelId,
|
||||
thinkingLevel: params.thinkingLevel,
|
||||
});
|
||||
sanitizeGoogleThinkingConfigContainer({
|
||||
container: payloadObj.generationConfig,
|
||||
modelId: params.modelId,
|
||||
thinkingLevel: params.thinkingLevel,
|
||||
});
|
||||
}
|
||||
|
||||
function sanitizeGoogleThinkingConfigContainer(params: {
|
||||
container: unknown;
|
||||
modelId?: string;
|
||||
thinkingLevel?: ThinkLevel;
|
||||
}): void {
|
||||
if (!params.container || typeof params.container !== "object") {
|
||||
return;
|
||||
}
|
||||
const configObj = config as Record<string, unknown>;
|
||||
const configObj = params.container as Record<string, unknown>;
|
||||
const thinkingConfig = configObj.thinkingConfig;
|
||||
if (!thinkingConfig || typeof thinkingConfig !== "object") {
|
||||
return;
|
||||
@@ -123,13 +134,9 @@ 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)
|
||||
stripInvalidGoogleThinkingBudget({ thinkingConfig: thinkingConfigObj, modelId: params.modelId })
|
||||
) {
|
||||
delete thinkingConfigObj.thinkingBudget;
|
||||
if (Object.keys(thinkingConfigObj).length === 0) {
|
||||
delete configObj.thinkingConfig;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user