mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-07 15:52:52 +03:00
Squash merge from local merge-train (Hard Rule owner-approved). Tip 029cdf4215cf465f0e1716ac9f84a84692b1e881 validated on 192.168.0.113: 26631/26653 pass.
114 lines
4.8 KiB
TypeScript
114 lines
4.8 KiB
TypeScript
/**
|
|
* Learned Thinking-Budget Caps — reactive cap memory for Gemini-family models.
|
|
*
|
|
* Proactive clamping (`capThinkingBudget` in src/lib/modelCapabilities.ts) only
|
|
* knows caps for models registered in MODEL_SPECS. Any Gemini model outside the
|
|
* registry (e.g. gemini-2.5-pro, or a Gemini id served through another provider)
|
|
* initially gets no cap, so a high client budget (xhigh = 131072) goes straight
|
|
* to the upstream and can bounce with:
|
|
*
|
|
* 400 GenerateContentRequest.generation_config.thinking_config.thinking_budget:
|
|
* thinking_budget must be in the range [-1, 65535]
|
|
*
|
|
* When base.ts's executor sees that 400 it calls `recordLearnedThinkingCap`,
|
|
* which walks the failed budget down a step ladder and stores the surviving cap
|
|
* in a module-level Map keyed "provider:model" (lowercased). Subsequent requests
|
|
* for the same provider+model read the cap via `getLearnedThinkingCap` (consulted
|
|
* by `capThinkingBudget`) so the 400→retry round-trip is paid at most once per
|
|
* process per provider+model — not once per chat session.
|
|
*
|
|
* In-memory only (per operator decision): restart resets, the first request after
|
|
* a restart may re-learn at the cost of one upstream 400.
|
|
*/
|
|
|
|
// Known Gemini thinking-budget ceilings, highest first. 32768 is the pro-tier
|
|
// cap; 24576 is the flash-tier cap (gemini-2.5-flash in MODEL_SPECS); 8192 is a
|
|
// conservative floor that every thinking-capable Gemini accepts. A failed budget
|
|
// walks to the first step strictly below it.
|
|
const GEMINI_STEPDOWN: readonly number[] = [32768, 24576, 8192];
|
|
|
|
/**
|
|
* Proactive cap applied by `capThinkingBudget` (src/lib/modelCapabilities.ts)
|
|
* when a model id contains "gemini" but has no registry entry and no learned cap.
|
|
* Set to the highest rung of the step ladder so unregistered Gemini models get
|
|
* the maximum reasoning the family is known to accept, rather than letting an
|
|
* xhigh budget (131072) through to a 400.
|
|
*/
|
|
export const GEMINI_FALLBACK_THINKING_CAP: number = GEMINI_STEPDOWN[0];
|
|
|
|
// key: `${provider}:${model}` lowercased → highest budget known to be accepted.
|
|
const learnedCaps = new Map<string, number>();
|
|
|
|
function buildKey(provider: string | null | undefined, model: string | null | undefined): string {
|
|
const p = typeof provider === "string" ? provider.trim().toLowerCase() : "";
|
|
const m = typeof model === "string" ? model.trim().toLowerCase() : "";
|
|
if (!p || !m) return "";
|
|
return `${p}:${m}`;
|
|
}
|
|
|
|
/**
|
|
* Return the learned cap for provider+model, or null when nothing has been
|
|
* learned yet (no upstream 400 recorded). Keyed case-insensitively.
|
|
*/
|
|
export function getLearnedThinkingCap(
|
|
provider: string | null | undefined,
|
|
model: string | null | undefined
|
|
): number | null {
|
|
const key = buildKey(provider, model);
|
|
if (!key) return null;
|
|
return learnedCaps.get(key) ?? null;
|
|
}
|
|
|
|
/**
|
|
* Record that `failedBudget` was rejected by the upstream for provider+model and
|
|
* store the next step down as the learned cap. Returns the new cap, or null when
|
|
* the step ladder is exhausted (even 8192 failed) or the key is unusable — the
|
|
* caller should then give up clamping and surface the upstream error.
|
|
*
|
|
* Always monotonically decreases: if a cap already stored is lower than the step
|
|
* we'd compute, the stored (lower) value wins and is returned unchanged. This
|
|
* keeps concurrent failures on the same provider+model from ratcheting the cap
|
|
* back up.
|
|
*/
|
|
export function recordLearnedThinkingCap(
|
|
provider: string | null | undefined,
|
|
model: string | null | undefined,
|
|
failedBudget: number
|
|
): number | null {
|
|
const key = buildKey(provider, model);
|
|
if (!key) return null;
|
|
if (!Number.isFinite(failedBudget)) return null;
|
|
|
|
const nextStep = GEMINI_STEPDOWN.find((step) => step < failedBudget);
|
|
if (nextStep === undefined) return null; // ladder exhausted
|
|
|
|
const existing = learnedCaps.get(key);
|
|
if (existing !== undefined && existing <= nextStep) {
|
|
return existing; // already learned an equal-or-lower cap; keep it
|
|
}
|
|
learnedCaps.set(key, nextStep);
|
|
return nextStep;
|
|
}
|
|
|
|
/**
|
|
* Extract the upstream-advertised maximum from a thinking_budget range error.
|
|
* Matches both snake_case and camelCase field namings:
|
|
* "thinking_budget must be in the range [-1, 65535]" → 65535
|
|
* "thinkingBudget must be in the range [-1, 24576]" → 24576
|
|
* Returns null when the text is not a thinking-budget range error.
|
|
*/
|
|
export function parseThinkingBudgetMax(errText: unknown): number | null {
|
|
if (typeof errText !== "string" || !errText) return null;
|
|
const match = /thinking_?budget[^\d-]*(?:must be in the range|range)[^\d-]*\[\s*-?\d+\s*,\s*(\d+)\s*\]/i.exec(
|
|
errText
|
|
);
|
|
if (!match) return null;
|
|
const max = Number(match[1]);
|
|
return Number.isFinite(max) ? max : null;
|
|
}
|
|
|
|
/** Test-only: clear the learned-cap Map between tests. */
|
|
export function __test_resetLearnedThinkingCaps(): void {
|
|
learnedCaps.clear();
|
|
}
|