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:
Diego Rodrigues de Sa e Souza 2026-10-18 22:56:51 -03:00 • committed by GitHub
parent 2d1de17940
commit 28443d87d6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 98 additions and 1 deletions

View 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.

View file

@ -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)
);
}

View file

@ -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",

View 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"
);
});