mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-05 23:02:10 +03:00
This commit is contained in:
committed by
GitHub
parent
e3d29d1419
commit
76b1b04495
@@ -21,6 +21,8 @@
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- **fix(sse):** the reasoning-token buffer no longer inflates **probe-sized `max_tokens`** ([#6274](https://github.com/diegosouzapw/OmniRoute/issues/6274)) — Claude Code's `/model` capability check sends `max_tokens: 1`, but for a thinking-capable model with a large output cap (e.g. `glm-5.2`) the #3587 headroom heuristic (`max(current + 1000, ceil(current * 1.5))`) rewrote it to `1001` and forwarded that upstream, wasting tokens on a request that was never a genuine reasoning budget. `resolveReasoningBufferedMaxTokens()` (`open-sse/services/reasoningTokenBuffer.ts`) now short-circuits and returns the caller's value verbatim when it is below the new `REASONING_BUFFER_MIN_TRIGGER` (256) threshold — a tiny explicit limit is a probe, not a reasoning request. Real budgets still receive the #3587 headroom unchanged, and the guard runs after the existing capability checks so unknown / non-reasoning models keep returning `null`. Regression guard: `tests/unit/reasoning-token-buffer-6274.test.ts`. (thanks @brightfiscalband)
|
||||
|
||||
- **fix(cli):** `omniroute reset-password` now works as a real subcommand, and password resets over piped (non-TTY) stdin actually apply ([#6261](https://github.com/diegosouzapw/OmniRoute/issues/6261), [#6258](https://github.com/diegosouzapw/OmniRoute/issues/6258)). Two coupled defects: (1) **#6261** — `bin/omniroute.mjs` routed everything through Commander with only two pre-Commander bypasses (`--mcp`, `reset-encrypted-columns`), so `omniroute reset-password` was rejected as an unknown command; only the separate `omniroute-reset-password` bin worked, while the docs falsely advertised the subcommand (incl. a bogus "legacy alias still works"). A pre-Commander bypass mirroring `reset-encrypted-columns` now dynamically imports `bin/reset-password.mjs` (which self-executes) before Commander parses; the three doc lines were corrected. (2) **#6258** — `bin/reset-password.mjs` issued two sequential `rl.question` prompts; under piped stdin the second read never settled at EOF, so `main()` never reached `resetManagementPassword` and the reset was a silent no-op (both prompts printed, no success, password unchanged). The CLI now detects non-TTY stdin and reads it once (first line = password, second line = confirm if present, else reused), adds a `--password-stdin` flag (entire stdin is the password, no confirmation), and exits `0` explicitly so the success line always flushes; interactive TTY behavior is unchanged. Regression guard: `tests/unit/reset-password-cli-6261-6258.test.ts` (3). (thanks @chirag127)
|
||||
|
||||
- **fix(db):** the mass-migration **safety abort** now tells the operator how to bypass it and stops flooding the log ([#6260](https://github.com/diegosouzapw/OmniRoute/issues/6260)) — after restoring a backup that wiped the migration tracking table, `runMigrations()` threw the abort on every downstream `ensureDbInitialized()`, re-logging the full banner 11+ times, and the message never mentioned the existing `OMNIROUTE_MAX_PENDING_MIGRATIONS` escape hatch. The abort text now appends a bypass hint (set `OMNIROUTE_MAX_PENDING_MIGRATIONS=0` in `server.env` / `DATA_DIR/.env`), and a new `MigrationSafetyAbortError` is memoized so repeated calls in the same process throw the same instance and emit a single concise line instead of the full cascade. Regression guard: `tests/unit/migration-safety-abort-6260.test.ts`. (thanks @chirag127)
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
import { getResolvedModelCapabilities } from "../../src/lib/modelCapabilities.ts";
|
||||
|
||||
/**
|
||||
* Below this caller-supplied `max_tokens`, the request is treated as a probe
|
||||
* (e.g. Claude Code's `/model` capability check sends `max_tokens: 1`) rather
|
||||
* than a genuine reasoning budget, so no headroom is added. Keeping it a named
|
||||
* constant makes the threshold easy to tune. See issue #6274 (probe inflated to
|
||||
* 1001 upstream) vs. issue #3587 (headroom for real reasoning budgets).
|
||||
*/
|
||||
export const REASONING_BUFFER_MIN_TRIGGER = 256;
|
||||
|
||||
export function toPositiveInteger(value: unknown): number | null {
|
||||
const numericValue =
|
||||
typeof value === "number"
|
||||
@@ -30,6 +39,10 @@ export function resolveReasoningBufferedMaxTokens(
|
||||
if (current > maxOutputTokens) return maxOutputTokens;
|
||||
if (current === maxOutputTokens) return current;
|
||||
|
||||
// Issue #6274: a tiny explicit budget is a capability probe, not a reasoning
|
||||
// request. Respect it verbatim instead of inflating (e.g. 1 -> 1001).
|
||||
if (current < REASONING_BUFFER_MIN_TRIGGER) return current;
|
||||
|
||||
const buffered = Math.max(current + 1000, Math.ceil(current * 1.5));
|
||||
if (buffered > maxOutputTokens) return current;
|
||||
|
||||
|
||||
@@ -2977,8 +2977,8 @@ test("#3587 reasoning buffer is disabled without explicit model capability data"
|
||||
"reasoning metadata without an explicit output cap is not safe enough to inflate"
|
||||
);
|
||||
assert.equal(
|
||||
resolveReasoningBufferedMaxTokens("openai/default-cap-reasoning", 100),
|
||||
1100,
|
||||
resolveReasoningBufferedMaxTokens("openai/default-cap-reasoning", 300),
|
||||
1300,
|
||||
"explicit default-sized caps are treated as real capability data"
|
||||
);
|
||||
});
|
||||
|
||||
93
tests/unit/reasoning-token-buffer-6274.test.ts
Normal file
93
tests/unit/reasoning-token-buffer-6274.test.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* #6274 — the reasoning-token buffer must not inflate probe-sized max_tokens.
|
||||
*
|
||||
* Claude Code's `/model` capability check sends `max_tokens: 1`; for a thinking-
|
||||
* capable model with a large output cap (e.g. glm-5.2) the #3587 headroom heuristic
|
||||
* (`max(current + 1000, ceil(current * 1.5))`) rewrote it to 1001 and forwarded that
|
||||
* upstream. A tiny explicit budget below REASONING_BUFFER_MIN_TRIGGER (256) is a
|
||||
* probe and must pass through verbatim; genuine budgets keep the #3587 headroom.
|
||||
*
|
||||
* Kept standalone against the pure `resolveReasoningBufferedMaxTokens` rather than
|
||||
* extending the frozen `combo-routing-engine.test.ts` god-file.
|
||||
*/
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-reasoning-buffer-"));
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
|
||||
const core = await import("../../src/lib/db/core.ts");
|
||||
const { saveModelsDevCapabilities, clearModelsDevCapabilities } = await import(
|
||||
"../../src/lib/modelsDevSync.ts"
|
||||
);
|
||||
const { resolveReasoningBufferedMaxTokens, REASONING_BUFFER_MIN_TRIGGER } = await import(
|
||||
"../../open-sse/services/reasoningTokenBuffer.ts"
|
||||
);
|
||||
|
||||
function capabilityEntry(limitContext: unknown, overrides: Record<string, unknown> = {}) {
|
||||
return {
|
||||
tool_call: true,
|
||||
reasoning: false,
|
||||
attachment: false,
|
||||
structured_output: true,
|
||||
temperature: true,
|
||||
modalities_input: JSON.stringify(["text"]),
|
||||
modalities_output: JSON.stringify(["text"]),
|
||||
knowledge_cutoff: null,
|
||||
release_date: null,
|
||||
last_updated: null,
|
||||
status: null,
|
||||
family: null,
|
||||
open_weights: false,
|
||||
limit_context: limitContext,
|
||||
limit_input: limitContext,
|
||||
limit_output: 4096,
|
||||
interleaved_field: null,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
test.before(() => {
|
||||
// A thinking-capable model with a large output cap: the #3587 guards all pass.
|
||||
saveModelsDevCapabilities({
|
||||
zhipu: {
|
||||
"glm-5.2": capabilityEntry(200000, { reasoning: true, limit_output: 65536 }),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test.after(() => {
|
||||
clearModelsDevCapabilities();
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test("#6274 reasoning buffer does not inflate probe-sized max_tokens", () => {
|
||||
// The Claude-Code `/model` probe (max_tokens: 1) must pass through (was 1001).
|
||||
assert.equal(
|
||||
resolveReasoningBufferedMaxTokens("zhipu/glm-5.2", 1),
|
||||
1,
|
||||
"probe-sized max_tokens=1 must not be inflated"
|
||||
);
|
||||
// Just below the trigger threshold is still treated as a probe.
|
||||
assert.equal(
|
||||
resolveReasoningBufferedMaxTokens("zhipu/glm-5.2", REASONING_BUFFER_MIN_TRIGGER - 1),
|
||||
REASONING_BUFFER_MIN_TRIGGER - 1,
|
||||
"budgets below REASONING_BUFFER_MIN_TRIGGER are respected verbatim"
|
||||
);
|
||||
// At the threshold, headroom resumes: max(256 + 1000, ceil(256 * 1.5)) = 1256.
|
||||
assert.equal(
|
||||
resolveReasoningBufferedMaxTokens("zhipu/glm-5.2", REASONING_BUFFER_MIN_TRIGGER),
|
||||
1256,
|
||||
"budgets at the threshold receive reasoning headroom"
|
||||
);
|
||||
// A realistic reasoning budget still gets buffered: max(32000 + 1000, 48000) = 48000.
|
||||
assert.equal(
|
||||
resolveReasoningBufferedMaxTokens("zhipu/glm-5.2", 32000),
|
||||
48000,
|
||||
"genuine reasoning budgets keep the #3587 headroom"
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user