Files
2026-07-13 13:39:12 +08:00

291 lines
9.3 KiB
TypeScript

import { getCodexRequestDefaults } from "../../src/lib/providers/requestDefaults.ts";
import { getProviderConnections } from "../../src/lib/db/providers.ts";
import { AI_PROVIDERS, NOAUTH_PROVIDERS } from "../../src/shared/constants/providers.ts";
type JsonRecord = Record<string, unknown>;
type McpCatalogStatus = "available" | "degraded" | "unavailable";
type McpCatalogResponse = {
models: Array<{
id: string;
provider: string;
capabilities: string[];
status: McpCatalogStatus;
thinkingEffort?: string;
pricing?: unknown;
}>;
source: string;
warning?: string;
};
type ProviderConnectionLike = {
id?: string;
provider?: string;
isActive?: boolean;
providerSpecificData?: unknown;
};
type McpCatalogRequestSpec = {
provider: string;
path: string;
thinkingEffort?: string;
};
function toRecord(value: unknown): JsonRecord {
return value && typeof value === "object" && !Array.isArray(value) ? (value as JsonRecord) : {};
}
function toString(value: unknown, fallback = ""): string {
return typeof value === "string" ? value : fallback;
}
function toStringArray(value: unknown, fallback: string[] = []): string[] {
return Array.isArray(value) ? value.map((item) => String(item)) : fallback;
}
function buildProviderAliasMap(): Record<string, string> {
const aliasMap: Record<string, string> = {};
for (const provider of Object.values(AI_PROVIDERS)) {
if (!provider?.id) continue;
aliasMap[provider.id] = provider.id;
if (typeof provider.alias === "string" && provider.alias.length > 0) {
aliasMap[provider.alias] = provider.id;
}
}
for (const provider of Object.values(NOAUTH_PROVIDERS)) {
if (!provider?.id) continue;
aliasMap[provider.id] = provider.id;
if ("alias" in provider && typeof provider.alias === "string" && provider.alias.length > 0) {
aliasMap[provider.alias] = provider.id;
}
}
return aliasMap;
}
function normalizeCapability(value: string): string {
switch (value) {
case "embeddings":
return "embedding";
case "images":
return "image";
case "videos":
return "video";
case "moderations":
return "moderation";
case "chat-completions":
return "chat";
default:
return value;
}
}
function getCatalogModelCapabilities(model: JsonRecord): string[] {
if (Array.isArray(model.capabilities) && model.capabilities.length > 0) {
return toStringArray(model.capabilities, ["chat"]).map(normalizeCapability);
}
if (Array.isArray(model.supportedEndpoints) && model.supportedEndpoints.length > 0) {
return toStringArray(model.supportedEndpoints, ["chat"]).map(normalizeCapability);
}
const type = toString(model.type);
if (type) return [normalizeCapability(type)];
return ["chat"];
}
function normalizeCatalogStatus(
model: JsonRecord,
source: string,
warning?: string
): McpCatalogStatus {
const explicitStatus = toString(model.status);
if (
explicitStatus === "available" ||
explicitStatus === "degraded" ||
explicitStatus === "unavailable"
) {
return explicitStatus;
}
if (warning || source === "local_catalog") return "degraded";
return "available";
}
function getConnectionThinkingEffort(connection: ProviderConnectionLike): string | undefined {
const provider = typeof connection.provider === "string" ? connection.provider : null;
const providerSpecificData = toRecord(connection.providerSpecificData);
if (provider === "codex") {
return getCodexRequestDefaults(providerSpecificData).reasoningEffort || "medium";
}
const rawThinkingEffort = toString(providerSpecificData.thinkingEffort);
return rawThinkingEffort || undefined;
}
function normalizeProviderModelRecord(
rawModel: unknown,
fallbackProvider: string,
source: string,
warning?: string,
thinkingEffort?: string
) {
const model = toRecord(rawModel);
const id = toString(model.id, "");
return {
id,
provider: toString(model.owned_by, toString(model.provider, fallbackProvider)),
capabilities: getCatalogModelCapabilities(model),
status: normalizeCatalogStatus(model, source, warning),
...(thinkingEffort ? { thinkingEffort } : {}),
pricing: model.pricing,
};
}
function activeProviderConnections(
connections: ProviderConnectionLike[],
normalizeProviderId: (value: string) => string,
requestedProvider: string | null
): ProviderConnectionLike[] {
return connections.filter((connection) => {
const provider =
typeof connection?.provider === "string" ? normalizeProviderId(connection.provider) : null;
return !!provider && !!connection?.id && connection.isActive !== false &&
(!requestedProvider || provider === requestedProvider);
});
}
function providerModelRequestSpecs(
connections: ProviderConnectionLike[],
normalizeProviderId: (value: string) => string
): McpCatalogRequestSpec[] {
return connections.map((connection) => ({
provider: normalizeProviderId(String(connection.provider)),
path: `/api/providers/${encodeURIComponent(String(connection.id))}/models?excludeHidden=true`,
thinkingEffort: getConnectionThinkingEffort(connection),
}));
}
function noAuthProviderSpec(requestedProvider: string): McpCatalogRequestSpec {
return {
provider: requestedProvider,
path: `/api/v1/providers/${encodeURIComponent(requestedProvider)}/models`,
thinkingEffort: undefined,
};
}
function emptyCatalogForProvider(requestedProvider: string): McpCatalogResponse {
return {
models: [],
source: "provider_connections",
warning: `No active connections found for provider '${requestedProvider}'.`,
};
}
function rawModelsFromCatalog(raw: JsonRecord): unknown[] {
if (Array.isArray(raw.models)) return raw.models;
if (Array.isArray(raw.data)) return raw.data;
return [];
}
function maybeCatalogModel(
rawModel: unknown,
spec: McpCatalogRequestSpec,
source: string,
warning: string | undefined,
requestedCapability: string | null
): McpCatalogResponse["models"][number] | null {
const normalized = normalizeProviderModelRecord(rawModel, spec.provider, source, warning);
if (spec.thinkingEffort && !normalized.thinkingEffort) normalized.thinkingEffort = spec.thinkingEffort;
if (!normalized.id) return null;
if (requestedCapability && !normalized.capabilities.includes(requestedCapability)) return null;
return normalized;
}
function addCatalogModels(
raw: JsonRecord,
spec: McpCatalogRequestSpec,
source: string,
warning: string | undefined,
requestedCapability: string | null,
collectedModels: Map<string, McpCatalogResponse["models"][number]>
) {
for (const rawModel of rawModelsFromCatalog(raw)) {
const normalized = maybeCatalogModel(rawModel, spec, source, warning, requestedCapability);
if (normalized) collectedModels.set(`${normalized.provider}:${normalized.id}`, normalized);
}
}
async function collectCatalogModels(
requestSpecs: McpCatalogRequestSpec[],
fetchJson: (path: string) => Promise<unknown>,
requestedCapability: string | null
) {
const collectedModels = new Map<string, McpCatalogResponse["models"][number]>();
const warnings = new Set<string>();
const sources = new Set<string>();
for (const spec of requestSpecs) {
const raw = toRecord(await fetchJson(spec.path));
const source = toString(raw.source, spec.path.startsWith("/api/providers/") ? "api" : "v1_catalog");
const warning = raw.warning ? String(raw.warning) : undefined;
if (warning) warnings.add(warning);
sources.add(source);
addCatalogModels(raw, spec, source, warning, requestedCapability, collectedModels);
}
return { collectedModels, warnings, sources };
}
export async function getMcpModelsCatalog(
args: { provider?: string; capability?: string },
deps: {
fetchJson?: (path: string) => Promise<unknown>;
listProviderConnections?: () => Promise<ProviderConnectionLike[]>;
} = {}
): Promise<McpCatalogResponse> {
const fetchJson = deps.fetchJson ?? ((path: string) => import("./server.ts").then((m) => m.omniRouteFetch(path)));
const listProviderConnections = deps.listProviderConnections ?? getProviderConnections;
const aliasMap = buildProviderAliasMap();
const normalizeProviderId = (value: string) => aliasMap[value] || value;
const requestedProvider = args.provider ? normalizeProviderId(args.provider) : null;
const requestedCapability = args.capability ? normalizeCapability(args.capability) : null;
let connections = await listProviderConnections();
connections = Array.isArray(connections) ? connections : [];
const activeConnections = activeProviderConnections(
connections,
normalizeProviderId,
requestedProvider
);
const requestSpecs = providerModelRequestSpecs(activeConnections, normalizeProviderId);
if (requestedProvider && requestSpecs.length === 0) {
const isNoAuthProvider = Object.values(NOAUTH_PROVIDERS).some(
(provider) => provider.id === requestedProvider
);
if (isNoAuthProvider) {
requestSpecs.push(noAuthProviderSpec(requestedProvider));
} else {
return emptyCatalogForProvider(requestedProvider);
}
}
const { collectedModels, warnings, sources } = await collectCatalogModels(
requestSpecs,
fetchJson,
requestedCapability
);
return {
models: [...collectedModels.values()],
source: sources.size === 1 ? [...sources][0] : "aggregated_provider_models",
...(warnings.size > 0 ? { warning: [...warnings].join(" | ") } : {}),
};
}