model catalog
This commit is contained in:
@@ -5,6 +5,7 @@ import swaggerUI from "@fastify/swagger-ui";
|
||||
import sensible from "@fastify/sensible";
|
||||
import { env } from "./env.js";
|
||||
import { ensureDatabaseReady } from "./db-init.js";
|
||||
import { warmModelCatalog } from "./llm/model-catalog.js";
|
||||
import { registerRoutes } from "./routes.js";
|
||||
|
||||
const app = Fastify({
|
||||
@@ -18,6 +19,7 @@ const app = Fastify({
|
||||
});
|
||||
|
||||
await ensureDatabaseReady(app.log);
|
||||
await warmModelCatalog(app.log);
|
||||
|
||||
await app.register(cors, {
|
||||
origin: true,
|
||||
|
||||
99
server/src/llm/model-catalog.ts
Normal file
99
server/src/llm/model-catalog.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import type { FastifyBaseLogger } from "fastify";
|
||||
import { anthropicClient, openaiClient, xaiClient } from "./providers.js";
|
||||
import type { Provider } from "./types.js";
|
||||
|
||||
export type ProviderModelSnapshot = {
|
||||
models: string[];
|
||||
loadedAt: string | null;
|
||||
error: string | null;
|
||||
};
|
||||
|
||||
export type ModelCatalogSnapshot = Record<Provider, ProviderModelSnapshot>;
|
||||
|
||||
const providers: Provider[] = ["openai", "anthropic", "xai"];
|
||||
const MODEL_FETCH_TIMEOUT_MS = 15000;
|
||||
|
||||
const modelCatalog: ModelCatalogSnapshot = {
|
||||
openai: { models: [], loadedAt: null, error: null },
|
||||
anthropic: { models: [], loadedAt: null, error: null },
|
||||
xai: { models: [], loadedAt: null, error: null },
|
||||
};
|
||||
|
||||
function uniqSorted(models: string[]) {
|
||||
return [...new Set(models.map((value) => value.trim()).filter(Boolean))].sort((a, b) => a.localeCompare(b));
|
||||
}
|
||||
|
||||
async function withTimeout<T>(promise: Promise<T>, timeoutMs: number, label: string) {
|
||||
let timeoutId: NodeJS.Timeout | null = null;
|
||||
try {
|
||||
return await Promise.race([
|
||||
promise,
|
||||
new Promise<T>((_, reject) => {
|
||||
timeoutId = setTimeout(() => {
|
||||
reject(new Error(`${label} timed out after ${timeoutMs}ms`));
|
||||
}, timeoutMs);
|
||||
}),
|
||||
]);
|
||||
} finally {
|
||||
if (timeoutId) clearTimeout(timeoutId);
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchProviderModels(provider: Provider) {
|
||||
if (provider === "openai") {
|
||||
const page = await openaiClient().models.list();
|
||||
return uniqSorted(page.data.map((model) => model.id));
|
||||
}
|
||||
|
||||
if (provider === "anthropic") {
|
||||
const page = await anthropicClient().models.list({ limit: 200 });
|
||||
return uniqSorted(page.data.map((model) => model.id));
|
||||
}
|
||||
|
||||
const page = await xaiClient().models.list();
|
||||
return uniqSorted(page.data.map((model) => model.id));
|
||||
}
|
||||
|
||||
async function refreshProviderModels(provider: Provider, logger?: FastifyBaseLogger) {
|
||||
try {
|
||||
const models = await withTimeout(fetchProviderModels(provider), MODEL_FETCH_TIMEOUT_MS, `${provider} model fetch`);
|
||||
modelCatalog[provider] = {
|
||||
models,
|
||||
loadedAt: new Date().toISOString(),
|
||||
error: null,
|
||||
};
|
||||
logger?.info({ provider, modelCount: models.length }, "model catalog loaded");
|
||||
} catch (err: any) {
|
||||
const message = err?.message ?? String(err);
|
||||
modelCatalog[provider] = {
|
||||
models: [],
|
||||
loadedAt: new Date().toISOString(),
|
||||
error: message,
|
||||
};
|
||||
logger?.warn({ provider, err: message }, "failed to load provider model catalog");
|
||||
}
|
||||
}
|
||||
|
||||
export async function warmModelCatalog(logger?: FastifyBaseLogger) {
|
||||
await Promise.all(providers.map((provider) => refreshProviderModels(provider, logger)));
|
||||
}
|
||||
|
||||
export function getModelCatalogSnapshot(): ModelCatalogSnapshot {
|
||||
return {
|
||||
openai: {
|
||||
models: [...modelCatalog.openai.models],
|
||||
loadedAt: modelCatalog.openai.loadedAt,
|
||||
error: modelCatalog.openai.error,
|
||||
},
|
||||
anthropic: {
|
||||
models: [...modelCatalog.anthropic.models],
|
||||
loadedAt: modelCatalog.anthropic.loadedAt,
|
||||
error: modelCatalog.anthropic.error,
|
||||
},
|
||||
xai: {
|
||||
models: [...modelCatalog.xai.models],
|
||||
loadedAt: modelCatalog.xai.loadedAt,
|
||||
error: modelCatalog.xai.error,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import { requireAdmin } from "./auth.js";
|
||||
import { env } from "./env.js";
|
||||
import { runMultiplex } from "./llm/multiplexer.js";
|
||||
import { runMultiplexStream } from "./llm/streaming.js";
|
||||
import { getModelCatalogSnapshot } from "./llm/model-catalog.js";
|
||||
import { exaClient } from "./search/exa.js";
|
||||
|
||||
type IncomingChatMessage = {
|
||||
@@ -135,6 +136,11 @@ export async function registerRoutes(app: FastifyInstance) {
|
||||
return { authenticated: true, mode: env.ADMIN_TOKEN ? "token" : "open" };
|
||||
});
|
||||
|
||||
app.get("/v1/models", async (req) => {
|
||||
requireAdmin(req);
|
||||
return { providers: getModelCatalogSnapshot() };
|
||||
});
|
||||
|
||||
app.get("/v1/chats", async (req) => {
|
||||
requireAdmin(req);
|
||||
const chats = await prisma.chat.findMany({
|
||||
|
||||
Reference in New Issue
Block a user