| |
| |
| |
| |
| |
| |
| |
| |
|
|
|
|
| import { randomUUID } from "node:crypto";
|
| import { getSettings, updateSettings } from "@/lib/localDb";
|
| import { getRuntimePorts } from "@/lib/runtime/ports";
|
|
|
| const DEFAULT_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
| const MODEL_SYNC_SETTING_KEY = "model_sync_last_run";
|
| const MODEL_SYNC_INTERNAL_AUTH_HEADER = "x-model-sync-internal-auth";
|
|
|
| const { dashboardPort } = getRuntimePorts();
|
|
|
| const INTERNAL_BASE_URL =
|
| process.env.BASE_URL ||
|
| process.env.NEXT_PUBLIC_BASE_URL ||
|
| process.env.NEXT_PUBLIC_APP_URL ||
|
| `http://127.0.0.1:${dashboardPort}`;
|
|
|
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| export function getModelSyncInternalBaseUrl(): string {
|
| return INTERNAL_BASE_URL;
|
| }
|
|
|
| const globalState = globalThis as typeof globalThis & {
|
| __omnirouteModelSyncInternalAuthToken?: string;
|
| };
|
|
|
| let schedulerTimer: NodeJS.Timeout | null = null;
|
| let isRunning = false;
|
| let internalAuthToken: string | null = null;
|
|
|
| function getInternalAuthToken(): string {
|
| if (!internalAuthToken) {
|
| internalAuthToken = globalState.__omnirouteModelSyncInternalAuthToken || randomUUID();
|
| globalState.__omnirouteModelSyncInternalAuthToken = internalAuthToken;
|
| }
|
| return internalAuthToken;
|
| }
|
|
|
| export function getModelSyncInternalAuthHeaderName(): string {
|
| return MODEL_SYNC_INTERNAL_AUTH_HEADER;
|
| }
|
|
|
| export function buildModelSyncInternalHeaders(): Record<string, string> {
|
| return { [MODEL_SYNC_INTERNAL_AUTH_HEADER]: getInternalAuthToken() };
|
| }
|
|
|
| export function isModelSyncInternalRequest(request: { headers: Headers }): boolean {
|
| if (!internalAuthToken && globalState.__omnirouteModelSyncInternalAuthToken) {
|
| internalAuthToken = globalState.__omnirouteModelSyncInternalAuthToken;
|
| }
|
| const headerToken = request.headers.get(MODEL_SYNC_INTERNAL_AUTH_HEADER);
|
| return Boolean(headerToken && internalAuthToken && headerToken === internalAuthToken);
|
| }
|
|
|
| |
| |
|
|
| async function getAutoSyncConnections(): Promise<
|
| Array<{ id: string; provider: string; name?: string }>
|
| > {
|
| try {
|
| const { getProviderConnections } = await import("@/lib/localDb");
|
| const connections = await getProviderConnections();
|
| return connections.filter((conn: any) => {
|
| if (!conn.isActive && conn.isActive !== undefined) return false;
|
| const psd =
|
| conn.providerSpecificData && typeof conn.providerSpecificData === "object"
|
| ? conn.providerSpecificData
|
| : {};
|
| return psd.autoSync === true;
|
| });
|
| } catch (err) {
|
| console.warn("[ModelSync] Failed to load connections:", (err as Error).message);
|
| return [];
|
| }
|
| }
|
|
|
| |
| |
|
|
| async function syncConnectionModels(
|
| connectionId: string,
|
| provider: string,
|
| baseUrl: string
|
| ): Promise<boolean> {
|
| try {
|
| const res = await fetch(`${baseUrl}/api/providers/${connectionId}/sync-models`, {
|
| method: "POST",
|
| headers: {
|
| "Content-Type": "application/json",
|
| ...buildModelSyncInternalHeaders(),
|
| },
|
| });
|
| if (!res.ok) {
|
| console.warn(
|
| `[ModelSync] ${provider} (${connectionId.slice(0, 8)}): sync returned ${res.status}`
|
| );
|
| return false;
|
| }
|
| const data = await res.json();
|
| console.log(
|
| `[ModelSync] ${provider} (${connectionId.slice(0, 8)}): β ${data.syncedModels || 0} models`
|
| );
|
| return true;
|
| } catch (err) {
|
| console.warn(
|
| `[ModelSync] ${provider} (${connectionId.slice(0, 8)}): fetch failed β`,
|
| (err as Error).message
|
| );
|
| return false;
|
| }
|
| }
|
|
|
| |
| |
|
|
| async function runSyncCycle(apiBaseUrl: string): Promise<void> {
|
| if (isRunning) {
|
| console.log("[ModelSync] Skipping cycle β previous run still in progress");
|
| return;
|
| }
|
| isRunning = true;
|
| const start = Date.now();
|
|
|
| try {
|
| const connections = await getAutoSyncConnections();
|
|
|
| if (connections.length === 0) {
|
| console.log("[ModelSync] No connections with autoSync enabled β skipping cycle");
|
| return;
|
| }
|
|
|
| console.log(`[ModelSync] Starting model sync cycle β ${connections.length} connection(s)`);
|
|
|
| const results = await Promise.allSettled(
|
| connections.map((conn) =>
|
| syncConnectionModels(conn.id, conn.name || conn.provider, apiBaseUrl)
|
| )
|
| );
|
|
|
| const succeeded = results.filter((r) => r.status === "fulfilled" && r.value === true).length;
|
| console.log(
|
| `[ModelSync] Cycle complete: ${succeeded}/${connections.length} synced in ${Date.now() - start}ms`
|
| );
|
|
|
|
|
| try {
|
| await updateSettings({ [MODEL_SYNC_SETTING_KEY]: new Date().toISOString() });
|
| } catch {
|
|
|
| }
|
| } finally {
|
| isRunning = false;
|
| }
|
| }
|
|
|
| |
| |
| |
| |
|
|
| export function startModelSyncScheduler(
|
| apiBaseUrl = INTERNAL_BASE_URL,
|
| intervalMs = DEFAULT_INTERVAL_MS
|
| ): void {
|
| if (schedulerTimer) {
|
| console.log("[ModelSync] Scheduler already running β skipping start");
|
| return;
|
| }
|
|
|
|
|
| const envHours = parseInt(process.env.MODEL_SYNC_INTERVAL_HOURS ?? "", 10);
|
| const effectiveIntervalMs =
|
| !isNaN(envHours) && envHours > 0 ? envHours * 60 * 60 * 1000 : intervalMs;
|
|
|
| console.log(`[ModelSync] Scheduler started β interval: ${effectiveIntervalMs / 3_600_000}h`);
|
|
|
|
|
| const startupDelay = setTimeout(() => runSyncCycle(apiBaseUrl), 5_000);
|
| startupDelay.unref?.();
|
|
|
|
|
| schedulerTimer = setInterval(() => runSyncCycle(apiBaseUrl), effectiveIntervalMs);
|
| schedulerTimer.unref?.();
|
| }
|
|
|
| |
| |
|
|
| export function stopModelSyncScheduler(): void {
|
| if (schedulerTimer) {
|
| clearInterval(schedulerTimer);
|
| schedulerTimer = null;
|
| console.log("[ModelSync] Scheduler stopped");
|
| }
|
| }
|
|
|
| |
| |
|
|
| export async function getLastModelSyncTime(): Promise<string | null> {
|
| try {
|
| const settings = await getSettings();
|
| return (settings as Record<string, string>)[MODEL_SYNC_SETTING_KEY] ?? null;
|
| } catch {
|
| return null;
|
| }
|
| }
|
|
|