mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-17 12:22:34 +03:00
fix(models): reconcile provider dashboards with active live catalogs (#13434)
Merged. The NVIDIA 116-vs-82 discrepancy is the visible symptom; the fix is the right one — reuse the existing `liveCatalogAuthoritative` policy on the dashboard listing instead of a provider-specific filter, refresh after a removals-only import, keep the last confirmed snapshot on a failed refresh, and apply the same membership rule to the OpenRouter/compatible/passthrough row builders so static fallbacks cannot resurrect retired rows. Operator custom models and overrides preserved. Validated as a combined board first (this PR merged with the 4 siblings of the JxnLexn wave on the release tip): eslint with the frozen suppressions, typecheck:core, check:open-sse-typecheck, complexity, cognitive-complexity, changelog-integrity, docs-sync, migration-numbering, provider-consistency and a duplicate-identifier audit all green, plus 77 passing / 0 failing focused node:test cases across the test files the wave touches. The wave's i18n fill (new keys carried to all 66 locales), free-tier doc counts and file-size rebaseline land in one follow-up PR right after the wave, as with #13904. Thank you — checking the projection against 14 production catalog snapshots is the kind of evidence that makes a listing change safe to land.
This commit is contained in:
@@ -0,0 +1 @@
|
||||
- **fix(models):** Reconcile provider dashboards with confirmed authoritative live catalogs, excluding retired built-in/imported rows while preserving manual custom models and partial-catalog fallbacks. ([#13434](https://github.com/diegosouzapw/OmniRoute/pull/13434)) — thanks @JxnLexn
|
||||
@@ -916,11 +916,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/providers/[id]/hooks/useModelImportHandlers.ts": {
|
||||
"@typescript-eslint/no-unused-vars": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/dashboard/providers/[id]/hooks/useModelVisibilityHandlers.ts": {
|
||||
"@typescript-eslint/no-unused-vars": {
|
||||
"count": 2
|
||||
|
||||
@@ -192,6 +192,7 @@ export default function ProviderDetailPageClient() {
|
||||
const {
|
||||
modelMeta,
|
||||
syncedAvailableModels,
|
||||
syncedCatalogAuthoritative,
|
||||
modelAliases,
|
||||
fetchProviderModelMeta,
|
||||
fetchAliases,
|
||||
@@ -286,14 +287,14 @@ export default function ProviderDetailPageClient() {
|
||||
NOAUTH_PROVIDERS[providerId]?.noAuth === true ||
|
||||
getProviderById(providerId)?.managedAccount === true;
|
||||
const registryModels = getModelsByProviderId(providerId);
|
||||
// Prefer synced API-discovered models when available, then merge built-ins
|
||||
// and user-managed custom models without duplicating IDs. Cursor exclusive
|
||||
// listing drops the static registry entirely when synced is non-empty.
|
||||
// Use the server's active-catalog authority decision for display and Test All.
|
||||
// Registry entries supply metadata/fallback; operator custom models remain.
|
||||
const models = useMemo(() => {
|
||||
return mergeProviderModelListing({
|
||||
providerId,
|
||||
registryModels,
|
||||
syncedModels: syncedAvailableModels,
|
||||
syncedCatalogAuthoritative,
|
||||
customModels: (modelMeta.customModels || []).map((cm) => ({
|
||||
...cm,
|
||||
id: cm.id,
|
||||
@@ -307,6 +308,7 @@ export default function ProviderDetailPageClient() {
|
||||
registryModels,
|
||||
syncedAvailableModels,
|
||||
modelMeta.customModels,
|
||||
syncedCatalogAuthoritative,
|
||||
usesCuratedModelsOnly,
|
||||
]);
|
||||
const isUpstreamProxyProvider = providerInfo?.category === "upstream-proxy";
|
||||
@@ -770,6 +772,7 @@ export default function ProviderDetailPageClient() {
|
||||
modelMeta={modelMeta}
|
||||
modelAliases={modelAliases}
|
||||
syncedAvailableModels={syncedAvailableModels}
|
||||
syncedCatalogAuthoritative={syncedCatalogAuthoritative}
|
||||
compatibleFallbackModels={compatibleFallbackModels}
|
||||
copied={copied}
|
||||
onCopy={copy}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"use client";
|
||||
import { filterUnavailableModelRows } from "@/lib/providers/mergeProviderModelListing";
|
||||
/**
|
||||
* CompatibleModelsSection — Issue #3501 Phase 1e
|
||||
*
|
||||
@@ -42,6 +43,7 @@ export interface CompatibleModelsSectionProps {
|
||||
providerDisplayAlias: string;
|
||||
modelAliases: Record<string, string>;
|
||||
availableModels?: CompatModelRow[];
|
||||
syncedCatalogAuthoritative?: boolean;
|
||||
customModels?: CompatModelRow[];
|
||||
fallbackModels?: CompatModelRow[];
|
||||
allowImport: boolean;
|
||||
@@ -86,6 +88,7 @@ export default function CompatibleModelsSection({
|
||||
providerDisplayAlias,
|
||||
modelAliases,
|
||||
availableModels = [],
|
||||
syncedCatalogAuthoritative = false,
|
||||
customModels = [],
|
||||
fallbackModels = [],
|
||||
description,
|
||||
@@ -223,9 +226,15 @@ export default function CompatibleModelsSection({
|
||||
seenModelIds.add(modelId);
|
||||
}
|
||||
|
||||
return rows;
|
||||
return filterUnavailableModelRows(
|
||||
rows,
|
||||
availableModels,
|
||||
customModels,
|
||||
syncedCatalogAuthoritative
|
||||
);
|
||||
}, [
|
||||
availableModels,
|
||||
syncedCatalogAuthoritative,
|
||||
customModelMap,
|
||||
customModels,
|
||||
fallbackModels,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"use client";
|
||||
import { filterUnavailableModelRows } from "@/lib/providers/mergeProviderModelListing";
|
||||
/**
|
||||
* PassthroughModelsSection — Issue #3501 Phase 1e
|
||||
*
|
||||
@@ -51,6 +52,7 @@ export interface PassthroughModelsSectionProps {
|
||||
modelAliases: Record<string, string>;
|
||||
catalogModels?: CompatModelRow[];
|
||||
availableModels?: CompatModelRow[];
|
||||
syncedCatalogAuthoritative?: boolean;
|
||||
customModels?: CompatModelRow[];
|
||||
description: string;
|
||||
inputLabel: string;
|
||||
@@ -96,6 +98,7 @@ export default function PassthroughModelsSection({
|
||||
modelAliases,
|
||||
catalogModels = [],
|
||||
availableModels = [],
|
||||
syncedCatalogAuthoritative = false,
|
||||
customModels = [],
|
||||
description,
|
||||
inputLabel,
|
||||
@@ -314,9 +317,15 @@ export default function PassthroughModelsSection({
|
||||
seenModelIds.add(modelId);
|
||||
}
|
||||
|
||||
return rows;
|
||||
return filterUnavailableModelRows(
|
||||
rows,
|
||||
availableModels,
|
||||
customModels,
|
||||
syncedCatalogAuthoritative
|
||||
);
|
||||
}, [
|
||||
availableModels,
|
||||
syncedCatalogAuthoritative,
|
||||
catalogModels,
|
||||
customModelMap,
|
||||
customModels,
|
||||
|
||||
@@ -49,6 +49,7 @@ export interface ProviderModelsSectionProps {
|
||||
modelMeta: { customModels: any[]; modelCompatOverrides?: any[] };
|
||||
modelAliases: Record<string, string>;
|
||||
syncedAvailableModels: any[];
|
||||
syncedCatalogAuthoritative?: boolean;
|
||||
compatibleFallbackModels: any[];
|
||||
|
||||
// Clipboard
|
||||
@@ -130,6 +131,7 @@ export default function ProviderModelsSection({
|
||||
modelMeta,
|
||||
modelAliases,
|
||||
syncedAvailableModels,
|
||||
syncedCatalogAuthoritative = false,
|
||||
compatibleFallbackModels,
|
||||
copied,
|
||||
onCopy,
|
||||
@@ -268,6 +270,7 @@ export default function ProviderModelsSection({
|
||||
providerDisplayAlias={providerDisplayAlias}
|
||||
modelAliases={modelAliases}
|
||||
availableModels={syncedAvailableModels}
|
||||
syncedCatalogAuthoritative={syncedCatalogAuthoritative}
|
||||
customModels={modelMeta.customModels}
|
||||
fallbackModels={compatibleFallbackModels}
|
||||
description={description}
|
||||
@@ -348,6 +351,7 @@ export default function ProviderModelsSection({
|
||||
providerAlias={providerAlias}
|
||||
modelAliases={modelAliases}
|
||||
catalogModels={models}
|
||||
syncedCatalogAuthoritative={syncedCatalogAuthoritative}
|
||||
availableModels={syncedAvailableModels}
|
||||
customModels={modelMeta.customModels}
|
||||
description={passthroughDescription}
|
||||
|
||||
@@ -150,6 +150,9 @@ export function useModelImportHandlers({
|
||||
return;
|
||||
}
|
||||
const fetchedModels = data.models || [];
|
||||
// Discovery persists its result even when no new models need importing.
|
||||
// Refresh the active listing so removals take effect without a page reload.
|
||||
await fetchProviderModelMeta();
|
||||
const importWarning = extractImportWarning(data);
|
||||
if (fetchedModels.length === 0) {
|
||||
setImportProgress((prev) => ({
|
||||
@@ -306,6 +309,8 @@ export function useModelImportHandlers({
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || t("failedImportModels"));
|
||||
}
|
||||
await fetchProviderModelMeta();
|
||||
await fetchAliases();
|
||||
|
||||
if (data.freeFilterEmpty) {
|
||||
setImportProgress((prev) => ({
|
||||
|
||||
@@ -28,6 +28,7 @@ export interface ModelMeta {
|
||||
export interface UseProviderModelsReturn {
|
||||
modelMeta: ModelMeta;
|
||||
syncedAvailableModels: any[];
|
||||
syncedCatalogAuthoritative: boolean;
|
||||
modelAliases: Record<string, string>;
|
||||
fetchProviderModelMeta: () => Promise<void>;
|
||||
fetchAliases: () => Promise<void>;
|
||||
@@ -46,7 +47,14 @@ export function useProviderModels(
|
||||
customModels: [],
|
||||
modelCompatOverrides: [],
|
||||
});
|
||||
const [syncedAvailableModels, setSyncedAvailableModels] = useState<any[]>([]);
|
||||
const [syncedCatalog, setSyncedCatalog] = useState({
|
||||
providerId: "",
|
||||
models: [] as any[],
|
||||
authoritative: false,
|
||||
});
|
||||
const syncedAvailableModels = syncedCatalog.providerId === providerId ? syncedCatalog.models : [];
|
||||
const syncedCatalogAuthoritative =
|
||||
syncedCatalog.providerId === providerId && syncedCatalog.authoritative;
|
||||
const [modelAliases, setModelAliases] = useState<Record<string, string>>({});
|
||||
|
||||
const fetchAliases = useCallback(async () => {
|
||||
@@ -133,12 +141,16 @@ export function useProviderModels(
|
||||
);
|
||||
if (syncRes.ok) {
|
||||
const syncData = await syncRes.json();
|
||||
setSyncedAvailableModels(syncData.models || []);
|
||||
} else {
|
||||
setSyncedAvailableModels([]);
|
||||
if (Array.isArray(syncData.models)) {
|
||||
setSyncedCatalog({
|
||||
providerId,
|
||||
models: syncData.models,
|
||||
authoritative: syncData.authoritative === true,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
setSyncedAvailableModels([]);
|
||||
// A transient dashboard request failure must not resurrect retired static models.
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("fetchProviderModelMeta", e);
|
||||
@@ -148,6 +160,7 @@ export function useProviderModels(
|
||||
return {
|
||||
modelMeta,
|
||||
syncedAvailableModels,
|
||||
syncedCatalogAuthoritative,
|
||||
modelAliases,
|
||||
fetchProviderModelMeta,
|
||||
fetchAliases,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { getSyncedAvailableModels, getAllSyncedAvailableModels } from "@/lib/db/models";
|
||||
import { getAllSyncedAvailableModels } from "@/lib/db/models";
|
||||
import { getActiveSyncedCatalog } from "@/lib/db/models/activeSyncedCatalog";
|
||||
import { isAuthenticated } from "@/shared/utils/apiAuth";
|
||||
|
||||
/**
|
||||
@@ -18,8 +19,10 @@ export async function GET(request: Request) {
|
||||
const provider = searchParams.get("provider");
|
||||
|
||||
if (provider) {
|
||||
const models = await getSyncedAvailableModels(provider);
|
||||
return Response.json({ models });
|
||||
// The dashboard merges operator-owned custom rows separately. Do not let
|
||||
// legacy imports act as evidence that a model still exists upstream.
|
||||
const catalog = await getActiveSyncedCatalog(provider, false);
|
||||
return Response.json(catalog);
|
||||
}
|
||||
|
||||
const allModels = await getAllSyncedAvailableModels();
|
||||
|
||||
@@ -238,7 +238,11 @@ async function loadConnectionCatalog(storedProviderId: string): Promise<Connecti
|
||||
};
|
||||
}
|
||||
|
||||
export async function getActiveSyncedCatalog(providerId: string): Promise<ActiveSyncedCatalog> {
|
||||
/** Set includeCustomModels=false for consumers that overlay custom rows separately. */
|
||||
export async function getActiveSyncedCatalog(
|
||||
providerId: string,
|
||||
includeCustomModels = true
|
||||
): Promise<ActiveSyncedCatalog> {
|
||||
const storedProviderId = resolveStoredProviderId(providerId);
|
||||
if (!storedProviderId) {
|
||||
return { authoritative: false, models: [] };
|
||||
@@ -249,12 +253,10 @@ export async function getActiveSyncedCatalog(providerId: string): Promise<Active
|
||||
const siblingCatalogs = await Promise.all(lookupIds.map(loadConnectionCatalog));
|
||||
// #12866 unions the agy/antigravity sibling catalogs; #12934 then overlays the
|
||||
// picker-added customModels so dispatch admits the same rows the picker REST shows.
|
||||
const discovered = unionModels(siblingCatalogs.map((catalog) => catalog.models));
|
||||
const models = enrichCursorCatalog(
|
||||
storedProviderId,
|
||||
await unionCustomModels(
|
||||
storedProviderId,
|
||||
unionModels(siblingCatalogs.map((catalog) => catalog.models))
|
||||
)
|
||||
includeCustomModels ? await unionCustomModels(storedProviderId, discovered) : discovered
|
||||
);
|
||||
if (models.length > 0) {
|
||||
// #12849: only gate on this catalog while at least one sibling connection
|
||||
@@ -283,7 +285,12 @@ export async function getActiveSyncedCatalog(providerId: string): Promise<Active
|
||||
authoritative: false,
|
||||
models: enrichCursorCatalog(
|
||||
storedProviderId,
|
||||
await unionCustomModels(storedProviderId, await getSyncedAvailableModels(storedProviderId))
|
||||
includeCustomModels
|
||||
? await unionCustomModels(
|
||||
storedProviderId,
|
||||
await getSyncedAvailableModels(storedProviderId)
|
||||
)
|
||||
: await getSyncedAvailableModels(storedProviderId)
|
||||
),
|
||||
};
|
||||
} catch {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Pure merge of registry / synced / custom model rows for the provider detail
|
||||
* dashboard (and thus Test All targets). Cursor exclusive listing prefers the
|
||||
* live synced catalog when non-empty.
|
||||
* dashboard (and thus Test All targets). Server-confirmed authoritative catalogs
|
||||
* exclude retired static/imported rows while preserving operator-owned models.
|
||||
*/
|
||||
|
||||
import { ensureCursorAutoCatalogEntry } from "@/lib/providerModels/cursorAutoCatalog";
|
||||
@@ -18,12 +18,28 @@ export type ProviderListingModel = {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
/** Apply the same membership rule to compatible/passthrough rows, including alias-only rows. */
|
||||
export function filterUnavailableModelRows<T extends { modelId: string }>(
|
||||
rows: T[],
|
||||
syncedModels: Array<{ id?: string }>,
|
||||
customModels: Array<{ id?: string; source?: string }>,
|
||||
authoritative: boolean
|
||||
): T[] {
|
||||
if (!authoritative) return rows;
|
||||
const allowed = new Set([
|
||||
...syncedModels.map((model) => model.id),
|
||||
...customModels.filter((model) => model.source !== "imported").map((model) => model.id),
|
||||
]);
|
||||
return rows.filter((row) => allowed.has(row.modelId));
|
||||
}
|
||||
|
||||
export type MergeProviderModelListingInput = {
|
||||
providerId: string;
|
||||
registryModels: Array<{ id: string; name?: string }>;
|
||||
syncedModels: Array<{ id: string; name?: string; [key: string]: unknown }>;
|
||||
customModels: Array<{ id: string; name?: string; source?: string; [key: string]: unknown }>;
|
||||
usesCuratedModelsOnly?: boolean;
|
||||
syncedCatalogAuthoritative?: boolean;
|
||||
};
|
||||
|
||||
function normalizeCustomSource(source: unknown): "imported" | "custom" {
|
||||
@@ -46,24 +62,33 @@ export function mergeProviderModelListing(
|
||||
const synced = curated ? [] : input.syncedModels.filter((m) => m?.id);
|
||||
const custom = curated ? [] : input.customModels.filter((m) => m?.id);
|
||||
|
||||
const exclusive = providerUsesExclusiveSyncedListing(input.providerId) && synced.length > 0;
|
||||
const exclusive =
|
||||
!curated &&
|
||||
(input.syncedCatalogAuthoritative ??
|
||||
(providerUsesExclusiveSyncedListing(input.providerId) && synced.length > 0));
|
||||
|
||||
if (exclusive) {
|
||||
const withAuto = ensureCursorAutoCatalogEntry(
|
||||
synced.map((model) => ({
|
||||
...model,
|
||||
id: model.id,
|
||||
name: model.name || model.id,
|
||||
owned_by: "cursor",
|
||||
source: "imported",
|
||||
}))
|
||||
);
|
||||
const normalizedCustom = custom.map((model) => ({
|
||||
const cursor = providerUsesExclusiveSyncedListing(input.providerId);
|
||||
const registryById = new Map(input.registryModels.map((model) => [model.id, model]));
|
||||
const liveModels = synced.map((model) => ({
|
||||
...(registryById.get(model.id) || {}),
|
||||
...model,
|
||||
id: model.id,
|
||||
name: model.name || model.id,
|
||||
source: normalizeCustomSource(model.source),
|
||||
owned_by: cursor ? "cursor" : input.providerId,
|
||||
source: "imported",
|
||||
}));
|
||||
const withAuto =
|
||||
cursor && liveModels.length > 0 ? ensureCursorAutoCatalogEntry(liveModels) : liveModels;
|
||||
const liveIds = new Set(withAuto.map((model) => model.id));
|
||||
const normalizedCustom = custom
|
||||
.filter((model) => model.source !== "imported" || liveIds.has(model.id))
|
||||
.map((model) => ({
|
||||
...model,
|
||||
id: model.id,
|
||||
name: model.name || model.id,
|
||||
source: normalizeCustomSource(model.source),
|
||||
}));
|
||||
return dedupeById(mergeModelsWithCustomPrecedence(withAuto, normalizedCustom));
|
||||
}
|
||||
|
||||
|
||||
55
tests/unit/active-catalog-display-snapshot.test.ts
Normal file
55
tests/unit/active-catalog-display-snapshot.test.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
const directory = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-display-catalog-"));
|
||||
process.env.DATA_DIR = directory;
|
||||
process.env.API_KEY_SECRET = "display-catalog-test-secret";
|
||||
const core = await import("../../src/lib/db/core.ts");
|
||||
const { replaceSyncedAvailableModelsForConnection } = await import("../../src/lib/db/models.ts");
|
||||
const { getActiveSyncedCatalog } = await import("../../src/lib/db/models/activeSyncedCatalog.ts");
|
||||
|
||||
test.after(() => {
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(directory, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test("display snapshot unions active connections, excludes inactive catalogs and custom/import overlays", async () => {
|
||||
const db = core.getDbInstance();
|
||||
const now = new Date().toISOString();
|
||||
for (const [id, active] of [
|
||||
["active-a", 1],
|
||||
["active-b", 1],
|
||||
["inactive", 0],
|
||||
] as const) {
|
||||
db.prepare(
|
||||
"INSERT INTO provider_connections (id,provider,is_active,created_at,updated_at) VALUES (?,?,?,?,?)"
|
||||
).run(id, "nvidia", active, now, now);
|
||||
await replaceSyncedAvailableModelsForConnection("nvidia", id, [
|
||||
{ id: `${id}-model`, name: id },
|
||||
]);
|
||||
}
|
||||
db.prepare("INSERT OR REPLACE INTO key_value (namespace,key,value) VALUES (?,?,?)").run(
|
||||
"customModels",
|
||||
"nvidia",
|
||||
JSON.stringify([
|
||||
{ id: "manual", source: "custom" },
|
||||
{ id: "old-import", source: "imported" },
|
||||
])
|
||||
);
|
||||
const catalog = await getActiveSyncedCatalog("nvidia", false);
|
||||
assert.equal(catalog.authoritative, true);
|
||||
assert.deepEqual(catalog.models.map((m) => m.id).sort(), ["active-a-model", "active-b-model"]);
|
||||
});
|
||||
|
||||
test("custom rows alone cannot establish an authoritative discovery snapshot", async () => {
|
||||
core
|
||||
.getDbInstance()
|
||||
.prepare("INSERT OR REPLACE INTO key_value (namespace,key,value) VALUES (?,?,?)")
|
||||
.run("customModels", "openai", JSON.stringify([{ id: "manual", source: "custom" }]));
|
||||
const catalog = await getActiveSyncedCatalog("openai", false);
|
||||
assert.equal(catalog.authoritative, false);
|
||||
assert.equal(catalog.models.length, 0);
|
||||
});
|
||||
139
tests/unit/authoritative-provider-listing.test.ts
Normal file
139
tests/unit/authoritative-provider-listing.test.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import {
|
||||
mergeProviderModelListing,
|
||||
filterUnavailableModelRows,
|
||||
} from "../../src/lib/providers/mergeProviderModelListing.ts";
|
||||
|
||||
test("OpenRouter and compatible/passthrough aliases cannot resurrect unavailable models", () => {
|
||||
const rows = ["live", "retired-fallback", "retired-alias", "manual", "old-import"].map(
|
||||
(modelId) => ({ modelId })
|
||||
);
|
||||
const live = [{ id: "live" }];
|
||||
const custom = [
|
||||
{ id: "manual", source: "custom" },
|
||||
{ id: "old-import", source: "imported" },
|
||||
];
|
||||
assert.deepEqual(
|
||||
filterUnavailableModelRows(rows, live, custom, true).map((row) => row.modelId),
|
||||
["live", "manual"]
|
||||
);
|
||||
assert.deepEqual(filterUnavailableModelRows(rows, live, custom, false), rows);
|
||||
});
|
||||
|
||||
const input = {
|
||||
providerId: "nvidia",
|
||||
registryModels: [
|
||||
{ id: "retired", name: "Retired model" },
|
||||
{ id: "live", name: "Live model", supportsVision: true },
|
||||
],
|
||||
syncedModels: [{ id: "live", contextWindow: 128000 }],
|
||||
customModels: [
|
||||
{ id: "retired-import", source: "imported" },
|
||||
{ id: "manual", name: "My endpoint", source: "custom" },
|
||||
{ id: "live", name: "My live model", supportsVision: false },
|
||||
],
|
||||
};
|
||||
|
||||
test("authoritative catalog removes retired built-ins and legacy imports, retaining manual overlays", () => {
|
||||
const models = mergeProviderModelListing({ ...input, syncedCatalogAuthoritative: true });
|
||||
assert.deepEqual(
|
||||
models.map((m) => m.id),
|
||||
["live", "manual"]
|
||||
);
|
||||
assert.equal(models[0].contextWindow, 128000);
|
||||
assert.equal(models[0].supportsVision, false);
|
||||
assert.equal(models[0].name, "My live model");
|
||||
assert.equal(
|
||||
models.some((m) => m.id.startsWith("auto")),
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
test("partial or unconfirmed catalogs retain static coverage", () => {
|
||||
for (const flag of [false, undefined]) {
|
||||
const ids = mergeProviderModelListing({ ...input, syncedCatalogAuthoritative: flag }).map(
|
||||
(m) => m.id
|
||||
);
|
||||
assert.ok(ids.includes("retired"));
|
||||
assert.ok(ids.includes("retired-import"));
|
||||
assert.ok(ids.includes("manual"));
|
||||
}
|
||||
});
|
||||
|
||||
test("a new successful catalog removes only models absent from the active union", () => {
|
||||
const ids = mergeProviderModelListing({
|
||||
...input,
|
||||
syncedCatalogAuthoritative: true,
|
||||
syncedModels: [{ id: "other-connection" }],
|
||||
customModels: [{ id: "manual", source: "custom" }],
|
||||
}).map((m) => m.id);
|
||||
assert.deepEqual(ids, ["other-connection", "manual"]);
|
||||
});
|
||||
|
||||
test("confirmed empty catalog preserves custom models without inventing Cursor auto rows", () => {
|
||||
const ids = mergeProviderModelListing({
|
||||
...input,
|
||||
providerId: "cursor",
|
||||
syncedCatalogAuthoritative: true,
|
||||
syncedModels: [],
|
||||
customModels: [{ id: "manual", source: "custom" }],
|
||||
}).map((m) => m.id);
|
||||
assert.deepEqual(ids, ["manual"]);
|
||||
});
|
||||
|
||||
test("explicit non-authoritative state overrides Cursor's legacy implicit exclusivity", () => {
|
||||
const ids = mergeProviderModelListing({
|
||||
...input,
|
||||
providerId: "cursor",
|
||||
syncedCatalogAuthoritative: false,
|
||||
}).map((m) => m.id);
|
||||
assert.ok(ids.includes("retired"));
|
||||
});
|
||||
|
||||
test("authoritative membership and manual metadata precedence are provider-independent", () => {
|
||||
for (const providerId of ["nvidia", "openai", "openrouter", "anthropic", "vertex"]) {
|
||||
const snapshot = JSON.stringify(input);
|
||||
const models = mergeProviderModelListing({
|
||||
...input,
|
||||
providerId,
|
||||
syncedCatalogAuthoritative: true,
|
||||
});
|
||||
assert.deepEqual(
|
||||
models.map((m) => m.id),
|
||||
["live", "manual"],
|
||||
providerId
|
||||
);
|
||||
assert.equal(models[0].owned_by, providerId);
|
||||
assert.equal(models[0].contextWindow, 128000);
|
||||
assert.equal(models[0].supportsVision, false);
|
||||
assert.equal(JSON.stringify(input), snapshot, "must not mutate inputs");
|
||||
}
|
||||
});
|
||||
|
||||
test("curated-only catalogs cannot be emptied or extended by a live-catalog flag", () => {
|
||||
for (const providerId of ["kimi-web", "zai-web", "chatgpt-web"]) {
|
||||
const models = mergeProviderModelListing({
|
||||
...input,
|
||||
providerId,
|
||||
syncedCatalogAuthoritative: true,
|
||||
});
|
||||
assert.deepEqual(
|
||||
models.map((m) => m.id),
|
||||
["retired", "live"]
|
||||
);
|
||||
assert.ok(models.every((m) => m.source === "system"));
|
||||
}
|
||||
});
|
||||
|
||||
test("Cursor legacy listing retains its synthetic auto entry without affecting other providers", () => {
|
||||
const models = mergeProviderModelListing({
|
||||
...input,
|
||||
providerId: "cursor",
|
||||
customModels: [],
|
||||
syncedModels: [{ id: "live" }],
|
||||
});
|
||||
assert.ok(models.some((m) => m.id === "auto"));
|
||||
assert.ok(models.some((m) => m.id === "live"));
|
||||
assert.ok(!models.some((m) => m.id === "retired"));
|
||||
});
|
||||
Reference in New Issue
Block a user