From 38d6cd9955d55548dd1f85fff3ab2b477537659d Mon Sep 17 00:00:00 2001 From: Rafael Dias Zendron Date: Sun, 12 Jul 2026 10:49:07 -0300 Subject: [PATCH] fix(6813): fix thinking budget zero drop and default thinkingConfig injection (#6943) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(electron): bump electron 42→43 + build better-sqlite3 from source (ABI 148) (#6605) fix(electron): bump electron 42→43 + rebuild better-sqlite3 from source against the Electron ABI (148). Electron 43 raises NODE_MODULE_VERSION to 148; better-sqlite3@12.11.1 has no electron-v148 prebuild, so the packaged app died with 'Nenhum driver SQLite disponível'. prepare-electron-standalone now compiles better-sqlite3 from source against the electron headers into build/Release (where 'bindings' resolves it). Validated by Electron Package Smoke (green) + local (node_register_module_v148). Supersedes #6378. (--admin: the only reds are SonarQube/SonarCloud failing on a coverage-report artifact digest-mismatch — a GitHub Actions infra flake, not this diff; Sonar is green on main and the diff touches only the electron build.) * deps: bump the development group across 1 directory with 6 updates (#6588) deps: bump the development group (6 updates). Rebased onto current main; all checks green after the electron-smoke fix (#6605). * fix(proxy): force CONNECT tunnel for HTTP proxied requests (undici 8.7) + production deps bump (#6620) fix(proxy): force CONNECT tunnel for HTTP proxied requests (undici 8.7) + production deps bump. undici 8.6+ changed ProxyAgent to forward plain-HTTP via request-proxy instead of CONNECT, breaking OAuth refresh through a connection proxy (501). proxyDispatcher now passes proxyTunnel:true. Validated: Unit Tests 3/8 (the OAuth-proxy test) green, new regression test green (fails without the fix on undici 8.7), SonarQube green. Supersedes #6380. (--admin: the only red is Electron Package Smoke failing on a next-build artifact 'digest-mismatch' — a GitHub Actions infra flake corrupting the asar ('file data stream has unexpected number of bytes'); the better-sqlite3 rebuild itself succeeded (gyp ok) and the electron path is unchanged from #6605 which passed the smoke. Not this diff.) * fix(6813): fix thinking budget zero drop and default thinkingConfig injection - Fix truthy check for budget_tokens to allow 0 - Stop injecting default thinkingConfig when no knobs present - Add tests covering all scenarios Related: #6813 --------- Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../fixes/6943-gemini-thinking-none-effort.md | 1 + .../__tests__/openai-to-gemini.test.ts | 97 +++++++++++++++++++ .../translator/request/openai-to-gemini.ts | 11 ++- 3 files changed, 106 insertions(+), 3 deletions(-) create mode 100644 changelog.d/fixes/6943-gemini-thinking-none-effort.md create mode 100644 open-sse/translator/request/__tests__/openai-to-gemini.test.ts diff --git a/changelog.d/fixes/6943-gemini-thinking-none-effort.md b/changelog.d/fixes/6943-gemini-thinking-none-effort.md new file mode 100644 index 0000000000..0c6f67f2b9 --- /dev/null +++ b/changelog.d/fixes/6943-gemini-thinking-none-effort.md @@ -0,0 +1 @@ +- fix(providers): `openai->gemini` transform now maps `reasoning_effort: "none"` to `thinkingConfig.thinkingBudget: 0` (with `includeThoughts: false`), giving callers an explicit, documented off-switch for Gemini thinking; the no-knob-at-all default injection (#4170) is unchanged (#6813, thanks @rafaumeu) diff --git a/open-sse/translator/request/__tests__/openai-to-gemini.test.ts b/open-sse/translator/request/__tests__/openai-to-gemini.test.ts new file mode 100644 index 0000000000..2fad3bfa94 --- /dev/null +++ b/open-sse/translator/request/__tests__/openai-to-gemini.test.ts @@ -0,0 +1,97 @@ +import { describe, it, expect } from "vitest"; +import { openaiToGeminiRequest } from "../openai-to-gemini"; + +describe("translator/request/openai-to-gemini.ts", () => { + describe("thinking budget handling (issue #6813)", () => { + it("should pass budget_tokens: 0 without dropping to default", () => { + // Zero budget yields no thoughts, so includeThoughts is false here — this is + // the already-merged #6821 fix for #6813 defect 1 (explicit numeric check, + // not truthy, so budget_tokens:0 isn't silently dropped to the default). + const body = { + model: "gemini/gemini-2.5-flash", + messages: [{ role: "user", content: "hi" }], + safetySettings: [], + thinking: { type: "enabled", budget_tokens: 0 }, + }; + const result = openaiToGeminiRequest("gemini/gemini-2.5-flash", body, false); + expect(result.generationConfig?.thinkingConfig?.thinkingBudget).toBe(0); + expect(result.generationConfig?.thinkingConfig?.includeThoughts).toBe(false); + }); + + it("should pass budget_tokens: 1", () => { + const body = { + model: "gemini/gemini-2.5-flash", + messages: [{ role: "user", content: "hi" }], + safetySettings: [], + thinking: { type: "enabled", budget_tokens: 1 }, + }; + const result = openaiToGeminiRequest("gemini/gemini-2.5-flash", body, false); + expect(result.generationConfig?.thinkingConfig?.thinkingBudget).toBe(1); + }); + + it("should still inject default thinkingConfig when no knobs present (#4170)", () => { + // Modern Gemini 2.5+ models think by default even with no thinkingConfig sent, + // so includeThoughts:true must stay on for the no-knob case or the model's + // reasoning leaks into visible content instead of reasoning_content (#4170). + // The supported off-switch for the "I don't want to pay for thinking" case + // (#6813 defect 2) is the explicit `reasoning_effort: "none"` knob below, + // not silent no-knob-at-all suppression. + const body = { + model: "gemini/gemini-2.5-flash", + messages: [{ role: "user", content: "hi" }], + safetySettings: [], + }; + const result = openaiToGeminiRequest("gemini/gemini-2.5-flash", body, false); + expect(result.generationConfig?.thinkingConfig?.includeThoughts).toBe(true); + expect(result.generationConfig?.thinkingConfig?.thinkingBudget).toBeGreaterThan(0); + }); + + it("should set thinkingBudget 0 (and includeThoughts false) when reasoning_effort: none", () => { + // A zero budget yields no thoughts at all, so includeThoughts is false here — + // consistent with the explicit budget_tokens:0 handling above (#6821/#6813). + const body = { + model: "gemini/gemini-2.5-flash", + messages: [{ role: "user", content: "hi" }], + safetySettings: [], + reasoning_effort: "none", + }; + const result = openaiToGeminiRequest("gemini/gemini-2.5-flash", body, false); + expect(result.generationConfig?.thinkingConfig?.thinkingBudget).toBe(0); + expect(result.generationConfig?.thinkingConfig?.includeThoughts).toBe(false); + }); + + it("should map reasoning_effort: low to thinkingBudget: 1024", () => { + const body = { + model: "gemini/gemini-2.5-flash", + messages: [{ role: "user", content: "hi" }], + safetySettings: [], + reasoning_effort: "low", + }; + const result = openaiToGeminiRequest("gemini/gemini-2.5-flash", body, false); + expect(result.generationConfig?.thinkingConfig?.thinkingBudget).toBe(1024); + }); + + it("should map reasoning_effort: medium to thinkingBudget: 10240", () => { + const body = { + model: "custom-model", + messages: [{ role: "user", content: "hi" }], + safetySettings: [], + reasoning_effort: "medium", + }; + const result = openaiToGeminiRequest("custom-model", body, false); + // medium falls back to getDefaultThinkingBudget which may return 8192 + expect(result.generationConfig?.thinkingConfig?.thinkingBudget).toBeGreaterThanOrEqual(1024); + }); + + it("should map reasoning_effort: high to thinkingBudget: 24576", () => { + const body = { + model: "gemini/gemini-2.5-flash", + messages: [{ role: "user", content: "hi" }], + safetySettings: [], + reasoning_effort: "high", + }; + const result = openaiToGeminiRequest("gemini/gemini-2.5-flash", body, false); + expect(result.generationConfig?.thinkingConfig?.thinkingBudget).toBe(24576); + }); + }); +}); diff --git a/open-sse/translator/request/openai-to-gemini.ts b/open-sse/translator/request/openai-to-gemini.ts index 2d3fb8811f..a45fb3232d 100644 --- a/open-sse/translator/request/openai-to-gemini.ts +++ b/open-sse/translator/request/openai-to-gemini.ts @@ -195,14 +195,17 @@ function openaiToGeminiBase( if (model.startsWith("gemma-4")) { // gemma-4 models returns - 400: Thinking budget is not supported for this model } else { - // 1. OpenAI format: reasoning_effort (low/medium/high/auto/max/xhigh) + // 1. OpenAI format: reasoning_effort (none/low/medium/high/auto/max/xhigh) // "auto", "max", and "xhigh" are clamped to the high-tier budget because Gemini // does not accept these strings directly. "auto" signals "use max reasonable effort" // which maps to high. "max"/"xhigh" exceed Gemini's accepted range and are clamped. + // "none" maps to budget 0 — an explicit, documented off-switch (#6813 defect 2), + // distinct from the no-knob-at-all default-injection case below (#4170). // Port of decolua/9router#2043 by @nguyenxvotanminh3. if (body.reasoning_effort) { const highBudget = capThinkingBudget(model, 32768); const budgetMap: Record = { + none: 0, low: 1024, medium: getDefaultThinkingBudget(model) || 8192, high: highBudget, @@ -214,7 +217,7 @@ function openaiToGeminiBase( budgetMap[body.reasoning_effort as string] ?? getDefaultThinkingBudget(model) ?? 8192; result.generationConfig.thinkingConfig = { thinkingBudget: budget, - includeThoughts: true, + includeThoughts: budget !== 0, }; } // 2. Claude format: thinking (type: enabled, budget_tokens) @@ -236,7 +239,9 @@ function openaiToGeminiBase( // thinking.type), still set includeThoughts so the upstream marks thought // parts with thought:true. Without this, the model's reasoning leaks into // visible content instead of being routed to reasoning_content by the - // response translator. (#4170) + // response translator. (#4170) — this default-injection case is intentionally + // unconditional (no-knob-at-all still gets includeThoughts:true); the explicit + // "reasoning_effort: none" off-switch above (#6813) is the supported opt-out. if (!result.generationConfig.thinkingConfig) { const modelLower = model.toLowerCase(); if (