diff --git a/changelog.d/fixes/10877-quota-alias-fetcher-lookup-gap.md b/changelog.d/fixes/10877-quota-alias-fetcher-lookup-gap.md new file mode 100644 index 0000000000..9501c6dad2 --- /dev/null +++ b/changelog.d/fixes/10877-quota-alias-fetcher-lookup-gap.md @@ -0,0 +1 @@ +- **fix(sse):** `getResetAwareProvider()` and the auto-combo quota lookup in `combo.ts` now canonicalize the provider id via `resolveProviderId()` before calling `getQuotaFetcher()`, so a fetcher registered under a provider's canonical id (e.g. `ollama-cloud`, `codex`) is found for combo targets stored under an alias spelling (e.g. `ollamacloud`, `cx`) instead of silently degrading reset-aware/reset-window/auto quota-aware routing to plain priority ordering (#10877) diff --git a/open-sse/services/combo.ts b/open-sse/services/combo.ts index cf602cf98d..f9947bb71c 100644 --- a/open-sse/services/combo.ts +++ b/open-sse/services/combo.ts @@ -64,6 +64,7 @@ import { getHiddenModelsByProvider } from "@/models"; import { resolveModelLockoutSettings } from "../../src/lib/resilience/modelLockoutSettings"; import { fetchCodexQuota } from "./codexQuotaFetcher.ts"; import { evaluateQuotaCutoff, getQuotaFetcher, type QuotaInfo } from "./quotaPreflight.ts"; +import { resolveProviderId } from "../../src/shared/constants/providers.ts"; import * as semaphore from "./rateLimitSemaphore.ts"; import { getCircuitBreaker } from "../../src/shared/utils/circuitBreaker"; import { parseModel } from "./model.ts"; @@ -491,7 +492,10 @@ export async function buildAutoCandidates( let quotaRemaining = 100; let quotaCutoffBlocked = false; let quotaCutoffReason: string | undefined; - const fetcher = getQuotaFetcher(provider); + // #10877: `provider` here may be a legacy/user-facing alias spelling + // (target.provider/parseModel output); canonicalize before the fetcher + // registry lookup so aliased combo members still hit quota-aware scoring. + const fetcher = getQuotaFetcher(resolveProviderId(provider)); const connection = target.connectionId ? connectionById.get(target.connectionId) : undefined; const authType = typeof connection?.authType === "string" ? connection.authType : null; const sessionAvailability = diff --git a/open-sse/services/combo/quotaScoring.ts b/open-sse/services/combo/quotaScoring.ts index 4a853b1455..a107768278 100644 --- a/open-sse/services/combo/quotaScoring.ts +++ b/open-sse/services/combo/quotaScoring.ts @@ -14,6 +14,7 @@ import { isRecord } from "./comboData.ts"; import type { SlaRoutingPolicy } from "../autoCombo/routerStrategy.ts"; import { RESET_WINDOW_NAMES } from "./types.ts"; import type { ResolvedComboTarget } from "./types.ts"; +import { resolveProviderId } from "../../../src/shared/constants/providers.ts"; const RESET_AWARE_SESSION_WINDOW_MS = 5 * 60 * 60 * 1000; const RESET_AWARE_WEEKLY_WINDOW_MS = 7 * 24 * 60 * 60 * 1000; @@ -138,7 +139,11 @@ export function resolveSlaRoutingPolicy( export function getResetAwareProvider(target: ResolvedComboTarget): string | null { const provider = (target.providerId || target.provider || "").toLowerCase(); - return provider || null; + // #10877: combo targets can carry a legacy/user-facing alias spelling + // (e.g. "ollamacloud", "cx") while quota fetchers register under the + // canonical provider id (e.g. "ollama-cloud", "codex"). Canonicalize here + // so getQuotaFetcher() lookups downstream (quotaStrategies.ts) find them. + return provider ? resolveProviderId(provider) : null; } function normalizeResetAt(value: unknown): string | null { diff --git a/tests/unit/quota-scoring-alias-lookup-10877.test.ts b/tests/unit/quota-scoring-alias-lookup-10877.test.ts new file mode 100644 index 0000000000..4039ceeffc --- /dev/null +++ b/tests/unit/quota-scoring-alias-lookup-10877.test.ts @@ -0,0 +1,66 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { getResetAwareProvider } from "../../open-sse/services/combo/quotaScoring.ts"; +import { registerQuotaFetcher, getQuotaFetcher } from "../../open-sse/services/quotaPreflight.ts"; +import { resolveProviderId } from "../../src/shared/constants/providers.ts"; +import type { ResolvedComboTarget } from "../../open-sse/services/combo/types.ts"; + +function buildTarget(provider: string): ResolvedComboTarget { + return { + kind: "model", + stepId: "s1", + executionKey: "e1", + modelStr: `${provider}/some-model`, + provider, + providerId: provider, + connectionId: "conn-1", + weight: 1, + label: null, + } as ResolvedComboTarget; +} + +test("#10877: getResetAwareProvider() canonicalizes an alias-spelled provider so the fetcher registered under the canonical id is found", () => { + registerQuotaFetcher("ollama-cloud", async () => ({ ok: true }) as never); + + const target = buildTarget("ollamacloud"); + const lookedUpProvider = getResetAwareProvider(target); + + assert.equal( + lookedUpProvider, + resolveProviderId("ollamacloud"), + "getResetAwareProvider() should return the canonical provider id, not the raw alias" + ); + + const fetcher = getQuotaFetcher(lookedUpProvider!); + assert.notEqual( + fetcher, + undefined, + "a fetcher registered under the canonical provider id must be found for an alias-spelled combo target" + ); +}); + +test("#10877: getResetAwareProvider() is a no-op (same cache key) for already-canonical provider ids", () => { + registerQuotaFetcher("codex", async () => ({ ok: true }) as never); + + const target = buildTarget("codex"); + const lookedUpProvider = getResetAwareProvider(target); + + assert.equal(lookedUpProvider, "codex"); + assert.notEqual(getQuotaFetcher(lookedUpProvider!), undefined); +}); + +test("#10877: getResetAwareProvider() returns null when neither providerId nor provider is set", () => { + const target = { + kind: "model", + stepId: "s1", + executionKey: "e1", + modelStr: "unknown/model", + provider: "", + providerId: "", + connectionId: "conn-1", + weight: 1, + label: null, + } as ResolvedComboTarget; + + assert.equal(getResetAwareProvider(target), null); +});