refactor(translator): extract thinking-budget fitting from openai-to-claude (#5932)

Extract the thinking-budget fitting cluster (fitThinkingToMaxTokens +
private safeCapMaxOutputTokens + MIN_* constants) verbatim into the pure
leaf openai-to-claude/thinkingBudget.ts. Host re-exports fitThinkingToMaxTokens
so external importers keep working and imports it back for internal use.

Host 822 -> 738 LOC (under the 800 cap). No behavior change: byte-identical
bodies, public export set unchanged. Adds a split-guard test; all consumer
tests stay green (translator-openai-to-claude, strip-empty, minimax-m3, passthrough).
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-07-02 14:15:39 -03:00
committed by GitHub
parent dfbc89f97b
commit 2e75ed28a4
3 changed files with 131 additions and 88 deletions

View File

@@ -6,8 +6,8 @@ import { adjustMaxTokens } from "../helpers/maxTokensHelper.ts";
import { sanitizeToolId } from "../helpers/schemaCoercion.ts";
import { safeParseJSON } from "../helpers/jsonUtil.ts";
import { DEFAULT_THINKING_CLAUDE_SIGNATURE } from "../../config/defaultThinkingSignature.ts";
import { capMaxOutputTokens } from "../../../src/lib/modelCapabilities.ts";
import { isAdaptiveThinkingOnly } from "../../../src/shared/constants/modelSpecs.ts";
import { fitThinkingToMaxTokens } from "./openai-to-claude/thinkingBudget.ts";
// Reasoning-effort levels Anthropic accepts on `output_config.effort`. Used to steer
// adaptive-only Claude models (Opus 4.7+/Fable 5) without ever emitting a manual budget.
@@ -36,93 +36,9 @@ function applyCopilotSummarizedThinkingDisplay(
};
}
// Anthropic constraints for the thinking + max_tokens contract:
// - thinking.budget_tokens must be >= 1024 when thinking is enabled
// - max_tokens must be > thinking.budget_tokens (covers thinking + response)
// - max_tokens must be <= model output cap (e.g. 128000 for Opus 4.7)
const MIN_CLAUDE_THINKING_BUDGET = 1024;
const MIN_RESPONSE_ROOM = 1024;
function safeCapMaxOutputTokens(model: string): number | null {
try {
const cap = capMaxOutputTokens(model);
return typeof cap === "number" && cap > 0 ? cap : null;
} catch {
return null;
}
}
/**
* Fit Claude thinking budget within the model's max output cap.
*
* Replaces the previous unconditional `max_tokens = budget + 8192` inflation,
* which could exceed the model output cap (e.g. Opus 4.7's 128000 ceiling) and
* trigger HTTP 400 from Anthropic ("max_tokens > 128000").
*
* Strategy (preserves caller intent up to the model cap):
* - Preserve caller's max_tokens as response room (floored to MIN_RESPONSE_ROOM)
* - Target max_tokens = responseRoom + requestedBudget, capped at modelCap
* - fittedBudget = max_tokens - responseRoom (the thinking budget actually used)
* - If the cap squeezes fittedBudget below the Anthropic minimum, retry with
* responseRoom shrunk to MIN_RESPONSE_ROOM; if still below MIN, disable
* thinking entirely (cap too tight for any reasoning).
*
* Worked example (real-world Opus 4.7 case that previously 400'd):
* caller max_tokens = 32000, reasoning_effort=high → budget = 131072,
* model cap = 128000.
* responseRoom = max(32000, 1024) = 32000
* target = min(32000 + 131072, 128000) = 128000
* fittedBudget = 128000 - 32000 = 96000 (>= 1024, OK)
* → max_tokens=128000, budget_tokens=96000 (vs. the old buggy 139264 / 131072).
*/
export function fitThinkingToMaxTokens(
model: string,
callerMaxTokens: number,
thinking: Record<string, unknown> | undefined
): { maxTokens: number; thinking: Record<string, unknown> | undefined } {
const modelCap = safeCapMaxOutputTokens(model);
const requestedBudget = Number(thinking?.budget_tokens) || 0;
// No budgeted thinking — just cap max_tokens to the model output ceiling.
if (!thinking || requestedBudget <= 0) {
return {
maxTokens:
modelCap === null
? Math.max(callerMaxTokens, 1)
: Math.min(Math.max(callerMaxTokens, 1), modelCap),
thinking,
};
}
let responseRoom = Math.max(callerMaxTokens, MIN_RESPONSE_ROOM);
let target =
modelCap === null
? responseRoom + requestedBudget
: Math.min(responseRoom + requestedBudget, modelCap);
let fittedBudget = target - responseRoom;
// If the cap squeezed thinking below Anthropic's floor, try shrinking
// response room to MIN_RESPONSE_ROOM to recover budget.
if (fittedBudget < MIN_CLAUDE_THINKING_BUDGET && responseRoom > MIN_RESPONSE_ROOM) {
responseRoom = MIN_RESPONSE_ROOM;
target =
modelCap === null
? responseRoom + requestedBudget
: Math.min(responseRoom + requestedBudget, modelCap);
fittedBudget = target - responseRoom;
}
// Cap too tight for any thinking — disable rather than send an invalid request.
if (fittedBudget < MIN_CLAUDE_THINKING_BUDGET) {
return { maxTokens: modelCap ?? Math.max(callerMaxTokens, 1), thinking: undefined };
}
const adjustedThinking: Record<string, unknown> = { ...thinking };
if (fittedBudget < requestedBudget) {
adjustedThinking.budget_tokens = fittedBudget;
}
return { maxTokens: target, thinking: adjustedThinking };
}
// Thinking-budget fitting extracted to a pure leaf; re-exported for external
// importers (tests). Host also uses fitThinkingToMaxTokens internally.
export { fitThinkingToMaxTokens } from "./openai-to-claude/thinkingBudget.ts";
type ClaudeContentBlock = Record<string, unknown>;
type ClaudeMessage = {

View File

@@ -0,0 +1,89 @@
import { capMaxOutputTokens } from "../../../../src/lib/modelCapabilities.ts";
// Anthropic constraints for the thinking + max_tokens contract:
// - thinking.budget_tokens must be >= 1024 when thinking is enabled
// - max_tokens must be > thinking.budget_tokens (covers thinking + response)
// - max_tokens must be <= model output cap (e.g. 128000 for Opus 4.7)
const MIN_CLAUDE_THINKING_BUDGET = 1024;
const MIN_RESPONSE_ROOM = 1024;
function safeCapMaxOutputTokens(model: string): number | null {
try {
const cap = capMaxOutputTokens(model);
return typeof cap === "number" && cap > 0 ? cap : null;
} catch {
return null;
}
}
/**
* Fit Claude thinking budget within the model's max output cap.
*
* Replaces the previous unconditional `max_tokens = budget + 8192` inflation,
* which could exceed the model output cap (e.g. Opus 4.7's 128000 ceiling) and
* trigger HTTP 400 from Anthropic ("max_tokens > 128000").
*
* Strategy (preserves caller intent up to the model cap):
* - Preserve caller's max_tokens as response room (floored to MIN_RESPONSE_ROOM)
* - Target max_tokens = responseRoom + requestedBudget, capped at modelCap
* - fittedBudget = max_tokens - responseRoom (the thinking budget actually used)
* - If the cap squeezes fittedBudget below the Anthropic minimum, retry with
* responseRoom shrunk to MIN_RESPONSE_ROOM; if still below MIN, disable
* thinking entirely (cap too tight for any reasoning).
*
* Worked example (real-world Opus 4.7 case that previously 400'd):
* caller max_tokens = 32000, reasoning_effort=high → budget = 131072,
* model cap = 128000.
* responseRoom = max(32000, 1024) = 32000
* target = min(32000 + 131072, 128000) = 128000
* fittedBudget = 128000 - 32000 = 96000 (>= 1024, OK)
* → max_tokens=128000, budget_tokens=96000 (vs. the old buggy 139264 / 131072).
*/
export function fitThinkingToMaxTokens(
model: string,
callerMaxTokens: number,
thinking: Record<string, unknown> | undefined
): { maxTokens: number; thinking: Record<string, unknown> | undefined } {
const modelCap = safeCapMaxOutputTokens(model);
const requestedBudget = Number(thinking?.budget_tokens) || 0;
// No budgeted thinking — just cap max_tokens to the model output ceiling.
if (!thinking || requestedBudget <= 0) {
return {
maxTokens:
modelCap === null
? Math.max(callerMaxTokens, 1)
: Math.min(Math.max(callerMaxTokens, 1), modelCap),
thinking,
};
}
let responseRoom = Math.max(callerMaxTokens, MIN_RESPONSE_ROOM);
let target =
modelCap === null
? responseRoom + requestedBudget
: Math.min(responseRoom + requestedBudget, modelCap);
let fittedBudget = target - responseRoom;
// If the cap squeezed thinking below Anthropic's floor, try shrinking
// response room to MIN_RESPONSE_ROOM to recover budget.
if (fittedBudget < MIN_CLAUDE_THINKING_BUDGET && responseRoom > MIN_RESPONSE_ROOM) {
responseRoom = MIN_RESPONSE_ROOM;
target =
modelCap === null
? responseRoom + requestedBudget
: Math.min(responseRoom + requestedBudget, modelCap);
fittedBudget = target - responseRoom;
}
// Cap too tight for any thinking — disable rather than send an invalid request.
if (fittedBudget < MIN_CLAUDE_THINKING_BUDGET) {
return { maxTokens: modelCap ?? Math.max(callerMaxTokens, 1), thinking: undefined };
}
const adjustedThinking: Record<string, unknown> = { ...thinking };
if (fittedBudget < requestedBudget) {
adjustedThinking.budget_tokens = fittedBudget;
}
return { maxTokens: target, thinking: adjustedThinking };
}

View File

@@ -0,0 +1,38 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";
// Split-guard for the openai-to-claude thinking-budget extraction.
// `fitThinkingToMaxTokens` (+ its private helpers safeCapMaxOutputTokens / MIN_*)
// live in the pure leaf `openai-to-claude/thinkingBudget.ts`; the host re-exports
// the public symbol so external importers (tests) keep working unchanged.
const HERE = dirname(fileURLToPath(import.meta.url));
const REQ = join(HERE, "../../open-sse/translator/request");
const HOST = join(REQ, "openai-to-claude.ts");
const LEAF = join(REQ, "openai-to-claude/thinkingBudget.ts");
test("leaf hosts fitThinkingToMaxTokens and does not import the host", () => {
const leaf = readFileSync(LEAF, "utf8");
assert.match(leaf, /export function fitThinkingToMaxTokens\(/);
assert.match(leaf, /function safeCapMaxOutputTokens\(/);
assert.doesNotMatch(leaf, /from "\.\.\/openai-to-claude\.ts"/);
});
test("host re-exports fitThinkingToMaxTokens from the leaf", () => {
const host = readFileSync(HOST, "utf8");
assert.match(
host,
/export \{ fitThinkingToMaxTokens \} from "\.\/openai-to-claude\/thinkingBudget\.ts"/
);
});
test("re-exported fitThinkingToMaxTokens is callable via the host module and behaves", async () => {
const mod = await import("../../open-sse/translator/request/openai-to-claude.ts");
assert.equal(typeof mod.fitThinkingToMaxTokens, "function");
// No budgeted thinking → max_tokens floored to >= 1, thinking passed through.
const out = mod.fitThinkingToMaxTokens("gpt-4o-mini", 0, undefined);
assert.equal(out.thinking, undefined);
assert.ok(out.maxTokens >= 1);
});