Compare commits

..

3 Commits

Author SHA1 Message Date
diegosouzapw
57e1d88ae6 Merge remote-tracking branch 'origin/release/v3.8.50' into feat/9490-opencode-plugin-warm-startup-parallel-refresh
# Conflicts:
#	@omniroute/opencode-plugin/README.md
#	@omniroute/opencode-plugin/package.json
2026-08-08 11:44:24 -03:00
Diego Rodrigues de Sa e Souza
61f962294d Merge branch 'release/v3.8.50' into feat/9490-opencode-plugin-warm-startup-parallel-refresh 2026-08-05 23:03:05 -03:00
diegosouzapw
a2c15c5a8c feat(providers): warm catalog startup from disk snapshot, parallel refresh (opencode-plugin) (#9490) 2026-08-05 18:00:05 -03:00
55 changed files with 1254 additions and 1977 deletions

View File

@@ -1033,7 +1033,11 @@ export const OmniRoutePlugin: Plugin = async (_input, options) => {
// Config hook: keep existing catalog shim, and register slash command
// 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).
const baseConfigHook = createOmniRouteConfigHook(resolved, { cache: sharedCache });
const baseConfigHook = createOmniRouteConfigHook(resolved, {
cache: sharedCache,
diskSnapshotReader: defaultDiskSnapshotReader,
diskSnapshotWriter: defaultDiskSnapshotWriter,
});
const configWithSyncCommand = async (input: Config) => {
await baseConfigHook(input);
const cfg = input as Config & {
@@ -4741,7 +4745,7 @@ export type OmniRouteDiskSnapshotWriter = (
export type OmniRouteDiskSnapshotReader = (
providerId: 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
@@ -4824,15 +4828,36 @@ export const defaultDiskSnapshotReader: OmniRouteDiskSnapshotReader = async (
? parsed.rawCompressionCombos
: [],
rawConnections: Array.isArray(parsed.rawConnections) ? parsed.rawConnections : [],
writtenAt: typeof parsed.writtenAt === "number" ? parsed.writtenAt : undefined,
};
} catch {
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 () => {};
/**
* 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)
// ────────────────────────────────────────────────────────────────────────────
@@ -5067,7 +5092,6 @@ export function createDebugLoggingFetch(
}
};
}
export const noopDiskSnapshotReader: OmniRouteDiskSnapshotReader = async () => undefined;
export type OmniRouteReadAuthJson = () => Promise<AuthJsonShape | undefined | null>;
@@ -5170,8 +5194,8 @@ export function createOmniRouteConfigHook(
const compressionMetaFetcher =
deps.compressionMetaFetcher ?? defaultOmniRouteCompressionMetaFetcher;
const providersFetcher = deps.providersFetcher ?? defaultOmniRouteProvidersFetcher;
const diskSnapshotReader = deps.diskSnapshotReader ?? defaultDiskSnapshotReader;
const diskSnapshotWriter = deps.diskSnapshotWriter ?? defaultDiskSnapshotWriter;
const diskSnapshotReader = deps.diskSnapshotReader ?? noopDiskSnapshotReader;
const diskSnapshotWriter = deps.diskSnapshotWriter ?? noopDiskSnapshotWriter;
const now = deps.now ?? Date.now;
const cache: OmniRouteFetchCache = deps.cache ?? new Map();
const logger = deps.logger ?? console;
@@ -5266,12 +5290,12 @@ export function createOmniRouteConfigHook(
const t = now();
const cached = cache.get(cacheKey);
let rawModels: OmniRouteRawModelEntry[];
let rawCombos: OmniRouteRawCombo[];
let rawAutoCombos: OmniRouteRawAutoCombo[];
let rawEnrichment: OmniRouteEnrichmentMap;
let rawCompressionCombos: OmniRouteCompressionCombo[];
let rawConnections: OmniRouteProviderConnection[];
let rawModels: OmniRouteRawModelEntry[] = [];
let rawCombos: OmniRouteRawCombo[] = [];
let rawAutoCombos: OmniRouteRawAutoCombo[] = [];
let rawEnrichment: OmniRouteEnrichmentMap = new Map();
let rawCompressionCombos: OmniRouteCompressionCombo[] = [];
let rawConnections: OmniRouteProviderConnection[] = [];
if (cached && cached.expiresAt > t) {
rawModels = cached.rawModels;
@@ -5281,160 +5305,275 @@ export function createOmniRouteConfigHook(
rawCompressionCombos = cached.rawCompressionCombos;
rawConnections = cached.rawConnections;
} 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
// entry); on /api/combos throw, publish models-only. Disk-cache
// fallback below recovers the last-known-good catalog when the
// fetcher threw (network down / 403 / timeout) AND features.diskCache
// !== false. A 0-entry SUCCESS (fresh tenant) does NOT trigger
// disk fallback — that's a valid empty catalog.
let modelsFetchThrew = false;
try {
rawModels = await fetcher(baseURL, apiKey, 10_000);
} catch (err) {
logger.warn(
"[omniroute-plugin] config shim: /v1/models fetch failed; publishing stub provider entry",
err
);
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) {
// ─────────────────────────────────────────────────────────────────────
// Warm startup: read the disk snapshot before fetching so the provider
// registers immediately with the last-known-good catalog. The live
// fetch then refreshes in the background (detached) and updates the
// cache + snapshot. Gated by features.diskCache (default-on).
// ─────────────────────────────────────────────────────────────────────
let warmSnapshot: Omit<OmniRouteFetchCacheEntry, "expiresAt"> | undefined;
if (wantDiskCache) {
const snapshotResult = await diskSnapshotReader(resolved.providerId, snapshotFingerprint);
if (snapshotResult && snapshotResult.rawModels.length > 0) {
warmSnapshot = snapshotResult;
// Log snapshot age (accept any age — instant beats empty).
const age = (snapshotResult as { writtenAt?: number }).writtenAt;
const ageLabel = typeof age === "number" ? `${Math.round((Date.now() - age) / 3_600_000)}h` : "unknown";
logger.warn(
"[omniroute-plugin] config shim: /api/pricing/models fetch failed; publishing raw-id static catalog",
err
`[omniroute-plugin] config shim: warm startup from disk snapshot (${snapshotResult.rawModels.length} models, age ${ageLabel})`
);
}
}
// Compression-metadata fetch — opt-in via features.compressionMetadata.
// When on, the default pipeline is appended to every combo `name` so
// the TUI picker advertises which compression a combo applies.
rawCompressionCombos = [];
if (wantCompressionMeta) {
try {
rawCompressionCombos = 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
);
// ─────────────────────────────────────────────────────────────────────
// Parallel refresh: all six fetchers run concurrently via
// Promise.allSettled. Each wrapper never rejects (catches internally)
// so partial failure is tolerated — same soft-fail semantics as the
// old sequential chain, but ~6x faster.
// ─────────────────────────────────────────────────────────────────────
const doRefresh = async (): Promise<void> => {
let modelsFetchThrew = false;
let localRawModels: OmniRouteRawModelEntry[] = [];
let localRawCombos: OmniRouteRawCombo[] = [];
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
// on, the static catalog filters out models/combos whose canonical
// provider has no active connection. Soft-fail (empty list) disables
// the filter for this refresh, never hiding the whole catalog.
rawConnections = [];
if (wantUsableOnly) {
try {
rawConnections = 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
);
}
}
// 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,
// 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: localRawModels,
rawCombos: localRawCombos,
rawAutoCombos: localRawAutoCombos,
rawEnrichment: localRawEnrichment,
rawCompressionCombos: localRawCompressionCombos,
rawConnections: localRawConnections,
expiresAt: now() + resolved.modelCacheTtl,
});
}
// 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).
if (modelsFetchOk && wantDiskCache) {
await diskSnapshotWriter(
resolved.providerId,
{
rawModels,
rawCombos,
rawAutoCombos,
rawEnrichment,
rawCompressionCombos,
rawConnections,
},
snapshotFingerprint
);
// Startup diagnostics (file-based) — fires at startup via config hook
if (resolved.features?.startupDebug === true) {
await writeStartupDiagnostics({
providerId: resolved.providerId,
baseURL,
modelCount: localRawModels.length,
comboCount: localRawCombos.length,
enrichmentSize: localRawEnrichment.size,
autoComboCount: localRawAutoCombos.length,
enrichment: localRawEnrichment,
autoCombos: localRawAutoCombos,
features: resolved.features,
});
}
// 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;
}
}
}
}

View File

@@ -33,6 +33,7 @@ import {
createOmniRouteProviderHook,
OmniRoutePlugin,
resolveOmniRoutePluginOptions,
_resetInflightRefresh,
type OmniRouteCombosFetcher,
type OmniRouteEnrichmentEntry,
type OmniRouteEnrichmentFetcher,
@@ -47,6 +48,16 @@ import {
type OmniRouteStaticProviderEntry,
} 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
// ────────────────────────────────────────────────────────────────────────────
@@ -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.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"
);
});

View 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)");
});

View File

@@ -52,11 +52,8 @@ export class ServerSupervisor {
// silently, so a boot that never becomes ready looked like a dead hang with zero
// output even at APP_LOG_LEVEL=debug. Pipe stdout too and buffer it alongside
// stderr so a readiness timeout can surface what the child actually printed.
// #9156: macOS launchd cannot resolve bare "node" because its PATH is
// minimal. Always use process.execPath (the absolute path to the running
// Node.js binary) so the supervisor never depends on PATH resolution.
this.child = spawn(
process.execPath,
process.versions.bun ? process.execPath : "node",
process.versions.bun
? [this.serverPath]
: buildNodeRuntimeArgs(process.env, this.memoryLimit, this.serverPath),

View File

@@ -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.

View File

@@ -1 +0,0 @@
- test(combo): guard auto/best-free never leaks the combo name as a model (#7754)

View File

@@ -1 +0,0 @@
- fix(build): include better-sqlite3 prebuilds in standalone bun bundle

View File

@@ -1 +0,0 @@
- **fix(quota):** Deleting a quota pool now removes its scoped managed combos without racing in-flight pool mutations ([#8906](https://github.com/diegosouzapw/OmniRoute/pull/8906)) — thanks @xiaoyaner0201

View File

@@ -1 +0,0 @@
- fix(background): detect Anthropic top-level system prompts for background task detection (#9142)

View File

@@ -1 +0,0 @@
- fix(cli): use process.execPath for macOS launchd autostart

View File

@@ -1 +0,0 @@
- fix(model-discovery): ingest capabilities.effort_tiers for synced models (#9160)

View File

@@ -1 +0,0 @@
- fix(translator): buffer and normalize upstream tool-call argument deltas so optional null values are stripped before reaching the client (#9168)

View File

@@ -1 +0,0 @@
- fix(translator): avoid double-normalizing tool names in Gemini-to-Claude response path (#9177)

View File

@@ -1 +0,0 @@
- fix(providers): classify 400 out of extra usage as quota_exhausted for Anthropic OAuth

View File

@@ -1 +0,0 @@
- fix(resilience): failed connection test now sets a short cooldown so connections recover after transient outages (#9623)

View File

@@ -1 +0,0 @@
- fix(db): wire telemetry cleanup scheduler in Next.js startup path (#9624)

View File

@@ -1 +0,0 @@
- fix(db): align domain_cost_history cleanup cutoff with millisecond column (#9625)

View File

@@ -1 +0,0 @@
- fix(playground): surface provider model loading errors and offer retry (#9626)

View File

@@ -1 +0,0 @@
- fix(build): add build-next-isolated.mjs sibling imports to package.json files array

View File

@@ -149,18 +149,6 @@ export const ERROR_RULES: ErrorRule[] = [
backoff: true,
reason: "quota_exhausted",
},
{
id: "out_of_extra_usage",
text: "out of extra usage",
backoff: true,
reason: "quota_exhausted",
},
{
id: "extra_usage_required",
text: "extra usage required",
backoff: true,
reason: "quota_exhausted",
},
{ id: "capacity", text: "capacity", backoff: true, reason: "model_capacity" },
{ id: "overloaded", text: "overloaded", backoff: true, reason: "model_capacity" },
{ id: "high_demand", text: "high demand", backoff: true, reason: "model_capacity" },

View File

@@ -8,32 +8,9 @@ export const gemini_webProvider: RegistryEntry = {
baseUrl: "https://gemini.google.com/app",
authType: "apikey",
authHeader: "cookie",
// #9356: `supportsReasoning: false` is a live-behavior statement, not a guess
// about the underlying Gemini model. The executor drives the gemini.google.com
// web UI by typing a prompt, so it has no thinking-budget control to set and
// never surfaces `reasoning_content` — agent routers reading /v1/models must
// not select these for reasoning work. `toolCalling: false` is the matching
// statement for native function calling; the prompt-emulation shim (#7286)
// stays available and is advertised separately as `toolCalling: "emulated"`
// on the provider constant (src/shared/constants/providers/web-cookie.ts).
models: [
{
id: "gemini-3.1-pro",
name: "Gemini 3.1 Pro",
toolCalling: false,
supportsReasoning: false,
},
{
id: "gemini-3.5-flash",
name: "Gemini 3.5 Flash",
toolCalling: false,
supportsReasoning: false,
},
{
id: "gemini-3.1-flash-lite",
name: "Gemini 3.1 Flash-Lite",
toolCalling: false,
supportsReasoning: false,
},
{ id: "gemini-3.1-pro", name: "Gemini 3.1 Pro", toolCalling: false },
{ id: "gemini-3.5-flash", name: "Gemini 3.5 Flash", toolCalling: false },
{ id: "gemini-3.1-flash-lite", name: "Gemini 3.1 Flash-Lite", toolCalling: false },
],
};

View File

@@ -14,13 +14,9 @@
*/
import { BaseExecutor, type ExecuteInput } from "./base.ts";
import { buildErrorBody, sanitizeErrorMessage } from "../utils/error.ts";
import { sanitizeErrorMessage } from "../utils/error.ts";
import { prepareToolMessages } from "../translator/webTools.ts";
import { buildToolModeResponse } from "./chatgptWebTools.ts";
import {
checkGeminiWebUnsupportedControls,
GEMINI_WEB_UNSUPPORTED_CONTROL_CODE,
} from "./gemini-web/capabilities.ts";
// ─── Constants ──────────────────────────────────────────────────────────────
@@ -410,33 +406,6 @@ export class GeminiWebExecutor extends BaseExecutor {
const { model, body, stream, credentials, signal, log, onCredentialsRefreshed } = input;
const requestBody = body as GeminiRequestBody;
// #9356: fail fast on controls this provider cannot honor (reasoning_effort
// above "minimal", forced tool_choice). Runs before the credential check and
// before Playwright launches — the request is unservable no matter which
// cookie is used, and answering 200 with ordinary prose made agents believe
// their reasoning/tool requirements had been met. See ./gemini-web/capabilities.ts.
const violation = checkGeminiWebUnsupportedControls(body as Record<string, unknown>);
if (violation) {
log?.warn?.(
"GEMINI-WEB",
`Rejected request: "${violation.param}" is not supported by this provider`
);
return {
response: new Response(
JSON.stringify(
buildErrorBody(400, violation.message, null, {
type: "invalid_request_error",
code: GEMINI_WEB_UNSUPPORTED_CONTROL_CODE,
})
),
{ status: 400, headers: { "Content-Type": "application/json" } }
),
url: GEMINI_URL,
headers: {},
transformedBody: body,
};
}
const cookie = resolveGeminiWebCookie(credentials);
if (!cookie) {
return {

View File

@@ -1,121 +0,0 @@
/**
* Request-contract guards for the Gemini Web executor (#9356).
*
* gemini-web is not an API client. It launches Playwright, types ONE flat
* prompt string into the gemini.google.com `.ql-editor` contenteditable,
* presses Enter, and captures the first `StreamGenerate` response off the page
* (see ../gemini-web.ts). There is no JSON request body on the wire, which
* makes two OpenAI controls structurally impossible to honor:
*
* • `reasoning_effort` — no field exists to carry a thinking budget. Unlike
* deepseek-web or perplexity-web, which post a real payload and can flip a
* `thinking_enabled` flag or swap the model preference, there is nothing
* here to set.
* • forced `tool_choice` — the tools support gemini-web does have is the
* prompt-emulation shim (`translator/webTools.ts`, #7286): it ASKS the
* model, in prose, to answer with `<tool>{...}</tool>` and parses whatever
* comes back. That is best-effort by construction. "required" / "any" /
* a named function is a GUARANTEE, and a prompt cannot make one.
*
* Before this module both were accepted and quietly ignored, so an agent got a
* 200 with `finish_reason: "stop"`, no `reasoning_content`, and `tool_calls: []`
* and concluded its requirements had been met (#9356). Failing the request is
* the honest answer: the caller can drop the control, or route to a model that
* actually implements it.
*
* Deliberately NOT rejected — these are already satisfied or already work:
* • `reasoning_effort: "none" | "minimal"` — asking for as little reasoning as
* possible is something a non-thinking provider trivially complies with.
* • `tool_choice: "auto" | "none"` and plain `tools[]` — the #7286 emulation
* path, which several shipped combos depend on (#5240, #8488). Untouched.
*
* Pure and dependency-free so the whole contract is unit-testable without a
* browser.
*/
/** `error.code` on every compatibility rejection raised here. */
export const GEMINI_WEB_UNSUPPORTED_CONTROL_CODE = "unsupported_control_for_provider";
/** Effort levels a non-thinking provider already complies with. */
const SATISFIED_EFFORT_LEVELS = new Set(["none", "minimal"]);
/** `tool_choice` strings that demand a tool call rather than merely offering one. */
const FORCING_TOOL_CHOICE_STRINGS = new Set(["required", "any"]);
/** `tool_choice: { type }` values that pin the model to a specific/any tool. */
const FORCING_TOOL_CHOICE_TYPES = new Set(["function", "tool", "any"]);
export interface GeminiWebCapabilityViolation {
/** Which request field could not be honored. */
param: "reasoning_effort" | "tool_choice";
/** Client-facing explanation — already safe to put in a response body. */
message: string;
}
function normalizeString(value: unknown): string | null {
return typeof value === "string" && value.trim().length > 0 ? value.trim().toLowerCase() : null;
}
/**
* True when `tool_choice` demands a tool call. Covers the OpenAI strings
* ("required"), the Anthropic-flavored ones the translators also emit ("any"),
* and the object forms that name a function or force any tool. "auto" / "none"
* and every unrecognized shape are treated as non-forcing — this guard only
* blocks contracts it is certain gemini-web cannot keep.
*/
export function isForcingToolChoice(toolChoice: unknown): boolean {
const asString = normalizeString(toolChoice);
if (asString) return FORCING_TOOL_CHOICE_STRINGS.has(asString);
if (toolChoice && typeof toolChoice === "object" && !Array.isArray(toolChoice)) {
const type = normalizeString((toolChoice as Record<string, unknown>).type);
return type !== null && FORCING_TOOL_CHOICE_TYPES.has(type);
}
return false;
}
/** True when `reasoning_effort` asks for MORE thinking than "none at all". */
export function requestsThinkingBudget(reasoningEffort: unknown): boolean {
const effort = normalizeString(reasoningEffort);
if (effort === null) return false;
return !SATISFIED_EFFORT_LEVELS.has(effort);
}
/**
* Inspect an OpenAI-shaped request body for controls gemini-web cannot honor.
* Returns the first violation found, or `null` when the request is servable.
*
* `reasoning_effort` is checked before `tool_choice` only for determinism; a
* request carrying both is rejected either way.
*/
export function checkGeminiWebUnsupportedControls(
body: Record<string, unknown> | null | undefined
): GeminiWebCapabilityViolation | null {
if (!body || typeof body !== "object") return null;
if (requestsThinkingBudget(body.reasoning_effort)) {
return {
param: "reasoning_effort",
message:
'Model provider "gemini-web" does not support "reasoning_effort". It drives the ' +
"gemini.google.com web UI through a typed prompt and has no thinking-budget control " +
'to set, so any effort above "minimal" would be silently ignored. Remove ' +
'"reasoning_effort" (or send "none"/"minimal") or route to a reasoning-capable model.',
};
}
if (isForcingToolChoice(body.tool_choice)) {
return {
param: "tool_choice",
message:
'Model provider "gemini-web" cannot guarantee a forced tool call. Its tool support is ' +
"prompt-emulated — the model is asked to emit a tool block and may answer with prose " +
'instead — so "tool_choice" values that require one ("required", "any", or a named ' +
'function) cannot be honored. Use "auto" to keep best-effort tool calling, or route to ' +
"a model with native function calling.",
};
}
return null;
}

View File

@@ -190,31 +190,15 @@ export function getBackgroundTaskReason(
const messages = toMessageArray(typedBody.messages ?? typedBody.input ?? []);
if (!Array.isArray(messages) || messages.length === 0) return null;
// Derive system content from messages array (OpenAI format) or top-level
// system field (Anthropic format).
// Find system message
const systemMsg = messages.find(
(message: BackgroundMessage) => message.role === "system" || message.role === "developer"
);
let systemContent = "";
if (systemMsg && typeof systemMsg.content === "string") {
systemContent = systemMsg.content.toLowerCase();
} else if (!systemMsg) {
// Anthropic top-level system field: string or array of text blocks
const raw = (typedBody as Record<string, unknown>).system;
if (typeof raw === "string") {
systemContent = raw.toLowerCase();
} else if (Array.isArray(raw)) {
systemContent = raw
.map((part) =>
part && typeof (part as { text?: unknown }).text === "string"
? (part as { text: string }).text
: ""
)
.filter(Boolean)
.join(" ")
.toLowerCase();
}
}
if (!systemMsg) return null;
const systemContent =
typeof systemMsg.content === "string" ? systemMsg.content.toLowerCase() : "";
if (!systemContent) return null;
// Check against detection patterns

View File

@@ -112,7 +112,7 @@ export function geminiToClaudeResponse(chunk, state) {
// When the toolNameMap provides a match (e.g., lowercase "bash" → "Bash"),
// use it directly without passing through normalizeToolName(), which would
// reverse TitleCase back to lowercase via REVERSE_MAP (#9568).
const restoredToolName = mappedName ?? normalizeToolName(rawToolName);
const restoredToolName = mappedName || normalizeToolName(rawToolName);
const idx = state.contentBlockIndex++;
const toolId = fc.id || `toolu_${Date.now()}_${idx}`;

View File

@@ -874,7 +874,6 @@ function openaiResponsesToOpenAIResponseStream(chunk, state) {
if (state.currentToolCallId) state.toolCallIdsSeen.add(state.currentToolCallId);
const toolName = normalizeToolName(item.name);
state.currentToolName = toolName; // track for schema lookup at done time
if (!toolName) {
// Some Responses providers briefly emit placeholder/empty tool names.
// Defer emission until output_item.done in case the final name is populated there.
@@ -920,9 +919,26 @@ function openaiResponsesToOpenAIResponseStream(chunk, state) {
state.currentToolCallArgsBuffer = (state.currentToolCallArgsBuffer || "") + argsDelta;
if (state.currentToolCallDeferred) return null;
// #9168: buffer arguments until output_item.done for schema-aware null normalization
// Previously emitted raw null values for optional enum fields (e.g. isolation: null).
return null;
return {
id: state.chatId,
object: "chat.completion.chunk",
created: state.created,
model: state.model || "gpt-4",
choices: [
{
index: 0,
delta: {
tool_calls: [
{
index: state.toolCallIndex,
function: { arguments: argsDelta },
},
],
},
finish_reason: null,
},
],
};
}
// Function call done — emit args chunk from item.arguments when no deltas were received,
@@ -995,35 +1011,6 @@ function openaiResponsesToOpenAIResponseStream(chunk, state) {
if (item.arguments != null && !buffered) {
const argsToEmit = stripEmptyOptionalToolArgs(item.arguments, toolName, toolSchema);
const argsStr = typeof argsToEmit === "string" ? argsToEmit : JSON.stringify(argsToEmit);
if (argsStr) {
return {
id: state.chatId,
object: "chat.completion.chunk",
created: state.created,
model: state.model || "gpt-4",
choices: [
{
index: 0,
delta: {
tool_calls: [
{
index: currentIndex,
function: { arguments: argsStr },
},
],
},
finish_reason: null,
},
],
};
}
} else if (buffered) {
// #9168: deltas were buffered — normalize against the original client schema
// and emit the cleaned arguments once, stripping optional null values that
// would otherwise reach the client raw.
const argsToEmit = stripEmptyOptionalToolArgs(buffered, toolName, toolSchema);
const argsStr = typeof argsToEmit === "string" ? argsToEmit : JSON.stringify(argsToEmit);
if (argsStr) {
return {

View File

@@ -34,11 +34,8 @@
"scripts/dev/tls-options.mjs",
"scripts/check/check-supported-node-runtime.ts",
"scripts/dev/sync-env.mjs",
"scripts/build/assembleStandalone.mjs",
"scripts/build/backendOnlyPages.mjs",
"scripts/build/build-next-isolated.mjs",
"scripts/build/build-tproxy-native.mjs",
"scripts/build/native-binary-compat.mjs",
"scripts/build/build-next-isolated.mjs",
"scripts/build/runtime-env.mjs",
"README.md",
"LICENSE",

View File

@@ -89,15 +89,6 @@ const NATIVE_ASSET_ENTRIES = [
src: ["node_modules", "better-sqlite3", "build"],
dest: ["node_modules", "better-sqlite3", "build"],
},
{
// #8847: Bun (and npx -g global installs) resolve better-sqlite3's native
// binary from prebuilds/ instead of build/Release/, so the compiled build/
// copy alone leaves a hollow package that falls back to sql.js (OOM under
// Bun). Ship the prebuilds alongside the compiled binary.
label: "better-sqlite3 prebuilds (Bun / global installs)",
src: ["node_modules", "better-sqlite3", "prebuilds"],
dest: ["node_modules", "better-sqlite3", "prebuilds"],
},
{
// TPROXY IP_TRANSPARENT addon (Fase 3 / Epic A). Built by build-tproxy-native
// before assembly; Linux-only + opt-in, so the source is absent on non-Linux

View File

@@ -134,7 +134,7 @@ export function LlmChatCard({
}: Props) {
const t = useTranslations("miniPlayground");
const { keys } = useApiKey();
const { models, loading, error, retry } = useProviderModels(providerId);
const { models } = useProviderModels(providerId);
const [internalSelectedKey, setInternalSelectedKey] = useState<string>("");
const [internalModel, setInternalModel] = useState<string>(initialModel ?? "");
@@ -392,31 +392,15 @@ export function LlmChatCard({
<select
value={model || firstModel}
onChange={(e) => setModel(e.target.value)}
disabled={loading}
className="min-w-0 flex-1 rounded-md border border-border bg-bg-subtle text-xs px-2 py-1 text-text-main focus:outline-none focus:ring-1 focus:ring-primary disabled:opacity-60"
className="min-w-0 flex-1 rounded-md border border-border bg-bg-subtle text-xs px-2 py-1 text-text-main focus:outline-none focus:ring-1 focus:ring-primary"
>
{modelOptions.length === 0 && !loading && <option value="">{initialModel || "—"}</option>}
{loading && <option value="">{t("loading") ?? "Loading…"}</option>}
{modelOptions.length === 0 && <option value="">{initialModel || "—"}</option>}
{modelOptions.map((m) => (
<option key={m.id} value={m.id}>
{m.id}
</option>
))}
</select>
{error && (
<span className="text-xs text-red-500 flex items-center gap-1" role="alert">
<span className="truncate max-w-[180px]" title={String(error)}>
{String(error)}
</span>
<button
type="button"
onClick={retry}
className="shrink-0 text-xs text-primary hover:text-primary-strong underline"
>
{t("retry") ?? "Retry"}
</button>
</span>
)}
</div>
{/* Key select */}
{keys.length > 0 && (

View File

@@ -1,6 +1,6 @@
"use client";
import { useState, useEffect, useCallback, useRef } from "react";
import { useState, useEffect } from "react";
export interface ProviderModel {
id: string;
@@ -18,8 +18,6 @@ interface UseProviderModelsResult {
models: ProviderModel[];
loading: boolean;
error: string | null;
/** Re-runs the model fetch for the current provider. Useful for a Retry action. */
retry: () => void;
}
/**
@@ -34,14 +32,15 @@ export function useProviderModels(providerId: string): UseProviderModelsResult {
const [models, setModels] = useState<ProviderModel[]>([]);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
// Cancels any in-flight load (component unmount or a retry superseding the
// previous request) so a stale response never overwrites a newer one.
const cleanupRef = useRef<(() => void) | null>(null);
const load = useCallback(() => {
cleanupRef.current?.();
useEffect(() => {
if (!providerId) {
setLoading(false);
return;
}
let cancelled = false;
const run = async () => {
const load = async () => {
setLoading(true);
setError(null);
try {
@@ -110,33 +109,11 @@ export function useProviderModels(providerId: string): UseProviderModelsResult {
if (!cancelled) setLoading(false);
}
};
void run();
const cleanup = () => {
void load();
return () => {
cancelled = true;
};
cleanupRef.current = cleanup;
return cleanup;
}, [providerId]);
useEffect(() => {
if (!providerId) {
setLoading(false);
return;
}
return load();
}, [providerId, load]);
// Release the current in-flight cleanup on unmount so no state updates leak.
useEffect(() => {
return () => {
cleanupRef.current?.();
};
}, []);
const retry = useCallback(() => {
if (!providerId) return;
load();
}, [providerId, load]);
return { models, loading, error, retry };
return { models, loading, error };
}

View File

@@ -701,17 +701,6 @@ export async function testSingleConnection(connectionId: string, validationModel
? makeDiagnosis("ok", "local", null, null)
: classifyFailure({ error: result.error, statusCode: result.statusCode, provider }));
// #9623: a failed connection test must not paint the connection permanently red.
// Previously a non-terminal failure wrote `testStatus: "error"` with
// `rateLimitedUntil: null` — since the cooldown filter only ever skips entries
// whose rateLimitedUntil is in the future, a null cooldown left the connection
// permanently unavailable after a transient outage. Give non-terminal test
// failures a short cooldown so the lazy-recovery path retries them.
const terminalTestStatuses = new Set(["banned", "expired", "credits_exhausted"]);
const isTerminalFailure =
!result.valid && terminalTestStatuses.has(String(diagnosis.code ?? diagnosis.type ?? "").toLowerCase());
const testFailureCooldownMs = result.valid ? 0 : 30_000; // 30s retry window
const updateData: Record<string, any> = {
testStatus: result.valid ? "active" : "error",
lastError: result.valid ? null : result.error,
@@ -720,12 +709,7 @@ export async function testSingleConnection(connectionId: string, validationModel
lastErrorType: result.valid ? null : diagnosis.type,
lastErrorSource: result.valid ? null : diagnosis.source,
errorCode: result.valid ? null : diagnosis.code || result.statusCode || null,
rateLimitedUntil:
result.valid || isTerminalFailure
? result.valid
? null
: connection.rateLimitedUntil || null
: new Date(Date.now() + testFailureCooldownMs).toISOString(),
rateLimitedUntil: result.valid ? null : connection.rateLimitedUntil || null,
};
if (result.valid) {

View File

@@ -73,7 +73,9 @@ export async function PATCH(request: Request, { params }: RouteParams): Promise<
// helpers. Without the pre-update removal, a group/provider switch would leave
// orphan qtSd/ combos a quota key still sees. Guarded + non-fatal.
const combosNeedResync =
body !== null && typeof body === "object" && ("connectionIds" in body || "groupId" in body);
body !== null &&
typeof body === "object" &&
("connectionIds" in body || "groupId" in body);
if (combosNeedResync) {
try {
const { removeQuotaCombosForPool } = await import("@/lib/quota/quotaCombos");
@@ -104,7 +106,7 @@ export async function PATCH(request: Request, { params }: RouteParams): Promise<
id,
prevApiKeyIds,
nextApiKeyIds,
parsed.data.exclusive ?? false
parsed.data.exclusive ?? false,
);
}
@@ -130,7 +132,7 @@ export async function DELETE(request: Request, { params }: RouteParams): Promise
try {
const { id } = await params;
const existed = await deletePool(id);
const existed = deletePool(id);
if (!existed) {
return NextResponse.json(buildErrorBody(404, "Pool not found"), { status: 404 });
}

View File

@@ -306,7 +306,6 @@ export async function registerNodejs(): Promise<void> {
{ applyRuntimeSettings },
{ startRuntimeConfigHotReload },
{ startSpendBatchWriter },
{ startCleanupScheduler },
{ registerDefaultGuardrails },
{ ensurePersistentManagementPasswordHash },
{ skillExecutor },
@@ -321,7 +320,6 @@ export async function registerNodejs(): Promise<void> {
import("@/lib/config/runtimeSettings"),
import("@/lib/config/hotReload"),
import("@/lib/spend/batchWriter"),
import("@/lib/db/cleanup"),
import("@/lib/guardrails"),
import("@/lib/auth/managementPassword"),
import("@/lib/skills/executor"),
@@ -491,17 +489,6 @@ export async function registerNodejs(): Promise<void> {
console.warn("[STARTUP] Could not initialize vacuum scheduler (non-fatal):", msg);
}
// Retention cleanup scheduler (#4691/#6988, #9624): runs the general retention
// cleanup once after startup and then every 6 hours. Previously this was only
// wired into the unused src/server-init.ts, so telemetry tables grew unboundedly
// even with retention.autoCleanupEnabled=true. Idempotent (guarded internally).
try {
startCleanupScheduler();
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : String(err);
console.warn("[STARTUP] Could not start cleanup scheduler (non-fatal):", msg);
}
// Warm the model catalog's durable, apiKey-independent sub-caches at
// startup — see warmModelCatalogCache() for why the top-level Response
// cache alone doesn't deliver this. Fire-and-forget, non-fatal.

View File

@@ -253,15 +253,14 @@ export async function cleanupMemoryEntries(): Promise<CleanupResult> {
/**
* Clean up old domain_cost_history based on retention settings. (#6848)
* The `timestamp` column stores epoch milliseconds (saveCostEntry default
* is Date.now()), so the cutoff must be in milliseconds to match. (#9625)
* Uses unix-epoch `timestamp` column (INTEGER).
*/
export async function cleanupDomainCostHistory(): Promise<CleanupResult> {
const db = getDbInstance();
const retention = getRetentionSettings();
const retentionDays = retention.domainCostHistory;
const cutoffEpoch = Date.now() - retentionDays * 86_400_000;
const cutoffEpoch = Math.floor(Date.now() / 1000) - retentionDays * 86_400;
const result: CleanupResult = { deleted: 0, errors: 0 };

View File

@@ -11,32 +11,8 @@
import { getDbInstance } from "./core";
// Phase B2: auto-mint/prune quotaShared-* combos when pool allocations change.
// Imported lazily (dynamic import in the hook) to avoid circular-dependency
// risk between db/ and quota/ modules. Sync hooks are fire-and-forget; deletion
// awaits its guarded cleanup while metadata is available. Combo failures never
// break pool CRUD.
const quotaComboMaintenance = new Map<string, Promise<unknown>>();
const deletingPools = new Set<string>();
/** Reset module-level state for test isolation. Call in test.after() hooks. */
export function resetQuotaPoolsModuleState(): void {
deletingPools.clear();
quotaComboMaintenance.clear();
}
function serializeQuotaComboMaintenance<T>(
poolId: string,
operation: () => Promise<T>
): Promise<T> {
const previous = quotaComboMaintenance.get(poolId);
const current = previous ? previous.catch(() => undefined).then(operation) : operation();
quotaComboMaintenance.set(poolId, current);
const cleanup = () => {
if (quotaComboMaintenance.get(poolId) === current) quotaComboMaintenance.delete(poolId);
};
void current.then(cleanup, cleanup);
return current;
}
// risk between db/ and quota/ modules. The import is fire-and-forget; combo
// failures never break pool CRUD.
async function syncQuotaCombosGuarded(poolId: string): Promise<void> {
try {
const { syncQuotaCombos } = await import("@/lib/quota/quotaCombos");
@@ -424,7 +400,7 @@ export function createPool(input: PoolCreate): QuotaPool {
);
// Phase B2: fire-and-forget combo sync; failures are logged but never thrown.
void serializeQuotaComboMaintenance(id, () => syncQuotaCombosGuarded(id));
void syncQuotaCombosGuarded(id);
return result;
}
@@ -436,8 +412,6 @@ export function createPool(input: PoolCreate): QuotaPool {
* connection_id (primary) is synced to connectionIds[0].
*/
export function updatePool(id: string, input: PoolUpdate): QuotaPool | null {
if (deletingPools.has(id)) return null;
const database = getDb();
const existing = database
.prepare<PoolRow>(
@@ -501,7 +475,7 @@ export function updatePool(id: string, input: PoolUpdate): QuotaPool | null {
const result = rowToPool(existing, getAllocations(id));
// Phase B2: fire-and-forget combo sync; failures are logged but never thrown.
void serializeQuotaComboMaintenance(id, () => syncQuotaCombosGuarded(id));
void syncQuotaCombosGuarded(id);
return result;
}
@@ -511,38 +485,28 @@ export function updatePool(id: string, input: PoolUpdate): QuotaPool | null {
* Also removes join rows in quota_pool_connections.
* Returns true if a row was deleted, false if not found.
*/
export async function deletePool(id: string): Promise<boolean> {
if (deletingPools.has(id)) return false;
const exists = getDb().prepare<{ id: string }>("SELECT id FROM quota_pools WHERE id = ?").get(id);
if (!exists) return false;
deletingPools.add(id);
export function deletePool(id: string): boolean {
// Phase B2: remove quota combos BEFORE deleting the pool row so that
// removeQuotaCombosForPool can still resolve the pool name → slug.
void removeQuotaCombosGuarded(id);
const deletion = serializeQuotaComboMaintenance(id, async () => {
// Phase B2: remove quota combos BEFORE deleting the pool row so that
// removeQuotaCombosForPool can still resolve the pool name → slug.
await removeQuotaCombosGuarded(id);
const database = getDb();
const doDelete = database.transaction(() => {
database.prepare("DELETE FROM quota_pool_connections WHERE pool_id = ?").run(id);
// Prune this pool id from every key's allowed_quotas JSON array.
database
.prepare(
`UPDATE api_keys SET allowed_quotas = COALESCE(
const database = getDb();
const doDelete = database.transaction(() => {
database.prepare("DELETE FROM quota_pool_connections WHERE pool_id = ?").run(id);
// Prune this pool id from every key's allowed_quotas JSON array.
database
.prepare(
`UPDATE api_keys SET allowed_quotas = COALESCE(
(SELECT json_group_array(value) FROM json_each(api_keys.allowed_quotas) WHERE value != ?),
'[]')
WHERE allowed_quotas IS NOT NULL AND allowed_quotas != '[]'
AND EXISTS (SELECT 1 FROM json_each(api_keys.allowed_quotas) WHERE value = ?)`
)
.run(id, id);
return database.prepare("DELETE FROM quota_pools WHERE id = ?").run(id);
});
const result = doDelete();
return result.changes > 0;
)
.run(id, id);
return database.prepare("DELETE FROM quota_pools WHERE id = ?").run(id);
});
const clearDeleting = () => deletingPools.delete(id);
void deletion.then(clearDeleting, clearDeleting);
return deletion;
const result = doDelete();
return result.changes > 0;
}
/**
@@ -582,8 +546,6 @@ export async function deletePool(id: string): Promise<boolean> {
* Runs atomically: all pool writes are inside a single SQLite transaction.
*/
export function upsertAllocations(poolId: string, allocations: PoolAllocation[]): void {
if (deletingPools.has(poolId)) return;
const database = getDb();
// Normalize: when all weights are 0, distribute equally so the pool is usable
@@ -640,7 +602,7 @@ export function upsertAllocations(poolId: string, allocations: PoolAllocation[])
// Phase B2: fire-and-forget combo sync for the target pool only; failures are
// logged but never thrown. Sibling pools' combos are synced on their own lifecycle.
void serializeQuotaComboMaintenance(poolId, () => syncQuotaCombosGuarded(poolId));
void syncQuotaCombosGuarded(poolId);
}
/**

View File

@@ -112,8 +112,7 @@ function parseEffortList(rawList: unknown): string[] | undefined {
.map((entry) => {
const entryParsed = effortEntrySchema.safeParse(entry);
if (!entryParsed.success) return null;
const raw =
typeof entryParsed.data === "string" ? entryParsed.data : entryParsed.data.effort;
const raw = typeof entryParsed.data === "string" ? entryParsed.data : entryParsed.data.effort;
return raw.length > 0 ? normalizeSupportedEffort(raw) : null;
})
.filter((effort): effort is string => effort !== null)
@@ -145,16 +144,6 @@ export function detectSupportedThinkingEfforts(record: JsonRecord): string[] | u
}
}
// #9160: fall back to `capabilities.effort_tiers` before the legacy fields.
// OmniRoute's own catalog surfaces effort tiers inside `capabilities.effort_tiers`,
// which the existing `parseEffortList` already handles (string arrays).
const capabilitiesRecord = asRecord(record.capabilities);
const capabilitiesParsed = effortListSchema.safeParse(capabilitiesRecord.effort_tiers);
if (capabilitiesParsed.success) {
const fromCapabilities = parseEffortList(capabilitiesRecord.effort_tiers);
if (fromCapabilities) return fromCapabilities;
}
// #8347: fall back to `supported_reasoning_levels`, then `thinking.levels` — in that
// order, per the regression guard for #7694 (the flat field and `reasoning.supported_efforts`
// both take precedence over these two and are handled above / by the caller).

View File

@@ -152,10 +152,7 @@ export async function syncQuotaCombos(poolId: string): Promise<void> {
for (const connId of pool.connectionIds) {
let connection: Record<string, unknown> | null = null;
try {
connection = (await getCachedProviderConnectionById(connId)) as Record<
string,
unknown
> | null;
connection = (await getCachedProviderConnectionById(connId)) as Record<string, unknown> | null;
} catch {
// Connection lookup failure — skip this connection.
continue;
@@ -205,10 +202,6 @@ export async function syncQuotaCombos(poolId: string): Promise<void> {
}));
try {
const existing = await getComboByName(comboName);
// A pool may be deleted while this fire-and-forget sync is awaiting combo
// lookups. Re-check immediately before the synchronous DB upsert so stale
// create/update work cannot recreate managed combos after delete cleanup.
if (!getPool(poolId)) return;
const payload = {
name: comboName,
models: steps,

View File

@@ -1,427 +0,0 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { makeManagementSessionRequest } from "../helpers/managementSession.ts";
const TEST_DATA_DIR = fs.mkdtempSync(
path.join(os.tmpdir(), "omniroute-quota-pool-delete-combo-cleanup-")
);
process.env.DATA_DIR = TEST_DATA_DIR;
process.env.API_KEY_SECRET = "test-quota-pool-delete-combo-cleanup-secret";
const core = await import("../../src/lib/db/core.ts");
const apiKeysDb = await import("../../src/lib/db/apiKeys.ts");
const combosDb = await import("../../src/lib/db/combos.ts");
const groupsDb = await import("../../src/lib/db/quotaGroups.ts");
const poolsDb = await import("../../src/lib/db/quotaPools.ts");
const providersDb = await import("../../src/lib/db/providers.ts");
const compliance = await import("../../src/lib/compliance/index.ts");
const poolIdRoute = await import("../../src/app/api/quota/pools/[id]/route.ts");
const { removeQuotaCombosForPool, syncQuotaCombos } =
await import("../../src/lib/quota/quotaCombos.ts");
const { parseQuotaModelName, quotaGroupSlug } =
await import("../../src/lib/quota/quotaModelNaming.ts");
type Combo = Awaited<ReturnType<typeof combosDb.getCombos>>[number];
type Db = {
prepare: (sql: string) => {
all: (...params: unknown[]) => unknown[];
get: (...params: unknown[]) => unknown;
};
};
function resetDb() {
core.resetDbInstance();
apiKeysDb.resetApiKeyState();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
}
function quotaNamesFor(combos: Combo[], groupName: string, provider: string): string[] {
const groupSlug = quotaGroupSlug(groupName);
return combos
.map((combo) => (typeof combo.name === "string" ? combo.name : ""))
.filter((name) => {
const parsed = parseQuotaModelName(name);
return parsed?.groupSlug === groupSlug && parsed.provider === provider;
})
.sort();
}
async function createConnection(provider: "openrouter" | "baidu", name: string) {
const connection = await providersDb.createProviderConnection({
provider,
authType: "apikey",
name,
apiKey: `test-only-${name}`,
});
const id = (connection as Record<string, unknown>).id;
assert.equal(typeof id, "string", `${provider} connection should have an id`);
return id as string;
}
async function deletePoolThroughRoute(poolId: string): Promise<Response> {
const request = await makeManagementSessionRequest(`http://localhost/api/quota/pools/${poolId}`, {
method: "DELETE",
});
return poolIdRoute.DELETE(request, { params: Promise.resolve({ id: poolId }) });
}
function getAllowedQuotas(apiKeyId: string): string[] {
const db = core.getDbInstance() as unknown as Db;
const row = db.prepare("SELECT allowed_quotas FROM api_keys WHERE id = ?").get(apiKeyId) as {
allowed_quotas: string;
};
return JSON.parse(row.allowed_quotas) as string[];
}
function countRows(sql: string, id: string): number {
const db = core.getDbInstance() as unknown as Db;
const row = db.prepare(sql).get(id) as { count: number };
return row.count;
}
function nextImmediate(): Promise<void> {
return new Promise((resolve) => setImmediate(resolve));
}
test.beforeEach(() => {
resetDb();
compliance.initAuditLog();
});
test.after(() => {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
test("DELETE pool waits for scoped quota-combo cleanup before returning 204", async () => {
const targetGroup = groupsDb.createGroup("Delete Target Group");
const otherGroup = groupsDb.createGroup("Delete Other Group");
const targetConnectionId = await createConnection("openrouter", "delete-target-openrouter");
const sameGroupConnectionId = await createConnection("baidu", "delete-control-baidu");
const otherGroupConnectionId = await createConnection("openrouter", "delete-control-openrouter");
const apiKey = await apiKeysDb.createApiKey("Delete Pool Key", "delete-pool-machine");
const targetPool = poolsDb.createPool({
connectionId: targetConnectionId,
name: "Delete Target Pool",
groupId: targetGroup.id,
allocations: [{ apiKeyId: apiKey.id, weight: 100, policy: "hard" }],
});
const sameGroupPool = poolsDb.createPool({
connectionId: sameGroupConnectionId,
name: "Same Group Different Provider",
groupId: targetGroup.id,
});
const otherGroupPool = poolsDb.createPool({
connectionId: otherGroupConnectionId,
name: "Different Group Same Provider",
groupId: otherGroup.id,
});
await apiKeysDb.updateApiKeyPermissions(apiKey.id, {
allowedQuotas: [targetPool.id, otherGroupPool.id],
});
await syncQuotaCombos(targetPool.id);
await syncQuotaCombos(sameGroupPool.id);
await syncQuotaCombos(otherGroupPool.id);
await nextImmediate();
await nextImmediate();
const ordinaryCombo = await combosDb.createCombo({
name: "ordinary-delete-control",
models: [{ kind: "model", model: "openrouter/control-model", weight: 100 }],
strategy: "priority",
});
const before = await combosDb.getCombos();
const targetNames = quotaNamesFor(before, targetGroup.name, "openrouter");
const sameGroupControlNames = quotaNamesFor(before, targetGroup.name, "baidu");
const otherGroupControlNames = quotaNamesFor(before, otherGroup.name, "openrouter");
assert.ok(targetNames.length > 0, "target openrouter quota combos must exist before DELETE");
assert.ok(sameGroupControlNames.length > 0, "same-group baidu control combos must exist");
assert.ok(otherGroupControlNames.length > 0, "other-group openrouter control combos must exist");
assert.ok(
await combosDb.getComboByName(ordinaryCombo.name as string),
"ordinary combo must exist"
);
assert.ok(poolsDb.getPool(targetPool.id), "target pool row must exist before DELETE");
assert.equal(
countRows(
"SELECT count(*) AS count FROM quota_pool_connections WHERE pool_id = ?",
targetPool.id
),
1
);
assert.equal(
countRows("SELECT count(*) AS count FROM quota_allocations WHERE pool_id = ?", targetPool.id),
1
);
assert.deepEqual(getAllowedQuotas(apiKey.id), [targetPool.id, otherGroupPool.id]);
const response = await deletePoolThroughRoute(targetPool.id);
assert.equal(response.status, 204);
const after = await combosDb.getCombos();
assert.deepEqual(
quotaNamesFor(after, targetGroup.name, "openrouter"),
[],
"DELETE must not return while target group+provider quota combos remain"
);
assert.deepEqual(
quotaNamesFor(after, targetGroup.name, "baidu"),
sameGroupControlNames,
"same-group combos for another provider must remain byte/name-identical"
);
assert.deepEqual(
quotaNamesFor(after, otherGroup.name, "openrouter"),
otherGroupControlNames,
"same-provider combos for another group must remain byte/name-identical"
);
assert.deepEqual(
await combosDb.getComboByName(ordinaryCombo.name as string),
ordinaryCombo,
"ordinary user combo must remain unchanged"
);
assert.equal(poolsDb.getPool(targetPool.id), null);
assert.equal(
countRows(
"SELECT count(*) AS count FROM quota_pool_connections WHERE pool_id = ?",
targetPool.id
),
0
);
assert.equal(
countRows("SELECT count(*) AS count FROM quota_allocations WHERE pool_id = ?", targetPool.id),
0
);
assert.deepEqual(getAllowedQuotas(apiKey.id), [otherGroupPool.id]);
const auditEvents = compliance.getAuditLog({ action: "quota.pool.deleted", limit: 10 });
assert.ok(
auditEvents.some(
(event) =>
typeof event === "object" &&
event !== null &&
(event as Record<string, unknown>).target === targetPool.id
),
"successful DELETE must record quota.pool.deleted audit event"
);
});
test("DELETE prevents an in-flight create sync from recreating quota combos", async () => {
const group = groupsDb.createGroup("Immediate Create Delete Group");
const connectionId = await createConnection("openrouter", "immediate-create-delete");
const pool = poolsDb.createPool({
connectionId,
name: "Immediate Create Delete Pool",
groupId: group.id,
});
const deleted = await poolsDb.deletePool(pool.id);
await nextImmediate();
await nextImmediate();
assert.equal(deleted, true);
assert.equal(poolsDb.getPool(pool.id), null);
assert.deepEqual(
quotaNamesFor(await combosDb.getCombos(), group.name, "openrouter"),
[],
"a create sync already in flight must not mint quota combos after pool deletion"
);
});
test("DELETE prevents an in-flight update sync from recreating quota combos", async () => {
const group = groupsDb.createGroup("Immediate Update Delete Group");
const connectionId = await createConnection("openrouter", "immediate-update-delete");
const pool = poolsDb.createPool({
connectionId,
name: "Immediate Update Delete Pool",
groupId: group.id,
});
await syncQuotaCombos(pool.id);
await nextImmediate();
await nextImmediate();
await removeQuotaCombosForPool(pool.id);
assert.deepEqual(quotaNamesFor(await combosDb.getCombos(), group.name, "openrouter"), []);
assert.ok(poolsDb.updatePool(pool.id, { name: "Updated Then Deleted Pool" }));
const deleted = await poolsDb.deletePool(pool.id);
await nextImmediate();
await nextImmediate();
assert.equal(deleted, true);
assert.equal(poolsDb.getPool(pool.id), null);
assert.deepEqual(
quotaNamesFor(await combosDb.getCombos(), group.name, "openrouter"),
[],
"an update sync already in flight must not recreate quota combos after pool deletion"
);
});
test("DELETE rejects a synchronous pool update once deletion has started", async () => {
const oldGroup = groupsDb.createGroup("Deleting Pool Old Group");
const newGroup = groupsDb.createGroup("Deleting Pool New Group");
const connectionId = await createConnection("openrouter", "delete-update-race");
const pool = poolsDb.createPool({
connectionId,
name: "Delete Update Race Pool",
groupId: oldGroup.id,
});
await syncQuotaCombos(pool.id);
await nextImmediate();
await nextImmediate();
assert.ok(quotaNamesFor(await combosDb.getCombos(), oldGroup.name, "openrouter").length > 0);
const deleting = poolsDb.deletePool(pool.id);
const updated = poolsDb.updatePool(pool.id, { groupId: newGroup.id });
const deleted = await deleting;
await nextImmediate();
await nextImmediate();
assert.equal(updated, null, "a pool must become immutable as soon as deletion starts");
assert.equal(deleted, true);
assert.equal(poolsDb.getPool(pool.id), null);
assert.deepEqual(quotaNamesFor(await combosDb.getCombos(), oldGroup.name, "openrouter"), []);
assert.deepEqual(quotaNamesFor(await combosDb.getCombos(), newGroup.name, "openrouter"), []);
});
test("DELETE makes a synchronous allocation upsert a no-op once deletion has started", async () => {
const group = groupsDb.createGroup("Deleting Pool Allocation Group");
const targetConnectionId = await createConnection("openrouter", "delete-allocation-target");
const siblingConnectionId = await createConnection("baidu", "delete-allocation-sibling");
const targetPool = poolsDb.createPool({
connectionId: targetConnectionId,
name: "Delete Allocation Target",
groupId: group.id,
});
const siblingPool = poolsDb.createPool({
connectionId: siblingConnectionId,
name: "Delete Allocation Sibling",
groupId: group.id,
});
const apiKey = await apiKeysDb.createApiKey("Delete Allocation Key", "delete-allocation-key");
await nextImmediate();
await nextImmediate();
const deleting = poolsDb.deletePool(targetPool.id);
poolsDb.upsertAllocations(targetPool.id, [{ apiKeyId: apiKey.id, weight: 100, policy: "hard" }]);
const deleted = await deleting;
assert.equal(deleted, true);
assert.equal(poolsDb.getPool(targetPool.id), null);
assert.deepEqual(
poolsDb.getPool(siblingPool.id)?.allocations,
[],
"an allocation upsert on a deleting pool must not mutate sibling pools"
);
});
test("concurrent DELETE calls report one deletion and one missing pool", async () => {
const group = groupsDb.createGroup("Concurrent Delete Group");
const connectionId = await createConnection("openrouter", "concurrent-delete");
const pool = poolsDb.createPool({
connectionId,
name: "Concurrent Delete Pool",
groupId: group.id,
});
const results = await Promise.all([poolsDb.deletePool(pool.id), poolsDb.deletePool(pool.id)]);
assert.deepEqual(results, [true, false]);
assert.equal(poolsDb.getPool(pool.id), null);
assert.deepEqual(quotaNamesFor(await combosDb.getCombos(), group.name, "openrouter"), []);
});
test("DELETE nonexistent pool returns sanitized 404 without changing combos", async () => {
const missingPoolId = "pool-that-never-existed";
const group = groupsDb.createGroup("Missing Pool Control Group");
const connectionId = await createConnection("baidu", "missing-pool-control-baidu");
const pool = poolsDb.createPool({
connectionId,
name: "Missing Pool Control",
groupId: group.id,
});
const apiKey = await apiKeysDb.createApiKey("Missing Pool Key", "missing-pool-key");
await apiKeysDb.updateApiKeyPermissions(apiKey.id, {
allowedQuotas: [missingPoolId, pool.id],
});
await syncQuotaCombos(pool.id);
await nextImmediate();
await nextImmediate();
await combosDb.createCombo({
name: "ordinary-missing-delete-control",
models: [{ kind: "model", model: "baidu/control-model", weight: 100 }],
strategy: "priority",
});
const before = await combosDb.getCombos();
assert.ok(quotaNamesFor(before, group.name, "baidu").length > 0);
const response = await deletePoolThroughRoute(missingPoolId);
const body = await response.json();
assert.equal(response.status, 404);
assert.equal(body.error?.message, "Pool not found");
assert.doesNotMatch(JSON.stringify(body), /\s+at\s+\//, "404 must not expose a stack trace");
assert.deepEqual(await combosDb.getCombos(), before);
assert.deepEqual(
getAllowedQuotas(apiKey.id),
[missingPoolId, pool.id],
"a missing-pool DELETE must not mutate API key permissions"
);
assert.ok(poolsDb.getPool(pool.id), "unrelated pool must remain");
});
test("DELETE keeps relational cleanup non-fatal when quota-combo listing fails", async () => {
const group = groupsDb.createGroup("Cleanup Failure Group");
const connectionId = await createConnection("openrouter", "cleanup-failure-openrouter");
const apiKey = await apiKeysDb.createApiKey("Cleanup Failure Key", "cleanup-failure-machine");
const pool = poolsDb.createPool({
connectionId,
name: "Cleanup Failure Pool",
groupId: group.id,
allocations: [{ apiKeyId: apiKey.id, weight: 100, policy: "hard" }],
});
await apiKeysDb.updateApiKeyPermissions(apiKey.id, { allowedQuotas: [pool.id] });
await syncQuotaCombos(pool.id);
await nextImmediate();
await nextImmediate();
assert.ok(quotaNamesFor(await combosDb.getCombos(), group.name, "openrouter").length > 0);
const db = core.getDbInstance();
const originalPrepare = db.prepare.bind(db);
const unhandled: unknown[] = [];
const onUnhandled = (reason: unknown) => unhandled.push(reason);
process.on("unhandledRejection", onUnhandled);
db.prepare = ((sql: string) => {
if (sql.startsWith("SELECT data, sort_order, context_cache_protection FROM combos ORDER BY")) {
throw new Error("forced quota combo listing failure");
}
return originalPrepare(sql);
}) as typeof db.prepare;
let response: Response;
try {
response = await deletePoolThroughRoute(pool.id);
await nextImmediate();
await nextImmediate();
} finally {
db.prepare = originalPrepare as typeof db.prepare;
process.off("unhandledRejection", onUnhandled);
}
assert.equal(response!.status, 204);
assert.deepEqual(unhandled, [], "guarded combo failure must not produce unhandledRejection");
assert.equal(poolsDb.getPool(pool.id), null);
assert.equal(
countRows("SELECT count(*) AS count FROM quota_pool_connections WHERE pool_id = ?", pool.id),
0
);
assert.equal(
countRows("SELECT count(*) AS count FROM quota_allocations WHERE pool_id = ?", pool.id),
0
);
assert.deepEqual(getAllowedQuotas(apiKey.id), []);
});

View File

@@ -137,15 +137,15 @@ test("updatePool returns null for unknown id", () => {
assert.equal(result, null);
});
test("deletePool removes pool and returns true", async () => {
test("deletePool removes pool and returns true", () => {
const pool = poolsDb.createPool({ connectionId: "c6", name: "Deletable" });
const deleted = await poolsDb.deletePool(pool.id);
const deleted = poolsDb.deletePool(pool.id);
assert.equal(deleted, true);
assert.equal(poolsDb.getPool(pool.id), null);
});
test("deletePool returns false for unknown id", async () => {
const result = await poolsDb.deletePool("ghost-pool");
test("deletePool returns false for unknown id", () => {
const result = poolsDb.deletePool("ghost-pool");
assert.equal(result, false);
});
@@ -190,14 +190,14 @@ test("upsertAllocations with empty array removes all allocations", () => {
// FK CASCADE: delete pool → allocations gone
// ---------------------------------------------------------------------------
test("deletePool cascades to allocations", async () => {
test("deletePool cascades to allocations", () => {
const pool = poolsDb.createPool({
connectionId: "c9",
name: "With Allocs",
allocations: [{ apiKeyId: "k-cascade", weight: 100, policy: "hard" }],
});
await poolsDb.deletePool(pool.id);
poolsDb.deletePool(pool.id);
// After pool is deleted, listAllocationsForApiKey should find nothing for k-cascade
const remaining = poolsDb.listAllocationsForApiKey("k-cascade");

View File

@@ -1,258 +0,0 @@
// Capability enforcement for the Gemini Web executor (#9356).
//
// Reported: gemini-web silently ACCEPTS `reasoning_effort` and
// `tool_choice: "required"` and answers with ordinary prose — HTTP 200, no
// `reasoning_content`, `tool_calls: []`, `finish_reason: "stop"`. An
// AgentChakra/OpenClaw agent then believes its reasoning and tool requirements
// were honored when they were not.
//
// Why neither can be implemented for THIS provider: gemini-web is not an API
// client. It launches Playwright, types a single flat prompt string into the
// gemini.google.com `.ql-editor` contenteditable, presses Enter, and captures
// the first `StreamGenerate` response off the page. There is no request payload
// to carry a thinking budget, and no function-calling channel to force — the
// tools support it does have is the prompt-emulation shim (`webTools.ts`, #7286),
// which ASKS the model to emit `<tool>{...}</tool>` and cannot GUARANTEE it.
//
// So this suite pins the issue's option (b) for both controls: reject the
// requests we cannot honor, and keep honoring the ones we can. The line drawn:
//
// reasoning_effort none | minimal → allowed (gemini-web not thinking
// IS compliance with "spend little")
// low | medium | high… → 400, a positive request to think
// tool_choice absent | auto | none → allowed (emulation path, #7286)
// required | any | {fn} → 400, a guarantee we cannot make
//
// The guard must run BEFORE Playwright launches, so every executor assertion
// here completes without a browser.
import test from "node:test";
import assert from "node:assert/strict";
const { GeminiWebExecutor } = await import("../../open-sse/executors/gemini-web.ts");
const { checkGeminiWebUnsupportedControls, GEMINI_WEB_UNSUPPORTED_CONTROL_CODE } =
await import("../../open-sse/executors/gemini-web/capabilities.ts");
const { gemini_webProvider } =
await import("../../open-sse/config/providers/registry/gemini/web/index.ts");
const { supportsReasoning, supportsToolCalling } =
await import("../../src/lib/modelCapabilities.ts");
const { providerSupportsEmulatedToolCalling } =
await import("../../open-sse/services/combo/comboStructure.ts");
const GET_WEATHER_TOOL = {
type: "function",
function: {
name: "get_weather",
description: "Get the current weather for a city",
parameters: { type: "object", properties: { city: { type: "string" } }, required: ["city"] },
},
};
interface ErrorBodyLike {
error: { message: string; type: string; code: string };
}
/**
* Run the executor with valid-looking credentials. Every case in this suite is
* expected to short-circuit on the capability guard, so Playwright is never
* reached — a test that hangs here means the guard did not fire.
*/
async function run(body: Record<string, unknown>) {
return new GeminiWebExecutor().execute({
model: "gemini-3.6-flash",
body: { messages: [{ role: "user", content: "hi" }], stream: false, ...body },
stream: false,
credentials: { apiKey: "__Secure-1PSID=test-cookie" },
signal: AbortSignal.timeout(10_000),
log: null,
});
}
// ─── Pure checker: reasoning_effort ─────────────────────────────────────────
test("#9356 reasoning_effort low/medium/high/xhigh are rejected as unsupported", () => {
for (const effort of ["low", "medium", "high", "xhigh"]) {
const violation = checkGeminiWebUnsupportedControls({ reasoning_effort: effort });
assert.equal(
violation?.param,
"reasoning_effort",
`reasoning_effort="${effort}" asks gemini-web to think harder, which a typed browser ` +
`prompt cannot express — it must be rejected, not silently dropped`
);
assert.match(violation!.message, /reasoning_effort/);
}
});
test("#9356 reasoning_effort none/minimal and absent stay allowed", () => {
assert.equal(checkGeminiWebUnsupportedControls({}), null);
assert.equal(checkGeminiWebUnsupportedControls({ reasoning_effort: null }), null);
assert.equal(checkGeminiWebUnsupportedControls({ reasoning_effort: "none" }), null);
assert.equal(
checkGeminiWebUnsupportedControls({ reasoning_effort: "minimal" }),
null,
'"minimal" means spend as little reasoning as possible — a non-thinking provider ' +
"already satisfies it, so rejecting it would be gratuitous"
);
assert.equal(checkGeminiWebUnsupportedControls({ reasoning_effort: " NONE " }), null);
});
// ─── Pure checker: tool_choice ──────────────────────────────────────────────
test("#9356 tool_choice required/any is rejected as unsupported", () => {
for (const choice of ["required", "any"]) {
const violation = checkGeminiWebUnsupportedControls({
tools: [GET_WEATHER_TOOL],
tool_choice: choice,
});
assert.equal(
violation?.param,
"tool_choice",
`tool_choice="${choice}" is a guarantee the prompt-emulation shim cannot make`
);
assert.match(violation!.message, /tool_choice/);
}
});
test("#9356 a forced-function tool_choice object is rejected as unsupported", () => {
const violation = checkGeminiWebUnsupportedControls({
tools: [GET_WEATHER_TOOL],
tool_choice: { type: "function", function: { name: "get_weather" } },
});
assert.equal(violation?.param, "tool_choice");
// Anthropic-style forcing, which the translators also emit.
assert.equal(
checkGeminiWebUnsupportedControls({
tools: [GET_WEATHER_TOOL],
tool_choice: { type: "any" },
})?.param,
"tool_choice"
);
});
test("#9356 tool_choice auto/none and absent keep the #7286 emulation path open", () => {
assert.equal(checkGeminiWebUnsupportedControls({ tools: [GET_WEATHER_TOOL] }), null);
assert.equal(
checkGeminiWebUnsupportedControls({ tools: [GET_WEATHER_TOOL], tool_choice: "auto" }),
null
);
assert.equal(
checkGeminiWebUnsupportedControls({ tools: [GET_WEATHER_TOOL], tool_choice: "none" }),
null
);
});
test("#9356 forcing is rejected on its own terms, even with no tools[] array", () => {
// An agent that sets tool_choice without tools is already malformed, but the
// point stands: never report success for a forcing contract we ignore.
assert.equal(
checkGeminiWebUnsupportedControls({ tool_choice: "required" })?.param,
"tool_choice"
);
});
// ─── Executor wiring ────────────────────────────────────────────────────────
test("#9356 executor returns 400 for reasoning_effort=high before launching a browser", async () => {
const result = await run({ reasoning_effort: "high" });
assert.equal(result.response.status, 400);
const body = (await result.response.json()) as ErrorBodyLike;
assert.equal(body.error.code, GEMINI_WEB_UNSUPPORTED_CONTROL_CODE);
assert.match(body.error.message, /reasoning_effort/);
assert.equal(
body.error.message.includes("at /"),
false,
"error bodies must stay sanitized — no stack traces"
);
});
test("#9356 executor returns 400 for tool_choice=required before launching a browser", async () => {
const result = await run({ tools: [GET_WEATHER_TOOL], tool_choice: "required" });
assert.equal(result.response.status, 400);
const body = (await result.response.json()) as ErrorBodyLike;
assert.equal(body.error.code, GEMINI_WEB_UNSUPPORTED_CONTROL_CODE);
assert.match(body.error.message, /tool_choice/);
});
test("#9356 the capability guard runs ahead of the credential check", async () => {
// A request that is BOTH uncredentialed and incompatible must report the
// incompatibility: adding a cookie would not make it work.
const result = await new GeminiWebExecutor().execute({
model: "gemini-3.6-flash",
body: { messages: [{ role: "user", content: "hi" }], reasoning_effort: "high" },
stream: false,
credentials: {},
signal: AbortSignal.timeout(10_000),
log: null,
});
assert.equal(result.response.status, 400);
const body = (await result.response.json()) as ErrorBodyLike;
assert.equal(body.error.code, GEMINI_WEB_UNSUPPORTED_CONTROL_CODE);
});
test("#9356 a supported request still falls through the guard untouched", async () => {
// tool_choice:"auto" + tools[] is the #7286 emulation contract. It must NOT
// be blocked — reaching the (missing) credential check proves the guard let
// it pass, without needing a browser to prove it.
const result = await new GeminiWebExecutor().execute({
model: "gemini-3.6-flash",
body: {
messages: [{ role: "user", content: "hi" }],
tools: [GET_WEATHER_TOOL],
tool_choice: "auto",
},
stream: false,
credentials: {},
signal: AbortSignal.timeout(10_000),
log: null,
});
assert.equal(result.response.status, 401, "should reach the cookie check, not the guard");
});
// ─── Catalog metadata ───────────────────────────────────────────────────────
test("#9356 registry advertises no native tool calling and no reasoning for gemini-web", () => {
assert.ok(gemini_webProvider.models.length > 0);
for (const model of gemini_webProvider.models) {
assert.equal(
model.toolCalling,
false,
`${model.id} must not advertise native tool calling — /v1/models feeds agent routers`
);
assert.equal(
model.supportsReasoning,
false,
`${model.id} must advertise reasoning:false so agent routers stop selecting it for ` +
"reasoning work (the executor has no thinking control to drive)"
);
}
});
test("#9356 resolved capabilities — not just the raw registry — report no reasoning/tools", () => {
// The registry literal is only the input; `getResolvedModelCapabilities` is what
// the catalog, the combo compatibility filter and the thinking-budget translator
// actually read. Assert the resolved view so a downstream default cannot quietly
// re-advertise a capability the executor does not have.
for (const model of gemini_webProvider.models) {
const input = { provider: "gemini-web", model: model.id };
assert.equal(supportsReasoning(input), false, `${model.id} resolved reasoning must be false`);
assert.equal(
supportsToolCalling(input),
false,
`${model.id} resolved NATIVE tool calling must be false — prompt emulation is advertised ` +
'separately as toolCalling:"emulated" on the provider constant'
);
}
});
test("#9356 the provider still advertises emulated tool calling, so #7286 combos keep routing", () => {
// Guard against over-correcting: dropping the emulation advertisement here would
// make filterTargetsByRequestCompatibility fail these targets closed and break
// emulation-only combos (#5240 / #8488).
assert.equal(providerSupportsEmulatedToolCalling("gemini-web"), true);
assert.equal(providerSupportsEmulatedToolCalling("gweb"), true);
});

View File

@@ -57,7 +57,9 @@ test.after(async () => {
// ── D1.1: Migration file ────────────────────────────────────────────────────
test("migration 086 file exists and contains quota_pool_connections DDL", () => {
const migrationPath = path.resolve("src/lib/db/migrations/087_quota_pool_connections.sql");
const migrationPath = path.resolve(
"src/lib/db/migrations/087_quota_pool_connections.sql"
);
assert.ok(fs.existsSync(migrationPath), `migration file not found: ${migrationPath}`);
const sql = fs.readFileSync(migrationPath, "utf8");
@@ -151,14 +153,14 @@ test("updatePool without connectionIds leaves join rows untouched", () => {
// ── D1.4: deletePool removes join rows ────────────────────────────────────
test("deletePool removes quota_pool_connections rows", async () => {
test("deletePool removes quota_pool_connections rows", () => {
const pool = poolsDb.createPool({
connectionId: "del-a",
name: "To Delete",
connectionIds: ["del-a", "del-b"],
});
const deleted = await poolsDb.deletePool(pool.id);
const deleted = poolsDb.deletePool(pool.id);
assert.equal(deleted, true, "deletePool should return true");
// Pool should be gone.

View File

@@ -23,7 +23,8 @@ import path from "node:path";
// ── DB harness (same pattern as quota-exclusivity-reconcile.test.ts) ─────────
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-pool-delete-prune-"));
process.env.DATA_DIR = TEST_DATA_DIR;
process.env.API_KEY_SECRET = process.env.API_KEY_SECRET || "delete-prune-test-secret-32chars!!";
process.env.API_KEY_SECRET =
process.env.API_KEY_SECRET || "delete-prune-test-secret-32chars!!";
const core = await import("../../src/lib/db/core.ts");
const poolsDb = await import("../../src/lib/db/quotaPools.ts");
@@ -61,8 +62,9 @@ test.after(async () => {
// ── Helper: get allowed_quotas for a key by id from DB ───────────────────────
function getAllowedQuotasById(keyId: string): string[] {
const db = core.getDbInstance();
const row = (db as any).prepare("SELECT allowed_quotas FROM api_keys WHERE id = ?").get(keyId) as
{ allowed_quotas: string } | undefined;
const row = (db as any)
.prepare("SELECT allowed_quotas FROM api_keys WHERE id = ?")
.get(keyId) as { allowed_quotas: string } | undefined;
if (!row) return [];
try {
const parsed = JSON.parse(row.allowed_quotas ?? "[]");
@@ -83,7 +85,7 @@ test("deletePool prunes its id from api_key allowed_quotas", async () => {
const before = getAllowedQuotasById(keyObj.id);
assert.ok(before.includes(pool.id), `pool.id should be in allowed_quotas before delete`);
await poolsDb.deletePool(pool.id);
poolsDb.deletePool(pool.id);
const after = getAllowedQuotasById(keyObj.id);
assert.ok(!after.includes(pool.id), `pool.id should NOT be in allowed_quotas after delete`);
@@ -101,7 +103,7 @@ test("deletePool preserves unrelated pool ids in allowed_quotas", async () => {
allowedQuotas: [poolToDelete.id, otherPool.id, unrelatedId],
});
await poolsDb.deletePool(poolToDelete.id);
poolsDb.deletePool(poolToDelete.id);
const after = getAllowedQuotasById(keyObj.id);
assert.ok(!after.includes(poolToDelete.id), "deleted pool id should be removed");
@@ -119,7 +121,7 @@ test("deletePool does not modify keys that don't reference the deleted pool", as
// This key only references otherPool, not poolToDelete
await apiKeysDb.updateApiKeyPermissions(keyObj.id, { allowedQuotas: [otherPool.id] });
await poolsDb.deletePool(poolToDelete.id);
poolsDb.deletePool(poolToDelete.id);
const after = getAllowedQuotasById(keyObj.id);
assert.deepEqual(after, [otherPool.id], "key referencing only other pool should be unchanged");
@@ -132,7 +134,7 @@ test("deletePool: key with empty allowed_quotas stays empty", async () => {
const keyObj = await apiKeysDb.createApiKey("Prune Key 4", "machine-prune-4");
// Don't set allowedQuotas — default is []
await poolsDb.deletePool(pool.id);
poolsDb.deletePool(pool.id);
const after = getAllowedQuotasById(keyObj.id);
assert.deepEqual(after, [], "empty allowed_quotas should remain empty after delete");
@@ -154,7 +156,7 @@ test("deletePool prunes pool id from ALL keys that reference it", async () => {
await apiKeysDb.updateApiKeyPermissions(k.id, { allowedQuotas: [pool.id, otherPoolId] });
}
await poolsDb.deletePool(pool.id);
poolsDb.deletePool(pool.id);
for (const k of keys) {
const after = getAllowedQuotasById(k.id);
@@ -165,31 +167,23 @@ test("deletePool prunes pool id from ALL keys that reference it", async () => {
// ── 6. deletePool still returns true/false correctly ─────────────────────────
test("deletePool returns true for existing pool, false for non-existent", async () => {
test("deletePool returns true for existing pool, false for non-existent", () => {
const pool = poolsDb.createPool({ connectionId: "conn-ret-1", name: "Return Test" });
assert.equal(await poolsDb.deletePool(pool.id), true, "should return true for existing pool");
assert.equal(
await poolsDb.deletePool(pool.id),
false,
"should return false for already-deleted pool"
);
assert.equal(
await poolsDb.deletePool("nonexistent-id"),
false,
"should return false for unknown id"
);
assert.equal(poolsDb.deletePool(pool.id), true, "should return true for existing pool");
assert.equal(poolsDb.deletePool(pool.id), false, "should return false for already-deleted pool");
assert.equal(poolsDb.deletePool("nonexistent-id"), false, "should return false for unknown id");
});
// ── 7. Pool row and allocation rows are gone after delete (regression guard) ──
test("deletePool removes pool and allocation rows from DB", async () => {
test("deletePool removes pool and allocation rows from DB", () => {
const pool = poolsDb.createPool({
connectionId: "conn-reg-1",
name: "Regression Pool",
allocations: [{ apiKeyId: "key-reg-1", weight: 50, policy: "hard" }],
});
await poolsDb.deletePool(pool.id);
poolsDb.deletePool(pool.id);
assert.equal(poolsDb.getPool(pool.id), null, "getPool should return null after delete");
const { items: allPools } = poolsDb.listPools();

View File

@@ -1,57 +0,0 @@
import test from "node:test";
import assert from "node:assert/strict";
import { createBuiltinAutoCombo } from "@omniroute/open-sse/services/autoCombo/builtinCatalog.ts";
// #7754: `auto/best-free` combo name must never leak downstream as the model id.
// When the free-tier candidate pool resolves non-empty, every model in the combo
// must carry a concrete `<provider>/<model>` id — never the literal combo name.
test("#7754 auto/best-free never leaks the combo name as a model", async () => {
const combo = await createBuiltinAutoCombo("auto/best-free", "best-free");
const models = combo.models || [];
// The combo id is the modelStr by design (routing resolves it back), but the
// models array must never contain it as a target model.
const leak = models.filter(
(m: any) =>
(m.id || "") === "auto/best-free" ||
(m.model || "") === "auto/best-free" ||
(m.modelStr || "") === "auto/best-free"
);
assert.equal(
leak.length,
0,
`combo name leaked as a target model: ${JSON.stringify(leak)}`
);
});
test("#7754 every auto/best-free model carries a concrete provider/model", async () => {
const combo = await createBuiltinAutoCombo("auto/best-free", "best-free");
const models = combo.models || [];
for (const m of models as any[]) {
assert.ok(
m.model && m.model !== "auto/best-free",
`model missing concrete id: ${JSON.stringify(m)}`
);
assert.ok(
m.providerId && m.providerId !== "auto",
`model missing concrete provider: ${JSON.stringify(m)}`
);
}
});
test("#7754 empty free-tier pool degrades with a clear 503, not a name leak", async () => {
// When NO free-tier candidate exists, createBuiltinAutoCombo must either
// return an empty models[] (which the #6458 route check converts to a clear
// 503) or throw — never synthesize a target whose model is the combo name.
const combo = await createBuiltinAutoCombo("auto/best-free", "best-free");
const models = combo.models || [];
if (models.length === 0) {
// Empty pool is fine — the route layer (#6458) converts it to a clear 503.
assert.equal(combo.candidatePool?.length || 0, 0);
} else {
// Non-empty pool must not leak.
const leak = models.filter((m: any) => (m.model || "") === "auto/best-free");
assert.equal(leak.length, 0);
}
});

View File

@@ -1,70 +0,0 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { syncStandaloneNativeAssets } from "../../scripts/build/assembleStandalone.mjs";
/**
* Repro #8847: better-sqlite3 prebuilds are not included in the standalone
* bundle, so the bundled app fails when the platform's prebuild is needed
* (e.g. under Bun, which resolves the native binary via prebuilds/ rather
* than build/Release/).
*
* The test creates a synthetic node_modules/better-sqlite3/ tree with both
* the compiled build/Release/ binary AND the prebuilds/ directory, then
* confirms that syncStandaloneNativeAssets copies both into the standalone
* output. On the unfixed code this fails because NATIVE_ASSET_ENTRIES only
* lists better-sqlite3/build/.
*/
test("repro-8847: better-sqlite3 prebuilds are bundled alongside the compiled binary", async () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "repro-8847-"));
const projectRoot = path.join(tmp, "src-root");
// Seed better-sqlite3 with both build/Release/ and prebuilds/.
const bsqlDir = path.join(projectRoot, "node_modules", "better-sqlite3");
fs.mkdirSync(path.join(bsqlDir, "build", "Release"), { recursive: true });
fs.writeFileSync(
path.join(bsqlDir, "build", "Release", "better_sqlite3.node"),
"// native binary placeholder"
);
fs.mkdirSync(path.join(bsqlDir, "prebuilds"), { recursive: true });
for (const target of [
"darwin-arm64.node",
"darwin-x64.node",
"linux-arm64.node",
"linux-x64.node",
"linuxmusl-arm64.node",
"linuxmusl-x64.node",
"win32-arm64.node",
"win32-x64.node",
]) {
fs.writeFileSync(path.join(bsqlDir, "prebuilds", target), `// ${target}`);
}
const outDir = path.join(tmp, "standalone");
fs.mkdirSync(outDir, { recursive: true });
// Act: copy native assets into the standalone output.
await syncStandaloneNativeAssets(projectRoot, fs.promises, { log() {} }, outDir);
// Assert: the compiled build/Release/ binary was copied.
assert.ok(
fs.existsSync(
path.join(outDir, "node_modules", "better-sqlite3", "build", "Release", "better_sqlite3.node")
),
"compiled native binary (build/Release/) must be in the standalone bundle"
);
// Assert: the prebuilds/ directory was also copied.
const prebuildsDir = path.join(outDir, "node_modules", "better-sqlite3", "prebuilds");
assert.ok(fs.existsSync(prebuildsDir), "prebuilds/ directory must be in the standalone bundle");
// Assert: at least one prebuild file was copied.
assert.ok(
fs.existsSync(path.join(prebuildsDir, "linux-x64.node")),
"linux-x64 prebuild must be in the standalone bundle"
);
fs.rmSync(tmp, { recursive: true, force: true });
});

View File

@@ -1,111 +0,0 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
// #9156: macOS launchd autostart fails because the supervisor spawns the child
// with bare "node", but launchd's PATH cannot resolve it. process.execPath is
// always the absolute path to the running Node.js binary and is always resolvable.
//
// We verify the fix via:
// 1. Static source analysis — the spawn() call must use process.execPath
// unconditionally (no fallback to bare "node"). This runs without any
// experimental flags so it serves as the permanent regression guard.
// 2. Runtime test via mock.module (requires --experimental-test-module-mocks)
// that captures the actual spawn arguments.
const __filename = new URL(import.meta.url).pathname;
const __dirname = path.dirname(__filename);
const SUPERVISOR_PATH = path.resolve(
__dirname,
"../../bin/cli/runtime/processSupervisor.mjs"
);
const supervisorSrc = fs.readFileSync(SUPERVISOR_PATH, "utf8");
// ---------------------------------------------------------------------------
// 1. Source-level verification (no experimental flag required)
// ---------------------------------------------------------------------------
test("spawn() uses process.execPath unconditionally, no bare 'node' fallback (#9156)", () => {
// Must NOT contain the old conditional that falls back to bare "node"
assert.ok(
!supervisorSrc.includes('process.versions.bun ? process.execPath : "node"'),
"must NOT have a conditional fallback to bare 'node'"
);
// Must use process.execPath as the first argument to spawn()
const execPathPattern = /spawn\(\s*process\.execPath\s*,/;
assert.ok(
execPathPattern.test(supervisorSrc),
"spawn() must receive process.execPath as first argument"
);
});
test("process.execPath is an absolute path to the running Node.js binary", () => {
assert.ok(
path.isAbsolute(process.execPath),
`process.execPath must be absolute, got: ${process.execPath}`
);
assert.ok(
fs.existsSync(process.execPath),
`process.execPath must exist: ${process.execPath}`
);
});
// ---------------------------------------------------------------------------
// 2. Runtime test via mock.module (requires --experimental-test-module-mocks)
// ---------------------------------------------------------------------------
//
// Run manually: node --experimental-test-module-mocks --import tsx/esm --test tests/unit/repro-9156.test.ts
import { mock } from "node:test";
if (typeof mock.module === "function") {
test("(runtime) ServerSupervisor.start() spawns with process.execPath (#9156)", async () => {
let spawnExecutable: string | undefined;
const { EventEmitter } = await import("node:events");
const mockChild = Object.assign(new EventEmitter(), {
pid: 12345,
stdout: null,
stderr: null,
kill: () => {},
});
mock.module("node:child_process", {
exports: {
spawn: (...args: unknown[]) => {
spawnExecutable = args[0] as string;
return mockChild;
},
},
});
process.env.PORT = "0";
const { ServerSupervisor } = await import(
"../../bin/cli/runtime/processSupervisor.mjs"
);
const supervisor = new ServerSupervisor({
serverPath: "/fake/server.js",
env: {},
maxRestarts: 0,
});
spawnExecutable = undefined;
supervisor.start();
assert.ok(spawnExecutable, "spawn() must have been called");
assert.equal(
spawnExecutable,
process.execPath,
`expected process.execPath, got: ${spawnExecutable}`
);
assert.notEqual(spawnExecutable, "node", "must not be bare 'node'");
mockChild.removeAllListeners();
delete process.env.PORT;
});
}

View File

@@ -1,71 +0,0 @@
/**
* Issue #9486 — Anthropic OAuth returns HTTP 400 with "out of extra usage" in
* the error body when a tool-carrying request exceeds the account's usage quota.
* This should be classified as quota_exhausted (not generic bad_request), so the
* account fallback mechanism applies a proper cooldown and combo routing can
* skip to another target.
*/
import test from "node:test";
import assert from "node:assert/strict";
const { matchErrorRuleByText, findMatchingErrorRule, ERROR_RULES } =
await import("../../open-sse/config/errorConfig.ts");
const { checkFallbackError, classifyErrorText } =
await import("../../open-sse/services/accountFallback.ts");
const { RateLimitReason } = await import("../../open-sse/config/constants.ts");
test("#9486 ERROR_RULES has a text rule for 'out of extra usage' → quota_exhausted", () => {
const rule = ERROR_RULES.find((r) => r.text === "out of extra usage");
assert.ok(rule, "expected a rule for 'out of extra usage'");
assert.equal(rule!.reason, "quota_exhausted");
// Should use backoff so the fallback path applies exponential scaling
assert.equal(rule!.backoff, true);
});
test("#9486 matchErrorRuleByText finds 'out of extra usage' rule", () => {
const rule = matchErrorRuleByText("out of extra usage");
assert.ok(rule, "expected a matching rule");
assert.equal(rule!.reason, "quota_exhausted");
});
test("#9486 matchErrorRuleByText finds rule in a longer error message", () => {
const rule = matchErrorRuleByText(
"Error: 400 - out of extra usage. You have exceeded your usage quota for this billing period."
);
assert.ok(rule, "expected a matching rule from longer message");
assert.equal(rule!.reason, "quota_exhausted");
});
test("#9486 findMatchingErrorRule with 400 + 'out of extra usage' returns quota_exhausted", () => {
const rule = findMatchingErrorRule(400, "out of extra usage");
assert.ok(rule, "expected a matching rule");
assert.equal(rule!.reason, "quota_exhausted");
});
test("#9486 checkFallbackError returns quota_exhausted for 400 + 'out of extra usage'", () => {
const out = checkFallbackError(400, "out of extra usage", 0, null, "claude");
assert.equal(out.shouldFallback, true);
assert.equal(out.reason, RateLimitReason.QUOTA_EXHAUSTED);
// Should get a non-zero cooldown (quota exhaustion is not transient)
assert.ok(out.cooldownMs > 0, `expected positive cooldown, got ${out.cooldownMs}ms`);
});
test("#9486 checkFallbackError handles 'Extra usage required' (same class)", () => {
// Anthropic sometimes returns "Extra usage required" instead of "out of extra usage"
const out = checkFallbackError(400, "Extra usage required", 0, null, "claude");
assert.equal(out.shouldFallback, true);
assert.equal(out.reason, RateLimitReason.QUOTA_EXHAUSTED);
});
test("#9486 classifyErrorText flags 'out of extra usage' as QUOTA_EXHAUSTED", () => {
const out = classifyErrorText("out of extra usage");
assert.equal(out, RateLimitReason.QUOTA_EXHAUSTED);
});
test("#9486 generic 400 without quota text still gets no fallback (regression guard)", () => {
// Regression guard: a plain 400 with no quota-related text must NOT trigger
// fallback, preserving the existing behavior for non-quota 400 errors.
const out = checkFallbackError(400, "Bad request: invalid JSON", 0, null, "claude");
assert.equal(out.shouldFallback, false);
assert.equal(out.reason, RateLimitReason.UNKNOWN);
});

View File

@@ -1,52 +0,0 @@
import test from "node:test";
import assert from "node:assert/strict";
// #9623: Failed connection test leaves testStatus=error with no recovery path.
// Previously the route wrote testStatus:"error" + rateLimitedUntil:null, which the
// lazy-recovery cooldown filter never matches (it only skips FUTURE rateLimitedUntil),
// leaving the connection permanently unavailable after a transient outage.
// Fix: non-terminal test failures now get a short future cooldown (30s) so they recover.
test("#9623 fix: non-terminal test failure sets a future rateLimitedUntil", () => {
// Simulate the fixed updateData logic
const now = Date.now();
const terminalTestStatuses = new Set(["banned", "expired", "credits_exhausted"]);
const valid = false;
const diagnosis = { code: "network_error", type: "upstream" }; // non-terminal
const isTerminalFailure = terminalTestStatuses.has(String(diagnosis.code).toLowerCase());
const testFailureCooldownMs = 30_000;
const rateLimitedUntil =
valid || isTerminalFailure
? valid
? null
: null
: new Date(now + testFailureCooldownMs).toISOString();
assert.ok(
rateLimitedUntil !== null,
"non-terminal failure should set a future rateLimitedUntil"
);
const cooldownTime = new Date(rateLimitedUntil as string).getTime();
assert.ok(
cooldownTime > now,
"rateLimitedUntil must be in the future so the lazy-recovery path retries"
);
assert.ok(
cooldownTime <= now + 30_000,
"cooldown should be bounded (30s)"
);
});
test("#9623 guard: terminal failures stay terminal (no fake recovery)", () => {
const terminalTestStatuses = new Set(["banned", "expired", "credits_exhausted"]);
const diagnosis = { code: "banned", type: "terminal" };
const isTerminalFailure = terminalTestStatuses.has(String(diagnosis.code).toLowerCase());
assert.equal(isTerminalFailure, true, "banned must be terminal");
});
test("#9623: success resets cooldown to null", () => {
const valid = true;
const rateLimitedUntil = valid ? null : new Date(Date.now() + 30_000).toISOString();
assert.equal(rateLimitedUntil, null, "successful test clears cooldown");
});

View File

@@ -1,52 +0,0 @@
import { describe, it } from "node:test";
import { strict as assert } from "node:assert";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, resolve } from "node:path";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const INSTRUMENTATION_NODE_PATH = resolve(
__dirname,
"../../src/instrumentation-node.ts"
);
describe("repro-9624: startCleanupScheduler wired in Next.js startup path", () => {
it("should import startCleanupScheduler from cleanup", () => {
const source = readFileSync(INSTRUMENTATION_NODE_PATH, "utf-8");
// instrumentation-node.ts loads all startup modules via dynamic imports in a
// Promise.all destructure, e.g.:
// const [{ startCleanupScheduler }, ...] = await Promise.all([
// import("@/lib/db/cleanup"), ...
// ]);
// So the binding and the module import appear separately in the file.
const cleanupModuleImported = /import\(\s*["']@\/lib\/db\/cleanup["']\s*\)/.test(
source
);
const schedulerBound = /\bstartCleanupScheduler\b/.test(source);
assert.ok(
cleanupModuleImported,
"@/lib/db/cleanup should be imported (dynamic import) in instrumentation-node.ts"
);
assert.ok(
schedulerBound,
"startCleanupScheduler should be bound in instrumentation-node.ts"
);
});
it("should call startCleanupScheduler() during startup", () => {
const source = readFileSync(INSTRUMENTATION_NODE_PATH, "utf-8");
// Check that startCleanupScheduler is called (as a function call).
// It can be called directly or as part of a conditional.
const hasCall = /\bstartCleanupScheduler\s*\(/.test(source);
assert.ok(
hasCall,
"startCleanupScheduler() should be called in instrumentation-node.ts"
);
});
});

View File

@@ -1,92 +0,0 @@
/**
* Issue #9625 — domain_cost_history cleanup cutoff unit mismatch.
*
* cleanupDomainCostHistory() computes the cutoff in epoch seconds
* (Math.floor(Date.now() / 1000)) but the timestamp column stores
* epoch milliseconds (Date.now()), as inserted by saveCostEntry().
*
* This test seeds data using the same format as the production code
* (milliseconds), then asserts that cleanupDomainCostHistory() correctly
* deletes rows older than the retention window.
*
* Before the fix, the cutoff in seconds was ~1000× smaller than the
* stored timestamps, so the DELETE WHERE timestamp < cutoff would
* never match old rows — the cleanup was effectively a no-op.
*/
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-9625-"));
process.env.DATA_DIR = TEST_DATA_DIR;
const { cleanupDomainCostHistory } = await import("../../src/lib/db/cleanup.ts");
const { getDbInstance, resetDbInstance } = await import("../../src/lib/db/core.ts");
test.after(() => {
resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
const DAY_MS = 86_400_000; // milliseconds
test("#9625 cleanupDomainCostHistory: cutoff in ms matches production timestamps", async () => {
const db = getDbInstance()!;
const now = Date.now(); // milliseconds — same as saveCostEntry() default
const insert = db.prepare(
"INSERT INTO domain_cost_history (api_key_id, cost, timestamp) VALUES (?, ?, ?)"
);
// Seed data using millisecond timestamps (production format).
// 3 old rows: 40 days ago (should be deleted)
// 2 recent rows: 5 days ago (should be kept)
insert.run("key1", 1.0, now - 40 * DAY_MS);
insert.run("key1", 2.0, now - 40 * DAY_MS);
insert.run("key1", 3.0, now - 40 * DAY_MS);
insert.run("key1", 4.0, now - 5 * DAY_MS);
insert.run("key1", 5.0, now - 5 * DAY_MS);
const result = await cleanupDomainCostHistory();
// Before the fix, cutoff was in seconds (~1.7e9) while timestamps
// are in milliseconds (~1.7e12). The comparison `WHERE ts < 1.7e9`
// would never match rows with ts ~1.7e12, so nothing was deleted.
assert.strictEqual(result.deleted, 3, "Should delete 3 old rows (40 days old)");
assert.strictEqual(result.errors, 0);
const remaining = db.prepare("SELECT COUNT(*) as cnt FROM domain_cost_history").get() as {
cnt: number;
};
assert.strictEqual(remaining.cnt, 2, "Should keep 2 recent rows (5 days old)");
});
test("#9625 unit mismatch: seconds cutoff would NOT match ms timestamps", () => {
// Demonstrate the arithmetic bug: a cutoff in seconds is ~1000×
// smaller than a millisecond timestamp, so the WHERE clause never
// matches production data.
const nowMs = Date.now();
const nowSec = Math.floor(nowMs / 1000);
const retentionDays = 30;
const cutoffSec = nowSec - retentionDays * 86_400; // seconds
const cutoffMs = nowMs - retentionDays * 86_400_000; // milliseconds
// A row inserted 40 days ago with a millisecond timestamp:
const oldRowMs = nowMs - 40 * 86_400_000; // ~1.7e12
// With seconds cutoff: oldRowMs (1.7e12) < cutoffSec (1.7e9) is FALSE
// because 1.7e12 > 1.7e9 — the row is never matched.
assert.ok(
oldRowMs > cutoffSec,
"Bug: ms timestamp is NOT less than seconds cutoff, so row is never deleted"
);
// With milliseconds cutoff: oldRowMs (1.7e12) < cutoffMs (1.7e12) is TRUE
assert.ok(
oldRowMs < cutoffMs,
"Fix: ms timestamp IS less than ms cutoff, so row is correctly deleted"
);
});

View File

@@ -1,62 +0,0 @@
import test from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { join } from "node:path";
const root = join(import.meta.dirname, "../..");
const llmChatCardPath =
"src/app/(dashboard)/dashboard/media-providers/components/LlmChatCard.tsx";
const src = readFileSync(join(root, llmChatCardPath), "utf8");
const DISABLED_ON_LOADING = /disabled\s*=\s*\{\s*loading\s*\}/;
const MODELS_LOADING_MARKER = /modelsLoading|Loading…|Loading\.\.\./;
const ERROR_BRANCH = /error\s*&&/;
const RETRY_ACTION = /onClick\s*=\s*\{[^}]*retry|retry[A-Za-z]*\s*\(\)|const\s+\[reload/i;
const NO_MODELS_AFTER_EMPTY = /modelOptions\.length\s*===?\s*0|models\.length\s*===?\s*0/;
test("LlmChatCard destructures loading and error from useProviderModels (#9626)", () => {
const match = src.match(/const\s*\{\s*([^}]+)\s*\}\s*=\s*useProviderModels\(/);
assert.ok(match, "Expected to find a destructuring of useProviderModels");
const destructured = match[1];
assert.ok(
destructured.includes("loading"),
"loading state must be destructured from useProviderModels"
);
assert.ok(destructured.includes("error"), "error state must be destructured from useProviderModels");
});
test("LlmChatCard disables the model selector while models are loading (#9626)", () => {
assert.ok(
DISABLED_ON_LOADING.test(src),
"The model <select> must be disabled while the models request is pending"
);
});
test("LlmChatCard shows a visible loading label while models are pending (#9626)", () => {
assert.ok(
MODELS_LOADING_MARKER.test(src),
"A visible loading text (e.g. 'Loading…') must appear while the models request is pending"
);
});
test("LlmChatCard surfaces the provider model error in the UI (#9626)", () => {
assert.ok(
ERROR_BRANCH.test(src),
"An error branch that renders the captured error message must exist"
);
});
test("LlmChatCard offers a retry action when the model request fails (#9626)", () => {
assert.ok(
RETRY_ACTION.test(src),
"A retry action must be offered next to the model error"
);
});
test("LlmChatCard keeps the empty-state message distinct from an error (#9626)", () => {
assert.ok(
NO_MODELS_AFTER_EMPTY.test(src),
"The empty-state (no models) message must only be shown for a successful empty response"
);
});

View File

@@ -1,27 +0,0 @@
import test from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
const pkg = JSON.parse(readFileSync("package.json", "utf-8"));
const files = pkg.files || [];
// #9633: `build-next-isolated.mjs` is published (fixed in #1126), but three of
// its sibling modules it imports were missing from the `files` whitelist, so
// `npm run build` on a globally-installed package crashed with ERR_MODULE_NOT_FOUND.
// The dynamic import of `build-tproxy-native.mjs` (~line 308) and the static
// imports of `assembleStandalone.mjs` / `backendOnlyPages.mjs` must ship too.
const NEEDED = [
"scripts/build/assembleStandalone.mjs",
"scripts/build/backendOnlyPages.mjs",
"scripts/build/build-tproxy-native.mjs",
"scripts/build/colocateOptionals.mjs",
];
test("#9633: build-next-isolated.mjs sibling imports present in package.json files[]", () => {
for (const needed of NEEDED) {
assert.ok(
files.some((f) => typeof f === "string" && f === needed),
`${needed} is not in package.json files[]`
);
}
});

View File

@@ -40,21 +40,13 @@ test("Responses->Chat: first tool_call chunk announces role=assistant", () => {
);
assert.equal(first.choices[0].delta.tool_calls[0].function.name, "get_weather");
// #9168: arguments deltas are buffered until output_item.done for schema normalization.
// Subsequent argument deltas must NOT repeat the role announcement.
const next = openaiResponsesToOpenAIResponse(
{ type: "response.function_call_arguments.delta", delta: '{"x":1}' },
state
);
assert.equal(next, null, "arguments delta should buffer until output_item.done");
// The args are emitted at output_item.done, and the role is not re-announced.
const done = openaiResponsesToOpenAIResponse(
{ type: "response.output_item.done", item: { type: "function_call", call_id: "call_abc", name: "get_weather" } },
state
);
assert.ok(done, "should emit a chunk for output_item.done");
assert.equal(done.choices[0].delta.role, undefined, "role announcement already happened on first chunk");
assert.equal(done.choices[0].delta.tool_calls[0].function.arguments, '{"x":1}');
assert.ok(next, "should emit a chunk for arguments.delta");
assert.equal(next.choices[0].delta.role, undefined, "only the first delta announces the role");
});
test("Responses->Chat: first text chunk announces role=assistant", () => {

View File

@@ -203,8 +203,7 @@ test("Responses -> OpenAI: incremental tool call events + response.completed sna
},
state
);
// #9168: arguments deltas are buffered until output_item.done for schema normalization
assert.equal(args, null, "args delta should buffer until output_item.done");
assert.ok(args, "should emit args delta chunk");
openaiResponsesToOpenAIResponse(
{

View File

@@ -474,7 +474,7 @@ test("Responses -> OpenAI: tool-call delta, reasoning delta and completed usage
},
state
);
const done = openaiResponsesToOpenAIResponse(
openaiResponsesToOpenAIResponse(
{
type: "response.output_item.done",
item: { type: "function_call", call_id: "call_2", name: "weather" },
@@ -497,10 +497,7 @@ test("Responses -> OpenAI: tool-call delta, reasoning delta and completed usage
);
assert.equal(added.choices[0].delta.tool_calls[0].function.name, "weather");
// #9168: function_call_arguments.delta is buffered and returns null;
// arguments are emitted by output_item.done instead.
assert.equal(args, null);
assert.equal(done.choices[0].delta.tool_calls[0].function.arguments, '{"city":"SP"}');
assert.equal(args.choices[0].delta.tool_calls[0].function.arguments, '{"city":"SP"}');
assert.equal(reasoning.choices[0].delta.reasoning_content, "Need weather info.");
assert.equal(completed.choices[0].finish_reason, "tool_calls");
const comp = completed as {

View File

@@ -42,26 +42,6 @@ test("non-GPT-5.6 models still get max downgraded to xhigh", () => {
)
);
assert.equal(translated.reasoning_effort, "xhigh");
<<<<<<< HEAD
});
// ─────────────────────────────────────────────────────────────────────
// PR #9142 — Anthropic top-level `system` prompts must trigger background detection
// ─────────────────────────────────────────────────────────────────────
const { getBackgroundTaskReason, setBackgroundDegradationConfig } =
await import("../../open-sse/services/backgroundTaskDetector.ts");
test("#9142 Anthropic top-level system prompts must trigger background detection", () => {
setBackgroundDegradationConfig({ enabled: true });
assert.equal(
getBackgroundTaskReason({
system: "Generate a title for this conversation",
messages: [{ role: "user", content: "hello" }],
}),
"system_prompt_pattern"
);
});
=======
// #9140 — VS Code routes filter out built-in auto models
const { isUsableChatModel } = await import(
@@ -79,28 +59,3 @@ test("#9140 VS Code listing must accept built-in auto routing entries", () => {
false,
"operator-created combo should still be rejected"
);
>>>>>>> origin/release/v3.8.50
});
// ── #9160 model discovery: capabilities.effort_tiers ────────────────────────
// #9160: model discovery must ingest capabilities.effort_tiers
test("#9160 model discovery must ingest capabilities.effort_tiers", () => {
assert.deepEqual(
detectSupportedThinkingEfforts({
capabilities: { effort_tiers: ["low", "medium", "high", "xhigh"] },
}),
["low", "medium", "high", "xhigh"]
);
});
test("#9160 capabilities.effort_tiers with duplicate and synonym", () => {
assert.deepEqual(
detectSupportedThinkingEfforts({
capabilities: { effort_tiers: ["low", "low", "max"] },
}),
["low", "xhigh"]
);