import { createHash } from "node:crypto"; import { homedir } from "node:os"; import { mkdir, readFile, unlink, writeFile } from "node:fs/promises"; import { dirname, join } from "node:path"; import type { OmniRouteEnrichmentEntry, OmniRouteEnrichmentMap, OmniRouteProviderConnection, OmniRouteRawAutoCombo, OmniRouteRawCombo, OmniRouteRawModelEntry, } from "./shared/index.js"; export const DEFAULT_MODEL_CACHE_TTL_MS = 300_000 as const; /** * Breather after a refresh whose models fetch came back empty (gateway down * or refusing). Transforms inside the window serve last-known-good without * re-firing the fetch suite. Short on purpose: it only guards the * pathological case, normal TTL expiry still refetches every window. */ export const UNREACHABLE_COOLDOWN_MS = 15_000 as const; export interface CatalogSnapshot { models: OmniRouteRawModelEntry[]; combos: OmniRouteRawCombo[]; autoCombos: OmniRouteRawAutoCombo[]; providers?: OmniRouteProviderConnection[]; enrichment?: OmniRouteEnrichmentMap; fetchedAt: number; } export const SNAPSHOT_FORMAT_VERSION = 2 as const; /** * A raw snapshot entry is stale when it cannot be mapped to a publishable * model: no string `id` (unroutable) or a pre-mapped `api` block without a * valid `npm` package (the runner would reject it as `Unsupported package`). * Plain `/v1/models` entries carry no `api` block -- it is synthesized at * publish time -- so only a present-but-invalid block drops the entry. */ export function isStaleSnapshotModel(entry: unknown): boolean { if (!entry || typeof entry !== "object") return true; const id = (entry as { id?: unknown }).id; if (typeof id !== "string" || id.length === 0) return true; const api = (entry as { api?: unknown }).api; if (api === undefined) return false; if (!api || typeof api !== "object") return true; const npm = (api as { npm?: unknown }).npm; return typeof npm !== "string" || npm.length === 0; } interface DiskSnapshotV2 { v: 2; identityFingerprint: string; models: OmniRouteRawModelEntry[]; combos: OmniRouteRawCombo[]; autoCombos?: OmniRouteRawAutoCombo[]; providers?: OmniRouteProviderConnection[]; /** * Display names, provider labels, pricing and free-tier budgets, as * `[key, entry]` pairs (a Map does not survive JSON). Persisted because a * cold start otherwise publishes raw model ids until the first refresh * completes — which is the moment the snapshot exists to cover. */ enrichment?: [string, OmniRouteEnrichmentEntry][]; writtenAt: number; } /** * Ceiling on what one snapshot may occupy on disk. A gateway with thousands of * models makes this file grow without bound otherwise; past the cap the * enrichment overlay is dropped first (it is rebuilt on the next refresh) * rather than losing the catalog itself. */ const MAX_SNAPSHOT_BYTES = 32 * 1024 * 1024; function trimTrailingSlashes(value: string): string { let i = value.length; while (i > 0 && value.charCodeAt(i - 1) === 0x2f) i -= 1; return i === value.length ? value : value.slice(0, i); } function normalizeBaseURL(baseURL: string): string { try { const parsed = new URL(baseURL); parsed.hash = ""; parsed.pathname = trimTrailingSlashes(parsed.pathname) || "/"; return parsed.toString(); } catch { return trimTrailingSlashes(baseURL); } } export function memoryCacheKey(baseURL: string, credentialId: string): string { return `${baseURL}::${createHash("sha256").update(credentialId).digest("hex")}`; } export function snapshotIdentityFingerprint( baseURL: string, apiKey: string, managementReadToken: string ): string { return createHash("sha256") .update(JSON.stringify([normalizeBaseURL(baseURL), apiKey, managementReadToken])) .digest("hex"); } export function diskSnapshotPath(providerId: string): string { // OPENCODE_DATA_DIR is honoured verbatim when set: whoever controls the // process environment already chooses where the process writes, so // resolving it further would only surprise. The providerId segment stays // bounded by the options schema (letters, digits, '.', '_' and '-'; never // "." or ".."), keeping the file inside