import { PROVIDER_MODELS, PROVIDER_ID_TO_ALIAS } from "@/shared/constants/models"; import { getProviderAlias, isAnthropicCompatibleProvider, isOpenAICompatibleProvider } from "@/shared/constants/providers"; import { getProviderConnections, getCombos, getCustomModels, getModelAliases } from "@/lib/localDb"; const parseOpenAIStyleModels = (data) => { if (Array.isArray(data)) return data; return data?.data || data?.models || data?.results || []; }; // Matches provider IDs that are upstream/cross-instance connections (contain a UUID suffix) const UPSTREAM_CONNECTION_RE = /[-_][0-9a-f]{8,}$/i; async function fetchCompatibleModelIds(connection) { if (!connection?.apiKey) return []; const baseUrl = typeof connection?.providerSpecificData?.baseUrl === "string" ? connection.providerSpecificData.baseUrl.trim().replace(/\/$/, "") : ""; if (!baseUrl) return []; let url = `${baseUrl}/models`; const headers = { "Content-Type": "application/json", }; if (isOpenAICompatibleProvider(connection.provider)) { headers.Authorization = `Bearer ${connection.apiKey}`; } else if (isAnthropicCompatibleProvider(connection.provider)) { if (url.endsWith("/messages/models")) { url = url.slice(0, -9); } else if (url.endsWith("/messages")) { url = `${url.slice(0, -9)}/models`; } headers["x-api-key"] = connection.apiKey; headers["anthropic-version"] = "2023-06-01"; headers.Authorization = `Bearer ${connection.apiKey}`; } else { return []; } try { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 5000); const response = await fetch(url, { method: "GET", headers, cache: "no-store", signal: controller.signal, }); clearTimeout(timeoutId); if (!response.ok) return []; const data = await response.json(); const rawModels = parseOpenAIStyleModels(data); return Array.from( new Set( rawModels .map((model) => model?.id || model?.name || model?.model) .filter((modelId) => typeof modelId === "string" && modelId.trim() !== "") ) ); } catch { return []; } } /** * Handle CORS preflight */ export async function OPTIONS() { return new Response(null, { headers: { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, OPTIONS", "Access-Control-Allow-Headers": "*", }, }); } /** * GET /v1/models - OpenAI compatible models list * Returns models from all active providers and combos in OpenAI format */ export async function GET() { try { // Get active provider connections let connections = []; try { connections = await getProviderConnections(); // Filter to only active connections connections = connections.filter(c => c.isActive !== false); } catch (e) { // If database not available, return all models console.log("Could not fetch providers, returning all models"); } // Get combos let combos = []; try { combos = await getCombos(); } catch (e) { console.log("Could not fetch combos"); } let customModels = []; try { customModels = await getCustomModels(); } catch (e) { console.log("Could not fetch custom models"); } let modelAliases = {}; try { modelAliases = await getModelAliases(); } catch (e) { console.log("Could not fetch model aliases"); } // Build first active connection per provider (connections already sorted by priority) const activeConnectionByProvider = new Map(); for (const conn of connections) { if (!activeConnectionByProvider.has(conn.provider)) { activeConnectionByProvider.set(conn.provider, conn); } } // Collect models from active providers (or all if none active) const models = []; const timestamp = Math.floor(Date.now() / 1000); // Add combos first (they appear at the top) for (const combo of combos) { models.push({ id: combo.name, object: "model", created: timestamp, owned_by: "combo", }); } // Add provider models if (connections.length === 0) { // DB unavailable or no active providers -> return all static models for (const [alias, providerModels] of Object.entries(PROVIDER_MODELS)) { for (const model of providerModels) { models.push({ id: `${alias}/${model.id}`, object: "model", created: timestamp, owned_by: alias, }); } } // Also include custom LLM models when no active connections are available. for (const customModel of customModels) { if (!customModel?.id || (customModel.type && customModel.type !== "llm")) continue; const providerAlias = customModel.providerAlias; if (!providerAlias) continue; const modelId = String(customModel.id).trim(); if (!modelId) continue; models.push({ id: `${providerAlias}/${modelId}`, object: "model", created: timestamp, owned_by: providerAlias, }); } } else { for (const [providerId, conn] of activeConnectionByProvider.entries()) { const staticAlias = PROVIDER_ID_TO_ALIAS[providerId] || providerId; const outputAlias = ( conn?.providerSpecificData?.prefix || getProviderAlias(providerId) || staticAlias ).trim(); const providerModels = PROVIDER_MODELS[staticAlias] || []; const enabledModels = conn?.providerSpecificData?.enabledModels; const hasExplicitEnabledModels = Array.isArray(enabledModels) && enabledModels.length > 0; const isCompatibleProvider = isOpenAICompatibleProvider(providerId) || isAnthropicCompatibleProvider(providerId); // Default: if no explicit selection, all static models are active. // For compatible providers with no explicit selection, fetch remote /models dynamically. // If explicit selection exists, expose exactly those model IDs (including non-static IDs). let rawModelIds = hasExplicitEnabledModels ? Array.from( new Set( enabledModels.filter( (modelId) => typeof modelId === "string" && modelId.trim() !== "", ), ), ) : providerModels.map((model) => model.id); if (isCompatibleProvider && rawModelIds.length === 0 && !UPSTREAM_CONNECTION_RE.test(providerId)) { rawModelIds = await fetchCompatibleModelIds(conn); } const modelIds = rawModelIds .map((modelId) => { if (modelId.startsWith(`${outputAlias}/`)) { return modelId.slice(outputAlias.length + 1); } if (modelId.startsWith(`${staticAlias}/`)) { return modelId.slice(staticAlias.length + 1); } if (modelId.startsWith(`${providerId}/`)) { return modelId.slice(providerId.length + 1); } return modelId; }) .filter((modelId) => typeof modelId === "string" && modelId.trim() !== ""); const customModelIds = customModels .filter((m) => { if (!m?.id || (m.type && m.type !== "llm")) return false; const alias = m.providerAlias; return alias === staticAlias || alias === outputAlias || alias === providerId; }) .map((m) => String(m.id).trim()) .filter((modelId) => modelId !== ""); const aliasModelIds = Object.values(modelAliases || {}) .filter((fullModel) => { if (typeof fullModel !== "string" || !fullModel.includes("/")) return false; return ( fullModel.startsWith(`${outputAlias}/`) || fullModel.startsWith(`${staticAlias}/`) || fullModel.startsWith(`${providerId}/`) ); }) .map((fullModel) => { if (fullModel.startsWith(`${outputAlias}/`)) { return fullModel.slice(outputAlias.length + 1); } if (fullModel.startsWith(`${staticAlias}/`)) { return fullModel.slice(staticAlias.length + 1); } if (fullModel.startsWith(`${providerId}/`)) { return fullModel.slice(providerId.length + 1); } return fullModel; }) .filter((modelId) => typeof modelId === "string" && modelId.trim() !== ""); const mergedModelIds = Array.from(new Set([...modelIds, ...customModelIds, ...aliasModelIds])); for (const modelId of mergedModelIds) { models.push({ id: `${outputAlias}/${modelId}`, object: "model", created: timestamp, owned_by: outputAlias, }); } } } const dedupedModels = []; const seenModelIds = new Set(); for (const model of models) { if (!model?.id || seenModelIds.has(model.id)) continue; seenModelIds.add(model.id); dedupedModels.push(model); } return Response.json({ object: "list", data: dedupedModels, }, { headers: { "Access-Control-Allow-Origin": "*", }, }); } catch (error) { console.log("Error fetching models:", error); return Response.json( { error: { message: error.message, type: "server_error" } }, { status: 500 } ); } }