File size: 3,552 Bytes
6111b2b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
/**

 * Model sync job for embedded services.

 *

 * Fetches /v1/models from a running service instance and persists the list

 * in the key_value table so the model catalog can expose them without calling

 * the service on every request. Also stamps last_sync_at in version_manager.

 *

 * Schedule: runs once when the service reaches "running" state, then every

 * SYNC_INTERVAL_MS. Stops automatically when the service stops.

 */

import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error";
import { getServiceModels, saveServiceModels, type ServiceModel } from "@/lib/db/serviceModels";
import { updateVersionManagerTool } from "@/lib/db/versionManager";

const SYNC_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
const FETCH_TIMEOUT_MS = 10_000;

const activeTimers = new Map<string, ReturnType<typeof setInterval>>();

/**

 * Fetch the model list from a running service and persist it.

 * Returns the number of models synced, or -1 on failure.

 */
export async function syncServiceModels(

  tool: string,

  baseUrl: string,

  apiKey: string

): Promise<number> {
  try {
    const res = await fetch(`${baseUrl}/v1/models`, {
      headers: { Authorization: `Bearer ${apiKey}` },
      signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
    });

    if (!res.ok) {
      console.warn(`[ModelSync:${tool}] /v1/models returned HTTP ${res.status}`);
      return -1;
    }

    const json = await res.json();
    const data: unknown = Array.isArray(json?.data) ? json.data : Array.isArray(json) ? json : [];
    const models = (data as unknown[])
      .filter(
        (m): m is ServiceModel =>
          typeof m === "object" &&
          m !== null &&
          typeof (m as Record<string, unknown>).id === "string"
      )
      .map((m) => ({
        ...m,
        // Prefix the model id with the tool name so downstream routing and the
        // executor strip it back off (e.g. "9router/cx/gpt-5-mini").
        id: m.id.startsWith(`${tool}/`) ? m.id : `${tool}/${m.id}`,
      }));

    saveServiceModels(tool, models);
    await updateVersionManagerTool(tool, { lastSyncAt: new Date().toISOString() });

    console.log(`[ModelSync:${tool}] synced ${models.length} model(s)`);
    return models.length;
  } catch (err) {
    const msg = sanitizeErrorMessage(err instanceof Error ? err.message : String(err));
    console.warn(`[ModelSync:${tool}] fetch failed: ${msg}`);
    return -1;
  }
}

/**

 * Start periodic model sync for a service.

 * Idempotent — calling again while already running is a no-op.

 */
export function scheduleServiceModelSync(

  tool: string,

  baseUrl: string,

  apiKey: string,

  intervalMs = SYNC_INTERVAL_MS

): void {
  if (activeTimers.has(tool)) return;

  // First sync immediately (non-blocking)
  syncServiceModels(tool, baseUrl, apiKey).catch(() => {});

  const timer = setInterval(() => {
    syncServiceModels(tool, baseUrl, apiKey).catch(() => {});
  }, intervalMs);
  timer.unref?.();

  activeTimers.set(tool, timer);
  console.log(`[ModelSync:${tool}] scheduler started (interval ${intervalMs / 1000}s)`);
}

/**

 * Stop the periodic sync for a service.

 */
export function stopServiceModelSync(tool: string): void {
  const timer = activeTimers.get(tool);
  if (!timer) return;
  clearInterval(timer);
  activeTimers.delete(tool);
  console.log(`[ModelSync:${tool}] scheduler stopped`);
}

/** Re-export read path so consumers don't need to import two modules. */
export { getServiceModels };