mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
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:
@@ -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": {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user