fix(routing): share provider alias pools with combo discovery (#15927)

Batch-merged with the owner's 2026-10-09 wave (open-sse/services/AGENTS.md one-line edit explicitly approved by the owner in-session). Reconciled on the current release tip: the only conflict was the stryker.conf.json test list against the already-landed #15922/#15939 (union of entries). Focused: 16/16 tests (provider-alias-combo-pool-12844 + combo-custom-provider-resolution); typecheck:core and check:open-sse-typecheck clean. Same change also validated earlier in the combined 57-PR tree.
This commit is contained in:
Diego Rodrigues de Sa e Souza 2026-10-09 13:31:05 -03:00 • committed by GitHub
parent c49f75a181
commit 6f9a6d12e6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 348 additions and 128 deletions

View file

@ -0,0 +1 @@
- Reuse direct-auth provider alias pools in combo/auto discovery so Antigravity CLI and OpenCode accounts remain visible to scoring, quota, tags and affinity while preserving account restrictions. (#12844)

View file

@ -22,6 +22,7 @@ Live count: `ls open-sse/services/*.ts | wc -l` (currently 134). More including
### Account & Token Management
- **`providerConnectionPool.ts`** — Shared alias/custom-node connection pools for direct authentication and combo discovery; preserves node ownership and leaves caller ACL/pin filtering in place.
- **`tokenRefresh.ts`** — OAuth token expiration detection and refresh.
- **`accountFallback.ts`** — Account switching on quota/rate-limit. Also houses model lockout.
- **`sessionManager.ts`** — Request session state across retries.

View file

@ -43,7 +43,7 @@ import { getCachedProviderConnectionById } from "../../src/lib/db/readCache.ts";
import { expandPromptCacheAffinityTargetsFromConnections } from "./combo/promptCacheAffinity.ts";
import { getCachedProviderConnections } from "../../src/lib/db/readCache";
import { getCachedProviderPoolConnections } from "./providerConnectionPool.ts";
import {
resolveResilienceSettings,
type ResilienceSettings,
@ -355,7 +355,7 @@ export async function buildAutoCandidates(
await Promise.all(
uniqueProviders.map(async (provider) => {
try {
const connections = (await getCachedProviderConnections({
const connections = (await getCachedProviderPoolConnections({
provider,
isActive: true,
})) as Array<Record<string, unknown>>;
@ -909,7 +909,7 @@ async function handleComboChatInner({
let pinnedLockWaitMs = 0;
if (activeNativeTurnPin) {
const activeConnections = (await getCachedProviderConnections({
const activeConnections = (await getCachedProviderPoolConnections({
provider: resolveProviderId(activeNativeTurnPin.provider),
isActive: true,
})) as Array<Record<string, unknown>>;

View file

@ -43,7 +43,7 @@ import {
type ScoringWeights,
} from "../autoCombo/scoring.ts";
import type { RoutingHint } from "../manifestAdapter";
import { getCachedProviderConnections } from "../../../src/lib/db/readCache";
import { getCachedProviderPoolConnections } from "../providerConnectionPool.ts";
import {
getSyncedAvailableModels,
getCustomModels,
@ -266,7 +266,7 @@ export async function applyRequestTagRouting(
await Promise.all(
providerIds.map(async (providerId) => {
try {
const connections = await getCachedProviderConnections({
const connections = await getCachedProviderPoolConnections({
provider: providerId,
isActive: true,
});
@ -483,7 +483,7 @@ export async function expandAutoComboCandidatePool(
if (Array.isArray(explicitModels) && explicitModels.length > 0) return eligibleTargets;
try {
const allConnections = await getCachedProviderConnections({ isActive: true });
const allConnections = await getCachedProviderPoolConnections({ isActive: true });
const providerIds = [
...new Set(
(allConnections as Array<{ provider?: unknown }>)

View file

@ -12,7 +12,7 @@
*
* Extracted from combo.ts as a pure move (#3501). No behaviour change.
*/
import { getCachedProviderConnections } from "../../../src/lib/db/readCache";
import { getCachedProviderPoolConnections } from "../providerConnectionPool.ts";
import { getCircuitBreaker } from "../../../src/shared/utils/circuitBreaker";
import { fisherYatesShuffle, getNextFromDeck } from "../../../src/shared/utils/shuffleDeck";
import { handleFusionChat, type FusionTuning } from "../fusion.ts";
@ -177,7 +177,7 @@ async function isPinnedModelDurablyUnhealthy(pinnedModel: string): Promise<boole
const provider = parseModel(pinnedModel).provider;
if (!provider) return false;
const circuitState = getCircuitBreaker(provider)?.getStatus?.()?.state;
const connections = (await getCachedProviderConnections({
const connections = (await getCachedProviderPoolConnections({
provider,
isActive: true,
})) as Array<{

View file

@ -3,7 +3,7 @@ import {
analyzePrefix,
generatePromptCacheKey,
} from "../../../src/lib/promptCache/prefixAnalyzer.ts";
import { getCachedProviderConnections } from "../../../src/lib/db/readCache";
import { getCachedProviderPoolConnections } from "../providerConnectionPool.ts";
import { parseModel } from "../model.ts";
import type { ResolvedComboTarget } from "./types.ts";
import { getOAuthSessionAvailability } from "../oauthSessionOccupancy.ts";
@ -199,7 +199,7 @@ export async function expandPromptCacheAffinityTargets(
await Promise.all(
providers.map(async (provider) => {
try {
const connections = (await getCachedProviderConnections({
const connections = (await getCachedProviderPoolConnections({
provider,
isActive: true,
})) as Array<Record<string, unknown>>;

View file

@ -53,6 +53,7 @@ import {
isAlibabaFreeTierVisionComboName,
} from "../dashscopeTextModels.ts";
import type { ComboLike } from "./types.ts";
import { isRecord } from "./comboData.ts";
/** Sentinel pattern used for "all models of a provider". */
const PROVIDER_WILDCARD_SENTINEL = "*";
@ -160,9 +161,9 @@ async function filterAlibabaFreeDrainedModelIds(
return modelIds;
}
try {
const { getProviderConnections } = await import("../../../src/lib/db/providers.ts");
const connections = await getProviderConnections({ provider: providerId });
const connection = connections.find((entry) => entry.id === connectionId);
const { getCachedProviderPoolConnections } = await import("../providerConnectionPool.ts");
const connections = await getCachedProviderPoolConnections({ provider: providerId });
const connection = connections.find((entry) => isRecord(entry) && entry.id === connectionId);
if (!connection) return modelIds;
if (isAlibabaFreeTierVisionComboName(comboName)) {

View file

@ -10,7 +10,7 @@
* State cohesion: `resetAwareQuotaCache` and
* `MAX_RESET_AWARE_CACHE` MUST remain single instances defined once here,
* alongside their only readers/writers (`fetchResetAwareQuotaWithCache`).
* Connection lists go through `getCachedProviderConnections` (5s TTL,
* Connection lists go through `getCachedProviderPoolConnections` (5s TTL,
* invalidated on connection writes). Do not add a second connection cache.
*
* Cross-module state: the tie-band round-robin in orderTargetsByResetAwareQuota
@ -32,7 +32,7 @@ import {
import { PRE_SCREEN_CONCURRENCY } from "../comboConfig.ts";
import { getQuotaFetcher } from "../quotaPreflight.ts";
import { getCircuitBreaker } from "../../../src/shared/utils/circuitBreaker";
import { getCachedProviderConnections } from "../../../src/lib/db/readCache";
import { getCachedProviderPoolConnections } from "../providerConnectionPool.ts";
import { MAX_RR_COUNTERS, rrCounters } from "./rrState.ts";
import type { ResolvedComboTarget, IsModelAvailable } from "./types.ts";
import {
@ -91,7 +91,10 @@ async function getQuotaAwareConnectionsForTarget(
provider,
(async () => {
try {
const connections = await getCachedProviderConnections({ provider, isActive: true });
const connections = await getCachedProviderPoolConnections({
provider,
isActive: true,
});
let activeConnections = Array.isArray(connections)
? (connections as Array<Record<string, unknown>>).filter(
(connection) =>
@ -222,10 +225,11 @@ export async function expandTargetsByQuotaAwareConnections(
continue;
}
const poolIds = new Set(connections.map((connection) => connection.id));
for (const connectionId of connectionIds) {
const provider = getResetAwareProvider(target);
const connection = connectionById.get(connectionId);
if (provider && getQuotaFetcher(provider) && connection?.provider !== provider) continue;
if (provider && getQuotaFetcher(provider) && !poolIds.has(connectionId)) continue;
if (
connection &&
typeof connection.rateLimitedUntil === "string" &&

View file

@ -144,11 +144,11 @@ async function resolveConnectionHealth(
if (_connectionFetcherOverride) return _connectionFetcherOverride(connectionId, provider);
try {
const mod = await import("../../../src/lib/db/readCache");
const getCachedProviderConnections = mod.getCachedProviderConnections as (
const mod = await import("../providerConnectionPool.ts");
const getCachedProviderPoolConnections = mod.getCachedProviderPoolConnections as (
filter: Record<string, unknown>
) => Promise<StickyConnectionHealth[]>;
const connections = (await getCachedProviderConnections({
const connections = (await getCachedProviderPoolConnections({
provider,
isActive: true,
})) as Array<StickyConnectionHealth & { id?: string }>;

View file

@ -0,0 +1,133 @@
import { nodeTypeFromId } from "@/lib/db/providerNodeSelect";
import { getCachedProviderConnections, getCachedProviderNodes } from "@/lib/db/readCache";
import { isCommonChatGptWebRetiredProviderId } from "@/shared/constants/chatgptWebRetirement";
import { getProviderById, getProviderAlias, resolveProviderId } from "@/shared/constants/providers";
function asRecord(value: unknown): Record<string, unknown> {
return value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: {};
}
const PROVIDER_SEARCH_PAIRS: string[][] = [
["nvidia", "nvidia_nim"],
["kimi-coding", "kimi-coding-apikey"],
// The model layer canonicalizes `agy/` to `antigravity`, but the Antigravity
// CLI card stores its connection under `agy`. Same account, either id serves.
["antigravity", "agy"],
// OpenCode connection card stores under `opencode`, but model alias resolves to `opencode-zen`.
["opencode", "opencode-zen"],
// One Jina token works on api.jina.ai, r.jina.ai, and s.jina.ai.
// Requested id stays first so embed/rerank do not silently pick a
// Reader-only row when both cards are filled. jina-search has no
// dashboard card — it must still see jina-ai / jina-reader keys
// before falling through to JINA_AI_API_KEY.
["jina-ai", "jina-reader", "jina-search"],
];
/** Resolve provider aliases (e.g., nvidia -> nvidia_nim) for DB lookup. */
export async function getProviderSearchPool(provider: string): Promise<string[]> {
const canonicalProvider = resolveProviderId(provider);
const canonicalAlias = getProviderAlias(canonicalProvider);
if (isCommonChatGptWebRetiredProviderId(provider)) return [];
const group = PROVIDER_SEARCH_PAIRS.find((aliases) => aliases.includes(provider));
if (group) return [provider, ...group.filter((id) => id !== provider)];
const searchPool = new Set([provider, canonicalProvider, canonicalAlias].filter(Boolean));
// Built-in providers already resolve through static ids/aliases. Only
// compatible/custom providers need provider_nodes expansion back to the
// generated internal connection ids. (#3058)
if (getProviderById(canonicalProvider)) {
return Array.from(searchPool);
}
// Custom provider nodes are referenced by user-facing prefixes in combos
// (for example "78code/gpt-5.4"), but live credentials are stored under
// internal provider ids like openai-compatible-responses-<uuid>.
try {
const providerNodes = await getCachedProviderNodes();
const compatibleNodes = Array.isArray(providerNodes) ? providerNodes : [];
const nodeTypes = new Map<string, number>();
for (const node of compatibleNodes) {
const nodeRecord = asRecord(node);
const nodeId = typeof nodeRecord.id === "string" ? nodeRecord.id.trim() : "";
if (!nodeId) continue;
const derivedType = nodeTypeFromId(nodeId);
nodeTypes.set(derivedType, (nodeTypes.get(derivedType) || 0) + 1);
}
for (const node of compatibleNodes) {
const nodeRecord = asRecord(node);
const nodePrefix = typeof nodeRecord.prefix === "string" ? nodeRecord.prefix.trim() : "";
const nodeId = typeof nodeRecord.id === "string" ? nodeRecord.id.trim() : "";
if (!nodeId) continue;
if (
nodePrefix &&
(nodePrefix === provider ||
nodePrefix === canonicalProvider ||
nodePrefix === canonicalAlias)
) {
searchPool.add(nodeId);
}
// #10085: bridge the concrete uuid node id (what the chat path resolves,
// "<generic-type>-<uuid>") to the GENERIC derived type id (what
// resolveProviderNodeForConnection also accepts for connection creation,
// #4421) -- and back. A connection created via the bare generic type
// (e.g. "openai-compatible-chat") must still be found when the chat path
// looks up the concrete node id, and vice versa.
//
// #10434: both bridging directions MUST require the derived type to be
// unambiguous (exactly one provider node of that type) before falling
// back to a generic-type match -- an explicit ownership check, not just
// a string-format coincidence. This mirrors the exact rule already
// enforced by selectProviderNodeForConnection() for connection CREATION
// (src/lib/db/providerNodeSelect.ts, #4421): "only when exactly one such
// node exists, so an ambiguous type never silently picks the wrong
// node". Without this guard on the generic->concrete direction, a bare
// generic-type lookup would pool in EVERY node sharing that derived
// type, including a connection scoped (via its own providerSpecificData
// baseUrl/headers) to one specific node -- leaking that node's
// credentials/upstream URL into a lookup for a different, unrelated
// node of the same generic type.
const derivedType = nodeTypeFromId(nodeId);
if (derivedType && derivedType !== nodeId) {
const typeIsUnambiguous = nodeTypes.get(derivedType) === 1;
if (typeIsUnambiguous) {
if (nodeId === provider || nodeId === canonicalProvider || nodeId === canonicalAlias) {
searchPool.add(derivedType);
}
if (
derivedType === provider ||
derivedType === canonicalProvider ||
derivedType === canonicalAlias
) {
searchPool.add(nodeId);
}
}
}
}
} catch {
// Best-effort alias expansion only.
}
return Array.from(searchPool);
}
/** Resolve the same account pool as direct auth, keeping the existing DB cache. */
export async function getCachedProviderPoolConnections(
filter?: Record<string, unknown>
): Promise<unknown[]> {
if (typeof filter?.provider !== "string") return getCachedProviderConnections(filter);
const providers = await getProviderSearchPool(filter.provider);
const results = await Promise.all(
providers.map((provider) => getCachedProviderConnections({ ...filter, provider }))
);
const seen = new Set<string>();
return results.flat().filter((connection) => {
const id = asRecord(connection).id;
if (typeof id !== "string" || !id || seen.has(id)) return false;
seen.add(id);
return true;
});
}

View file

@ -2,7 +2,7 @@ import type { CredentialSelectionOptions } from "./credentialSelectionOptions";
export type { CredentialSelectionOptions } from "./credentialSelectionOptions";
import { qoderSupportsCallerTools } from "@omniroute/open-sse/services/qoderCapabilities";
import { randomUUID } from "crypto";
import { nodeTypeFromId } from "@/lib/db/providerNodeSelect";
import { getProviderSearchPool } from "@omniroute/open-sse/services/providerConnectionPool.ts";
import { hydrateConnectionProviderSpecificData } from "./compatibleNodeBaseUrl.ts"; // #13452
import { extractGoogApiKeyHeader } from "./googApiKeyAuth.ts";
import { describeUpstreamFailure } from "@/shared/utils/upstreamError";
@ -39,7 +39,6 @@ import {
} from "@/lib/providers/peakHourProtection";
import { buildJinaEnvCredentials } from "@/lib/providers/jina";
import { buildGeminiEnvCredentials } from "@/lib/providers/gemini";
import { isCommonChatGptWebRetiredProviderId } from "@/shared/constants/chatgptWebRetirement";
import { toNumber } from "@/shared/utils/numeric";
import { timingSafeCompare } from "@/shared/utils/timingSafeCompare";
import { isMicrosoftDesignerWebRetiredProviderId } from "@/shared/constants/designerWebRetirement";
@ -144,7 +143,6 @@ import {
} from "@omniroute/open-sse/services/codexAccount/index.ts";
import {
getProviderById,
getProviderAlias,
resolveProviderId,
NOAUTH_PROVIDERS,
WEB_COOKIE_PROVIDERS,
@ -925,110 +923,6 @@ export { fisherYatesShuffle, getNextFromDeckSync as getNextFromDeck };
// backwards compat with existing imports (e.g. googApiKeyAuth.ts).
export { readHeaderValue, type AuthRequestHeaders } from "./headerReader.ts";
export { extractSessionAffinityKey } from "./sessionAffinityPin";
const PROVIDER_SEARCH_PAIRS: string[][] = [
["nvidia", "nvidia_nim"],
["kimi-coding", "kimi-coding-apikey"],
// The model layer canonicalizes `agy/` to `antigravity`, but the Antigravity
// CLI card stores its connection under `agy`. Same account, either id serves.
["antigravity", "agy"],
// OpenCode connection card stores under `opencode`, but model alias resolves to `opencode-zen`.
["opencode", "opencode-zen"],
// One Jina token works on api.jina.ai, r.jina.ai, and s.jina.ai.
// Requested id stays first so embed/rerank do not silently pick a
// Reader-only row when both cards are filled. jina-search has no
// dashboard card — it must still see jina-ai / jina-reader keys
// before falling through to JINA_AI_API_KEY.
["jina-ai", "jina-reader", "jina-search"],
];
/** Resolve provider aliases (e.g., nvidia -> nvidia_nim) for DB lookup. */
async function getProviderSearchPool(provider: string): Promise<string[]> {
const canonicalProvider = resolveProviderId(provider);
const canonicalAlias = getProviderAlias(canonicalProvider);
if (isCommonChatGptWebRetiredProviderId(provider)) return [];
const group = PROVIDER_SEARCH_PAIRS.find((aliases) => aliases.includes(provider));
if (group) return [provider, ...group.filter((id) => id !== provider)];
const searchPool = new Set([provider, canonicalProvider, canonicalAlias].filter(Boolean));
// Built-in providers already resolve through static ids/aliases. Only
// compatible/custom providers need provider_nodes expansion back to the
// generated internal connection ids. (#3058)
if (getProviderById(canonicalProvider)) {
return Array.from(searchPool);
}
// Custom provider nodes are referenced by user-facing prefixes in combos
// (for example "78code/gpt-5.4"), but live credentials are stored under
// internal provider ids like openai-compatible-responses-<uuid>.
try {
const providerNodes = await getCachedProviderNodes();
const compatibleNodes = Array.isArray(providerNodes) ? providerNodes : [];
const nodeTypes = new Map<string, number>();
for (const node of compatibleNodes) {
const nodeRecord = asRecord(node);
const nodeId = typeof nodeRecord.id === "string" ? nodeRecord.id.trim() : "";
if (!nodeId) continue;
const derivedType = nodeTypeFromId(nodeId);
nodeTypes.set(derivedType, (nodeTypes.get(derivedType) || 0) + 1);
}
for (const node of compatibleNodes) {
const nodeRecord = asRecord(node);
const nodePrefix = typeof nodeRecord.prefix === "string" ? nodeRecord.prefix.trim() : "";
const nodeId = typeof nodeRecord.id === "string" ? nodeRecord.id.trim() : "";
if (!nodeId) continue;
if (
nodePrefix &&
(nodePrefix === provider ||
nodePrefix === canonicalProvider ||
nodePrefix === canonicalAlias)
) {
searchPool.add(nodeId);
}
// #10085: bridge the concrete uuid node id (what the chat path resolves,
// "<generic-type>-<uuid>") to the GENERIC derived type id (what
// resolveProviderNodeForConnection also accepts for connection creation,
// #4421) -- and back. A connection created via the bare generic type
// (e.g. "openai-compatible-chat") must still be found when the chat path
// looks up the concrete node id, and vice versa.
//
// #10434: both bridging directions MUST require the derived type to be
// unambiguous (exactly one provider node of that type) before falling
// back to a generic-type match -- an explicit ownership check, not just
// a string-format coincidence. This mirrors the exact rule already
// enforced by selectProviderNodeForConnection() for connection CREATION
// (src/lib/db/providerNodeSelect.ts, #4421): "only when exactly one such
// node exists, so an ambiguous type never silently picks the wrong
// node". Without this guard on the generic->concrete direction, a bare
// generic-type lookup would pool in EVERY node sharing that derived
// type, including a connection scoped (via its own providerSpecificData
// baseUrl/headers) to one specific node -- leaking that node's
// credentials/upstream URL into a lookup for a different, unrelated
// node of the same generic type.
const derivedType = nodeTypeFromId(nodeId);
if (derivedType && derivedType !== nodeId) {
const typeIsUnambiguous = nodeTypes.get(derivedType) === 1;
if (typeIsUnambiguous) {
if (nodeId === provider || nodeId === canonicalProvider || nodeId === canonicalAlias) {
searchPool.add(derivedType);
}
if (
derivedType === provider ||
derivedType === canonicalProvider ||
derivedType === canonicalAlias
) {
searchPool.add(nodeId);
}
}
}
}
} catch {
// Best-effort alias expansion only.
}
return Array.from(searchPool);
}
function invalidateManagedLease(
options: CredentialSelectionOptions,

View file

@ -604,6 +604,7 @@
"tests/unit/github-copilot-model-not-supported-learning-15634.test.ts",
"tests/unit/alibaba-free-quota-drain-preserves-model-catalog-cache.test.ts",
"tests/unit/upstreamErrorEnvelope.test.ts",
"tests/unit/provider-alias-combo-pool-12844.test.ts",
"tests/unit/model-capacity-failure-scope-15788.test.ts",
"tests/unit/provider-node-local-dispatch-13452.test.ts"
],

View file

@ -160,7 +160,7 @@ test("#2778 matching logic: node with prefix=flymux and id=UUID-id still matches
test("custom provider auth lookup search pool maps alias prefixes to internal provider ids", async () => {
const authSrc = fs.readFileSync(
path.resolve(__dirname, "../../src/sse/services/auth.ts"),
path.resolve(__dirname, "../../open-sse/services/providerConnectionPool.ts"),
"utf8"
);

View file

@ -0,0 +1,185 @@
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import test from "node:test";
import type { ResolvedComboTarget } from "../../open-sse/services/combo/types.ts";
const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), "alias-combo-12844-"));
process.env.DATA_DIR = dataDir;
const { resetDbInstance } = await import("../../src/lib/db/core.ts");
const { createProviderConnection, updateProviderConnection } =
await import("../../src/lib/db/providers.ts");
const { getProviderCredentials } = await import("../../src/sse/services/auth.ts");
const { buildAutoCandidates, handleComboChat } = await import("../../open-sse/services/combo.ts");
const { expandPromptCacheAffinityTargets } =
await import("../../open-sse/services/combo/promptCacheAffinity.ts");
const { expandTargetsByQuotaAwareConnections } =
await import("../../open-sse/services/combo/quotaStrategies.ts");
const { applyRequestTagRouting } = await import("../../open-sse/services/combo/autoStrategy.ts");
const { registerQuotaFetcher } = await import("../../open-sse/services/quotaPreflight.ts");
const noOpLog = { info() {}, debug() {}, warn() {}, error() {} };
// Quota is deterministic and never contacts an upstream in these routing tests.
for (const provider of ["antigravity", "agy", "opencode", "opencode-zen", "gemini"])
registerQuotaFetcher(provider, async () => ({ used: 0, total: 100, percentUsed: 0 }));
test.after(() => {
resetDbInstance();
fs.rmSync(dataDir, { recursive: true, force: true });
});
async function seed(provider: string, name: string) {
return createProviderConnection({
provider,
name,
authType: "oauth",
accessToken: "fixture-token",
isActive: true,
testStatus: "active",
providerSpecificData: { projectId: "fixture-project", tags: ["fixture"] },
});
}
function target(provider: string, allowedConnectionIds?: string[]): ResolvedComboTarget {
const modelStr = `${provider}/gemini-3-pro`;
return {
kind: "model",
provider,
providerId: provider,
modelStr,
stepId: modelStr,
executionKey: modelStr,
connectionId: null,
weight: 1,
label: null,
allowedConnectionIds,
};
}
for (const [stored, requested] of [
["agy", "antigravity"],
["antigravity", "agy"],
["opencode", "opencode-zen"],
["opencode-zen", "opencode"],
]) {
test(`#12844 ${stored} account is visible to ${requested} direct, auto and affinity routing`, async () => {
const connection = await seed(stored, `${stored}-only`);
const step = target(requested, [connection.id]);
const direct = await getProviderCredentials(requested, null, [connection.id]);
assert.equal(direct?.connectionId, connection.id, "direct routing control already works");
const candidates = await buildAutoCandidates([step], `alias-${stored}`);
assert.deepEqual(
candidates.map((entry) => entry.connectionId),
[connection.id]
);
const affinity = await expandPromptCacheAffinityTargets([step]);
assert.deepEqual(
affinity.map((entry) => entry.connectionId),
[connection.id]
);
});
}
test("#12844 quota expansion intersects aliases with step/API-key restrictions and rejects foreign pins", async () => {
const allowed = await seed("agy", "allowed");
const denied = await seed("antigravity", "denied");
const foreign = await seed("gemini", "foreign");
const result = await expandTargetsByQuotaAwareConnections(
[target("antigravity", [allowed.id, denied.id])],
"alias-quota",
noOpLog,
[allowed.id]
);
assert.deepEqual(
result.expandedTargets.map((entry) => entry.connectionId),
[allowed.id]
);
const wrongPin = { ...target("antigravity"), connectionId: foreign.id };
const foreignResult = await expandTargetsByQuotaAwareConnections(
[wrongPin, { ...target("gemini"), connectionId: foreign.id }],
"alias-foreign-pin",
noOpLog
);
assert.deepEqual(
foreignResult.expandedTargets.map((entry) => entry.provider),
["gemini"]
);
const blocked = await getProviderCredentials("antigravity", null, [foreign.id]);
assert.equal(blocked?.blockedByKeyPolicy, true);
});
test("#12844 alias pool deduplicates accounts and tag routing preserves the step allowlist", async () => {
const first = await seed("agy", "first");
const second = await seed("antigravity", "second");
const step = target("antigravity", [first.id, second.id]);
const candidates = await buildAutoCandidates([step], "alias-dedup");
assert.deepEqual(
candidates.map((entry) => entry.connectionId).sort(),
[first.id, second.id].sort()
);
const tagged = await applyRequestTagRouting(
[target("antigravity", [first.id])],
{ metadata: { tags: ["fixture"] } },
noOpLog
);
assert.deepEqual(tagged[0]?.allowedConnectionIds, [first.id]);
});
test("#12844 round-robin dispatch selects an aliased account within API-key and step restrictions", async () => {
const allowed = await seed("agy", "rr-allowed");
await seed("antigravity", "rr-denied");
const seen: string[] = [];
for (let i = 0; i < 3; i++) {
const response = await handleComboChat({
body: { model: "alias-rr", messages: [{ role: "user", content: "hello" }] },
combo: {
name: "alias-rr",
strategy: "round-robin",
models: [
{ kind: "model", model: "antigravity/gemini-3-pro", allowedConnectionIds: [allowed.id] },
],
},
apiKeyAllowedConnections: [allowed.id],
log: noOpLog,
handleSingleModel: async (_body, _model, resolved) => {
const credentials = await getProviderCredentials(
"antigravity",
null,
resolved.allowedConnectionIds,
"gemini-3-pro",
{ forcedConnectionId: resolved.connectionId }
);
assert.equal(credentials?.connectionId, allowed.id);
seen.push(credentials.connectionId);
return Response.json({ choices: [{ message: { content: "fixture" } }] });
},
});
assert.equal(response.status, 200);
}
assert.deepEqual(seen, [allowed.id, allowed.id, allowed.id]);
});
test("#12844 sticky health sees a banned alias account and drops its pin", async () => {
const sticky = await import("../../open-sse/services/combo/sessionStickiness.ts");
const healthy = await seed("antigravity", "sticky-healthy");
const banned = await seed("agy", "sticky-banned");
await updateProviderConnection(banned.id, { testStatus: "banned" });
const targets = [healthy, banned].map((connection) => ({
...target("antigravity"),
connectionId: connection.id,
}));
const messages = [{ role: "user", content: "alias sticky fixture" }];
sticky.__setStickinessHeadroomFetcherForTests(async () => ({ util5h: 0, util7d: 0 }));
try {
const initial = await sticky.applySessionStickiness(targets, messages, "alias-health");
assert.ok(initial.messageHash);
sticky.recordStickyBinding(initial.messageHash, banned.id, "alias-health");
const result = await sticky.applySessionStickiness(targets, messages, "alias-health");
assert.equal(result.stuck, false);
assert.equal(result.targets[0].connectionId, healthy.id);
assert.equal(sticky.peekStickyConnectionId(initial.messageHash), null);
} finally {
sticky.clearAllStickyBindings();
sticky.__setStickinessHeadroomFetcherForTests(null);
}
});