mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-14 10:52:17 +03:00
Deriva as modalidades do auto-combo a partir do pool de targets efetivo, com teste próprio robusto (174 linhas). Validado no worktree combinado. Obrigado!
2044 lines
89 KiB
TypeScript
2044 lines
89 KiB
TypeScript
import { PROVIDER_MODELS, PROVIDER_ID_TO_ALIAS } from "@/shared/constants/models";
|
||
import { NOAUTH_PROVIDERS } from "@/shared/constants/providers";
|
||
import { getCombos } from "@/lib/db/combos";
|
||
import { getSettings } from "@/lib/db/settings";
|
||
import { getUserDatabaseSettings } from "@/lib/db/databaseSettings";
|
||
import { createLazyConnectionView } from "@/lib/db/providers/lazyConnectionView";
|
||
import { extractAliasBackedModels } from "./aliasBackedModels";
|
||
import {
|
||
buildSyncedModelIdsByCanonicalProvider,
|
||
shouldSuppressStaticModelForExclusiveListing,
|
||
} from "./catalogSyncedCoverage";
|
||
import { buildSyncedCapabilities, mergeSyncedCapabilities } from "./syncedCapabilities";
|
||
import { getAllEmbeddingModels } from "@omniroute/open-sse/config/embeddingRegistry";
|
||
import {
|
||
getAllImageModels,
|
||
isRegisteredImageModel,
|
||
} from "@omniroute/open-sse/config/imageRegistry";
|
||
import { aiHordeImageCatalog } from "@omniroute/open-sse/services/aihordeImageCatalog";
|
||
import { getAllRerankModels } from "@omniroute/open-sse/config/rerankRegistry";
|
||
import { getAllAudioModels } from "@omniroute/open-sse/config/audioRegistry";
|
||
import { getAllModerationModels } from "@omniroute/open-sse/config/moderationRegistry";
|
||
import { getAllVideoModels } from "@omniroute/open-sse/config/videoRegistry";
|
||
import { getAllMusicModels } from "@omniroute/open-sse/config/musicRegistry";
|
||
import {
|
||
getRegistryModelThinkingEfforts,
|
||
getRegistryThinkingEfforts,
|
||
providerUsesAuthoritativeLiveCatalog,
|
||
REGISTRY,
|
||
} from "@omniroute/open-sse/config/providerRegistry";
|
||
import { CODEX_NATIVE_UNPREFIXED_MODELS } from "@omniroute/open-sse/services/model";
|
||
import { isModelSelectable } from "@omniroute/open-sse/services/modelLifecycle";
|
||
import { resolveNestedComboTargets } from "@omniroute/open-sse/services/combo";
|
||
import {
|
||
AUTO_TEMPLATE_VARIANTS,
|
||
AUTO_SUFFIX_VARIANTS,
|
||
AUTO_FAMILY_IDS,
|
||
createBuiltinAutoCombo,
|
||
prepareBuiltinAutoComboInputs,
|
||
isPaidTierAutoId,
|
||
} from "@omniroute/open-sse/services/autoCombo/builtinCatalog";
|
||
import {
|
||
getSyncedAvailableModelsByConnection,
|
||
SYNCED_AVAILABLE_MODELS_MALFORMED,
|
||
type SyncedAvailableModel,
|
||
getAllCustomModels,
|
||
getModelAliases,
|
||
getHiddenModelsByProvider,
|
||
} from "@/lib/db/models";
|
||
import { getAllActiveSyncedModels } from "@/lib/db/models/activeSyncedCatalog";
|
||
import {
|
||
getModelCatalogCacheVersion,
|
||
getCachedRawProviderConnections,
|
||
getCachedProviderNodes,
|
||
} from "@/lib/db/readCache";
|
||
import { getCompatibleFallbackModels } from "@/lib/providers/managedAvailableModels";
|
||
import {
|
||
providerUsesCuratedModelsOnly,
|
||
providerUsesExclusiveSyncedListing,
|
||
} from "@/lib/providers/modelListingCapability";
|
||
import { ensureCursorAutoCatalogEntry } from "@/lib/providerModels/cursorAutoCatalog";
|
||
import { mergeCustomModelMetadata } from "@/lib/providers/modelMetadataPrecedence";
|
||
import { getOpenRouterCatalog } from "@/lib/catalog/openrouterCatalog";
|
||
import { hasEligibleConnectionForModel } from "@/domain/connectionModelRules";
|
||
import {
|
||
INTERNAL_PROXY_ERROR,
|
||
getCanonicalModelMetadata,
|
||
getCatalogDiagnosticsHeaders,
|
||
type CatalogEnrichmentSnapshot,
|
||
} from "@/lib/modelMetadataRegistry";
|
||
import { createModelCapabilityResolutionSnapshot } from "@/lib/modelCapabilityResolutionSnapshot";
|
||
import { getModelsDevPricing, getSyncedCapability } from "@/lib/modelsDevSync";
|
||
import { getModelSpec } from "@/shared/constants/modelSpecs";
|
||
import { classifyModelSupportedEndpoints } from "@/shared/constants/modelSupportedEndpoints";
|
||
import { getModelsCatalogPrefixMode } from "@/shared/utils/featureFlags";
|
||
import {
|
||
isProviderNodePrefixReserved,
|
||
selectCompatibleNodeForPrefix,
|
||
} from "@/lib/providerNodePrefixes";
|
||
import { applyCatalogPostFilters, finalizeCatalogResponse } from "./catalogResponse";
|
||
import {
|
||
isNoAuthProviderBlocked,
|
||
isNoAuthProviderKey,
|
||
isNoAuthRawProviderPrefix,
|
||
normalizeBlockedProviderSet,
|
||
} from "@/shared/utils/noAuthProviders";
|
||
import { getSourcedTokenLimit, getTokenLimit } from "@omniroute/open-sse/services/contextManager";
|
||
import { extractApiKey } from "@/sse/services/auth";
|
||
import type { ComboModelStep } from "@/lib/combos/steps";
|
||
import {
|
||
type CustomModelEntry,
|
||
type ComboCatalogTarget,
|
||
type ComboTargetCatalogMetadata,
|
||
isPositiveFiniteNumber,
|
||
parseJsonStringArray,
|
||
intersectStringArrays,
|
||
minKnownNumber,
|
||
maybeOmitCatalogModelName,
|
||
getThinkingCapabilityFields,
|
||
mergeComboCapabilities,
|
||
getConnectionScopedEffortTiers,
|
||
type ConnectionScopedReasoningCatalog,
|
||
} from "./catalogHelpers";
|
||
import {
|
||
qualifyOpenRouterModelId,
|
||
normalizeOpenRouterModalities,
|
||
getOpenRouterModelType,
|
||
isOpenRouterFreeModel,
|
||
getOpenRouterDisplayName,
|
||
} from "./catalogOpenrouter";
|
||
import { getVisionCapabilityFields, getCustomVisionCapabilityFields } from "./catalogVision";
|
||
import {
|
||
buildAliasMaps,
|
||
prefixRoutesToProvider,
|
||
resolveCanonicalProviderId as resolveCanonicalProviderIdFromMaps,
|
||
getProviderPrefixes as getProviderPrefixesFromMaps,
|
||
getComboTargetModelId as getComboTargetModelIdFromMaps,
|
||
} from "./catalogProviderMaps";
|
||
import {
|
||
getModelCatalogAuthRejection,
|
||
isCodexModelCatalogClient,
|
||
isCcDiscoveryModelCatalogClient,
|
||
} from "./catalogRequest";
|
||
import { incrementCcDiscoveryHitCount } from "@/lib/db/ccDiscoveryMetrics";
|
||
import { isUnifiedChatSourceModelSelectable } from "./catalogModelPolicy";
|
||
import { isFreeModel } from "@/shared/utils/freeModels";
|
||
import { isModelExposureAllowed } from "@/shared/utils/modelExposureList";
|
||
import { isCodexDiscoveryModelExcluded } from "@/shared/services/codexDiscoveryPolicy";
|
||
import { buildErrorBody } from "@omniroute/open-sse/utils/error";
|
||
|
||
// Public API of this module is preserved after the catalog helper extraction:
|
||
// `isVisionModelId` (vision-detection-consistency.test.ts) and
|
||
// `getCustomVisionCapabilityFields` (llm-selector-custom-vision-models.test.ts)
|
||
// are still importable from here.
|
||
export { isVisionModelId } from "@/shared/constants/visionModels";
|
||
export { getCustomVisionCapabilityFields };
|
||
|
||
// The response cache (coalescing, short-TTL memoization and stale-while-revalidate)
|
||
// lives in ./catalogCache. Re-exported here because the existing tests import the
|
||
// hooks from this module, and CATALOG_STALE_WHILE_REVALIDATE_MS is part of the
|
||
// documented behavior of this endpoint.
|
||
import {
|
||
CATALOG_CACHE_TTL_MS_DEFAULT,
|
||
resolveCachedCatalogResponse,
|
||
type BackgroundRefreshScheduler,
|
||
} from "./catalogCache";
|
||
|
||
export {
|
||
CATALOG_STALE_WHILE_REVALIDATE_MS,
|
||
__resetCatalogBuilderRunsForTest,
|
||
__getCatalogBuilderRunsForTest,
|
||
__expireCatalogCacheForTest,
|
||
__setCatalogCacheEntryForTest,
|
||
__flushCatalogBackgroundRefreshForTest,
|
||
__forceCatalogInFlightRejectionForTest,
|
||
} from "./catalogCache";
|
||
export type { CachedCatalog, BackgroundRefreshScheduler } from "./catalogCache";
|
||
|
||
/**
|
||
* Per-call options for {@link getUnifiedModelsResponse}.
|
||
*
|
||
* Restored in #11551: `/v1/models` passes Next's `after()` so the stale-while-
|
||
* revalidate rebuild is deferred until after the response flush. #9199 had removed
|
||
* the injection point while the route kept passing it, so the argument was silently
|
||
* dropped and the refresh ran on a plain `setTimeout`.
|
||
*/
|
||
export type CatalogResponseOptions = {
|
||
scheduleBackgroundRefresh?: BackgroundRefreshScheduler;
|
||
};
|
||
|
||
const BUILTIN_AUTO_YIELD_INTERVAL = 2;
|
||
|
||
function yieldCatalogBuildTurn(): Promise<void> {
|
||
return new Promise((resolve) => setImmediate(resolve));
|
||
}
|
||
|
||
/**
|
||
* Build unified OpenAI-compatible model catalog response.
|
||
* Reused by `/api/v1/models` and `/api/v1` to avoid semantic drift (T09).
|
||
*
|
||
* `options.scheduleBackgroundRefresh` is the App Router's injection point for the
|
||
* stale-while-revalidate rebuild (#8728): the route passes Next's `after()` so the
|
||
* rebuild starts only once the stale body has been flushed. Omitted by non-route
|
||
* callers, which fall back to the cache module's own default.
|
||
*/
|
||
export async function getUnifiedModelsResponse(
|
||
request: Request,
|
||
corsHeaders: Record<string, string> = {},
|
||
options: { scheduleBackgroundRefresh?: BackgroundRefreshScheduler } = {}
|
||
) {
|
||
const diagnosticHeaders = getCatalogDiagnosticsHeaders({ request });
|
||
|
||
// #6408 fast path: reject unauthorized callers first (auth state is per-request
|
||
// and MUST NOT be cached), then coalesce identical concurrent requests + short-
|
||
// TTL memoize the serialized JSON body.
|
||
let settingsForAuth: Record<string, any> = {};
|
||
try {
|
||
try {
|
||
settingsForAuth = await getSettings();
|
||
} catch {}
|
||
// #9147: yield before auth check to allow event loop tick
|
||
await yieldCatalogBuildTurn();
|
||
const authRejection = await getModelCatalogAuthRejection(request, settingsForAuth, {
|
||
...corsHeaders,
|
||
...diagnosticHeaders,
|
||
});
|
||
if (authRejection) return authRejection;
|
||
} catch {
|
||
// Fall through to full builder on auth-check failure; core handles errors.
|
||
}
|
||
|
||
// Best-effort cc-discovery usage metric — count every authorized GET /v1/models
|
||
// hit from a Claude Code client, cache hit or not. Never blocks/slows the
|
||
// request (incrementCcDiscoveryHitCount already swallows its own errors).
|
||
if (isCcDiscoveryModelCatalogClient(request)) {
|
||
incrementCcDiscoveryHitCount();
|
||
}
|
||
|
||
try {
|
||
return await resolveCachedCatalogResponse(
|
||
request,
|
||
{ corsHeaders, diagnosticHeaders },
|
||
buildCatalogPayload,
|
||
{
|
||
// #10831: a disabled router hides auto/* just as hideAutoCombos does, so
|
||
// the two collapse into one cache dimension — the resulting catalogs are
|
||
// identical and do not need separate entries.
|
||
hideAutoCombos:
|
||
settingsForAuth?.hideAutoCombos === true || settingsForAuth?.autoRoutingEnabled === false,
|
||
hideNoThinkVariants: settingsForAuth?.hideNoThinkVariants === true,
|
||
scheduleBackgroundRefresh: options.scheduleBackgroundRefresh,
|
||
}
|
||
);
|
||
} catch (err) {
|
||
// Hard rule #12: never put a raw err.message/err.stack in a response body.
|
||
// Route it through the shared sanitizer instead — same status/type/code as
|
||
// before, minus the stack-trace/path leak.
|
||
const message = err instanceof Error ? err.message : String(err);
|
||
return Response.json(
|
||
buildErrorBody(500, message, undefined, {
|
||
type: "server_error",
|
||
code: INTERNAL_PROXY_ERROR,
|
||
}),
|
||
{ status: 500, headers: { ...corsHeaders, ...diagnosticHeaders } }
|
||
);
|
||
}
|
||
}
|
||
|
||
async function buildCatalogPayload(
|
||
request: Request
|
||
): Promise<{ body: string; headers: Record<string, string>; status: number; cacheTTL: number }> {
|
||
const built = await buildUnifiedModelsResponseCore(request);
|
||
const body = await built.text();
|
||
const headers: Record<string, string> = {};
|
||
built.headers.forEach((value, key) => {
|
||
headers[key] = value;
|
||
});
|
||
// Read the configurable cache TTL from database settings.
|
||
// Falls back to the hardcoded default if not set or on error.
|
||
let cacheTTL = CATALOG_CACHE_TTL_MS_DEFAULT;
|
||
try {
|
||
// Only the persisted cache section is needed here. The full database-settings
|
||
// view also calculates dbstat, WAL, schema and integrity diagnostics, which are
|
||
// synchronous and can pin the event loop after an otherwise cooperative build.
|
||
const dbSettings = getUserDatabaseSettings();
|
||
cacheTTL = dbSettings.cache?.modelCatalogCacheTtlMs ?? CATALOG_CACHE_TTL_MS_DEFAULT;
|
||
} catch {
|
||
// Swallow — use default TTL on DB error
|
||
}
|
||
return { body, headers, status: built.status, cacheTTL };
|
||
}
|
||
|
||
/**
|
||
* Original catalog builder. Runs once per unique cache key per TTL window.
|
||
*/
|
||
async function buildUnifiedModelsResponseCore(
|
||
request: Request,
|
||
corsHeaders: Record<string, string> = {}
|
||
) {
|
||
const diagnosticHeaders = getCatalogDiagnosticsHeaders({ request });
|
||
// #9147: this builder walks connections + model registries at catalog scale with no
|
||
// event-loop yield, so a large deployment pins the single Node.js thread for the
|
||
// whole build (reporter: 183 connections / 2000+ models → 10.1s stall that blocks the
|
||
// dashboard WS heartbeat). Yield every `catYIELD_EVERY` items across the hot loops.
|
||
const catYIELD_EVERY = 5;
|
||
let catYieldCount = 0;
|
||
const maybeYieldCatalogBuild = async (): Promise<void> => {
|
||
catYieldCount++;
|
||
if (catYieldCount % catYIELD_EVERY === 0) {
|
||
await yieldCatalogBuildTurn();
|
||
}
|
||
};
|
||
try {
|
||
// #9147: `getModelIsHidden()` is a SQLite read per call (custom row + compat list)
|
||
// and the build consults it ~16× per entry. Bulk-load the hidden-model map once
|
||
// (one query — `getHiddenModelsByProvider`) and resolve from memory for the whole
|
||
// build. A provider absent from the map has no hidden models at all — `false`,
|
||
// no on-demand fallback (that would reintroduce the per-call SQLite reads).
|
||
// Deliberately kept INSIDE this try block (not hoisted above it): the builder's
|
||
// own catch below is what converts a build-time failure into a sanitized 500
|
||
// Response instead of a rejected promise — hoisting this bulk read above the
|
||
// try would let a crash here propagate as an unhandled rejection instead
|
||
// (catalogCache.ts's in-flight coalescing does not fully consume rejections).
|
||
const hiddenModelsByProvider = getHiddenModelsByProvider();
|
||
let settings: Record<string, any> = {};
|
||
try {
|
||
settings = await getSettings();
|
||
} catch {}
|
||
|
||
const authRejection = await getModelCatalogAuthRejection(request, settings, {
|
||
...corsHeaders,
|
||
...diagnosticHeaders,
|
||
});
|
||
if (authRejection) return authRejection;
|
||
|
||
// #9147: yield after auth check before DB initialization prologue
|
||
await yieldCatalogBuildTurn();
|
||
|
||
const capabilityResolutionSnapshot = createModelCapabilityResolutionSnapshot();
|
||
const { aliasToProviderId, providerIdToAlias } = buildAliasMaps();
|
||
const _qp = new URL(request.url).searchParams.get("prefix");
|
||
const prefixMode =
|
||
_qp === "alias" || _qp === "canonical" || _qp === "dual" ? _qp : getModelsCatalogPrefixMode();
|
||
const includeAlias = prefixMode !== "canonical";
|
||
const includeCanonical = prefixMode !== "alias";
|
||
const resolveCanonicalProviderId = (aliasOrProviderId: string, fallbackProviderId?: string) =>
|
||
resolveCanonicalProviderIdFromMaps(aliasToProviderId, aliasOrProviderId, fallbackProviderId);
|
||
const aliasMaps = { aliasToProviderId, providerIdToAlias };
|
||
// Issue #96: Allow blocking specific providers from the models list
|
||
const blockedProviders = normalizeBlockedProviderSet(settings.blockedProviders);
|
||
// #6316: Opt-in filter — hide paid-only models via `isFreeModel()`. Only applied to
|
||
// PROVIDER_MODELS + OpenRouter loops (where pricing metadata / :free suffix / catalog
|
||
// membership is available). Modality registries (embedding/image/rerank/audio/
|
||
// moderation/video/music) represent local capabilities without pricing, so they are
|
||
// exempt. Combos + auto/* + synced/custom/alias-backed rows also stay unfiltered —
|
||
// extending v1 scope to those requires per-entry pricing lookup not available today.
|
||
const hidePaid = settings.hidePaidModels === true;
|
||
// #9418: Opt-in filter — skip the entire auto/* synthesis loop when the operator
|
||
// does not want built-in virtual combos advertised in the catalog. User-defined
|
||
// combos are unaffected; routing still works for ids sent explicitly.
|
||
// #10831: also drop them when auto routing is switched off. Unlike
|
||
// hideAutoCombos — which only unadvertises ids that still route when sent
|
||
// explicitly — a disabled router rejects every auto/* id with a 400, so
|
||
// listing them offers the client a choice that cannot succeed.
|
||
const hideAuto = settings.hideAutoCombos === true || settings.autoRoutingEnabled === false;
|
||
const shouldHidePaid = (
|
||
providerKey: string,
|
||
modelId: string,
|
||
pricing?: unknown,
|
||
isFree?: boolean
|
||
): boolean => {
|
||
if (!hidePaid) return false;
|
||
const provider = aliasToProviderId[providerKey] || providerKey;
|
||
// isFree:true is the first door — custom row kept even when its provider is outside FREE_MODEL_BUDGETS.
|
||
if (isFreeModel(provider, { id: modelId, pricing: pricing as any, isFree })) return false;
|
||
// hidePaid is on and model is non-free → hidden. No need to consult FREE_MODEL_BUDGETS
|
||
// separately: paid on a free-capable provider stays hidden, free on a non-budget provider
|
||
// already returned above.
|
||
return true;
|
||
};
|
||
// #11481: opt-in explicit model exposure allow/deny list — same call sites
|
||
// as shouldHidePaid above (mirrored into the auto/* combo candidate pool
|
||
// via open-sse/services/autoCombo/modelExposureFilter.ts, per #6512's
|
||
// catalog-only-filter-leaks-into-combo-routing lesson). Independent of
|
||
// hidePaidModels — operator curation, not a cost signal.
|
||
const shouldHideByExposure = (providerKey: string, modelId: string): boolean =>
|
||
!isModelExposureAllowed(aliasToProviderId[providerKey] || providerKey, modelId, settings);
|
||
|
||
// Get active provider connections
|
||
let connections = [];
|
||
let totalConnectionCount = 0; // Track if DB has ANY connections (even disabled)
|
||
try {
|
||
connections = (await getCachedRawProviderConnections()).map(createLazyConnectionView);
|
||
totalConnectionCount = connections.length;
|
||
// Filter to only active connections
|
||
connections = connections.filter((c) => c.isActive !== false);
|
||
} catch (e) {
|
||
// If database not available, show no provider models (safe default)
|
||
console.log("[catalog] Could not fetch providers:", e);
|
||
}
|
||
|
||
// Get provider nodes (for compatible providers with custom prefixes)
|
||
let providerNodes = [];
|
||
try {
|
||
providerNodes = await getCachedProviderNodes();
|
||
} catch (e) {
|
||
console.log("Could not fetch provider nodes");
|
||
}
|
||
|
||
// Build map of provider node ID to prefix and type for compatible providers
|
||
const providerIdToPrefix: Record<string, string> = {};
|
||
const providerNodeIdByPrefix: Record<string, string> = {};
|
||
const nodeIdToProviderType: Record<string, string> = {};
|
||
for (const node of providerNodes) {
|
||
const resolvedPrefix =
|
||
node.prefix?.trim() ||
|
||
node.name
|
||
?.trim()
|
||
?.toLowerCase()
|
||
?.replace(/\s+/g, "-")
|
||
?.replace(/[^a-z0-9-]/g, "") ||
|
||
null;
|
||
if (resolvedPrefix) {
|
||
providerIdToPrefix[node.id] = resolvedPrefix;
|
||
}
|
||
if (node.type) {
|
||
nodeIdToProviderType[node.id] = node.type;
|
||
}
|
||
}
|
||
for (const prefix of new Set(Object.values(providerIdToPrefix))) {
|
||
if (isProviderNodePrefixReserved(prefix)) continue;
|
||
const winner = selectCompatibleNodeForPrefix(providerNodes, prefix);
|
||
if (winner?.id) providerNodeIdByPrefix[prefix] = winner.id;
|
||
}
|
||
|
||
// #8327: `resolveCanonicalProviderId`/`canonicalProviderId` only know the static
|
||
// AI_PROVIDERS/PROVIDER_MODELS alias maps, so a compatible-provider node (whose raw
|
||
// `id` is an internal UUID, never present in those static maps) falls through every
|
||
// lookup and returns the raw UUID verbatim. That UUID is still required for the
|
||
// internal registry/connection/hidden-model lookups that key off `canonicalProviderId`
|
||
// (getConnectionsForProvider, getModelIsHidden, etc. are keyed by the raw node id, not
|
||
// the prefix) — so `canonicalProviderId` itself must stay untouched. What must NOT leak
|
||
// is the raw UUID in the *public* `owned_by` field: resolve it to the operator's
|
||
// configured prefix there, and only there.
|
||
const resolvePublicOwnerId = (providerId: string, canonicalProviderId: string): string =>
|
||
providerIdToPrefix[providerId] || canonicalProviderId;
|
||
|
||
// #11300: the visibility toggle on a provider's dashboard page persists the
|
||
// hidden-model row under whatever key the route's `[id]` param happened to be
|
||
// (a node UUID, an alias like `cc`/`gh`/`cx`, or a canonical provider id) —
|
||
// see `PATCH /api/provider-models`. The catalog loops below each key their own
|
||
// lookup differently (raw connection provider, canonical id, or alias), so a
|
||
// single-key lookup missed the override whenever the write key and the read key
|
||
// diverged. Check every key a model could plausibly have been hidden under:
|
||
// the raw key passed in, its resolved canonical provider id, that canonical id's
|
||
// alias, and the compatible-provider-node prefix for either.
|
||
const isModelHiddenBulk = (
|
||
providerKey: string | null | undefined,
|
||
modelId: string,
|
||
canonicalProviderId?: string | null
|
||
): boolean => {
|
||
if (!providerKey || !modelId) return false;
|
||
const canonical = canonicalProviderId || resolveCanonicalProviderId(providerKey);
|
||
const alias = providerIdToAlias[canonical] || providerIdToAlias[providerKey] || undefined;
|
||
const nodePrefix = providerIdToPrefix[providerKey] || providerIdToPrefix[canonical];
|
||
const keysToCheck = [providerKey, canonical, alias, nodePrefix].filter((k): k is string =>
|
||
Boolean(k)
|
||
);
|
||
for (const key of keysToCheck) {
|
||
const hiddenSet = hiddenModelsByProvider.get(key);
|
||
if (hiddenSet?.has(modelId)) return true;
|
||
}
|
||
return false;
|
||
};
|
||
|
||
// Get combos
|
||
let combos = [];
|
||
await yieldCatalogBuildTurn();
|
||
try {
|
||
combos = await getCombos();
|
||
} catch (e) {
|
||
console.log("Could not fetch combos");
|
||
}
|
||
|
||
// Build set of active provider aliases
|
||
const activeAliases = new Set();
|
||
const connectionsByProvider = new Map<string, typeof connections>();
|
||
const registerConnectionKey = (
|
||
key: string | null | undefined,
|
||
connection: (typeof connections)[number]
|
||
) => {
|
||
if (!key) return;
|
||
const existing = connectionsByProvider.get(key) || [];
|
||
existing.push(connection);
|
||
connectionsByProvider.set(key, existing);
|
||
};
|
||
for (const conn of connections) {
|
||
const alias = providerIdToAlias[conn.provider] || conn.provider;
|
||
activeAliases.add(alias);
|
||
activeAliases.add(conn.provider);
|
||
registerConnectionKey(alias, conn);
|
||
registerConnectionKey(conn.provider, conn);
|
||
}
|
||
|
||
// noAuth providers have no DB rows; settings.blockedProviders disables them.
|
||
for (const p of Object.values(NOAUTH_PROVIDERS)) {
|
||
if (isNoAuthProviderBlocked(blockedProviders, p.id, "alias" in p ? p.alias : null)) continue;
|
||
activeAliases.add(p.id);
|
||
if ("alias" in p && typeof p.alias === "string") activeAliases.add(p.alias);
|
||
}
|
||
|
||
// #9147 follow-up: this is called ~1-3x per model at catalog scale (providerSupportsModel,
|
||
// isExcludedByProviderConnections). Connections do not change mid-build, so memoize per
|
||
// unique (unordered) key-set instead of rescanning connectionsByProvider on every call —
|
||
// otherwise the O(models) hot loop regains an O(connections) cost per model and blows the
|
||
// single-stretch event-loop budget this file's own yield mechanism is meant to protect.
|
||
const connectionsForProviderCache = new Map<string, typeof connections>();
|
||
const getConnectionsForProvider = (...keys: Array<string | null | undefined>) => {
|
||
const cacheKey = keys
|
||
.filter((k): k is string => Boolean(k))
|
||
.sort()
|
||
.join(" |