big backend refactor
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
import type { FastifyBaseLogger } from "fastify";
|
||||
import { env } from "../env.js";
|
||||
import { anthropicClient, hermesAgentClient, isHermesAgentConfigured, openaiClient, xaiClient } from "./providers.js";
|
||||
import {
|
||||
fetchProviderCatalogModels,
|
||||
getProviderCatalogFallbackModels,
|
||||
listModelCatalogProviders,
|
||||
} from "./provider-adapters.js";
|
||||
import type { Provider } from "./types.js";
|
||||
|
||||
export type ProviderModelSnapshot = {
|
||||
@@ -11,35 +14,13 @@ export type ProviderModelSnapshot = {
|
||||
|
||||
export type ModelCatalogSnapshot = Partial<Record<Provider, ProviderModelSnapshot>>;
|
||||
|
||||
const baseProviders: Provider[] = ["openai", "anthropic", "xai"];
|
||||
const MODEL_FETCH_TIMEOUT_MS = 15000;
|
||||
const MODEL_CATALOG_REFRESH_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
||||
|
||||
const modelCatalog: ModelCatalogSnapshot = {
|
||||
openai: { models: [], loadedAt: null, error: null },
|
||||
anthropic: { models: [], loadedAt: null, error: null },
|
||||
xai: { models: [], loadedAt: null, error: null },
|
||||
};
|
||||
const modelCatalog: ModelCatalogSnapshot = {};
|
||||
|
||||
let catalogRefreshPromise: Promise<void> | null = null;
|
||||
|
||||
function getCatalogProviders(): Provider[] {
|
||||
return isHermesAgentConfigured() ? [...baseProviders, "hermes-agent"] : baseProviders;
|
||||
}
|
||||
|
||||
function uniqSorted(models: string[]) {
|
||||
return [...new Set(models.map((value) => value.trim()).filter(Boolean))].sort((a, b) => a.localeCompare(b));
|
||||
}
|
||||
|
||||
function isLikelyOpenAIResponsesModel(model: string) {
|
||||
const id = model.toLowerCase();
|
||||
if (id.includes("embedding") || id.includes("moderation")) return false;
|
||||
if (id.includes("audio") || id.includes("realtime") || id.includes("transcribe") || id.includes("tts")) return false;
|
||||
if (id.includes("image") || id.includes("dall-e") || id.includes("sora")) return false;
|
||||
if (id.includes("search") || id.includes("computer-use")) return false;
|
||||
return /^(gpt-|o\d|chatgpt-)/.test(id);
|
||||
}
|
||||
|
||||
async function withTimeout<T>(promise: Promise<T>, timeoutMs: number, label: string) {
|
||||
let timeoutId: NodeJS.Timeout | null = null;
|
||||
try {
|
||||
@@ -56,31 +37,9 @@ async function withTimeout<T>(promise: Promise<T>, timeoutMs: number, label: str
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchProviderModels(provider: Provider) {
|
||||
if (provider === "openai") {
|
||||
const page = await openaiClient().models.list();
|
||||
return uniqSorted(page.data.map((model) => model.id).filter(isLikelyOpenAIResponsesModel));
|
||||
}
|
||||
|
||||
if (provider === "anthropic") {
|
||||
const page = await anthropicClient().models.list({ limit: 200 });
|
||||
return uniqSorted(page.data.map((model) => model.id));
|
||||
}
|
||||
|
||||
if (provider === "xai") {
|
||||
const page = await xaiClient().models.list();
|
||||
return uniqSorted(page.data.map((model) => model.id));
|
||||
}
|
||||
|
||||
const page = await hermesAgentClient().models.list();
|
||||
const models = page.data.map((model) => model.id);
|
||||
if (env.HERMES_AGENT_MODEL) models.push(env.HERMES_AGENT_MODEL);
|
||||
return uniqSorted(models);
|
||||
}
|
||||
|
||||
async function refreshProviderModels(provider: Provider, logger?: FastifyBaseLogger) {
|
||||
try {
|
||||
const models = await withTimeout(fetchProviderModels(provider), MODEL_FETCH_TIMEOUT_MS, `${provider} model fetch`);
|
||||
const models = await withTimeout(fetchProviderCatalogModels(provider), MODEL_FETCH_TIMEOUT_MS, `${provider} model fetch`);
|
||||
modelCatalog[provider] = {
|
||||
models,
|
||||
loadedAt: new Date().toISOString(),
|
||||
@@ -90,7 +49,7 @@ async function refreshProviderModels(provider: Provider, logger?: FastifyBaseLog
|
||||
} catch (err: any) {
|
||||
const message = err?.message ?? String(err);
|
||||
const previous = modelCatalog[provider];
|
||||
const fallbackModels = provider === "hermes-agent" && env.HERMES_AGENT_MODEL ? [env.HERMES_AGENT_MODEL] : [];
|
||||
const fallbackModels = getProviderCatalogFallbackModels(provider);
|
||||
modelCatalog[provider] = {
|
||||
models: previous?.models.length ? previous.models : fallbackModels,
|
||||
loadedAt: previous?.loadedAt ?? null,
|
||||
@@ -103,7 +62,7 @@ async function refreshProviderModels(provider: Provider, logger?: FastifyBaseLog
|
||||
export async function refreshModelCatalog(logger?: FastifyBaseLogger) {
|
||||
if (catalogRefreshPromise) return catalogRefreshPromise;
|
||||
|
||||
catalogRefreshPromise = Promise.all(getCatalogProviders().map((provider) => refreshProviderModels(provider, logger)))
|
||||
catalogRefreshPromise = Promise.all(listModelCatalogProviders().map((provider) => refreshProviderModels(provider, logger)))
|
||||
.then(() => undefined)
|
||||
.finally(() => {
|
||||
catalogRefreshPromise = null;
|
||||
@@ -129,7 +88,7 @@ export function startModelCatalogRefreshLoop(logger?: FastifyBaseLogger) {
|
||||
|
||||
export function getModelCatalogSnapshot(): ModelCatalogSnapshot {
|
||||
const snapshot: ModelCatalogSnapshot = {};
|
||||
for (const provider of getCatalogProviders()) {
|
||||
for (const provider of listModelCatalogProviders()) {
|
||||
const entry = modelCatalog[provider] ?? { models: [], loadedAt: null, error: null };
|
||||
snapshot[provider] = {
|
||||
models: [...entry.models],
|
||||
|
||||
Reference in New Issue
Block a user