fix(6813): fix thinking budget zero drop and default thinkingConfig injection (#6943)

* 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>
This commit is contained in:
Rafael Dias Zendron
2026-07-12 10:49:07 -03:00
committed by GitHub
parent d2d4cf0e17
commit 38d6cd9955
3 changed files with 106 additions and 3 deletions

View File

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

View File

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

View File

@@ -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<string, number> = {
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 (