2026-01-20 01:16:34 -05:00
|
|
|
import { PROVIDER_MODELS, PROVIDER_ID_TO_ALIAS } from "@/shared/constants/models";
|
2026-04-06 04:09:58 -04:00
|
|
|
import { getProviderAlias, isAnthropicCompatibleProvider, isOpenAICompatibleProvider } from "@/shared/constants/providers";
|
2026-01-20 01:16:34 -05:00
|
|
|
import { getProviderConnections, getCombos } from "@/lib/localDb";
|
|
|
|
|
|
2026-04-06 04:09:58 -04:00
|
|
|
const parseOpenAIStyleModels = (data) => {
|
|
|
|
|
if (Array.isArray(data)) return data;
|
|
|
|
|
return data?.data || data?.models || data?.results || [];
|
|
|
|
|
};
|
|
|
|
|
|
2026-04-09 23:30:07 -04:00
|
|
|
// Matches provider IDs that are upstream/cross-instance connections (contain a UUID suffix)
|
|
|
|
|
const UPSTREAM_CONNECTION_RE = /[-_][0-9a-f]{8,}$/i;
|
|
|
|
|
|
2026-04-06 04:09:58 -04:00
|
|
|
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 {
|
2026-04-09 23:30:07 -04:00
|
|
|
const controller = new AbortController();
|
|
|
|
|
const timeoutId = setTimeout(() => controller.abort(), 5000);
|
2026-04-06 04:09:58 -04:00
|
|
|
const response = await fetch(url, {
|
|
|
|
|
method: "GET",
|
|
|
|
|
headers,
|
|
|
|
|
cache: "no-store",
|
2026-04-09 23:30:07 -04:00
|
|
|
signal: controller.signal,
|
2026-04-06 04:09:58 -04:00
|
|
|
});
|
2026-04-09 23:30:07 -04:00
|
|
|
clearTimeout(timeoutId);
|
2026-04-06 04:09:58 -04:00
|
|
|
|
|
|
|
|
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 [];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-20 01:16:34 -05:00
|
|
|
/**
|
|
|
|
|
* 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");
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-24 23:40:50 -05:00
|
|
|
// Build first active connection per provider (connections already sorted by priority)
|
|
|
|
|
const activeConnectionByProvider = new Map();
|
2026-01-20 01:16:34 -05:00
|
|
|
for (const conn of connections) {
|
2026-02-24 23:40:50 -05:00
|
|
|
if (!activeConnectionByProvider.has(conn.provider)) {
|
|
|
|
|
activeConnectionByProvider.set(conn.provider, conn);
|
|
|
|
|
}
|
2026-01-20 01:16:34 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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",
|
|
|
|
|
permission: [],
|
|
|
|
|
root: combo.name,
|
|
|
|
|
parent: null,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Add provider models
|
2026-02-24 23:40:50 -05:00
|
|
|
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,
|
|
|
|
|
permission: [],
|
|
|
|
|
root: model.id,
|
|
|
|
|
parent: null,
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-01-20 01:16:34 -05:00
|
|
|
}
|
2026-02-24 23:40:50 -05:00
|
|
|
} else {
|
|
|
|
|
for (const [providerId, conn] of activeConnectionByProvider.entries()) {
|
|
|
|
|
const staticAlias = PROVIDER_ID_TO_ALIAS[providerId] || providerId;
|
2026-04-06 04:09:58 -04:00
|
|
|
const outputAlias = (
|
|
|
|
|
conn?.providerSpecificData?.prefix
|
|
|
|
|
|| getProviderAlias(providerId)
|
|
|
|
|
|| staticAlias
|
|
|
|
|
).trim();
|
2026-02-24 23:40:50 -05:00
|
|
|
const providerModels = PROVIDER_MODELS[staticAlias] || [];
|
|
|
|
|
const enabledModels = conn?.providerSpecificData?.enabledModels;
|
|
|
|
|
const hasExplicitEnabledModels =
|
|
|
|
|
Array.isArray(enabledModels) && enabledModels.length > 0;
|
2026-04-06 04:09:58 -04:00
|
|
|
const isCompatibleProvider =
|
|
|
|
|
isOpenAICompatibleProvider(providerId) || isAnthropicCompatibleProvider(providerId);
|
2026-02-24 23:40:50 -05:00
|
|
|
|
|
|
|
|
// Default: if no explicit selection, all static models are active.
|
2026-04-06 04:09:58 -04:00
|
|
|
// For compatible providers with no explicit selection, fetch remote /models dynamically.
|
2026-02-24 23:40:50 -05:00
|
|
|
// If explicit selection exists, expose exactly those model IDs (including non-static IDs).
|
2026-04-06 04:09:58 -04:00
|
|
|
let rawModelIds = hasExplicitEnabledModels
|
2026-02-24 23:40:50 -05:00
|
|
|
? Array.from(
|
2026-04-06 04:09:58 -04:00
|
|
|
new Set(
|
|
|
|
|
enabledModels.filter(
|
|
|
|
|
(modelId) => typeof modelId === "string" && modelId.trim() !== "",
|
|
|
|
|
),
|
2026-02-24 23:40:50 -05:00
|
|
|
),
|
2026-04-06 04:09:58 -04:00
|
|
|
)
|
2026-02-24 23:40:50 -05:00
|
|
|
: providerModels.map((model) => model.id);
|
|
|
|
|
|
2026-04-09 23:30:07 -04:00
|
|
|
if (isCompatibleProvider && rawModelIds.length === 0 && !UPSTREAM_CONNECTION_RE.test(providerId)) {
|
2026-04-06 04:09:58 -04:00
|
|
|
rawModelIds = await fetchCompatibleModelIds(conn);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-24 23:40:50 -05:00
|
|
|
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() !== "");
|
2026-01-20 01:16:34 -05:00
|
|
|
|
2026-02-24 23:40:50 -05:00
|
|
|
for (const modelId of modelIds) {
|
|
|
|
|
models.push({
|
|
|
|
|
id: `${outputAlias}/${modelId}`,
|
|
|
|
|
object: "model",
|
|
|
|
|
created: timestamp,
|
|
|
|
|
owned_by: outputAlias,
|
|
|
|
|
permission: [],
|
|
|
|
|
root: modelId,
|
|
|
|
|
parent: null,
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-01-20 01:16:34 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return Response.json({
|
|
|
|
|
object: "list",
|
|
|
|
|
data: models,
|
|
|
|
|
}, {
|
|
|
|
|
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 }
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|