mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-26 09:02:11 +03:00
* fix(sse): restore task-aware routing config on restart (#8601)
The T05 Task-Aware Smart Routing config was persisted to settings.taskRouting
by PUT /api/settings/task-routing but never read back, so it silently reverted
to enabled:false + the hardcoded default model map on every restart.
Two root causes, both fixed:
- No boot hydration existed. Adds hydrateTaskRoutingConfig(settings), wired into
src/instrumentation-node.ts next to the Thinking-Budget restore (#5312). It
accepts either the JSON string the route persists or an already-parsed object,
and fails open on malformed values. applyRuntimeSettings does not cover this
key, same as the Global System Prompt (#2470).
- The config lived in a plain module-level `let`, which is duplicated per module
graph — a boot hydration would have landed on the instrumentation graph's copy
and never reached the one src/sse/handlers/chat.ts reads. This is the exact
break #5312 fix-A hit on the VPS. Moves the store to the globalThis pattern
already used by thinkingBudget.ts and systemPrompt.ts.
Runtime stats are never restored from the persisted blob.
Note the hydration is wired into instrumentation-node.ts, not the unused
src/server-init.ts.
* docs(changelog): add fragment for #8604 task-routing boot restore
* fix(sse): route task-aware defaults by intent, guard fitness pattern order (#8602, #8603)
Two related defects in the hand-maintained model-quality tables.
#8602 — DEFAULT_TASK_MODEL_MAP hardcoded literal provider/model ids
(openai/gpt-4o, gemini/gemini-2.5-flash-lite, deepseek/deepseek-chat, ...).
Wrong twice over: the ids rotted by a generation or two, and applyTaskAwareRouting
overwrites body.model directly, so a literal target skipped auto-combo's 13-factor
scoring (quota, circuit-breaker health, cost, latency, stability), connection
cooldown and model lockout — hard-failing for any operator with no connection for
that provider. Refreshing the strings would only reset the rot clock, so the
defaults now name auto/* INTENTS that resolve against the operator's actually
connected backends:
coding -> auto/coding
analysis -> auto/reasoning
vision -> auto/vision
summarization -> auto/chat:fast
background -> auto/chat:cheap
creative and chat stay pass-through. Operators can still pin a specific model via
PUT /api/settings/task-routing; only the shipped defaults change. No provider/model
literal remains in the module.
#8603 — the pattern-shadowing fix LANDED UPSTREAM while this PR was open
(9f5be229b, Train 1D). lookupStaticFitnessTable now ranks patterns longest-first,
so gpt-4o-mini no longer inherits gpt-4o's 0.9 and deepseek-v3.2 no longer inherits
deepseek-v3's 0.85. This PR therefore no longer changes that behaviour — the
upstream scan is kept verbatim.
What remains for #8603 is the regression guard. The resolution chain hits the DB
(user_override / arena_elo / models.dev tier) before reaching layer 4, so asserting
the ordering through getTaskFitness would depend on DB fixture state. The layer is
exposed as getStaticFitnessTableScore and pinned directly by
taskFitness-pattern-order-8603.test.ts (7 cases), so the guarantee survives future
edits to FITNESS_TABLE. Those 7 cases were written against this PR's original
implementation and pass unchanged against the upstream one — independent
confirmation that the two are behaviourally equivalent.
* docs(changelog): add fragment for #8605 task-routing intent + fitness order
98 lines
3.7 KiB
TypeScript
98 lines
3.7 KiB
TypeScript
/**
|
|
* TDD regression for #8602: DEFAULT_TASK_MODEL_MAP hardcoded literal provider/model
|
|
* ids (`openai/gpt-4o`, `gemini/gemini-2.5-flash-lite`, …) as routing destinations.
|
|
*
|
|
* Two problems, both guarded here:
|
|
*
|
|
* 1. The ids rot. They pointed at models one to two generations old, and refreshing
|
|
* the strings would only reset the rot clock — so the guard is structural: no
|
|
* concrete provider/model literal may appear in the default map at all.
|
|
*
|
|
* 2. The shape bypasses the router. `src/sse/handlers/chat.ts` applies the override by
|
|
* overwriting `body.model`, so a hardcoded target skips the 13-factor auto-combo
|
|
* scoring (quota / circuit-breaker health / cost / latency / stability), connection
|
|
* cooldown and model lockout. An operator with no OpenAI connection got a request
|
|
* rewritten to `openai/gpt-4o` and a failure, where pass-through would have worked.
|
|
*
|
|
* The defaults must therefore be `auto/*` INTENT ids, which resolve against whatever
|
|
* backends the operator actually has connected.
|
|
*/
|
|
import { test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import {
|
|
getDefaultTaskModelMap,
|
|
setTaskRoutingConfig,
|
|
applyTaskAwareRouting,
|
|
type TaskType,
|
|
} from "../../open-sse/services/taskAwareRouter.ts";
|
|
import { isRecognizedBuiltinAuto } from "../../open-sse/services/autoCombo/builtinCatalog.ts";
|
|
|
|
/** Task types that intentionally carry no override (pass the requested model through). */
|
|
const NO_OVERRIDE: TaskType[] = ["creative", "chat"];
|
|
|
|
test("#8602 default task→model map contains no concrete provider/model literals", () => {
|
|
const map = getDefaultTaskModelMap();
|
|
|
|
for (const [taskType, target] of Object.entries(map)) {
|
|
if (NO_OVERRIDE.includes(taskType as TaskType)) {
|
|
assert.equal(target, "", `${taskType} must stay pass-through`);
|
|
continue;
|
|
}
|
|
assert.notEqual(target, "", `${taskType} should route somewhere`);
|
|
assert.ok(
|
|
target.startsWith("auto/"),
|
|
`${taskType} must route by intent, not to a hardcoded model — got "${target}"`
|
|
);
|
|
}
|
|
});
|
|
|
|
test("#8602 every default target is a resolvable built-in auto id", () => {
|
|
const map = getDefaultTaskModelMap();
|
|
|
|
for (const [taskType, target] of Object.entries(map)) {
|
|
if (target === "") continue;
|
|
const suffix = target.slice("auto/".length);
|
|
assert.ok(
|
|
isRecognizedBuiltinAuto(target, suffix),
|
|
`${taskType} → "${target}" is not a recognized auto id, so it would fail to resolve`
|
|
);
|
|
}
|
|
});
|
|
|
|
test("#8602 no default target names a known provider prefix", () => {
|
|
// Belt-and-braces: catches a regression that reintroduces a literal while still
|
|
// (incorrectly) prefixing it, e.g. "auto/openai/gpt-4o".
|
|
const PROVIDER_LITERALS = ["openai/", "gemini/", "deepseek/", "anthropic/", "claude/", "glm/"];
|
|
for (const [taskType, target] of Object.entries(getDefaultTaskModelMap())) {
|
|
for (const literal of PROVIDER_LITERALS) {
|
|
assert.ok(
|
|
!target.includes(literal),
|
|
`${taskType} → "${target}" still embeds the provider literal "${literal}"`
|
|
);
|
|
}
|
|
}
|
|
});
|
|
|
|
test("#8602 routing a detected task yields the auto intent id", () => {
|
|
setTaskRoutingConfig({
|
|
enabled: true,
|
|
detectionEnabled: true,
|
|
taskModelMap: getDefaultTaskModelMap(),
|
|
});
|
|
|
|
try {
|
|
const result = applyTaskAwareRouting("some-provider/some-model", {
|
|
messages: [{ role: "user", content: "please implement a binary search function" }],
|
|
});
|
|
|
|
assert.equal(result.taskType, "coding");
|
|
assert.equal(result.wasRouted, true);
|
|
assert.ok(
|
|
result.model.startsWith("auto/"),
|
|
`expected an auto intent id, got "${result.model}"`
|
|
);
|
|
} finally {
|
|
setTaskRoutingConfig({ enabled: false, taskModelMap: getDefaultTaskModelMap() });
|
|
}
|
|
});
|