2026-02-14 21:00:30 -08:00
|
|
|
import type { FastifyBaseLogger } from "fastify";
|
2026-06-13 12:02:22 -07:00
|
|
|
import {
|
|
|
|
|
fetchProviderCatalogModels,
|
|
|
|
|
getProviderCatalogFallbackModels,
|
|
|
|
|
listModelCatalogProviders,
|
|
|
|
|
} from "./provider-adapters.js";
|
2026-02-14 21:00:30 -08:00
|
|
|
import type { Provider } from "./types.js";
|
|
|
|
|
|
|
|
|
|
export type ProviderModelSnapshot = {
|
|
|
|
|
models: string[];
|
|
|
|
|
loadedAt: string | null;
|
|
|
|
|
error: string | null;
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-04 21:52:39 -07:00
|
|
|
export type ModelCatalogSnapshot = Partial<Record<Provider, ProviderModelSnapshot>>;
|
2026-02-14 21:00:30 -08:00
|
|
|
|
|
|
|
|
const MODEL_FETCH_TIMEOUT_MS = 15000;
|
2026-05-20 22:08:45 -07:00
|
|
|
const MODEL_CATALOG_REFRESH_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
2026-02-14 21:00:30 -08:00
|
|
|
|
2026-06-13 12:02:22 -07:00
|
|
|
const modelCatalog: ModelCatalogSnapshot = {};
|
2026-02-14 21:00:30 -08:00
|
|
|
|
2026-05-20 22:08:45 -07:00
|
|
|
let catalogRefreshPromise: Promise<void> | null = null;
|
|
|
|
|
|
2026-02-14 21:00:30 -08:00
|
|
|
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 refreshProviderModels(provider: Provider, logger?: FastifyBaseLogger) {
|
|
|
|
|
try {
|
2026-06-13 12:02:22 -07:00
|
|
|
const models = await withTimeout(fetchProviderCatalogModels(provider), MODEL_FETCH_TIMEOUT_MS, `${provider} model fetch`);
|
2026-02-14 21:00:30 -08:00
|
|
|
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);
|
2026-05-20 22:08:45 -07:00
|
|
|
const previous = modelCatalog[provider];
|
2026-06-13 12:02:22 -07:00
|
|
|
const fallbackModels = getProviderCatalogFallbackModels(provider);
|
2026-02-14 21:00:30 -08:00
|
|
|
modelCatalog[provider] = {
|
2026-05-20 22:08:45 -07:00
|
|
|
models: previous?.models.length ? previous.models : fallbackModels,
|
|
|
|
|
loadedAt: previous?.loadedAt ?? null,
|
2026-02-14 21:00:30 -08:00
|
|
|
error: message,
|
|
|
|
|
};
|
|
|
|
|
logger?.warn({ provider, err: message }, "failed to load provider model catalog");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-20 22:08:45 -07:00
|
|
|
export async function refreshModelCatalog(logger?: FastifyBaseLogger) {
|
|
|
|
|
if (catalogRefreshPromise) return catalogRefreshPromise;
|
|
|
|
|
|
2026-06-13 12:02:22 -07:00
|
|
|
catalogRefreshPromise = Promise.all(listModelCatalogProviders().map((provider) => refreshProviderModels(provider, logger)))
|
2026-05-20 22:08:45 -07:00
|
|
|
.then(() => undefined)
|
|
|
|
|
.finally(() => {
|
|
|
|
|
catalogRefreshPromise = null;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return catalogRefreshPromise;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-14 21:00:30 -08:00
|
|
|
export async function warmModelCatalog(logger?: FastifyBaseLogger) {
|
2026-05-20 22:08:45 -07:00
|
|
|
await refreshModelCatalog(logger);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function startModelCatalogRefreshLoop(logger?: FastifyBaseLogger) {
|
|
|
|
|
const timer = setInterval(() => {
|
|
|
|
|
void refreshModelCatalog(logger);
|
|
|
|
|
}, MODEL_CATALOG_REFRESH_INTERVAL_MS);
|
|
|
|
|
timer.unref?.();
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
clearInterval(timer);
|
|
|
|
|
};
|
2026-02-14 21:00:30 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function getModelCatalogSnapshot(): ModelCatalogSnapshot {
|
2026-05-04 21:52:39 -07:00
|
|
|
const snapshot: ModelCatalogSnapshot = {};
|
2026-06-13 12:02:22 -07:00
|
|
|
for (const provider of listModelCatalogProviders()) {
|
2026-05-04 21:52:39 -07:00
|
|
|
const entry = modelCatalog[provider] ?? { models: [], loadedAt: null, error: null };
|
|
|
|
|
snapshot[provider] = {
|
|
|
|
|
models: [...entry.models],
|
|
|
|
|
loadedAt: entry.loadedAt,
|
|
|
|
|
error: entry.error,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
return snapshot;
|
2026-02-14 21:00:30 -08:00
|
|
|
}
|