feat(quota): /v1/models lists only quotaShared-* models for quota-exclusive keys (Phase B3)

This commit is contained in:
diegosouzapw
2026-05-30 21:41:13 -03:00
parent 78c5a30cf9
commit 3742afcd64
3 changed files with 105 additions and 11 deletions

View File

@@ -1138,20 +1138,31 @@ export async function getUnifiedModelsResponse(
const apiKey = extractBearer(request.headers);
let finalModels = models;
if (apiKey) {
const { isModelAllowedForKey } = await import("@/lib/db/apiKeys");
const { isModelAllowedForKey, getApiKeyMetadata } = await import("@/lib/db/apiKeys");
const filtered = [];
for (const m of models) {
// m.id is the full identifier (e.g. openai/gpt-4o), m.root is the raw model string
// check either one as the config could use either patterns
if (
(await isModelAllowedForKey(apiKey, m.id)) ||
(await isModelAllowedForKey(apiKey, m.root))
) {
filtered.push(m);
// Quota-exclusive keys (allowedQuotas non-empty): show only the
// quotaShared-* virtual models for the key's assigned pools (Phase B3).
// This takes precedence over the normal allowedModels filter.
const keyMeta = await getApiKeyMetadata(apiKey);
if (keyMeta && keyMeta.allowedQuotas && keyMeta.allowedQuotas.length > 0) {
const { resolveQuotaKeyScope } = await import("@/lib/quota/quotaKey");
const { filterModelsToQuotaPools } = await import("@/lib/quota/quotaCombos");
const scope = await resolveQuotaKeyScope(keyMeta.allowedQuotas);
finalModels = filterModelsToQuotaPools(models, scope.poolSlugs);
} else {
const filtered = [];
for (const m of models) {
// m.id is the full identifier (e.g. openai/gpt-4o), m.root is the raw model string
// check either one as the config could use either patterns
if (
(await isModelAllowedForKey(apiKey, m.id)) ||
(await isModelAllowedForKey(apiKey, m.root))
) {
filtered.push(m);
}
}
finalModels = filtered;
}
finalModels = filtered;
}
const getDefaultContextFallback = (model: any): number | undefined => {

View File

@@ -171,6 +171,33 @@ export async function syncQuotaCombos(poolId: string): Promise<void> {
}
}
// ---------------------------------------------------------------------------
// Catalog filter helper (Phase B3)
// ---------------------------------------------------------------------------
/**
* Given a flat model list and a set of pool slugs, return only the entries
* whose `id` is a `quotaShared-*` virtual model name AND whose parsed
* `poolSlug` is in `poolSlugs`.
*
* Fail-closed: an empty `poolSlugs` array returns an empty list — a
* quota-exclusive API key with no resolvable pools sees NO models.
*
* Pure function — no I/O, easily unit-tested.
*/
export function filterModelsToQuotaPools<T extends { id: string }>(
models: T[],
poolSlugs: string[]
): T[] {
if (poolSlugs.length === 0) return [];
const slugSet = new Set(poolSlugs);
return models.filter((m) => {
if (!isQuotaModelName(m.id)) return false;
const parsed = parseQuotaModelName(m.id);
return parsed !== null && slugSet.has(parsed.poolSlug);
});
}
/**
* Delete ALL `quotaShared-*` combos that belong to the given pool.
*

View File

@@ -0,0 +1,56 @@
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { filterModelsToQuotaPools } from "../../src/lib/quota/quotaCombos.js";
describe("filterModelsToQuotaPools", () => {
const models = [
{ id: "quotaShared-times-codex/gpt-5.5" },
{ id: "quotaShared-times-codex/gpt-5.4" },
{ id: "cx/gpt-5.5" },
{ id: "quotaShared-other-codex/m" },
];
it("returns only quotaShared-* entries whose poolSlug is in the given pool slugs", () => {
const result = filterModelsToQuotaPools(models, ["times"]);
assert.deepEqual(result, [
{ id: "quotaShared-times-codex/gpt-5.5" },
{ id: "quotaShared-times-codex/gpt-5.4" },
]);
});
it("returns empty array when poolSlugs is empty (fail-closed)", () => {
const result = filterModelsToQuotaPools(models, []);
assert.deepEqual(result, []);
});
it("returns empty array when no quota models are present in the list", () => {
const plainModels = [{ id: "cx/gpt-5.5" }, { id: "openai/gpt-4o" }];
const result = filterModelsToQuotaPools(plainModels, ["times"]);
assert.deepEqual(result, []);
});
it("matches multiple pool slugs simultaneously", () => {
const result = filterModelsToQuotaPools(models, ["times", "other"]);
assert.deepEqual(result, [
{ id: "quotaShared-times-codex/gpt-5.5" },
{ id: "quotaShared-times-codex/gpt-5.4" },
{ id: "quotaShared-other-codex/m" },
]);
});
it("preserves extra fields on model entries (generic T extends { id })", () => {
const richModels = [
{ id: "quotaShared-times-cx/gpt-5.5", object: "model", owned_by: "combo" },
{ id: "cx/gpt-5.5", object: "model", owned_by: "cx" },
];
const result = filterModelsToQuotaPools(richModels, ["times"]);
assert.deepEqual(result, [
{ id: "quotaShared-times-cx/gpt-5.5", object: "model", owned_by: "combo" },
]);
});
it("does not match a model from a different pool when only one slug is provided", () => {
const result = filterModelsToQuotaPools(models, ["other"]);
assert.deepEqual(result, [{ id: "quotaShared-other-codex/m" }]);
});
});