mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-06 07:12:12 +03:00
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:
committed by
GitHub
parent
dfbc89f97b
commit
2e75ed28a4
@@ -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 = {
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
38
tests/unit/openai-to-claude-thinking-budget-split.test.ts
Normal file
38
tests/unit/openai-to-claude-thinking-budget-split.test.ts
Normal 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);
|
||||
});
|
||||
Reference in New Issue
Block a user