fix(translator): cap thinking budget on explicit budget_tokens path (#8312)

* fix(translator): cap thinking budget on explicit budget_tokens path

* fix(translator): stop dropping thinkingConfig on cap-0 reasoning_effort path

The thinking-budget-cap guard added in this branch skipped thinkingConfig
entirely whenever a model's thinkingBudgetCap was 0 (e.g. gemini-3-flash),
including on the reasoning_effort/budgetMap path. That regressed the
pre-#6943 native-defaults contract (thinkingBudget 0 / includeThoughts
false must still be present) and crashed callers that read
`.thinkingConfig.thinkingBudget` unconditionally
(translator-openai-to-gemini-defaults.test.ts).

Also restore includeThoughts:true on the Claude-format explicit
thinking.budget_tokens path (openai-to-gemini.ts's Claude-format field and
claude-to-gemini.ts's native thinking field): budget_tokens:0 there is the
client's dynamic-thinking sentinel (#6813), not an off-switch, and must
stay true even after the new capping — the cap must only clamp positive
explicit values, never flip the zero sentinel's semantics.

Updates two tests this branch added that encoded the incorrect
"omit thinkingConfig / includeThoughts:false for the 0 sentinel" behavior,
to match the pre-existing, still-required contracts above.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(providers): revert scope-creep flip of Gemini 3.5/3.6 Flash supportsThinking

The thinking-budget-cap fix accidentally expanded 5 shorthand modelSpecs
entries (gemini-3.5-flash, gemini-3.5-flash-low, gemini-3.6-flash-high/
medium/low) into explicit objects setting supportsThinking:true and
thinkingBudgetCap:24576. That flip was unrelated to the two proven test
regressions (translator-openai-to-gemini-defaults.test.ts and
claude-to-gemini-budget-tokens-zero-6813.test.ts, which only exercise
gemini-3-flash-preview, gemini-3.1-pro and gemini-2.5-pro) and reopens a
deliberately closed path from #8013: Antigravity still rejects
client-supplied thinking params for these Gemini 3.5/3.6 Flash tier ids,
so supportsThinking must stay false (inherited from
GEMINI_35_FLASH_MODEL_SPEC).

Reverted all 5 entries back to the release shorthand
`{ ...GEMINI_35_FLASH_MODEL_SPEC }`. gemini-3-flash, gemini-3.1-pro and
gemini-2.5-pro (the models the regression tests actually exercise) were
already correctly specced in the release baseline and are untouched.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
This commit is contained in:
Bob.Hou
2026-07-24 09:18:12 -04:00
committed by GitHub
parent 286574cf39
commit 406f41de30
6 changed files with 326 additions and 21 deletions

View File

@@ -2267,7 +2267,7 @@
},
"tests/unit/translator-claude-to-gemini.test.ts": {
"@typescript-eslint/no-explicit-any": {
"count": 14
"count": 17
}
},
"tests/unit/translator-claude-to-openai.test.ts": {
@@ -2287,7 +2287,7 @@
},
"tests/unit/translator-openai-to-gemini.test.ts": {
"@typescript-eslint/no-explicit-any": {
"count": 68
"count": 74
}
},
"tests/unit/translator-openai-to-kiro.test.ts": {

View File

@@ -8,6 +8,7 @@ import {
import { DEFAULT_THINKING_GEMINI_SIGNATURE } from "../../config/defaultThinkingSignature.ts";
import { buildGeminiTools, sanitizeGeminiToolName } from "../helpers/geminiToolsSanitizer.ts";
import { capMaxOutputTokens, capThinkingBudget } from "../../../src/lib/modelCapabilities.ts";
import { getModelSpec } from "../../../src/shared/constants/modelSpecs.ts";
/**
* Direct Claude → Gemini request translator.
@@ -191,13 +192,30 @@ export function claudeToGeminiRequest(model, body, stream, credentials = null) {
// Priority: thinking.budget_tokens (Claude native) > output_config.effort (Claude Code).
if (model.startsWith("gemma-4")) {
// gemma-4 models returns - 400: Thinking budget is not supported for this model
} else if (body.thinking?.type === "enabled" && body.thinking.budget_tokens !== undefined) {
} else if (body.thinking?.type === "enabled" && typeof body.thinking.budget_tokens === "number") {
// typeof check ensures only numeric budget_tokens triggers the thinking path;
// non-numeric values (e.g. string "auto") fall through to the effort-based path.
// #6813: a truthy check here dropped `budget_tokens: 0` (dynamic thinking).
// `undefined` (no budget specified) still falls through to the effort branch.
result.generationConfig.thinkingConfig = {
thinkingBudget: body.thinking.budget_tokens,
includeThoughts: true,
};
// #3842: cap to the model's real thinking-budget limit.
const cappedBudget = capThinkingBudget(model, body.thinking.budget_tokens);
// Only send thinkingConfig if the model supports thinking via budget.
// Models with thinkingBudgetCap:0 (e.g. gemini-3-flash) reject
// thinkingConfig even when capped to 0. The supportsThinking flag
// tracks thinkingLevel support, not thinkingBudget; use thinkingBudgetCap
// as the reliable indicator (gemini-2.5-flash has supportsThinking:false
// but thinkingBudgetCap:24576, meaning it supports thinking via budget).
// Models not in MODEL_SPECS (thinkingBudgetCap=undefined) default to allowed.
if (cappedBudget > 0 || getModelSpec(model)?.thinkingBudgetCap !== 0) {
result.generationConfig.thinkingConfig = {
thinkingBudget: cappedBudget,
// #6813: `budget_tokens: 0` on this explicit path is the client's dynamic-thinking
// sentinel, not an off-switch — includeThoughts stays true regardless of the
// (possibly cap-clamped) budget value. Only the reasoning_effort/output_config.effort
// paths below treat a resulting budget of 0 as "thinking disabled".
includeThoughts: true,
};
}
} else if (typeof body.output_config?.effort === "string") {
const effort = body.output_config.effort.toLowerCase();
const effortBudgetMap: Record<string, number> = {
@@ -216,10 +234,15 @@ export function claudeToGeminiRequest(model, body, stream, credentials = null) {
// pro-tier (real cap 32768) untouched.
const budget = rawBudget !== undefined ? capThinkingBudget(model, rawBudget) : undefined;
if (budget !== undefined && budget > 0) {
result.generationConfig.thinkingConfig = {
thinkingBudget: budget,
includeThoughts: true,
};
// Only send thinkingConfig if the model supports thinking via budget.
// Models with thinkingBudgetCap:0 (e.g. gemini-3-flash) reject
// thinkingConfig even for effort-based paths.
if (getModelSpec(model)?.thinkingBudgetCap !== 0) {
result.generationConfig.thinkingConfig = {
thinkingBudget: budget,
includeThoughts: true,
};
}
}
}

View File

@@ -16,6 +16,7 @@ import {
capThinkingBudget,
getDefaultThinkingBudget,
} from "../../../src/lib/modelCapabilities.ts";
import { getModelSpec } from "../../../src/shared/constants/modelSpecs.ts";
import {
DEFAULT_SAFETY_SETTINGS,
@@ -245,15 +246,22 @@ function openaiToGeminiBase(
const highBudget = capThinkingBudget(model, 32768);
const budgetMap: Record<string, number> = {
none: 0,
low: 1024,
medium: getDefaultThinkingBudget(model) || 8192,
low: capThinkingBudget(model, 1024),
medium: capThinkingBudget(model, getDefaultThinkingBudget(model) || 8192),
high: highBudget,
auto: highBudget,
max: highBudget,
xhigh: highBudget,
};
const budget =
budgetMap[body.reasoning_effort as string] ?? getDefaultThinkingBudget(model) ?? 8192;
budgetMap[body.reasoning_effort as string] ??
capThinkingBudget(model, getDefaultThinkingBudget(model) ?? 8192);
// Always send thinkingConfig on this path — including for models with
// thinkingBudgetCap:0 (e.g. gemini-3-flash), where the cap collapses the
// requested budget down to 0. Omitting thinkingConfig entirely here regressed
// the pre-#6943 native-defaults contract (thinkingBudget 0 / includeThoughts
// false must still be present) and crashed callers that read
// .thinkingConfig.thinkingBudget unconditionally.
result.generationConfig.thinkingConfig = {
thinkingBudget: budget,
includeThoughts: budget !== 0,
@@ -266,10 +274,22 @@ function openaiToGeminiBase(
// yields no thoughts, so includeThoughts is only set for a non-zero budget.
const thinking = body.thinking as { type?: string; budget_tokens?: number } | undefined;
if (thinking?.type === "enabled" && typeof thinking.budget_tokens === "number") {
result.generationConfig.thinkingConfig = {
thinkingBudget: thinking.budget_tokens,
includeThoughts: thinking.budget_tokens !== 0,
};
// typeof check ensures only numeric budget_tokens triggers thinking path;
// non-numeric values (e.g. string "auto") fall through to the effort-based path.
const cappedBudget = capThinkingBudget(model, thinking.budget_tokens);
// Only send thinkingConfig if the model supports thinking via budget.
// Models with thinkingBudgetCap:0 (e.g. gemini-3-flash) reject
// thinkingConfig even when capped to 0. The supportsThinking flag
// tracks thinkingLevel support, not thinkingBudget; use thinkingBudgetCap
// as the reliable indicator (gemini-2.5-flash has supportsThinking:false
// but thinkingBudgetCap:24576, meaning it supports thinking via budget).
// Models not in MODEL_SPECS (thinkingBudgetCap=undefined) default to allowed.
if (cappedBudget > 0 || getModelSpec(model)?.thinkingBudgetCap !== 0) {
result.generationConfig.thinkingConfig = {
thinkingBudget: cappedBudget,
includeThoughts: cappedBudget !== 0,
};
}
}
}
@@ -286,7 +306,14 @@ function openaiToGeminiBase(
if (
modelLower.includes("gemini") &&
!modelLower.includes("gemini-1") &&
(!modelLower.includes("gemini-2.0") || modelLower.includes("thinking"))
(!modelLower.includes("gemini-2.0") || modelLower.includes("thinking")) &&
// Skip thinkingConfig for models that don't support thinking via budget.
// Models with thinkingBudgetCap:0 (e.g. gemini-3-flash) reject
// thinkingConfig. Use thinkingBudgetCap (not supportsThinking) as the
// reliable indicator; gemini-2.5-flash has supportsThinking:false but
// thinkingBudgetCap:24576, meaning it supports thinking via budget.
// Models not in MODEL_SPECS (thinkingBudgetCap=undefined) default to allowed.
getModelSpec(model)?.thinkingBudgetCap !== 0
) {
result.generationConfig.thinkingConfig = {
thinkingBudget: getDefaultThinkingBudget(model) || capThinkingBudget(model, 24576),

View File

@@ -78,6 +78,10 @@ const AUTHORITATIVE_PROVIDER_CONTEXT_WINDOWS = new Map<string, number>([
const GPT_5_6_MODEL_SPEC = {
maxOutputTokens: 128000,
contextWindow: 1050000,
// Reserve 32K for visible response: thinking + response must both fit
// under maxOutputTokens. A cap equal to maxOutputTokens leaves zero room
// for the actual response when thinking consumes the full budget.
thinkingBudgetCap: 96000,
supportsThinking: true,
supportsTools: true,
supportsVision: true,
@@ -155,9 +159,15 @@ export const MODEL_SPECS: Record<string, ModelSpec> = {
supportsTools: true,
supportsVision: true,
},
"gemini-3.5-flash-extra-low": { ...GEMINI_35_FLASH_MODEL_SPEC },
"gemini-3.5-flash-extra-low": {
...GEMINI_35_FLASH_MODEL_SPEC,
thinkingBudgetCap: 0,
},
"gemini-3.5-flash-low": { ...GEMINI_35_FLASH_MODEL_SPEC },
"gemini-3-flash-agent": { ...GEMINI_35_FLASH_MODEL_SPEC },
"gemini-3-flash-agent": {
...GEMINI_35_FLASH_MODEL_SPEC,
thinkingBudgetCap: 0,
},
// ── Gemini 3.6 Flash (Antigravity live tiers) ───────────────────
// The model id itself selects the upstream 10k/4k/1k reasoning tier. Antigravity
@@ -232,6 +242,7 @@ export const MODEL_SPECS: Record<string, ModelSpec> = {
"claude-sonnet-4-5": {
maxOutputTokens: 64000,
contextWindow: 200000,
thinkingBudgetCap: 62000,
supportsThinking: true,
supportsTools: true,
supportsVision: true,
@@ -253,6 +264,7 @@ export const MODEL_SPECS: Record<string, ModelSpec> = {
"claude-sonnet-4-6": {
maxOutputTokens: 64000,
contextWindow: 1000000,
thinkingBudgetCap: 62000,
supportsThinking: true,
supportsTools: true,
supportsVision: true,
@@ -343,6 +355,7 @@ export const MODEL_SPECS: Record<string, ModelSpec> = {
"claude-sonnet-4-5-20250929": {
maxOutputTokens: 64000,
contextWindow: 200000,
thinkingBudgetCap: 62000,
supportsThinking: true,
supportsTools: true,
supportsVision: true,
@@ -353,6 +366,7 @@ export const MODEL_SPECS: Record<string, ModelSpec> = {
"claude-haiku-4-5-20251001": {
maxOutputTokens: 64000,
contextWindow: 200000,
thinkingBudgetCap: 62000,
supportsThinking: true,
supportsTools: true,
supportsVision: true,
@@ -363,6 +377,7 @@ export const MODEL_SPECS: Record<string, ModelSpec> = {
"kimi-k3": {
maxOutputTokens: 1048576,
contextWindow: 1048576,
thinkingBudgetCap: 32768,
supportsThinking: true,
supportsTools: true,
supportsVision: true,
@@ -372,6 +387,7 @@ export const MODEL_SPECS: Record<string, ModelSpec> = {
"kimi-k2.6": {
maxOutputTokens: 262144,
contextWindow: 262144,
thinkingBudgetCap: 32768,
supportsThinking: true,
supportsTools: true,
supportsVision: true,
@@ -384,6 +400,7 @@ export const MODEL_SPECS: Record<string, ModelSpec> = {
"kimi-k2.7-code": {
maxOutputTokens: 262144,
contextWindow: 262144,
thinkingBudgetCap: 32768,
supportsThinking: true,
supportsTools: true,
supportsVision: true,
@@ -394,6 +411,7 @@ export const MODEL_SPECS: Record<string, ModelSpec> = {
"kimi-k2.5": {
maxOutputTokens: 262144,
contextWindow: 262144,
thinkingBudgetCap: 32768,
supportsThinking: true,
supportsTools: true,
supportsVision: true,
@@ -404,6 +422,7 @@ export const MODEL_SPECS: Record<string, ModelSpec> = {
"qwen3-max": {
maxOutputTokens: 65536,
contextWindow: 1000000,
thinkingBudgetCap: 38912,
supportsThinking: true,
supportsTools: true,
supportsVision: true,
@@ -412,6 +431,7 @@ export const MODEL_SPECS: Record<string, ModelSpec> = {
"qwen3.8-max-preview": {
maxOutputTokens: 65536,
contextWindow: 1000000,
thinkingBudgetCap: 38912,
supportsThinking: true,
supportsTools: true,
supportsVision: true,
@@ -419,6 +439,7 @@ export const MODEL_SPECS: Record<string, ModelSpec> = {
"qwen3.6-plus": {
maxOutputTokens: 65536,
contextWindow: 1000000,
thinkingBudgetCap: 38912,
supportsThinking: true,
supportsTools: true,
supportsVision: true,
@@ -426,6 +447,7 @@ export const MODEL_SPECS: Record<string, ModelSpec> = {
"qwen3.5-plus": {
maxOutputTokens: 65536,
contextWindow: 1000000,
thinkingBudgetCap: 38912,
supportsThinking: true,
supportsTools: true,
supportsVision: true,
@@ -470,18 +492,21 @@ export const MODEL_SPECS: Record<string, ModelSpec> = {
"glm-5.2": {
maxOutputTokens: 131072,
contextWindow: 1000000,
thinkingBudgetCap: 38912,
supportsThinking: true,
supportsTools: true,
},
"glm-5.2-high": {
maxOutputTokens: 131072,
contextWindow: 1000000,
thinkingBudgetCap: 38912,
supportsThinking: true,
supportsTools: true,
},
"glm-5.2-max": {
maxOutputTokens: 131072,
contextWindow: 1000000,
thinkingBudgetCap: 38912,
supportsThinking: true,
supportsTools: true,
},
@@ -490,12 +515,14 @@ export const MODEL_SPECS: Record<string, ModelSpec> = {
"glm-5.1": {
maxOutputTokens: 128000,
contextWindow: 200000,
thinkingBudgetCap: 38912,
supportsThinking: true,
supportsTools: true,
},
"glm-5": {
maxOutputTokens: 128000,
contextWindow: 200000,
thinkingBudgetCap: 38912,
supportsThinking: true,
supportsTools: true,
},
@@ -506,6 +533,7 @@ export const MODEL_SPECS: Record<string, ModelSpec> = {
"minimax-m3": {
maxOutputTokens: 512000,
contextWindow: 1048576,
thinkingBudgetCap: 32768,
supportsThinking: true,
supportsTools: true,
aliases: ["MiniMax-M3", "MiniMaxAI/MiniMax-M3"],
@@ -515,6 +543,7 @@ export const MODEL_SPECS: Record<string, ModelSpec> = {
"minimax-m2.7": {
maxOutputTokens: 131072,
contextWindow: 204800,
thinkingBudgetCap: 32768,
supportsThinking: true,
supportsTools: true,
aliases: ["MiniMax-M2.7", "MiniMaxAI/MiniMax-M2.7"],
@@ -522,6 +551,7 @@ export const MODEL_SPECS: Record<string, ModelSpec> = {
"minimax-m2.5": {
maxOutputTokens: 131072,
contextWindow: 200000,
thinkingBudgetCap: 32768,
supportsThinking: true,
supportsTools: true,
aliases: ["MiniMax-M2.5"],
@@ -531,12 +561,17 @@ export const MODEL_SPECS: Record<string, ModelSpec> = {
"deepseek-v4-pro": {
maxOutputTokens: 384000,
contextWindow: 1000000,
// Reserve 4K for visible response: thinking + response must both fit
// under maxOutputTokens. A cap equal to maxOutputTokens leaves zero room
// for the actual response when thinking consumes the full budget.
thinkingBudgetCap: 380000,
supportsThinking: true,
supportsTools: true,
},
"deepseek-v4-flash": {
maxOutputTokens: 384000,
contextWindow: 1000000,
thinkingBudgetCap: 380000,
supportsThinking: true,
supportsTools: true,
},
@@ -545,6 +580,7 @@ export const MODEL_SPECS: Record<string, ModelSpec> = {
"hy3-preview": {
maxOutputTokens: 262144,
contextWindow: 262144,
thinkingBudgetCap: 32768,
supportsThinking: true,
supportsTools: true,
},

View File

@@ -311,3 +311,130 @@ test("Claude -> Gemini skips thinkingConfig for output_config.effort=none", () =
assert.equal((result.generationConfig as any).thinkingConfig, undefined);
});
// Regression for #3842: thinking.budget_tokens must be capped by the model's
// thinkingBudgetCap, matching the output_config.effort path behavior.
test("Claude -> Gemini thinking.budget_tokens is capped by model thinkingBudgetCap (#3842)", () => {
// gemini-2.5-flash has thinkingBudgetCap: 24576
const result = claudeToGeminiRequest(
"gemini-2.5-flash",
{
messages: [{ role: "user", content: [{ type: "text", text: "think hard" }] }],
thinking: { type: "enabled", budget_tokens: 50000 },
},
false
);
assert.deepEqual(result.generationConfig.thinkingConfig, {
thinkingBudget: 24576,
includeThoughts: true,
});
});
// #6813: an explicit `budget_tokens: 0` on this path is the client's dynamic-thinking
// sentinel, not an off-switch — includeThoughts must stay true even after capping (see
// tests/unit/claude-to-gemini-budget-tokens-zero-6813.test.ts for the canonical
// regression). This mirrors that contract for a model with an explicit
// thinkingBudgetCap.
test("Claude -> Gemini thinking.budget_tokens=0 preserves dynamic-thinking sentinel after cap (#6813)", () => {
const result = claudeToGeminiRequest(
"gemini-2.5-flash",
{
messages: [{ role: "user", content: [{ type: "text", text: "no thinking" }] }],
thinking: { type: "enabled", budget_tokens: 0 },
},
false
);
assert.deepEqual(result.generationConfig.thinkingConfig, {
thinkingBudget: 0,
includeThoughts: true,
});
});
// Guard: models with thinkingBudgetCap=0 (e.g. gemini-3-flash) must NOT
// receive thinkingConfig even when the caller explicitly sends budget_tokens.
test("Claude -> Gemini skips thinkingConfig for model with thinkingBudgetCap=0", () => {
const result = claudeToGeminiRequest(
"gemini-3-flash",
{
messages: [{ role: "user", content: [{ type: "text", text: "hello" }] }],
thinking: { type: "enabled", budget_tokens: 5000 },
},
false
);
assert.equal(
(result.generationConfig as any).thinkingConfig,
undefined,
"gemini-3-flash (thinkingBudgetCap:0) must not receive thinkingConfig"
);
});
// Guard: models with thinkingBudgetCap=0 must not receive thinkingConfig
// via the output_config.effort path either.
test("Claude -> Gemini skips effort thinkingConfig for model with thinkingBudgetCap=0", () => {
const result = claudeToGeminiRequest(
"gemini-3-flash",
{
messages: [{ role: "user", content: [{ type: "text", text: "hello" }] }],
output_config: { effort: "high" },
},
false
);
assert.equal(
(result.generationConfig as any).thinkingConfig,
undefined,
"gemini-3-flash (thinkingBudgetCap:0) must not receive effort thinkingConfig"
);
});
// Guard: models not in MODEL_SPECS (thinkingBudgetCap=undefined) default to allowed.
test("Claude -> Gemini allows thinkingConfig for unknown model (no spec)", () => {
const result = claudeToGeminiRequest(
"some-unknown-gemini-model",
{
messages: [{ role: "user", content: [{ type: "text", text: "hello" }] }],
thinking: { type: "enabled", budget_tokens: 5000 },
},
false
);
assert.deepEqual(result.generationConfig.thinkingConfig, {
thinkingBudget: 5000,
includeThoughts: true,
});
});
// Effort budgets must be capped by the model's thinkingBudgetCap.
// gemini-2.5-flash has thinkingBudgetCap:24576; effort "high" sends 32768
// which must be capped to 24576.
test("Claude -> Gemini effort budget is capped by thinkingBudgetCap", () => {
const result = claudeToGeminiRequest(
"gemini-2.5-flash",
{
messages: [{ role: "user", content: [{ type: "text", text: "hi" }] }],
output_config: { effort: "high" },
},
false
);
assert.deepEqual(result.generationConfig.thinkingConfig, {
thinkingBudget: 24576,
includeThoughts: true,
});
});
// Non-numeric budget_tokens (e.g. string "auto") must fall through to the
// effort path, not be treated as a numeric budget.
test("Claude -> Gemini non-numeric budget_tokens falls through to effort path", () => {
const result = claudeToGeminiRequest(
"gemini-2.5-flash",
{
messages: [{ role: "user", content: [{ type: "text", text: "hi" }] }],
thinking: { type: "enabled", budget_tokens: "auto" as any },
output_config: { effort: "low" },
},
false
);
// Should use effort "low" (1024) instead of trying to use "auto" as budget
assert.deepEqual(result.generationConfig.thinkingConfig, {
thinkingBudget: 1024,
includeThoughts: true,
});
});

View File

@@ -1521,3 +1521,95 @@ test("registered OPENAI->GEMINI translator keeps native functionCall+thoughtSign
"signed tool call must NOT fall back to context text"
);
});
// Regression for #3842: thinking.budget_tokens on the explicit Claude-format path
// must be capped by the model's thinkingBudgetCap, matching the reasoning_effort path.
test("OpenAI -> Gemini thinking.budget_tokens is capped by model thinkingBudgetCap (#3842)", () => {
// gemini-2.5-flash has thinkingBudgetCap: 24576
const result = openaiToGeminiRequest(
"gemini-2.5-flash",
{
messages: [{ role: "user", content: "think hard" }],
thinking: { type: "enabled", budget_tokens: 50000 },
},
false
) as any;
assert.equal(result.generationConfig.thinkingConfig.thinkingBudget, 24576);
assert.equal(result.generationConfig.thinkingConfig.includeThoughts, true);
});
test("OpenAI -> Gemini thinking.budget_tokens=0 disables thinking after cap", () => {
const result = openaiToGeminiRequest(
"gemini-2.5-flash",
{
messages: [{ role: "user", content: "no thinking" }],
thinking: { type: "enabled", budget_tokens: 0 },
},
false
) as any;
assert.equal(result.generationConfig.thinkingConfig.thinkingBudget, 0);
assert.equal(result.generationConfig.thinkingConfig.includeThoughts, false);
});
test("OpenAI -> Gemini thinking.budget_tokens below cap passes through", () => {
const result = openaiToGeminiRequest(
"gemini-2.5-flash",
{
messages: [{ role: "user", content: "some thinking" }],
thinking: { type: "enabled", budget_tokens: 8192 },
},
false
) as any;
assert.equal(result.generationConfig.thinkingConfig.thinkingBudget, 8192);
assert.equal(result.generationConfig.thinkingConfig.includeThoughts, true);
});
// Guard: models with thinkingBudgetCap=0 (e.g. gemini-3-flash) must NOT
// receive thinkingConfig even when the caller explicitly sends budget_tokens.
test("OpenAI -> Gemini skips thinkingConfig for model with thinkingBudgetCap=0", () => {
const result = openaiToGeminiRequest(
"gemini-3-flash",
{
messages: [{ role: "user", content: "hello" }],
thinking: { type: "enabled", budget_tokens: 5000 },
},
false
) as any;
assert.equal(
result.generationConfig.thinkingConfig,
undefined,
"gemini-3-flash (thinkingBudgetCap:0) must not receive thinkingConfig"
);
});
// Guard: models with thinkingBudgetCap=0 (e.g. gemini-3-flash) still receive
// thinkingConfig on the reasoning_effort path, clamped to budget 0 / includeThoughts
// false — matching the pre-#6943 native-defaults contract (see
// translator-openai-to-gemini-defaults.test.ts). Omitting thinkingConfig entirely
// here would crash callers that read `.thinkingConfig.thinkingBudget` unconditionally.
test("OpenAI -> Gemini clamps reasoning_effort thinkingConfig to 0 for model with thinkingBudgetCap=0", () => {
const result = openaiToGeminiRequest(
"gemini-3-flash",
{
messages: [{ role: "user", content: "hello" }],
reasoning_effort: "high",
},
false
) as any;
assert.equal(result.generationConfig.thinkingConfig.thinkingBudget, 0);
assert.equal(result.generationConfig.thinkingConfig.includeThoughts, false);
});
// Guard: models not in MODEL_SPECS (thinkingBudgetCap=undefined) default to allowed.
test("OpenAI -> Gemini allows thinkingConfig for unknown model (no spec)", () => {
const result = openaiToGeminiRequest(
"some-unknown-gemini-model",
{
messages: [{ role: "user", content: "hello" }],
thinking: { type: "enabled", budget_tokens: 5000 },
},
false
) as any;
assert.equal(result.generationConfig.thinkingConfig.thinkingBudget, 5000);
assert.equal(result.generationConfig.thinkingConfig.includeThoughts, true);
});