From 28fe0296c40bb2c6f444352d397679b66c86cdd4 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 18 Apr 2026 18:18:22 +0100 Subject: [PATCH] fix: support Gemini latest thinking config --- docs/providers/google.md | 29 ++++--- extensions/google/provider-models.test.ts | 48 +++++++++++ extensions/google/provider-models.ts | 19 ++++- src/agents/google-thinking-compat.ts | 80 +++++++++++++++++++ src/agents/google-transport-stream.test.ts | 68 ++++++++++++++++ src/agents/google-transport-stream.ts | 77 +++++++++--------- .../pi-embedded-runner-extraparams.test.ts | 4 +- .../google-stream-wrappers.test.ts | 50 ++++++++++++ .../google-stream-wrappers.ts | 62 ++++++-------- 9 files changed, 341 insertions(+), 96 deletions(-) diff --git a/docs/providers/google.md b/docs/providers/google.md index 7dabc43a100..8e91f596793 100644 --- a/docs/providers/google.md +++ b/docs/providers/google.md @@ -128,20 +128,25 @@ Choose your preferred auth method and follow the setup steps. ## Capabilities -| Capability | Supported | -| ---------------------- | ----------------- | -| Chat completions | Yes | -| Image generation | Yes | -| Music generation | Yes | -| Text-to-speech | Yes | -| Image understanding | Yes | -| Audio transcription | Yes | -| Video understanding | Yes | -| Web search (Grounding) | Yes | -| Thinking/reasoning | Yes (Gemini 3.1+) | -| Gemma 4 models | Yes | +| Capability | Supported | +| ---------------------- | ----------------------------- | +| Chat completions | Yes | +| Image generation | Yes | +| Music generation | Yes | +| Text-to-speech | Yes | +| Image understanding | Yes | +| Audio transcription | Yes | +| Video understanding | Yes | +| Web search (Grounding) | Yes | +| Thinking/reasoning | Yes (Gemini 2.5+ / Gemini 3+) | +| Gemma 4 models | Yes | +Gemini 3 models use `thinkingLevel` rather than `thinkingBudget`. OpenClaw maps +Gemini 3, Gemini 3.1, and `gemini-*-latest` alias reasoning controls to +`thinkingLevel` so default/low-latency runs do not send disabled +`thinkingBudget` values. + Gemma 4 models (for example `gemma-4-26b-a4b-it`) support thinking mode. OpenClaw rewrites `thinkingBudget` to a supported Google `thinkingLevel` for Gemma 4. Setting thinking to `off` preserves thinking disabled instead of mapping to diff --git a/extensions/google/provider-models.test.ts b/extensions/google/provider-models.test.ts index 6dc07b4c733..71bcc8c1ec9 100644 --- a/extensions/google/provider-models.test.ts +++ b/extensions/google/provider-models.test.ts @@ -186,6 +186,48 @@ describe("resolveGoogleGeminiForwardCompatModel", () => { }); }); + it("resolves Gemini latest aliases from current Google templates", () => { + const models = [ + createTemplateModel("google", "gemini-3-pro-preview", { reasoning: true }), + createTemplateModel("google", "gemini-3-flash-preview", { reasoning: true }), + createTemplateModel("google", "gemini-3.1-flash-lite-preview", { reasoning: true }), + ]; + + expect( + resolveGoogleGeminiForwardCompatModel({ + providerId: "google", + ctx: createContext({ provider: "google", modelId: "gemini-pro-latest", models }), + }), + ).toMatchObject({ + provider: "google", + id: "gemini-pro-latest", + api: "google-generative-ai", + reasoning: true, + }); + expect( + resolveGoogleGeminiForwardCompatModel({ + providerId: "google", + ctx: createContext({ provider: "google", modelId: "gemini-flash-latest", models }), + }), + ).toMatchObject({ + provider: "google", + id: "gemini-flash-latest", + api: "google-generative-ai", + reasoning: true, + }); + expect( + resolveGoogleGeminiForwardCompatModel({ + providerId: "google", + ctx: createContext({ provider: "google", modelId: "gemini-flash-lite-latest", models }), + }), + ).toMatchObject({ + provider: "google", + id: "gemini-flash-lite-latest", + api: "google-generative-ai", + reasoning: true, + }); + }); + it("prefers the flash-lite template before the broader flash prefix", () => { const model = resolveGoogleGeminiForwardCompatModel({ providerId: "google-vertex", @@ -217,6 +259,12 @@ describe("resolveGoogleGeminiForwardCompatModel", () => { expect(isModernGoogleModel("gemini-1.5-pro")).toBe(false); }); + it("treats Gemini latest aliases as modern google models", () => { + expect(isModernGoogleModel("gemini-pro-latest")).toBe(true); + expect(isModernGoogleModel("gemini-flash-latest")).toBe(true); + expect(isModernGoogleModel("gemini-flash-lite-latest")).toBe(true); + }); + it("treats gemma models as modern google models", () => { expect(isModernGoogleModel("gemma-4-26b-a4b-it")).toBe(true); expect(isModernGoogleModel("gemma-3-4b-it")).toBe(true); diff --git a/extensions/google/provider-models.ts b/extensions/google/provider-models.ts index cf23ba6fa57..2aff4e2ad06 100644 --- a/extensions/google/provider-models.ts +++ b/extensions/google/provider-models.ts @@ -12,6 +12,9 @@ const GEMINI_2_5_FLASH_PREFIX = "gemini-2.5-flash"; const GEMINI_3_1_PRO_PREFIX = "gemini-3.1-pro"; const GEMINI_3_1_FLASH_LITE_PREFIX = "gemini-3.1-flash-lite"; const GEMINI_3_1_FLASH_PREFIX = "gemini-3.1-flash"; +const GEMINI_PRO_LATEST_ID = "gemini-pro-latest"; +const GEMINI_FLASH_LATEST_ID = "gemini-flash-latest"; +const GEMINI_FLASH_LITE_LATEST_ID = "gemini-flash-lite-latest"; const GEMMA_PREFIX = "gemma-"; const GEMINI_2_5_PRO_TEMPLATE_IDS = ["gemini-2.5-pro"] as const; const GEMINI_2_5_FLASH_LITE_TEMPLATE_IDS = ["gemini-2.5-flash-lite"] as const; @@ -128,7 +131,7 @@ export function resolveGoogleGeminiForwardCompatModel(params: { cliTemplateIds: GEMINI_3_1_FLASH_TEMPLATE_IDS, preferExternalFirstForCli: true, }; - } else if (lower.startsWith(GEMINI_3_1_PRO_PREFIX)) { + } else if (lower.startsWith(GEMINI_3_1_PRO_PREFIX) || lower === GEMINI_PRO_LATEST_ID) { family = { googleTemplateIds: GEMINI_3_1_PRO_TEMPLATE_IDS, cliTemplateIds: GEMINI_3_1_PRO_TEMPLATE_IDS, @@ -136,12 +139,15 @@ export function resolveGoogleGeminiForwardCompatModel(params: { if (params.providerId === "google" || params.providerId === GOOGLE_GEMINI_CLI_PROVIDER_ID) { patch = { reasoning: true }; } - } else if (lower.startsWith(GEMINI_3_1_FLASH_LITE_PREFIX)) { + } else if ( + lower.startsWith(GEMINI_3_1_FLASH_LITE_PREFIX) || + lower === GEMINI_FLASH_LITE_LATEST_ID + ) { family = { googleTemplateIds: GEMINI_3_1_FLASH_LITE_TEMPLATE_IDS, cliTemplateIds: GEMINI_3_1_FLASH_LITE_TEMPLATE_IDS, }; - } else if (lower.startsWith(GEMINI_3_1_FLASH_PREFIX)) { + } else if (lower.startsWith(GEMINI_3_1_FLASH_PREFIX) || lower === GEMINI_FLASH_LATEST_ID) { family = { googleTemplateIds: GEMINI_3_1_FLASH_TEMPLATE_IDS, cliTemplateIds: GEMINI_3_1_FLASH_TEMPLATE_IDS, @@ -182,6 +188,11 @@ export function resolveGoogleGeminiForwardCompatModel(params: { export function isModernGoogleModel(modelId: string): boolean { const lower = normalizeOptionalLowercaseString(modelId) ?? ""; return ( - lower.startsWith("gemini-2.5") || lower.startsWith("gemini-3") || lower.startsWith(GEMMA_PREFIX) + lower.startsWith("gemini-2.5") || + lower.startsWith("gemini-3") || + lower === GEMINI_PRO_LATEST_ID || + lower === GEMINI_FLASH_LATEST_ID || + lower === GEMINI_FLASH_LITE_LATEST_ID || + lower.startsWith(GEMMA_PREFIX) ); } diff --git a/src/agents/google-thinking-compat.ts b/src/agents/google-thinking-compat.ts index 1e8cddd06d9..9d8f91f7601 100644 --- a/src/agents/google-thinking-compat.ts +++ b/src/agents/google-thinking-compat.ts @@ -1,11 +1,91 @@ import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js"; +export type GoogleThinkingLevel = "MINIMAL" | "LOW" | "MEDIUM" | "HIGH"; +export type GoogleThinkingInputLevel = + | "off" + | "minimal" + | "low" + | "medium" + | "adaptive" + | "high" + | "xhigh"; + // 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 isGoogleGemini3ProModel(modelId: string): boolean { + const normalized = normalizeLowercaseStringOrEmpty(modelId); + return /(?:^|\/)gemini-(?:3(?:\.\d+)?-pro|pro-latest)(?:-|$)/.test(normalized); +} + +export function isGoogleGemini3FlashModel(modelId: string): boolean { + const normalized = normalizeLowercaseStringOrEmpty(modelId); + return /(?:^|\/)gemini-(?:3(?:\.\d+)?-flash|flash(?:-lite)?-latest)(?:-|$)/.test(normalized); +} + +export function isGoogleGemini3ThinkingLevelModel(modelId: string): boolean { + return isGoogleGemini3ProModel(modelId) || isGoogleGemini3FlashModel(modelId); +} + +export function resolveGoogleGemini3ThinkingLevel(params: { + modelId?: string; + thinkingLevel?: GoogleThinkingInputLevel; + thinkingBudget?: number; +}): GoogleThinkingLevel | undefined { + if (typeof params.modelId !== "string") { + return undefined; + } + if (isGoogleGemini3ProModel(params.modelId)) { + switch (params.thinkingLevel) { + case "off": + case "minimal": + case "low": + return "LOW"; + case "medium": + case "adaptive": + case "high": + case "xhigh": + return "HIGH"; + } + if (typeof params.thinkingBudget === "number") { + return params.thinkingBudget <= 2048 ? "LOW" : "HIGH"; + } + return undefined; + } + if (!isGoogleGemini3FlashModel(params.modelId)) { + return undefined; + } + switch (params.thinkingLevel) { + case "off": + case "minimal": + return "MINIMAL"; + case "low": + return "LOW"; + case "medium": + case "adaptive": + return "MEDIUM"; + case "high": + case "xhigh": + return "HIGH"; + } + if (typeof params.thinkingBudget !== "number") { + return undefined; + } + if (params.thinkingBudget <= 0) { + return "MINIMAL"; + } + if (params.thinkingBudget <= 2048) { + return "LOW"; + } + if (params.thinkingBudget <= 8192) { + return "MEDIUM"; + } + return "HIGH"; +} + export function stripInvalidGoogleThinkingBudget(params: { thinkingConfig: Record; modelId?: string; diff --git a/src/agents/google-transport-stream.test.ts b/src/agents/google-transport-stream.test.ts index 750a0b8dcb7..6ff250b94db 100644 --- a/src/agents/google-transport-stream.test.ts +++ b/src/agents/google-transport-stream.test.ts @@ -334,6 +334,74 @@ describe("google transport stream", () => { }); }); + it.each([ + ["gemini-pro-latest", "LOW"], + ["gemini-flash-latest", "MINIMAL"], + ["gemini-flash-lite-latest", "MINIMAL"], + ] as const)( + "uses thinkingLevel instead of disabled thinkingBudget for %s defaults", + (id, level) => { + const params = buildGoogleGenerativeAiParams( + buildGeminiModel({ id }), + { + messages: [{ role: "user", content: "hello", timestamp: 0 }], + } as never, + { + maxTokens: 128, + } as never, + ); + + expect(params.generationConfig).toMatchObject({ + maxOutputTokens: 128, + thinkingConfig: { thinkingLevel: level }, + }); + expect(params.generationConfig).not.toMatchObject({ + thinkingConfig: { thinkingBudget: 0 }, + }); + }, + ); + + it("maps explicit Gemini 3 thinking budgets to thinkingLevel", () => { + const params = buildGoogleGenerativeAiParams( + buildGeminiModel({ id: "gemini-3-flash-preview" }), + { + messages: [{ role: "user", content: "hello", timestamp: 0 }], + } as never, + { + thinking: { + enabled: true, + budgetTokens: 8192, + }, + } as never, + ); + + expect(params.generationConfig).toMatchObject({ + thinkingConfig: { includeThoughts: true, thinkingLevel: "MEDIUM" }, + }); + expect(params.generationConfig).not.toMatchObject({ + thinkingConfig: { thinkingBudget: 8192 }, + }); + }); + + it("normalizes explicit Gemini 3 Pro thinking levels", () => { + const params = buildGoogleGenerativeAiParams( + buildGeminiModel({ id: "gemini-3.1-pro-preview" }), + { + messages: [{ role: "user", content: "hello", timestamp: 0 }], + } as never, + { + thinking: { + enabled: true, + level: "MINIMAL", + }, + } as never, + ); + + expect(params.generationConfig).toMatchObject({ + thinkingConfig: { includeThoughts: true, thinkingLevel: "LOW" }, + }); + }); + it("includes cachedContent in direct Gemini payloads when requested", () => { const params = buildGoogleGenerativeAiParams( buildGeminiModel(), diff --git a/src/agents/google-transport-stream.ts b/src/agents/google-transport-stream.ts index 56bd5864821..65204a153f5 100644 --- a/src/agents/google-transport-stream.ts +++ b/src/agents/google-transport-stream.ts @@ -10,7 +10,14 @@ 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 { + isGoogleGemini3FlashModel, + isGoogleGemini3ProModel, + resolveGoogleGemini3ThinkingLevel, + stripInvalidGoogleThinkingBudget, + type GoogleThinkingInputLevel, + type GoogleThinkingLevel, +} 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"; @@ -30,8 +37,6 @@ type GoogleTransportModel = Model<"google-generative-ai"> & { provider: string; }; -type GoogleThinkingLevel = "MINIMAL" | "LOW" | "MEDIUM" | "HIGH"; - type GoogleTransportOptions = SimpleStreamOptions & { cachedContent?: string; toolChoice?: @@ -114,14 +119,6 @@ type GoogleSseChunk = { let toolCallCounter = 0; -function isGemini3ProModel(modelId: string): boolean { - return /gemini-3(?:\.\d+)?-pro/.test(normalizeLowercaseStringOrEmpty(modelId)); -} - -function isGemini3FlashModel(modelId: string): boolean { - return /gemini-3(?:\.\d+)?-flash/.test(normalizeLowercaseStringOrEmpty(modelId)); -} - function requiresToolCallId(modelId: string): boolean { return modelId.startsWith("claude-") || modelId.startsWith("gpt-oss-"); } @@ -189,37 +186,29 @@ function buildGoogleRequestUrl(model: GoogleTransportModel): string { } function resolveThinkingLevel(level: ThinkingLevel, modelId: string): GoogleThinkingLevel { - if (isGemini3ProModel(modelId)) { - switch (level) { - case "minimal": - case "low": - return "LOW"; - case "medium": - case "high": - case "xhigh": - return "HIGH"; - } - } - switch (level) { - case "minimal": - return "MINIMAL"; - case "low": - return "LOW"; - case "medium": - return "MEDIUM"; - case "high": - case "xhigh": - return "HIGH"; + const resolved = resolveGoogleGemini3ThinkingLevel({ modelId, thinkingLevel: level }); + if (resolved) { + return resolved; } throw new Error("Unsupported thinking level"); } +function resolveExplicitThinkingLevel( + level: GoogleThinkingLevel, + modelId: string, +): GoogleThinkingLevel { + return ( + resolveGoogleGemini3ThinkingLevel({ + modelId, + thinkingLevel: level.toLowerCase() as GoogleThinkingInputLevel, + }) ?? level + ); +} + function getDisabledThinkingConfig(modelId: string): Record | undefined { - if (isGemini3ProModel(modelId)) { - return { thinkingLevel: "LOW" }; - } - if (isGemini3FlashModel(modelId)) { - return { thinkingLevel: "MINIMAL" }; + const thinkingLevel = resolveGoogleGemini3ThinkingLevel({ modelId, thinkingLevel: "off" }); + if (thinkingLevel) { + return { thinkingLevel }; } return normalizeGoogleThinkingConfig(modelId, { thinkingBudget: 0 }); } @@ -255,16 +244,24 @@ function resolveGoogleThinkingConfig( } const config: Record = { includeThoughts: true }; if (options.thinking.level) { - config.thinkingLevel = options.thinking.level; + config.thinkingLevel = resolveExplicitThinkingLevel(options.thinking.level, model.id); } else if (typeof options.thinking.budgetTokens === "number") { - config.thinkingBudget = options.thinking.budgetTokens; + const thinkingLevel = resolveGoogleGemini3ThinkingLevel({ + modelId: model.id, + thinkingBudget: options.thinking.budgetTokens, + }); + if (thinkingLevel) { + config.thinkingLevel = thinkingLevel; + } else { + config.thinkingBudget = options.thinking.budgetTokens; + } } return normalizeGoogleThinkingConfig(model.id, config); } if (!options?.reasoning) { return getDisabledThinkingConfig(model.id); } - if (isGemini3ProModel(model.id) || isGemini3FlashModel(model.id)) { + if (isGoogleGemini3ProModel(model.id) || isGoogleGemini3FlashModel(model.id)) { return { includeThoughts: true, thinkingLevel: resolveThinkingLevel(options.reasoning, model.id), diff --git a/src/agents/pi-embedded-runner-extraparams.test.ts b/src/agents/pi-embedded-runner-extraparams.test.ts index cacd71b2ec5..dbdd2982fee 100644 --- a/src/agents/pi-embedded-runner-extraparams.test.ts +++ b/src/agents/pi-embedded-runner-extraparams.test.ts @@ -1221,7 +1221,7 @@ describe("applyExtraParamsToAgent", () => { }); }); - it("keeps valid Google thinkingBudget unchanged", () => { + it("rewrites Gemini 3 thinkingBudget to thinkingLevel", () => { const payloads: Record[] = []; const baseStreamFn: StreamFn = (_model, _context, options) => { const payload: Record = { @@ -1252,7 +1252,7 @@ describe("applyExtraParamsToAgent", () => { expect(payloads[0]?.config).toEqual({ thinkingConfig: { includeThoughts: true, - thinkingBudget: 2048, + thinkingLevel: "HIGH", }, }); }); diff --git a/src/agents/pi-embedded-runner/google-stream-wrappers.test.ts b/src/agents/pi-embedded-runner/google-stream-wrappers.test.ts index 4965e3a8816..e7b4c8d58bf 100644 --- a/src/agents/pi-embedded-runner/google-stream-wrappers.test.ts +++ b/src/agents/pi-embedded-runner/google-stream-wrappers.test.ts @@ -63,4 +63,54 @@ describe("sanitizeGoogleThinkingPayload — gemini-2.5-pro zero budget", () => { sanitizeGoogleThinkingPayload({ payload, modelId: "gemini-2.5-pro" }); expect(payload.config.thinkingConfig).toHaveProperty("thinkingBudget", 1000); }); + + it("rewrites Gemini 3 Pro budgets to thinkingLevel", () => { + const payload = { + config: { + thinkingConfig: { thinkingBudget: 2048, includeThoughts: true }, + }, + }; + sanitizeGoogleThinkingPayload({ + payload, + modelId: "gemini-3.1-pro-preview", + thinkingLevel: "high", + }); + expect(payload.config.thinkingConfig).toEqual({ + includeThoughts: true, + thinkingLevel: "HIGH", + }); + }); + + it("rewrites Gemini 3 Flash latest disabled budgets to minimal thinkingLevel", () => { + const payload = { + generationConfig: { + thinkingConfig: { thinkingBudget: 0 }, + }, + }; + sanitizeGoogleThinkingPayload({ + payload, + modelId: "gemini-flash-latest", + thinkingLevel: "off", + }); + expect(payload.generationConfig.thinkingConfig).toEqual({ + thinkingLevel: "MINIMAL", + }); + }); + + it("fills thinkingLevel for Gemini 3 Flash negative budgets", () => { + const payload = { + config: { + thinkingConfig: { thinkingBudget: -1, includeThoughts: true }, + }, + }; + sanitizeGoogleThinkingPayload({ + payload, + modelId: "gemini-3-flash-preview", + thinkingLevel: "medium", + }); + expect(payload.config.thinkingConfig).toEqual({ + includeThoughts: true, + thinkingLevel: "MEDIUM", + }); + }); }); diff --git a/src/agents/pi-embedded-runner/google-stream-wrappers.ts b/src/agents/pi-embedded-runner/google-stream-wrappers.ts index 9f7b479402a..cd25414e534 100644 --- a/src/agents/pi-embedded-runner/google-stream-wrappers.ts +++ b/src/agents/pi-embedded-runner/google-stream-wrappers.ts @@ -2,37 +2,17 @@ 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 { + isGoogleGemini3ThinkingLevelModel, + resolveGoogleGemini3ThinkingLevel, + stripInvalidGoogleThinkingBudget, +} from "../google-thinking-compat.js"; import { streamWithPayloadPatch } from "./stream-payload-utils.js"; -function isGemini31Model(modelId: string): boolean { - const normalized = normalizeLowercaseStringOrEmpty(modelId); - return normalized.includes("gemini-3.1-pro") || normalized.includes("gemini-3.1-flash"); -} - function isGemma4Model(modelId: string): boolean { return normalizeLowercaseStringOrEmpty(modelId).startsWith("gemma-4"); } -function mapThinkLevelToGoogleThinkingLevel( - thinkingLevel: ThinkLevel, -): "MINIMAL" | "LOW" | "MEDIUM" | "HIGH" | undefined { - switch (thinkingLevel) { - case "minimal": - return "MINIMAL"; - case "low": - return "LOW"; - case "medium": - case "adaptive": - return "MEDIUM"; - case "high": - case "xhigh": - return "HIGH"; - default: - return undefined; - } -} - function mapThinkLevelToGemma4ThinkingLevel( thinkingLevel?: ThinkLevel, ): "MINIMAL" | "HIGH" | undefined { @@ -134,6 +114,22 @@ function sanitizeGoogleThinkingConfigContainer(params: { const thinkingBudget = thinkingConfigObj.thinkingBudget; + if (typeof params.modelId === "string" && isGoogleGemini3ThinkingLevelModel(params.modelId)) { + const mappedLevel = resolveGoogleGemini3ThinkingLevel({ + modelId: params.modelId, + thinkingLevel: params.thinkingLevel, + thinkingBudget: typeof thinkingBudget === "number" ? thinkingBudget : undefined, + }); + delete thinkingConfigObj.thinkingBudget; + if (mappedLevel) { + thinkingConfigObj.thinkingLevel = mappedLevel; + } + if (Object.keys(thinkingConfigObj).length === 0) { + delete configObj.thinkingConfig; + } + return; + } + if ( stripInvalidGoogleThinkingBudget({ thinkingConfig: thinkingConfigObj, modelId: params.modelId }) ) { @@ -147,21 +143,11 @@ function sanitizeGoogleThinkingConfigContainer(params: { return; } - // pi-ai can emit thinkingBudget=-1 for some Gemini 3.1 IDs; a negative budget + // pi-ai can emit thinkingBudget=-1 for some Google model IDs; a negative budget // is invalid for Google-compatible backends and can lead to malformed handling. delete thinkingConfigObj.thinkingBudget; - - if ( - typeof params.modelId === "string" && - isGemini31Model(params.modelId) && - params.thinkingLevel && - params.thinkingLevel !== "off" && - thinkingConfigObj.thinkingLevel === undefined - ) { - const mappedLevel = mapThinkLevelToGoogleThinkingLevel(params.thinkingLevel); - if (mappedLevel) { - thinkingConfigObj.thinkingLevel = mappedLevel; - } + if (Object.keys(thinkingConfigObj).length === 0) { + delete configObj.thinkingConfig; } }