mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-10-19 13:30:37 +00:00
fix(routing): respect wire alias locks during family fallback (#17113)
Merged via /merge-batch (2026-10-19). Evidence: validated on a combined board of the six new PRs over release/v3.8.52 — typecheck core/open-sse/API, ESLint on every changed file, complexity + cognitive within baseline, boundary + cycles ratchets, dead-code, test-masking, i18n new-key coverage, changelog-integrity, docs-sync, check:docs-all, env-doc-sync, provider-consistency, agent-skills 0-diff; 574 focused node tests (the only reds — vision-key-scope-locales and onnx-install-env-16225 — fail identically on the bare tip). CI reds are the inherited base-red #15306 (i18n drift of 18 sources, owned by #16980).
This commit is contained in:
parent
2d1de17940
commit
28443d87d6
4 changed files with 98 additions and 1 deletions
1
changelog.d/fixes/16227-family-wire-lockout.md
Normal file
1
changelog.d/fixes/16227-family-wire-lockout.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
- Skip family fallback candidates locked under their provider wire alias while preserving the selected account and catalog visibility checks. Refs #16227; complements #16223 and #16810.
|
||||
|
|
@ -13,6 +13,7 @@
|
|||
|
||||
import { getModelContextLimit } from "../../src/lib/modelCapabilities";
|
||||
import { parseModel } from "./model.ts";
|
||||
import { resolveProviderModelAlias } from "./modelCanonicalization.ts";
|
||||
import {
|
||||
CONTEXT_OVERFLOW_REGEX,
|
||||
containsModelUnavailableMessage,
|
||||
|
|
@ -318,6 +319,7 @@ function isFamilyFallbackCandidateEligible(
|
|||
): boolean {
|
||||
const parsedCandidate = parseModel(candidate);
|
||||
const candidateModel = parsedCandidate.model || candidate;
|
||||
const wireModel = resolveProviderModelAlias(provider, candidateModel);
|
||||
// A public prefix can select a different runtime identity than its catalog id
|
||||
// (agy/ routes to antigravity). Bare wire ids retain the explicit provider hint.
|
||||
const visibilityProvider = parsedCandidate.providerAlias
|
||||
|
|
@ -325,7 +327,8 @@ function isFamilyFallbackCandidateEligible(
|
|||
: provider;
|
||||
return (
|
||||
isVisible(candidate, visibilityProvider) &&
|
||||
!isModelLocked(provider, connectionId, candidateModel)
|
||||
!isModelLocked(provider, connectionId, candidateModel) &&
|
||||
!isModelLocked(provider, connectionId, wireModel)
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -470,6 +470,7 @@
|
|||
"tests/unit/rate-limit-long-body-retry-lockout.test.ts",
|
||||
"tests/unit/rate-limit-manager.test.ts",
|
||||
"tests/unit/rate-limit-queue-timeout-lockout.test.ts",
|
||||
"tests/unit/repro-16227-wire-lockout.test.ts",
|
||||
"tests/unit/repro-7503-no-choices.test.ts",
|
||||
"tests/unit/repro-9486.test.ts",
|
||||
"tests/unit/repro-9630-combo-false-503.test.ts",
|
||||
|
|
|
|||
92
tests/unit/repro-16227-wire-lockout.test.ts
Normal file
92
tests/unit/repro-16227-wire-lockout.test.ts
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
import assert from "node:assert/strict";
|
||||
import { mkdtempSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import test from "node:test";
|
||||
|
||||
const directory = mkdtempSync(join(tmpdir(), "family-wire-16227-"));
|
||||
const previousEnvironment = {
|
||||
DATA_DIR: process.env.DATA_DIR,
|
||||
MEMORY_STATIC_CACHE_DIR: process.env.MEMORY_STATIC_CACHE_DIR,
|
||||
OMNIROUTE_PLUGINS_DIR: process.env.OMNIROUTE_PLUGINS_DIR,
|
||||
API_KEY_SECRET: process.env.API_KEY_SECRET,
|
||||
};
|
||||
process.env.DATA_DIR = directory;
|
||||
process.env.MEMORY_STATIC_CACHE_DIR = join(directory, "memory-cache");
|
||||
process.env.OMNIROUTE_PLUGINS_DIR = join(directory, "plugins");
|
||||
process.env.API_KEY_SECRET = "wire-lockout-16227-fixture-only";
|
||||
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = async () => {
|
||||
throw new Error("Unexpected network request in the local wire-lockout regression");
|
||||
};
|
||||
|
||||
const core = await import("../../src/lib/db/core.ts");
|
||||
const providers = await import("../../src/lib/db/providers.ts");
|
||||
const auth = await import("../../src/sse/services/auth.ts");
|
||||
const locks = await import("../../open-sse/services/accountFallback.ts");
|
||||
const family = await import("../../open-sse/services/modelFamilyFallback.ts");
|
||||
|
||||
test.after(() => {
|
||||
locks.clearAllModelLockouts();
|
||||
core.resetDbInstance();
|
||||
globalThis.fetch = originalFetch;
|
||||
for (const [name, value] of Object.entries(previousEnvironment)) {
|
||||
if (value === undefined) delete process.env[name];
|
||||
else process.env[name] = value;
|
||||
}
|
||||
rmSync(directory, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test("family fallback skips a sibling locked by a real Anthropic404 under its dashed wire ID", async () => {
|
||||
const connection = await providers.createProviderConnection({
|
||||
provider: "anthropic",
|
||||
authType: "apikey",
|
||||
apiKey: "selected-account-fixture-only",
|
||||
isActive: true,
|
||||
testStatus: "active",
|
||||
});
|
||||
const otherConnection = await providers.createProviderConnection({
|
||||
provider: "anthropic",
|
||||
authType: "apikey",
|
||||
apiKey: "other-account-fixture-only",
|
||||
isActive: true,
|
||||
testStatus: "active",
|
||||
});
|
||||
const next = () =>
|
||||
family.getNextEligibleFamilyFallback(
|
||||
"anthropic/claude-opus-5",
|
||||
new Set(["anthropic/claude-opus-5"]),
|
||||
"anthropic",
|
||||
connection.id
|
||||
);
|
||||
const errorBody = JSON.stringify({
|
||||
error: { type: "not_found_error", message: "model: claude-opus-4-8" },
|
||||
});
|
||||
|
||||
assert.equal(next(), "anthropic/claude-opus-4.8", "unlocked sibling control");
|
||||
await auth.markAccountUnavailable(
|
||||
otherConnection.id,
|
||||
404,
|
||||
errorBody,
|
||||
"anthropic",
|
||||
"claude-opus-4-8"
|
||||
);
|
||||
assert.equal(locks.isModelLocked("anthropic", otherConnection.id, "claude-opus-4-8"), true);
|
||||
assert.equal(next(), "anthropic/claude-opus-4.8", "another account's lock must not apply");
|
||||
|
||||
await auth.markAccountUnavailable(connection.id, 404, errorBody, "anthropic", "claude-opus-4-8");
|
||||
assert.equal(
|
||||
locks.isModelLocked("anthropic", connection.id, "claude-opus-4-8"),
|
||||
true,
|
||||
"the actual wire model must really be locked on the selected connection"
|
||||
);
|
||||
const stored = await providers.getProviderConnectionById(connection.id);
|
||||
assert.equal(stored.testStatus, "active", "a model404 must preserve account health");
|
||||
assert.ok(!stored.rateLimitedUntil, "a model404 must not cool the whole account");
|
||||
assert.equal(
|
||||
next(),
|
||||
"anthropic/claude-opus-4.7",
|
||||
"the locked wire alias must be excluded from family dispatch"
|
||||
);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue