mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-11 01:32:22 +03:00
Compare commits
4 Commits
fix/9981-i
...
feat/9490-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
49def2f0c3 | ||
|
|
57e1d88ae6 | ||
|
|
61f962294d | ||
|
|
a2c15c5a8c |
@@ -1033,7 +1033,11 @@ export const OmniRoutePlugin: Plugin = async (_input, options) => {
|
|||||||
// Config hook: keep existing catalog shim, and register slash command
|
// Config hook: keep existing catalog shim, and register slash command
|
||||||
// templates that ask the agent to call the force-sync tool (OpenCode has no
|
// templates that ask the agent to call the force-sync tool (OpenCode has no
|
||||||
// Pi-style registerCommand API; tools + command templates are the native path).
|
// Pi-style registerCommand API; tools + command templates are the native path).
|
||||||
const baseConfigHook = createOmniRouteConfigHook(resolved, { cache: sharedCache });
|
const baseConfigHook = createOmniRouteConfigHook(resolved, {
|
||||||
|
cache: sharedCache,
|
||||||
|
diskSnapshotReader: defaultDiskSnapshotReader,
|
||||||
|
diskSnapshotWriter: defaultDiskSnapshotWriter,
|
||||||
|
});
|
||||||
const configWithSyncCommand = async (input: Config) => {
|
const configWithSyncCommand = async (input: Config) => {
|
||||||
await baseConfigHook(input);
|
await baseConfigHook(input);
|
||||||
const cfg = input as Config & {
|
const cfg = input as Config & {
|
||||||
@@ -4741,7 +4745,7 @@ export type OmniRouteDiskSnapshotWriter = (
|
|||||||
export type OmniRouteDiskSnapshotReader = (
|
export type OmniRouteDiskSnapshotReader = (
|
||||||
providerId: string,
|
providerId: string,
|
||||||
identityFingerprint: string
|
identityFingerprint: string
|
||||||
) => Promise<Omit<OmniRouteFetchCacheEntry, "expiresAt"> | undefined>;
|
) => Promise<(Omit<OmniRouteFetchCacheEntry, "expiresAt"> & { writtenAt?: number }) | undefined>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bind a snapshot to the endpoint and effective credential tuple without
|
* Bind a snapshot to the endpoint and effective credential tuple without
|
||||||
@@ -4824,15 +4828,36 @@ export const defaultDiskSnapshotReader: OmniRouteDiskSnapshotReader = async (
|
|||||||
? parsed.rawCompressionCombos
|
? parsed.rawCompressionCombos
|
||||||
: [],
|
: [],
|
||||||
rawConnections: Array.isArray(parsed.rawConnections) ? parsed.rawConnections : [],
|
rawConnections: Array.isArray(parsed.rawConnections) ? parsed.rawConnections : [],
|
||||||
|
writtenAt: typeof parsed.writtenAt === "number" ? parsed.writtenAt : undefined,
|
||||||
};
|
};
|
||||||
} catch {
|
} catch {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/** No-op disk-cache pair — used by tests to avoid filesystem side effects. */
|
/** No-op disk-cache pair — used by tests to avoid filesystem side effects.
|
||||||
|
* Also used as the default in createOmniRouteConfigHook so that tests
|
||||||
|
* that don't pass a diskSnapshotReader don't read real snapshot files
|
||||||
|
* from the user's ~/.local/share/opencode/plugins/ directory.
|
||||||
|
* The OmniRoutePlugin function passes the real defaultDiskSnapshotReader
|
||||||
|
* explicitly. */
|
||||||
|
export const noopDiskSnapshotReader: OmniRouteDiskSnapshotReader = async () => undefined;
|
||||||
export const noopDiskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {};
|
export const noopDiskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In-flight refresh guard: prevents concurrent refreshes for the same
|
||||||
|
* cacheKey. When a warm snapshot is served, the refresh runs detached; if
|
||||||
|
* a second hook invocation arrives before the refresh completes, it should
|
||||||
|
* piggyback on the in-flight promise rather than starting a second one.
|
||||||
|
* Cleared on settle so it doesn't leak.
|
||||||
|
*/
|
||||||
|
const _inflightRefresh: Map<string, Promise<void>> = new Map();
|
||||||
|
|
||||||
|
/** Reset the in-flight refresh guard (for test isolation). */
|
||||||
|
export function _resetInflightRefresh(): void {
|
||||||
|
_inflightRefresh.clear();
|
||||||
|
}
|
||||||
|
|
||||||
// ────────────────────────────────────────────────────────────────────────────
|
// ────────────────────────────────────────────────────────────────────────────
|
||||||
// Debug logging (features.debugLog)
|
// Debug logging (features.debugLog)
|
||||||
// ────────────────────────────────────────────────────────────────────────────
|
// ────────────────────────────────────────────────────────────────────────────
|
||||||
@@ -5067,7 +5092,6 @@ export function createDebugLoggingFetch(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
export const noopDiskSnapshotReader: OmniRouteDiskSnapshotReader = async () => undefined;
|
|
||||||
|
|
||||||
export type OmniRouteReadAuthJson = () => Promise<AuthJsonShape | undefined | null>;
|
export type OmniRouteReadAuthJson = () => Promise<AuthJsonShape | undefined | null>;
|
||||||
|
|
||||||
@@ -5170,8 +5194,8 @@ export function createOmniRouteConfigHook(
|
|||||||
const compressionMetaFetcher =
|
const compressionMetaFetcher =
|
||||||
deps.compressionMetaFetcher ?? defaultOmniRouteCompressionMetaFetcher;
|
deps.compressionMetaFetcher ?? defaultOmniRouteCompressionMetaFetcher;
|
||||||
const providersFetcher = deps.providersFetcher ?? defaultOmniRouteProvidersFetcher;
|
const providersFetcher = deps.providersFetcher ?? defaultOmniRouteProvidersFetcher;
|
||||||
const diskSnapshotReader = deps.diskSnapshotReader ?? defaultDiskSnapshotReader;
|
const diskSnapshotReader = deps.diskSnapshotReader ?? noopDiskSnapshotReader;
|
||||||
const diskSnapshotWriter = deps.diskSnapshotWriter ?? defaultDiskSnapshotWriter;
|
const diskSnapshotWriter = deps.diskSnapshotWriter ?? noopDiskSnapshotWriter;
|
||||||
const now = deps.now ?? Date.now;
|
const now = deps.now ?? Date.now;
|
||||||
const cache: OmniRouteFetchCache = deps.cache ?? new Map();
|
const cache: OmniRouteFetchCache = deps.cache ?? new Map();
|
||||||
const logger = deps.logger ?? console;
|
const logger = deps.logger ?? console;
|
||||||
@@ -5266,12 +5290,12 @@ export function createOmniRouteConfigHook(
|
|||||||
const t = now();
|
const t = now();
|
||||||
const cached = cache.get(cacheKey);
|
const cached = cache.get(cacheKey);
|
||||||
|
|
||||||
let rawModels: OmniRouteRawModelEntry[];
|
let rawModels: OmniRouteRawModelEntry[] = [];
|
||||||
let rawCombos: OmniRouteRawCombo[];
|
let rawCombos: OmniRouteRawCombo[] = [];
|
||||||
let rawAutoCombos: OmniRouteRawAutoCombo[];
|
let rawAutoCombos: OmniRouteRawAutoCombo[] = [];
|
||||||
let rawEnrichment: OmniRouteEnrichmentMap;
|
let rawEnrichment: OmniRouteEnrichmentMap = new Map();
|
||||||
let rawCompressionCombos: OmniRouteCompressionCombo[];
|
let rawCompressionCombos: OmniRouteCompressionCombo[] = [];
|
||||||
let rawConnections: OmniRouteProviderConnection[];
|
let rawConnections: OmniRouteProviderConnection[] = [];
|
||||||
|
|
||||||
if (cached && cached.expiresAt > t) {
|
if (cached && cached.expiresAt > t) {
|
||||||
rawModels = cached.rawModels;
|
rawModels = cached.rawModels;
|
||||||
@@ -5281,160 +5305,275 @@ export function createOmniRouteConfigHook(
|
|||||||
rawCompressionCombos = cached.rawCompressionCombos;
|
rawCompressionCombos = cached.rawCompressionCombos;
|
||||||
rawConnections = cached.rawConnections;
|
rawConnections = cached.rawConnections;
|
||||||
} else {
|
} else {
|
||||||
// Fail-open fetcher errors: on /v1/models throw, fall back to empty
|
// ─────────────────────────────────────────────────────────────────────
|
||||||
// catalog (still publish a stub block so OC has a complete-shape
|
// Warm startup: read the disk snapshot before fetching so the provider
|
||||||
// entry); on /api/combos throw, publish models-only. Disk-cache
|
// registers immediately with the last-known-good catalog. The live
|
||||||
// fallback below recovers the last-known-good catalog when the
|
// fetch then refreshes in the background (detached) and updates the
|
||||||
// fetcher threw (network down / 403 / timeout) AND features.diskCache
|
// cache + snapshot. Gated by features.diskCache (default-on).
|
||||||
// !== false. A 0-entry SUCCESS (fresh tenant) does NOT trigger
|
// ─────────────────────────────────────────────────────────────────────
|
||||||
// disk fallback — that's a valid empty catalog.
|
let warmSnapshot: Omit<OmniRouteFetchCacheEntry, "expiresAt"> | undefined;
|
||||||
let modelsFetchThrew = false;
|
if (wantDiskCache) {
|
||||||
try {
|
const snapshotResult = await diskSnapshotReader(resolved.providerId, snapshotFingerprint);
|
||||||
rawModels = await fetcher(baseURL, apiKey, 10_000);
|
if (snapshotResult && snapshotResult.rawModels.length > 0) {
|
||||||
} catch (err) {
|
warmSnapshot = snapshotResult;
|
||||||
logger.warn(
|
// Log snapshot age (accept any age — instant beats empty).
|
||||||
"[omniroute-plugin] config shim: /v1/models fetch failed; publishing stub provider entry",
|
const age = (snapshotResult as { writtenAt?: number }).writtenAt;
|
||||||
err
|
const ageLabel = typeof age === "number" ? `${Math.round((Date.now() - age) / 3_600_000)}h` : "unknown";
|
||||||
);
|
|
||||||
rawModels = [];
|
|
||||||
modelsFetchThrew = true;
|
|
||||||
}
|
|
||||||
const modelsFetchOk = !modelsFetchThrew && rawModels.length > 0;
|
|
||||||
|
|
||||||
rawCombos = [];
|
|
||||||
try {
|
|
||||||
rawCombos = await combosFetcher(baseURL, managementReadToken, 10_000);
|
|
||||||
} catch (err) {
|
|
||||||
logger.warn(
|
|
||||||
"[omniroute-plugin] config shim: /api/combos fetch failed; publishing models-only static catalog",
|
|
||||||
err
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
rawAutoCombos = [];
|
|
||||||
if (wantAutoCombos) {
|
|
||||||
try {
|
|
||||||
rawAutoCombos = await autoCombosFetcher(baseURL, managementReadToken, 5_000);
|
|
||||||
} catch {
|
|
||||||
// Already handled inside the default fetcher
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Eagerly fetch enrichment so the static block can overlay human
|
|
||||||
// display names on raw model ids. On OC ≤1.15.5 the dynamic
|
|
||||||
// `provider.models` hook never fires in `serve` mode, so the static
|
|
||||||
// block IS what reaches `/provider` and the TUI model picker.
|
|
||||||
// Gated by `features.enrichment` (default-on). Soft-fail on error —
|
|
||||||
// we still publish a name-less catalog if /api/pricing/models is
|
|
||||||
// unreachable.
|
|
||||||
rawEnrichment = new Map();
|
|
||||||
if (wantEnrichment) {
|
|
||||||
try {
|
|
||||||
rawEnrichment = await enrichmentFetcher(baseURL, managementReadToken, 10_000);
|
|
||||||
} catch (err) {
|
|
||||||
logger.warn(
|
logger.warn(
|
||||||
"[omniroute-plugin] config shim: /api/pricing/models fetch failed; publishing raw-id static catalog",
|
`[omniroute-plugin] config shim: warm startup from disk snapshot (${snapshotResult.rawModels.length} models, age ${ageLabel})`
|
||||||
err
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compression-metadata fetch — opt-in via features.compressionMetadata.
|
// ─────────────────────────────────────────────────────────────────────
|
||||||
// When on, the default pipeline is appended to every combo `name` so
|
// Parallel refresh: all six fetchers run concurrently via
|
||||||
// the TUI picker advertises which compression a combo applies.
|
// Promise.allSettled. Each wrapper never rejects (catches internally)
|
||||||
rawCompressionCombos = [];
|
// so partial failure is tolerated — same soft-fail semantics as the
|
||||||
if (wantCompressionMeta) {
|
// old sequential chain, but ~6x faster.
|
||||||
try {
|
// ─────────────────────────────────────────────────────────────────────
|
||||||
rawCompressionCombos = await compressionMetaFetcher(baseURL, managementReadToken, 10_000);
|
const doRefresh = async (): Promise<void> => {
|
||||||
} catch (err) {
|
let modelsFetchThrew = false;
|
||||||
logger.warn(
|
let localRawModels: OmniRouteRawModelEntry[] = [];
|
||||||
"[omniroute-plugin] config shim: /api/context/combos fetch failed; publishing combos without compression suffix",
|
let localRawCombos: OmniRouteRawCombo[] = [];
|
||||||
err
|
let localRawAutoCombos: OmniRouteRawAutoCombo[] = [];
|
||||||
);
|
let localRawEnrichment: OmniRouteEnrichmentMap = new Map();
|
||||||
|
let localRawCompressionCombos: OmniRouteCompressionCombo[] = [];
|
||||||
|
let localRawConnections: OmniRouteProviderConnection[] = [];
|
||||||
|
|
||||||
|
// Each wrapper keeps the existing try/catch, default value, and
|
||||||
|
// exact warn message so per-endpoint fallbacks are preserved.
|
||||||
|
const doModels = async (): Promise<void> => {
|
||||||
|
try {
|
||||||
|
localRawModels = await fetcher(baseURL, apiKey, 10_000);
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn(
|
||||||
|
"[omniroute-plugin] config shim: /v1/models fetch failed; publishing stub provider entry",
|
||||||
|
err
|
||||||
|
);
|
||||||
|
localRawModels = [];
|
||||||
|
modelsFetchThrew = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const doCombos = async (): Promise<void> => {
|
||||||
|
try {
|
||||||
|
localRawCombos = await combosFetcher(baseURL, managementReadToken, 10_000);
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn(
|
||||||
|
"[omniroute-plugin] config shim: /api/combos fetch failed; publishing models-only static catalog",
|
||||||
|
err
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const doAutoCombos = async (): Promise<void> => {
|
||||||
|
if (!wantAutoCombos) return;
|
||||||
|
try {
|
||||||
|
localRawAutoCombos = await autoCombosFetcher(baseURL, managementReadToken, 5_000);
|
||||||
|
} catch {
|
||||||
|
// Already handled inside the default fetcher
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const doEnrichment = async (): Promise<void> => {
|
||||||
|
if (!wantEnrichment) return;
|
||||||
|
try {
|
||||||
|
localRawEnrichment = await enrichmentFetcher(baseURL, managementReadToken, 10_000);
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn(
|
||||||
|
"[omniroute-plugin] config shim: /api/pricing/models fetch failed; publishing raw-id static catalog",
|
||||||
|
err
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const doCompression = async (): Promise<void> => {
|
||||||
|
if (!wantCompressionMeta) return;
|
||||||
|
try {
|
||||||
|
localRawCompressionCombos = await compressionMetaFetcher(baseURL, managementReadToken, 10_000);
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn(
|
||||||
|
"[omniroute-plugin] config shim: /api/context/combos fetch failed; publishing combos without compression suffix",
|
||||||
|
err
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const doConnections = async (): Promise<void> => {
|
||||||
|
if (!wantUsableOnly) return;
|
||||||
|
try {
|
||||||
|
localRawConnections = await providersFetcher(baseURL, managementReadToken, 10_000);
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn(
|
||||||
|
"[omniroute-plugin] config shim: /api/providers fetch failed; usableOnly filter disabled for this refresh",
|
||||||
|
err
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await Promise.allSettled([
|
||||||
|
doModels(),
|
||||||
|
doCombos(),
|
||||||
|
doAutoCombos(),
|
||||||
|
doEnrichment(),
|
||||||
|
doCompression(),
|
||||||
|
doConnections(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const modelsFetchOk = !modelsFetchThrew && localRawModels.length > 0;
|
||||||
|
|
||||||
|
// Disk-cache fallback (cold first run, no warm snapshot): when the
|
||||||
|
// live fetch returned no models AND features.diskCache !== false,
|
||||||
|
// hydrate from the last-known-good snapshot so OC still surfaces a
|
||||||
|
// usable catalog (e.g. IP whitelist drop, offline laptop).
|
||||||
|
if (modelsFetchThrew && wantDiskCache && !warmSnapshot) {
|
||||||
|
const snapshot = await diskSnapshotReader(resolved.providerId, snapshotFingerprint);
|
||||||
|
if (snapshot && snapshot.rawModels.length > 0) {
|
||||||
|
logger.warn(
|
||||||
|
`[omniroute-plugin] config shim: /v1/models unreachable; using stale disk cache (${snapshot.rawModels.length} models)`
|
||||||
|
);
|
||||||
|
localRawModels = snapshot.rawModels;
|
||||||
|
localRawCombos = snapshot.rawCombos;
|
||||||
|
localRawAutoCombos = snapshot.rawAutoCombos ?? [];
|
||||||
|
localRawEnrichment = snapshot.rawEnrichment;
|
||||||
|
localRawCompressionCombos = snapshot.rawCompressionCombos;
|
||||||
|
localRawConnections = snapshot.rawConnections;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Provider-connections fetch — opt-in via features.usableOnly. When
|
// Cache even partial results — a subsequent provider-hook call should
|
||||||
// on, the static catalog filters out models/combos whose canonical
|
// not re-burn the timeout window on the same broken endpoint.
|
||||||
// provider has no active connection. Soft-fail (empty list) disables
|
cache.set(cacheKey, {
|
||||||
// the filter for this refresh, never hiding the whole catalog.
|
rawModels: localRawModels,
|
||||||
rawConnections = [];
|
rawCombos: localRawCombos,
|
||||||
if (wantUsableOnly) {
|
rawAutoCombos: localRawAutoCombos,
|
||||||
try {
|
rawEnrichment: localRawEnrichment,
|
||||||
rawConnections = await providersFetcher(baseURL, managementReadToken, 10_000);
|
rawCompressionCombos: localRawCompressionCombos,
|
||||||
} catch (err) {
|
rawConnections: localRawConnections,
|
||||||
logger.warn(
|
expiresAt: now() + resolved.modelCacheTtl,
|
||||||
"[omniroute-plugin] config shim: /api/providers fetch failed; usableOnly filter disabled for this refresh",
|
|
||||||
err
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Disk-cache fallback: when the live fetch returned no models AND
|
|
||||||
// features.diskCache !== false, hydrate from the last-known-good
|
|
||||||
// snapshot so OC still surfaces a usable catalog (e.g. IP whitelist
|
|
||||||
// drop, offline laptop). The snapshot is whatever we last wrote on
|
|
||||||
// a healthy refresh; staleness is bounded only by how recently the
|
|
||||||
// user was online.
|
|
||||||
if (modelsFetchThrew && wantDiskCache) {
|
|
||||||
const snapshot = await diskSnapshotReader(resolved.providerId, snapshotFingerprint);
|
|
||||||
if (snapshot && snapshot.rawModels.length > 0) {
|
|
||||||
logger.warn(
|
|
||||||
`[omniroute-plugin] config shim: /v1/models unreachable; using stale disk cache (${snapshot.rawModels.length} models)`
|
|
||||||
);
|
|
||||||
rawModels = snapshot.rawModels;
|
|
||||||
rawCombos = snapshot.rawCombos;
|
|
||||||
rawAutoCombos = snapshot.rawAutoCombos ?? [];
|
|
||||||
rawEnrichment = snapshot.rawEnrichment;
|
|
||||||
rawCompressionCombos = snapshot.rawCompressionCombos;
|
|
||||||
rawConnections = snapshot.rawConnections;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cache even partial results — a subsequent provider-hook call should
|
|
||||||
// not re-burn the timeout window on the same broken endpoint.
|
|
||||||
cache.set(cacheKey, {
|
|
||||||
rawModels,
|
|
||||||
rawCombos,
|
|
||||||
rawAutoCombos,
|
|
||||||
rawEnrichment,
|
|
||||||
rawCompressionCombos,
|
|
||||||
rawConnections,
|
|
||||||
expiresAt: t + resolved.modelCacheTtl,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Startup diagnostics (file-based) — fires at startup via config hook
|
|
||||||
if (resolved.features?.startupDebug === true) {
|
|
||||||
await writeStartupDiagnostics({
|
|
||||||
providerId: resolved.providerId,
|
|
||||||
baseURL,
|
|
||||||
modelCount: rawModels.length,
|
|
||||||
comboCount: rawCombos.length,
|
|
||||||
enrichmentSize: rawEnrichment.size,
|
|
||||||
autoComboCount: rawAutoCombos.length,
|
|
||||||
enrichment: rawEnrichment,
|
|
||||||
autoCombos: rawAutoCombos,
|
|
||||||
features: resolved.features,
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
// Disk-cache write: persist the last successful (or any non-empty)
|
// Startup diagnostics (file-based) — fires at startup via config hook
|
||||||
// catalog so a subsequent cold start with a failed fetch can recover.
|
if (resolved.features?.startupDebug === true) {
|
||||||
// Best-effort; soft-fail keeps us moving when the data dir isn't
|
await writeStartupDiagnostics({
|
||||||
// writable (e.g. read-only container).
|
providerId: resolved.providerId,
|
||||||
if (modelsFetchOk && wantDiskCache) {
|
baseURL,
|
||||||
await diskSnapshotWriter(
|
modelCount: localRawModels.length,
|
||||||
resolved.providerId,
|
comboCount: localRawCombos.length,
|
||||||
{
|
enrichmentSize: localRawEnrichment.size,
|
||||||
rawModels,
|
autoComboCount: localRawAutoCombos.length,
|
||||||
rawCombos,
|
enrichment: localRawEnrichment,
|
||||||
rawAutoCombos,
|
autoCombos: localRawAutoCombos,
|
||||||
rawEnrichment,
|
features: resolved.features,
|
||||||
rawCompressionCombos,
|
});
|
||||||
rawConnections,
|
}
|
||||||
},
|
|
||||||
snapshotFingerprint
|
// Disk-cache write: persist the last successful (or any non-empty)
|
||||||
);
|
// catalog so a subsequent cold start with a failed fetch can recover.
|
||||||
|
// Best-effort; soft-fail keeps us moving when the data dir isn't
|
||||||
|
// writable (e.g. read-only container). A failed refresh never
|
||||||
|
// overwrites the snapshot (modelsFetchOk gate).
|
||||||
|
if (modelsFetchOk && wantDiskCache) {
|
||||||
|
await diskSnapshotWriter(
|
||||||
|
resolved.providerId,
|
||||||
|
{
|
||||||
|
rawModels: localRawModels,
|
||||||
|
rawCombos: localRawCombos,
|
||||||
|
rawAutoCombos: localRawAutoCombos,
|
||||||
|
rawEnrichment: localRawEnrichment,
|
||||||
|
rawCompressionCombos: localRawCompressionCombos,
|
||||||
|
rawConnections: localRawConnections,
|
||||||
|
},
|
||||||
|
snapshotFingerprint
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-publish a fresh block via the shared cache so OC >=1.14.49's
|
||||||
|
// dynamic provider hook picks it up from the cache. When the models
|
||||||
|
// fetch threw and a warm snapshot was served, keep the warm block
|
||||||
|
// (no downgrade to stub).
|
||||||
|
if (modelsFetchOk || !warmSnapshot) {
|
||||||
|
const freshBlock = buildStaticProviderEntry(
|
||||||
|
localRawModels,
|
||||||
|
localRawCombos,
|
||||||
|
resolved,
|
||||||
|
baseURL,
|
||||||
|
apiKey,
|
||||||
|
localRawEnrichment,
|
||||||
|
localRawCompressionCombos,
|
||||||
|
localRawConnections,
|
||||||
|
localRawAutoCombos
|
||||||
|
);
|
||||||
|
const inputWithProvider2 = input as { provider?: Record<string, unknown> };
|
||||||
|
if (inputWithProvider2.provider) {
|
||||||
|
inputWithProvider2.provider[resolved.providerId] = freshBlock;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (warmSnapshot) {
|
||||||
|
// Warm startup: publish the snapshot block immediately, then run
|
||||||
|
// the refresh detached (never a floating unhandled rejection).
|
||||||
|
rawModels = warmSnapshot.rawModels;
|
||||||
|
rawCombos = warmSnapshot.rawCombos;
|
||||||
|
rawAutoCombos = warmSnapshot.rawAutoCombos ?? [];
|
||||||
|
rawEnrichment = warmSnapshot.rawEnrichment;
|
||||||
|
rawCompressionCombos = warmSnapshot.rawCompressionCombos;
|
||||||
|
rawConnections = warmSnapshot.rawConnections;
|
||||||
|
|
||||||
|
// In-flight guard: if a refresh is already running for this
|
||||||
|
// cacheKey, piggyback on it instead of starting a second one.
|
||||||
|
const existing = _inflightRefresh.get(cacheKey);
|
||||||
|
if (existing) {
|
||||||
|
// Another refresh is in-flight — don't start a second one.
|
||||||
|
// The existing refresh will update the cache when it completes.
|
||||||
|
} else {
|
||||||
|
const refreshP = doRefresh()
|
||||||
|
.catch((err: unknown) => {
|
||||||
|
logger.warn("[omniroute-plugin] config shim: background refresh failed", err);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
_inflightRefresh.delete(cacheKey);
|
||||||
|
});
|
||||||
|
_inflightRefresh.set(cacheKey, refreshP);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Cold first run (no warm snapshot): await the refresh so the
|
||||||
|
// first publish is always correct. In-flight guard still applies.
|
||||||
|
const existing = _inflightRefresh.get(cacheKey);
|
||||||
|
if (existing) {
|
||||||
|
await existing;
|
||||||
|
// After the in-flight refresh completes, the cache has the data.
|
||||||
|
const fresh = cache.get(cacheKey);
|
||||||
|
if (fresh) {
|
||||||
|
rawModels = fresh.rawModels;
|
||||||
|
rawCombos = fresh.rawCombos;
|
||||||
|
rawAutoCombos = fresh.rawAutoCombos;
|
||||||
|
rawEnrichment = fresh.rawEnrichment;
|
||||||
|
rawCompressionCombos = fresh.rawCompressionCombos;
|
||||||
|
rawConnections = fresh.rawConnections;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const refreshP = doRefresh()
|
||||||
|
.catch((err: unknown) => {
|
||||||
|
logger.warn("[omniroute-plugin] config shim: refresh failed", err);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
_inflightRefresh.delete(cacheKey);
|
||||||
|
});
|
||||||
|
_inflightRefresh.set(cacheKey, refreshP);
|
||||||
|
await refreshP;
|
||||||
|
// After the refresh, the cache has the data.
|
||||||
|
const fresh = cache.get(cacheKey);
|
||||||
|
if (fresh) {
|
||||||
|
rawModels = fresh.rawModels;
|
||||||
|
rawCombos = fresh.rawCombos;
|
||||||
|
rawAutoCombos = fresh.rawAutoCombos;
|
||||||
|
rawEnrichment = fresh.rawEnrichment;
|
||||||
|
rawCompressionCombos = fresh.rawCompressionCombos;
|
||||||
|
rawConnections = fresh.rawConnections;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import {
|
|||||||
createOmniRouteProviderHook,
|
createOmniRouteProviderHook,
|
||||||
OmniRoutePlugin,
|
OmniRoutePlugin,
|
||||||
resolveOmniRoutePluginOptions,
|
resolveOmniRoutePluginOptions,
|
||||||
|
_resetInflightRefresh,
|
||||||
type OmniRouteCombosFetcher,
|
type OmniRouteCombosFetcher,
|
||||||
type OmniRouteEnrichmentEntry,
|
type OmniRouteEnrichmentEntry,
|
||||||
type OmniRouteEnrichmentFetcher,
|
type OmniRouteEnrichmentFetcher,
|
||||||
@@ -47,6 +48,16 @@ import {
|
|||||||
type OmniRouteStaticProviderEntry,
|
type OmniRouteStaticProviderEntry,
|
||||||
} from "../src/index.js";
|
} from "../src/index.js";
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────
|
||||||
|
// Test isolation: reset the module-level in-flight refresh guard between
|
||||||
|
// tests so a detached refresh from a previous test doesn't leak into the
|
||||||
|
// next one.
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
test.beforeEach(() => {
|
||||||
|
_resetInflightRefresh();
|
||||||
|
});
|
||||||
|
|
||||||
// ────────────────────────────────────────────────────────────────────────────
|
// ────────────────────────────────────────────────────────────────────────────
|
||||||
// Fixtures
|
// Fixtures
|
||||||
// ────────────────────────────────────────────────────────────────────────────
|
// ────────────────────────────────────────────────────────────────────────────
|
||||||
@@ -1239,7 +1250,10 @@ test("config: diskCache hydrates stale snapshot when /v1/models throws", async (
|
|||||||
);
|
);
|
||||||
assert.equal(writes, 0, "disk write skipped when live fetch failed");
|
assert.equal(writes, 0, "disk write skipped when live fetch failed");
|
||||||
assert.ok(
|
assert.ok(
|
||||||
logger.entries.some((e) => String(e[0]).includes("using stale disk cache")),
|
logger.entries.some((e) =>
|
||||||
|
String(e[0]).includes("using stale disk cache") ||
|
||||||
|
String(e[0]).includes("warm startup from disk snapshot")
|
||||||
|
),
|
||||||
"disk-cache hydration breadcrumb emitted"
|
"disk-cache hydration breadcrumb emitted"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
827
@omniroute/opencode-plugin/tests/warm-startup.test.ts
Normal file
827
@omniroute/opencode-plugin/tests/warm-startup.test.ts
Normal file
@@ -0,0 +1,827 @@
|
|||||||
|
/**
|
||||||
|
* Warm-startup + parallel-refresh tests for the opencode-plugin config shim.
|
||||||
|
*
|
||||||
|
* Covers `createOmniRouteConfigHook(opts, deps)`:
|
||||||
|
* - (a) Warm startup: cache miss + matching snapshot → provider block
|
||||||
|
* populated from snapshot data (not live fetch data).
|
||||||
|
* - (b) Fingerprint mismatch: reader returns undefined → no warm publish,
|
||||||
|
* falls through to awaited fetch (cold-start behavior).
|
||||||
|
* - (c) Successful parallel refresh: all fetchers resolve → cache updated,
|
||||||
|
* disk snapshot written.
|
||||||
|
* - (d) Failed refresh keeps the snapshot: warm-served + models fetcher
|
||||||
|
* rejects → no disk overwrite, block stays at warm-snapshot shape.
|
||||||
|
* - (e) Parallelism: all six fetchers start concurrently (not sequential).
|
||||||
|
* - (f) Soft-fail parity under Promise.allSettled: per-endpoint
|
||||||
|
* fallbacks + logger.warn breadcrumbs preserved.
|
||||||
|
* - (g) No double-refresh: concurrent hook invocations on the same cacheKey
|
||||||
|
* trigger only one refresh (in-flight guard).
|
||||||
|
* - (h) features.diskCache: false disables the warm read entirely.
|
||||||
|
*
|
||||||
|
* Mocking strategy: every dependency is DI-injected at hook construction
|
||||||
|
* (same pattern as config-shim.test.ts). No global monkey-patching.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import test from "node:test";
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import type { Config } from "@opencode-ai/plugin";
|
||||||
|
|
||||||
|
import {
|
||||||
|
createOmniRouteConfigHook,
|
||||||
|
resolveOmniRoutePluginOptions,
|
||||||
|
_resetInflightRefresh,
|
||||||
|
type OmniRouteAutoCombosFetcher,
|
||||||
|
type OmniRouteCombosFetcher,
|
||||||
|
type OmniRouteCompressionMetaFetcher,
|
||||||
|
type OmniRouteEnrichmentEntry,
|
||||||
|
type OmniRouteEnrichmentFetcher,
|
||||||
|
type OmniRouteEnrichmentMap,
|
||||||
|
type OmniRouteFetchCache,
|
||||||
|
type OmniRouteModelsFetcher,
|
||||||
|
type OmniRouteProviderConnection,
|
||||||
|
type OmniRouteProvidersFetcher,
|
||||||
|
type OmniRouteRawAutoCombo,
|
||||||
|
type OmniRouteRawCombo,
|
||||||
|
type OmniRouteRawModelEntry,
|
||||||
|
type OmniRouteReadAuthJson,
|
||||||
|
type OmniRouteStaticProviderEntry,
|
||||||
|
type OmniRouteDiskSnapshotReader,
|
||||||
|
type OmniRouteDiskSnapshotWriter,
|
||||||
|
type OmniRouteCompressionCombo,
|
||||||
|
} from "../src/index.js";
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────
|
||||||
|
// Test isolation: reset the module-level in-flight refresh guard between
|
||||||
|
// tests so a detached refresh from a previous test doesn't leak into the
|
||||||
|
// next one (same cacheKey, different cache instance).
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
test.beforeEach(() => {
|
||||||
|
_resetInflightRefresh();
|
||||||
|
});
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────
|
||||||
|
// Fixtures
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const MODEL_CLAUDE: OmniRouteRawModelEntry = {
|
||||||
|
id: "claude-sonnet-4-6",
|
||||||
|
capabilities: {
|
||||||
|
tool_calling: true,
|
||||||
|
reasoning: true,
|
||||||
|
vision: true,
|
||||||
|
thinking: false,
|
||||||
|
temperature: true,
|
||||||
|
},
|
||||||
|
context_length: 200_000,
|
||||||
|
max_output_tokens: 64_000,
|
||||||
|
max_input_tokens: 180_000,
|
||||||
|
input_modalities: ["text", "image"],
|
||||||
|
output_modalities: ["text"],
|
||||||
|
};
|
||||||
|
|
||||||
|
const MODEL_GEMINI: OmniRouteRawModelEntry = {
|
||||||
|
id: "gemini-3-flash",
|
||||||
|
capabilities: { tool_calling: true, reasoning: false, vision: true, thinking: false },
|
||||||
|
context_length: 1_000_000,
|
||||||
|
max_output_tokens: 8_192,
|
||||||
|
input_modalities: ["text", "image"],
|
||||||
|
output_modalities: ["text"],
|
||||||
|
};
|
||||||
|
|
||||||
|
const COMBO_CLAUDE_TIER: OmniRouteRawCombo = {
|
||||||
|
id: "combo-claude-tier",
|
||||||
|
name: "Claude Tier",
|
||||||
|
models: [
|
||||||
|
{ id: "s1", kind: "model", model: "claude-sonnet-4-6", weight: 100 },
|
||||||
|
{ id: "s2", kind: "model", model: "gemini-3-flash", weight: 50 },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const AUTO_COMBO: OmniRouteRawAutoCombo = {
|
||||||
|
id: "auto",
|
||||||
|
name: "Auto",
|
||||||
|
};
|
||||||
|
|
||||||
|
const COMPRESSION_COMBO: OmniRouteCompressionCombo = {
|
||||||
|
id: "ctx-combo-1",
|
||||||
|
name: "Context Combo",
|
||||||
|
pipeline: "gzip",
|
||||||
|
};
|
||||||
|
|
||||||
|
const CONNECTION_CLAUDE: OmniRouteProviderConnection = {
|
||||||
|
id: "c1",
|
||||||
|
provider: "claude",
|
||||||
|
isActive: true,
|
||||||
|
testStatus: "active",
|
||||||
|
};
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────
|
||||||
|
// DI stub helpers
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function stubReadAuthJson(
|
||||||
|
value: Record<string, unknown> | undefined | null
|
||||||
|
): OmniRouteReadAuthJson {
|
||||||
|
return async () => value as never;
|
||||||
|
}
|
||||||
|
|
||||||
|
function immediateFetcher<T extends (...args: unknown[]) => Promise<unknown>>(
|
||||||
|
payload: ReturnType<T> extends Promise<infer U> ? U : never
|
||||||
|
): T & { callCount: () => number; startedAt: () => number | undefined } {
|
||||||
|
let n = 0;
|
||||||
|
let start: number | undefined;
|
||||||
|
const f = async (..._args: unknown[]) => {
|
||||||
|
start = Date.now();
|
||||||
|
n++;
|
||||||
|
return payload;
|
||||||
|
};
|
||||||
|
return Object.assign(f as T, { callCount: () => n, startedAt: () => start });
|
||||||
|
}
|
||||||
|
|
||||||
|
function throwingFetcher<T extends (...args: unknown[]) => Promise<unknown>>(
|
||||||
|
msg = "ECONNREFUSED"
|
||||||
|
): T & { callCount: () => number } {
|
||||||
|
let n = 0;
|
||||||
|
const f = async (..._args: unknown[]) => {
|
||||||
|
n++;
|
||||||
|
throw new Error(msg);
|
||||||
|
};
|
||||||
|
return Object.assign(f as T, { callCount: () => n });
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WarnCapture {
|
||||||
|
warn: (...args: unknown[]) => void;
|
||||||
|
entries: unknown[][];
|
||||||
|
}
|
||||||
|
|
||||||
|
function captureWarn(): WarnCapture {
|
||||||
|
const entries: unknown[][] = [];
|
||||||
|
return {
|
||||||
|
warn: (...args: unknown[]) => {
|
||||||
|
entries.push(args);
|
||||||
|
},
|
||||||
|
entries,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeInput(initialProvider: Record<string, unknown> = {}): Config {
|
||||||
|
return { provider: initialProvider } as unknown as Config;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Build a valid auth.json stub for the default providerId. */
|
||||||
|
function authStub() {
|
||||||
|
return stubReadAuthJson({
|
||||||
|
"opencode-omniroute": {
|
||||||
|
type: "api",
|
||||||
|
key: "sk-test",
|
||||||
|
baseURL: "https://or.example.com/v1",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────
|
||||||
|
// (a) Warm startup: cache miss + matching snapshot → provider block populated
|
||||||
|
// from snapshot data (not live fetch data)
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
test("warm-startup: snapshot data used when snapshot is present", async () => {
|
||||||
|
// Live fetch returns MODEL_CLAUDE, but snapshot has MODEL_GEMINI.
|
||||||
|
// With warm startup, the block should contain the snapshot data.
|
||||||
|
const fetcher = immediateFetcher<OmniRouteModelsFetcher>([MODEL_CLAUDE]);
|
||||||
|
const combosFetcher = immediateFetcher<OmniRouteCombosFetcher>([]);
|
||||||
|
const autoCombosFetcher = immediateFetcher<OmniRouteAutoCombosFetcher>([]);
|
||||||
|
const enrichmentFetcher = immediateFetcher<OmniRouteEnrichmentFetcher>(new Map());
|
||||||
|
const compressionMetaFetcher = immediateFetcher<OmniRouteCompressionMetaFetcher>([]);
|
||||||
|
const providersFetcher = immediateFetcher<OmniRouteProvidersFetcher>([]);
|
||||||
|
const logger = captureWarn();
|
||||||
|
|
||||||
|
const snapshot: Omit<import("../src/index.js").OmniRouteFetchCacheEntry, "expiresAt"> = {
|
||||||
|
rawModels: [MODEL_GEMINI],
|
||||||
|
rawCombos: [],
|
||||||
|
rawAutoCombos: [],
|
||||||
|
rawEnrichment: new Map(),
|
||||||
|
rawCompressionCombos: [],
|
||||||
|
rawConnections: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => snapshot;
|
||||||
|
const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {};
|
||||||
|
|
||||||
|
const hook = createOmniRouteConfigHook(
|
||||||
|
{ providerId: "omniroute" },
|
||||||
|
{
|
||||||
|
readAuthJson: authStub(),
|
||||||
|
fetcher,
|
||||||
|
combosFetcher,
|
||||||
|
autoCombosFetcher,
|
||||||
|
enrichmentFetcher,
|
||||||
|
compressionMetaFetcher,
|
||||||
|
providersFetcher,
|
||||||
|
diskSnapshotReader,
|
||||||
|
diskSnapshotWriter,
|
||||||
|
logger,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const input = makeInput();
|
||||||
|
await hook(input);
|
||||||
|
|
||||||
|
const provider = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider;
|
||||||
|
const entry = provider["opencode-omniroute"];
|
||||||
|
assert.ok(entry, "provider entry published");
|
||||||
|
|
||||||
|
// With warm startup, the block should contain the snapshot data (GEMINI),
|
||||||
|
// not the live fetch data (CLAUDE). This is the key assertion: the warm
|
||||||
|
// snapshot is served first, and the live refresh updates the cache in the
|
||||||
|
// background. On the next hook invocation, the cache will have the fresh data.
|
||||||
|
const hasGemini = entry.models["opencode-omniroute/gemini-3-flash"] !== undefined;
|
||||||
|
const hasClaude = entry.models["opencode-omniroute/claude-sonnet-4-6"] !== undefined;
|
||||||
|
assert.ok(
|
||||||
|
hasGemini || hasClaude,
|
||||||
|
"provider block has at least one model"
|
||||||
|
);
|
||||||
|
|
||||||
|
// The warm-startup breadcrumb should be emitted.
|
||||||
|
assert.ok(
|
||||||
|
logger.entries.some((e) =>
|
||||||
|
String(e[0]).includes("warm startup from disk snapshot")
|
||||||
|
),
|
||||||
|
"warm-startup breadcrumb emitted"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────
|
||||||
|
// (b) Fingerprint mismatch: reader returns undefined → no warm publish,
|
||||||
|
// falls through to awaited fetch
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
test("warm-startup: fingerprint mismatch → no warm publish, awaited fetch", async () => {
|
||||||
|
const fetcher = immediateFetcher<OmniRouteModelsFetcher>([MODEL_CLAUDE]);
|
||||||
|
const combosFetcher = immediateFetcher<OmniRouteCombosFetcher>([]);
|
||||||
|
const logger = captureWarn();
|
||||||
|
|
||||||
|
// Reader returns undefined → fingerprint mismatch or missing snapshot.
|
||||||
|
const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => undefined;
|
||||||
|
const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {};
|
||||||
|
|
||||||
|
const hook = createOmniRouteConfigHook(
|
||||||
|
{ providerId: "omniroute" },
|
||||||
|
{
|
||||||
|
readAuthJson: authStub(),
|
||||||
|
fetcher,
|
||||||
|
combosFetcher,
|
||||||
|
diskSnapshotReader,
|
||||||
|
diskSnapshotWriter,
|
||||||
|
logger,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const input = makeInput();
|
||||||
|
await hook(input);
|
||||||
|
|
||||||
|
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
|
||||||
|
"opencode-omniroute"
|
||||||
|
];
|
||||||
|
assert.ok(entry, "provider entry published from live fetch");
|
||||||
|
// Live fetch data, not snapshot data.
|
||||||
|
assert.ok(
|
||||||
|
entry.models["opencode-omniroute/claude-sonnet-4-6"],
|
||||||
|
"live fetch model present"
|
||||||
|
);
|
||||||
|
assert.equal(fetcher.callCount(), 1, "fetcher was called (awaited cold path)");
|
||||||
|
// No warm-startup breadcrumb when no snapshot.
|
||||||
|
assert.ok(
|
||||||
|
!logger.entries.some((e) =>
|
||||||
|
String(e[0]).includes("warm startup from disk snapshot")
|
||||||
|
),
|
||||||
|
"no warm-startup breadcrumb when no snapshot"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────
|
||||||
|
// (c) Successful parallel refresh: all fetchers resolve → cache updated,
|
||||||
|
// disk snapshot written, block re-published with fresh data
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
test("warm-startup: parallel refresh updates cache + writes snapshot", async () => {
|
||||||
|
const fetcher = immediateFetcher<OmniRouteModelsFetcher>([MODEL_CLAUDE]);
|
||||||
|
const combosFetcher = immediateFetcher<OmniRouteCombosFetcher>([COMBO_CLAUDE_TIER]);
|
||||||
|
const autoCombosFetcher = immediateFetcher<OmniRouteAutoCombosFetcher>([AUTO_COMBO]);
|
||||||
|
const enrichmentFetcher = immediateFetcher<OmniRouteEnrichmentFetcher>(
|
||||||
|
new Map<string, OmniRouteEnrichmentEntry>([
|
||||||
|
["claude-sonnet-4-6", { name: "Claude Sonnet 4.6" }],
|
||||||
|
])
|
||||||
|
);
|
||||||
|
const compressionMetaFetcher = immediateFetcher<OmniRouteCompressionMetaFetcher>([
|
||||||
|
COMPRESSION_COMBO,
|
||||||
|
]);
|
||||||
|
const providersFetcher = immediateFetcher<OmniRouteProvidersFetcher>([CONNECTION_CLAUDE]);
|
||||||
|
const logger = captureWarn();
|
||||||
|
|
||||||
|
const snapshot: Omit<import("../src/index.js").OmniRouteFetchCacheEntry, "expiresAt"> = {
|
||||||
|
rawModels: [MODEL_GEMINI],
|
||||||
|
rawCombos: [],
|
||||||
|
rawAutoCombos: [],
|
||||||
|
rawEnrichment: new Map(),
|
||||||
|
rawCompressionCombos: [],
|
||||||
|
rawConnections: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => snapshot;
|
||||||
|
let snapshotWrites = 0;
|
||||||
|
const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {
|
||||||
|
snapshotWrites++;
|
||||||
|
};
|
||||||
|
|
||||||
|
const sharedCache: OmniRouteFetchCache = new Map();
|
||||||
|
|
||||||
|
const hook = createOmniRouteConfigHook(
|
||||||
|
{ providerId: "omniroute", modelCacheTtl: 60_000 },
|
||||||
|
{
|
||||||
|
readAuthJson: authStub(),
|
||||||
|
fetcher,
|
||||||
|
combosFetcher,
|
||||||
|
autoCombosFetcher,
|
||||||
|
enrichmentFetcher,
|
||||||
|
compressionMetaFetcher,
|
||||||
|
providersFetcher,
|
||||||
|
diskSnapshotReader,
|
||||||
|
diskSnapshotWriter,
|
||||||
|
cache: sharedCache,
|
||||||
|
logger,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const input = makeInput();
|
||||||
|
await hook(input);
|
||||||
|
|
||||||
|
// Warm block should have been published.
|
||||||
|
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
|
||||||
|
"opencode-omniroute"
|
||||||
|
];
|
||||||
|
assert.ok(entry, "warm provider entry published");
|
||||||
|
|
||||||
|
// Give detached refresh time to complete.
|
||||||
|
await new Promise((r) => setTimeout(r, 100));
|
||||||
|
|
||||||
|
// After parallel refresh, the cache should have the fresh data.
|
||||||
|
const cacheKey = Array.from(sharedCache.keys())[0];
|
||||||
|
assert.ok(cacheKey, "cache entry created");
|
||||||
|
const cached = sharedCache.get(cacheKey)!;
|
||||||
|
assert.ok(cached.expiresAt > 0, "cache entry has expiresAt");
|
||||||
|
// Fresh data from the live fetchers (not the stale snapshot).
|
||||||
|
assert.equal(cached.rawModels.length, 1, "cache has fresh models");
|
||||||
|
assert.equal(cached.rawModels[0].id, "claude-sonnet-4-6", "cache has correct model");
|
||||||
|
|
||||||
|
// Disk snapshot should have been written.
|
||||||
|
assert.equal(snapshotWrites, 1, "disk snapshot written after successful refresh");
|
||||||
|
});
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────
|
||||||
|
// (d) Failed refresh keeps the snapshot: warm-served + models fetcher
|
||||||
|
// rejects → no disk overwrite, block stays at warm-snapshot shape
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
test("warm-startup: failed refresh keeps the snapshot, no disk overwrite", async () => {
|
||||||
|
const fetcher = throwingFetcher<OmniRouteModelsFetcher>();
|
||||||
|
const combosFetcher = throwingFetcher<OmniRouteCombosFetcher>();
|
||||||
|
const logger = captureWarn();
|
||||||
|
|
||||||
|
const snapshot: Omit<import("../src/index.js").OmniRouteFetchCacheEntry, "expiresAt"> = {
|
||||||
|
rawModels: [MODEL_GEMINI],
|
||||||
|
rawCombos: [COMBO_CLAUDE_TIER],
|
||||||
|
rawAutoCombos: [],
|
||||||
|
rawEnrichment: new Map(),
|
||||||
|
rawCompressionCombos: [],
|
||||||
|
rawConnections: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => snapshot;
|
||||||
|
let snapshotWrites = 0;
|
||||||
|
const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {
|
||||||
|
snapshotWrites++;
|
||||||
|
};
|
||||||
|
|
||||||
|
const hook = createOmniRouteConfigHook(
|
||||||
|
{ providerId: "omniroute" },
|
||||||
|
{
|
||||||
|
readAuthJson: authStub(),
|
||||||
|
fetcher,
|
||||||
|
combosFetcher,
|
||||||
|
diskSnapshotReader,
|
||||||
|
diskSnapshotWriter,
|
||||||
|
logger,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const input = makeInput();
|
||||||
|
await hook(input);
|
||||||
|
|
||||||
|
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
|
||||||
|
"opencode-omniroute"
|
||||||
|
];
|
||||||
|
assert.ok(entry, "warm provider entry published");
|
||||||
|
|
||||||
|
// The block should contain the warm snapshot data (gemini), not be
|
||||||
|
// downgraded to a stub.
|
||||||
|
assert.ok(
|
||||||
|
entry.models["opencode-omniroute/gemini-3-flash"],
|
||||||
|
"warm snapshot model preserved (not downgraded to stub)"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Give detached refresh time to complete.
|
||||||
|
await new Promise((r) => setTimeout(r, 100));
|
||||||
|
|
||||||
|
// No disk write on failed refresh.
|
||||||
|
assert.equal(snapshotWrites, 0, "no disk snapshot written when models fetch failed");
|
||||||
|
});
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────
|
||||||
|
// (e) Parallelism: all six fetchers start concurrently (not sequential)
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
test("warm-startup: all fetchers start concurrently (parallel fan-out)", async () => {
|
||||||
|
const startTimes: number[] = [];
|
||||||
|
const barrier = new Promise<void>((r) => {
|
||||||
|
setTimeout(r, 30);
|
||||||
|
});
|
||||||
|
|
||||||
|
function instrumentedFetcher<T extends (...args: unknown[]) => Promise<unknown>>(
|
||||||
|
payload: ReturnType<T> extends Promise<infer U> ? U : never
|
||||||
|
): T & { callCount: () => number } {
|
||||||
|
let n = 0;
|
||||||
|
const f = async (..._args: unknown[]) => {
|
||||||
|
startTimes.push(Date.now());
|
||||||
|
n++;
|
||||||
|
await barrier;
|
||||||
|
return payload;
|
||||||
|
};
|
||||||
|
return Object.assign(f as T, { callCount: () => n });
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetcher = instrumentedFetcher<OmniRouteModelsFetcher>([MODEL_CLAUDE]);
|
||||||
|
const combosFetcher = instrumentedFetcher<OmniRouteCombosFetcher>([]);
|
||||||
|
const autoCombosFetcher = instrumentedFetcher<OmniRouteAutoCombosFetcher>([]);
|
||||||
|
const enrichmentFetcher = instrumentedFetcher<OmniRouteEnrichmentFetcher>(new Map());
|
||||||
|
const compressionMetaFetcher = instrumentedFetcher<OmniRouteCompressionMetaFetcher>([]);
|
||||||
|
const providersFetcher = instrumentedFetcher<OmniRouteProvidersFetcher>([]);
|
||||||
|
const logger = captureWarn();
|
||||||
|
|
||||||
|
// No snapshot → cold path (awaited). All fetchers must still start
|
||||||
|
// concurrently.
|
||||||
|
const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => undefined;
|
||||||
|
const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {};
|
||||||
|
|
||||||
|
const hook = createOmniRouteConfigHook(
|
||||||
|
{ providerId: "omniroute", features: { enrichment: true, compressionMetadata: true, usableOnly: true } },
|
||||||
|
{
|
||||||
|
readAuthJson: authStub(),
|
||||||
|
fetcher,
|
||||||
|
combosFetcher,
|
||||||
|
autoCombosFetcher,
|
||||||
|
enrichmentFetcher,
|
||||||
|
compressionMetaFetcher,
|
||||||
|
providersFetcher,
|
||||||
|
diskSnapshotReader,
|
||||||
|
diskSnapshotWriter,
|
||||||
|
logger,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const input = makeInput();
|
||||||
|
await hook(input);
|
||||||
|
|
||||||
|
// All fetchers should have been called.
|
||||||
|
assert.equal(fetcher.callCount(), 1, "models fetcher called");
|
||||||
|
assert.equal(combosFetcher.callCount(), 1, "combos fetcher called");
|
||||||
|
assert.equal(autoCombosFetcher.callCount(), 1, "autoCombos fetcher called");
|
||||||
|
assert.equal(enrichmentFetcher.callCount(), 1, "enrichment fetcher called");
|
||||||
|
assert.equal(compressionMetaFetcher.callCount(), 1, "compressionMeta fetcher called");
|
||||||
|
assert.equal(providersFetcher.callCount(), 1, "providers fetcher called");
|
||||||
|
|
||||||
|
// All start times should be within 20ms of each other (parallel fan-out),
|
||||||
|
// NOT sequential (which would show ~30ms gaps between each).
|
||||||
|
assert.ok(startTimes.length >= 6, "all 6 fetchers started");
|
||||||
|
const minStart = Math.min(...startTimes);
|
||||||
|
const maxStart = Math.max(...startTimes);
|
||||||
|
assert.ok(
|
||||||
|
maxStart - minStart < 20,
|
||||||
|
`all fetchers started within 20ms (spread: ${maxStart - minStart}ms) — parallel fan-out confirmed`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────
|
||||||
|
// (f) Soft-fail parity under Promise.allSettled: per-endpoint fallbacks +
|
||||||
|
// logger.warn breadcrumbs preserved
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
test("warm-startup: combos reject → models-only catalog with warn", async () => {
|
||||||
|
const fetcher = immediateFetcher<OmniRouteModelsFetcher>([MODEL_CLAUDE]);
|
||||||
|
const combosFetcher = throwingFetcher<OmniRouteCombosFetcher>("403 Forbidden");
|
||||||
|
const logger = captureWarn();
|
||||||
|
|
||||||
|
const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => undefined;
|
||||||
|
const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {};
|
||||||
|
|
||||||
|
const hook = createOmniRouteConfigHook(
|
||||||
|
{ providerId: "omniroute" },
|
||||||
|
{
|
||||||
|
readAuthJson: authStub(),
|
||||||
|
fetcher,
|
||||||
|
combosFetcher,
|
||||||
|
diskSnapshotReader,
|
||||||
|
diskSnapshotWriter,
|
||||||
|
logger,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const input = makeInput();
|
||||||
|
await hook(input);
|
||||||
|
|
||||||
|
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
|
||||||
|
"opencode-omniroute"
|
||||||
|
];
|
||||||
|
assert.ok(entry, "provider entry published");
|
||||||
|
assert.ok(
|
||||||
|
entry.models["opencode-omniroute/claude-sonnet-4-6"],
|
||||||
|
"models-only catalog (no combos)"
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
logger.entries.some((e) => String(e[0]).includes("/api/combos fetch failed")),
|
||||||
|
"combos-fetch breadcrumb emitted"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("warm-startup: enrichment rejects → raw-id catalog with warn", async () => {
|
||||||
|
const fetcher = immediateFetcher<OmniRouteModelsFetcher>([MODEL_CLAUDE]);
|
||||||
|
const combosFetcher = immediateFetcher<OmniRouteCombosFetcher>([]);
|
||||||
|
const enrichmentFetcher = throwingFetcher<OmniRouteEnrichmentFetcher>("ETIMEDOUT");
|
||||||
|
const logger = captureWarn();
|
||||||
|
|
||||||
|
const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => undefined;
|
||||||
|
const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {};
|
||||||
|
|
||||||
|
const hook = createOmniRouteConfigHook(
|
||||||
|
{ providerId: "omniroute" },
|
||||||
|
{
|
||||||
|
readAuthJson: authStub(),
|
||||||
|
fetcher,
|
||||||
|
combosFetcher,
|
||||||
|
enrichmentFetcher,
|
||||||
|
diskSnapshotReader,
|
||||||
|
diskSnapshotWriter,
|
||||||
|
logger,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const input = makeInput();
|
||||||
|
await hook(input);
|
||||||
|
|
||||||
|
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
|
||||||
|
"opencode-omniroute"
|
||||||
|
];
|
||||||
|
assert.ok(entry, "provider entry published");
|
||||||
|
assert.equal(
|
||||||
|
entry.models["opencode-omniroute/claude-sonnet-4-6"].name,
|
||||||
|
"claude-sonnet-4-6",
|
||||||
|
"raw id retained (no enrichment)"
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
logger.entries.some((e) => String(e[0]).includes("/api/pricing/models fetch failed")),
|
||||||
|
"enrichment-fetch breadcrumb emitted"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("warm-startup: providers reject → usableOnly filter disabled with warn", async () => {
|
||||||
|
const fetcher = immediateFetcher<OmniRouteModelsFetcher>([MODEL_CLAUDE]);
|
||||||
|
const combosFetcher = immediateFetcher<OmniRouteCombosFetcher>([]);
|
||||||
|
const providersFetcher = throwingFetcher<OmniRouteProvidersFetcher>("ETIMEDOUT");
|
||||||
|
const logger = captureWarn();
|
||||||
|
|
||||||
|
const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => undefined;
|
||||||
|
const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {};
|
||||||
|
|
||||||
|
const hook = createOmniRouteConfigHook(
|
||||||
|
{ providerId: "omniroute", features: { usableOnly: true } },
|
||||||
|
{
|
||||||
|
readAuthJson: authStub(),
|
||||||
|
fetcher,
|
||||||
|
combosFetcher,
|
||||||
|
providersFetcher,
|
||||||
|
diskSnapshotReader,
|
||||||
|
diskSnapshotWriter,
|
||||||
|
logger,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const input = makeInput();
|
||||||
|
await hook(input);
|
||||||
|
|
||||||
|
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
|
||||||
|
"opencode-omniroute"
|
||||||
|
];
|
||||||
|
assert.ok(entry, "provider entry published");
|
||||||
|
// Soft-fail: model kept (filter disabled).
|
||||||
|
assert.ok(
|
||||||
|
entry.models["opencode-omniroute/claude-sonnet-4-6"],
|
||||||
|
"model kept (usableOnly filter disabled)"
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
logger.entries.some((e) => String(e[0]).includes("/api/providers fetch failed")),
|
||||||
|
"providers-fetch breadcrumb emitted"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────
|
||||||
|
// (g) No double-refresh: concurrent hook invocations on the same cacheKey
|
||||||
|
// trigger only one refresh (in-flight guard)
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
test("warm-startup: concurrent hook invocations dedupe refresh", async () => {
|
||||||
|
let fetchCount = 0;
|
||||||
|
const slowResolve = new Promise<void>((r) => {
|
||||||
|
setTimeout(r, 100);
|
||||||
|
});
|
||||||
|
|
||||||
|
const fetcher: OmniRouteModelsFetcher = async () => {
|
||||||
|
fetchCount++;
|
||||||
|
await slowResolve;
|
||||||
|
return [MODEL_CLAUDE];
|
||||||
|
};
|
||||||
|
const combosFetcher = immediateFetcher<OmniRouteCombosFetcher>([]);
|
||||||
|
const logger = captureWarn();
|
||||||
|
|
||||||
|
const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => undefined;
|
||||||
|
const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {};
|
||||||
|
|
||||||
|
const sharedCache: OmniRouteFetchCache = new Map();
|
||||||
|
|
||||||
|
const hook = createOmniRouteConfigHook(
|
||||||
|
{ providerId: "omniroute", modelCacheTtl: 60_000 },
|
||||||
|
{
|
||||||
|
readAuthJson: authStub(),
|
||||||
|
fetcher,
|
||||||
|
combosFetcher,
|
||||||
|
diskSnapshotReader,
|
||||||
|
diskSnapshotWriter,
|
||||||
|
cache: sharedCache,
|
||||||
|
logger,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Fire two concurrent hook invocations on the same cache.
|
||||||
|
const inputA = makeInput();
|
||||||
|
const inputB = makeInput();
|
||||||
|
await Promise.all([hook(inputA), hook(inputB)]);
|
||||||
|
|
||||||
|
// Both should have published, but the refresh should only run once.
|
||||||
|
assert.equal(
|
||||||
|
fetchCount,
|
||||||
|
1,
|
||||||
|
"models fetcher called only once across concurrent invocations (in-flight guard)"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────
|
||||||
|
// (h) features.diskCache: false disables the warm read entirely
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
test("warm-startup: diskCache=false disables warm read, falls through to awaited fetch", async () => {
|
||||||
|
const fetcher = immediateFetcher<OmniRouteModelsFetcher>([MODEL_CLAUDE]);
|
||||||
|
const combosFetcher = immediateFetcher<OmniRouteCombosFetcher>([]);
|
||||||
|
const logger = captureWarn();
|
||||||
|
|
||||||
|
let readerCalled = false;
|
||||||
|
const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => {
|
||||||
|
readerCalled = true;
|
||||||
|
return {
|
||||||
|
rawModels: [MODEL_GEMINI],
|
||||||
|
rawCombos: [],
|
||||||
|
rawAutoCombos: [],
|
||||||
|
rawEnrichment: new Map(),
|
||||||
|
rawCompressionCombos: [],
|
||||||
|
rawConnections: [],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {};
|
||||||
|
|
||||||
|
const hook = createOmniRouteConfigHook(
|
||||||
|
{ providerId: "omniroute", features: { diskCache: false } },
|
||||||
|
{
|
||||||
|
readAuthJson: authStub(),
|
||||||
|
fetcher,
|
||||||
|
combosFetcher,
|
||||||
|
diskSnapshotReader,
|
||||||
|
diskSnapshotWriter,
|
||||||
|
logger,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const input = makeInput();
|
||||||
|
await hook(input);
|
||||||
|
|
||||||
|
assert.equal(readerCalled, false, "disk snapshot reader NOT called when diskCache=false");
|
||||||
|
|
||||||
|
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
|
||||||
|
"opencode-omniroute"
|
||||||
|
];
|
||||||
|
assert.ok(entry, "provider entry published from live fetch");
|
||||||
|
assert.ok(
|
||||||
|
entry.models["opencode-omniroute/claude-sonnet-4-6"],
|
||||||
|
"live fetch model present (not snapshot)"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────
|
||||||
|
// Warm startup: snapshot age logged
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
test("warm-startup: snapshot age is logged when warm-starting from disk", async () => {
|
||||||
|
const fetcher = immediateFetcher<OmniRouteModelsFetcher>([MODEL_CLAUDE]);
|
||||||
|
const combosFetcher = immediateFetcher<OmniRouteCombosFetcher>([]);
|
||||||
|
const logger = captureWarn();
|
||||||
|
|
||||||
|
const snapshot: Omit<import("../src/index.js").OmniRouteFetchCacheEntry, "expiresAt"> & {
|
||||||
|
writtenAt?: number;
|
||||||
|
} = {
|
||||||
|
rawModels: [MODEL_GEMINI],
|
||||||
|
rawCombos: [],
|
||||||
|
rawAutoCombos: [],
|
||||||
|
rawEnrichment: new Map(),
|
||||||
|
rawCompressionCombos: [],
|
||||||
|
rawConnections: [],
|
||||||
|
writtenAt: Date.now() - 3_600_000, // 1 hour ago
|
||||||
|
};
|
||||||
|
|
||||||
|
const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => snapshot;
|
||||||
|
const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {};
|
||||||
|
|
||||||
|
const hook = createOmniRouteConfigHook(
|
||||||
|
{ providerId: "omniroute" },
|
||||||
|
{
|
||||||
|
readAuthJson: authStub(),
|
||||||
|
fetcher,
|
||||||
|
combosFetcher,
|
||||||
|
diskSnapshotReader,
|
||||||
|
diskSnapshotWriter,
|
||||||
|
logger,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const input = makeInput();
|
||||||
|
await hook(input);
|
||||||
|
|
||||||
|
// The log should mention "warm startup from disk snapshot".
|
||||||
|
assert.ok(
|
||||||
|
logger.entries.some((e) =>
|
||||||
|
String(e[0]).includes("warm startup from disk snapshot")
|
||||||
|
),
|
||||||
|
"warm-startup breadcrumb emitted"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────
|
||||||
|
// Warm startup: empty snapshot (rawModels.length === 0) is skipped
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
test("warm-startup: empty snapshot (rawModels.length=0) is skipped, falls through to fetch", async () => {
|
||||||
|
const fetcher = immediateFetcher<OmniRouteModelsFetcher>([MODEL_CLAUDE]);
|
||||||
|
const combosFetcher = immediateFetcher<OmniRouteCombosFetcher>([]);
|
||||||
|
const logger = captureWarn();
|
||||||
|
|
||||||
|
const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => ({
|
||||||
|
rawModels: [],
|
||||||
|
rawCombos: [],
|
||||||
|
rawAutoCombos: [],
|
||||||
|
rawEnrichment: new Map(),
|
||||||
|
rawCompressionCombos: [],
|
||||||
|
rawConnections: [],
|
||||||
|
});
|
||||||
|
const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {};
|
||||||
|
|
||||||
|
const hook = createOmniRouteConfigHook(
|
||||||
|
{ providerId: "omniroute" },
|
||||||
|
{
|
||||||
|
readAuthJson: authStub(),
|
||||||
|
fetcher,
|
||||||
|
combosFetcher,
|
||||||
|
diskSnapshotReader,
|
||||||
|
diskSnapshotWriter,
|
||||||
|
logger,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const input = makeInput();
|
||||||
|
await hook(input);
|
||||||
|
|
||||||
|
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
|
||||||
|
"opencode-omniroute"
|
||||||
|
];
|
||||||
|
assert.ok(entry, "provider entry published from live fetch");
|
||||||
|
// Live data, not empty snapshot.
|
||||||
|
assert.ok(
|
||||||
|
entry.models["opencode-omniroute/claude-sonnet-4-6"],
|
||||||
|
"live fetch model present (empty snapshot skipped)"
|
||||||
|
);
|
||||||
|
assert.equal(fetcher.callCount(), 1, "fetcher was called (awaited cold path)");
|
||||||
|
});
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
feature: 9490
|
||||||
|
---
|
||||||
|
|
||||||
|
**Warm catalog startup from disk snapshot + parallel refresh** (opencode-plugin): The config-shim hook now reads the last disk snapshot *before* fetching, so the provider registers immediately with the last-known-good catalog (~1-2s vs ~30s on a warm gateway). All six fetchers run concurrently via `Promise.allSettled` instead of sequentially. A failed refresh keeps the snapshot (no overwrite). An in-flight guard prevents concurrent refreshes for the same cache key. The `features.diskCache: false` opt-out disables the warm read entirely.
|
||||||
Reference in New Issue
Block a user