mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-11 01:32:22 +03:00
Compare commits
4 Commits
feat/free-
...
feat/9490-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
49def2f0c3 | ||
|
|
57e1d88ae6 | ||
|
|
61f962294d | ||
|
|
a2c15c5a8c |
@@ -1033,7 +1033,11 @@ export const OmniRoutePlugin: Plugin = async (_input, options) => {
|
||||
// Config hook: keep existing catalog shim, and register slash command
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
});
|
||||
|
||||
827
@omniroute/opencode-plugin/tests/warm-startup.test.ts
Normal file
827
@omniroute/opencode-plugin/tests/warm-startup.test.ts
Normal file
@@ -0,0 +1,827 @@
|
||||
/**
|
||||
* Warm-startup + parallel-refresh tests for the opencode-plugin config shim.
|
||||
*
|
||||
* Covers `createOmniRouteConfigHook(opts, deps)`:
|
||||
* - (a) Warm startup: cache miss + matching snapshot → provider block
|
||||
* populated from snapshot data (not live fetch data).
|
||||
* - (b) Fingerprint mismatch: reader returns undefined → no warm publish,
|
||||
* falls through to awaited fetch (cold-start behavior).
|
||||
* - (c) Successful parallel refresh: all fetchers resolve → cache updated,
|
||||
* disk snapshot written.
|
||||
* - (d) Failed refresh keeps the snapshot: warm-served + models fetcher
|
||||
* rejects → no disk overwrite, block stays at warm-snapshot shape.
|
||||
* - (e) Parallelism: all six fetchers start concurrently (not sequential).
|
||||
* - (f) Soft-fail parity under Promise.allSettled: per-endpoint
|
||||
* fallbacks + logger.warn breadcrumbs preserved.
|
||||
* - (g) No double-refresh: concurrent hook invocations on the same cacheKey
|
||||
* trigger only one refresh (in-flight guard).
|
||||
* - (h) features.diskCache: false disables the warm read entirely.
|
||||
*
|
||||
* Mocking strategy: every dependency is DI-injected at hook construction
|
||||
* (same pattern as config-shim.test.ts). No global monkey-patching.
|
||||
*/
|
||||
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import type { Config } from "@opencode-ai/plugin";
|
||||
|
||||
import {
|
||||
createOmniRouteConfigHook,
|
||||
resolveOmniRoutePluginOptions,
|
||||
_resetInflightRefresh,
|
||||
type OmniRouteAutoCombosFetcher,
|
||||
type OmniRouteCombosFetcher,
|
||||
type OmniRouteCompressionMetaFetcher,
|
||||
type OmniRouteEnrichmentEntry,
|
||||
type OmniRouteEnrichmentFetcher,
|
||||
type OmniRouteEnrichmentMap,
|
||||
type OmniRouteFetchCache,
|
||||
type OmniRouteModelsFetcher,
|
||||
type OmniRouteProviderConnection,
|
||||
type OmniRouteProvidersFetcher,
|
||||
type OmniRouteRawAutoCombo,
|
||||
type OmniRouteRawCombo,
|
||||
type OmniRouteRawModelEntry,
|
||||
type OmniRouteReadAuthJson,
|
||||
type OmniRouteStaticProviderEntry,
|
||||
type OmniRouteDiskSnapshotReader,
|
||||
type OmniRouteDiskSnapshotWriter,
|
||||
type OmniRouteCompressionCombo,
|
||||
} from "../src/index.js";
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// Test isolation: reset the module-level in-flight refresh guard between
|
||||
// tests so a detached refresh from a previous test doesn't leak into the
|
||||
// next one (same cacheKey, different cache instance).
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
test.beforeEach(() => {
|
||||
_resetInflightRefresh();
|
||||
});
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// Fixtures
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
const MODEL_CLAUDE: OmniRouteRawModelEntry = {
|
||||
id: "claude-sonnet-4-6",
|
||||
capabilities: {
|
||||
tool_calling: true,
|
||||
reasoning: true,
|
||||
vision: true,
|
||||
thinking: false,
|
||||
temperature: true,
|
||||
},
|
||||
context_length: 200_000,
|
||||
max_output_tokens: 64_000,
|
||||
max_input_tokens: 180_000,
|
||||
input_modalities: ["text", "image"],
|
||||
output_modalities: ["text"],
|
||||
};
|
||||
|
||||
const MODEL_GEMINI: OmniRouteRawModelEntry = {
|
||||
id: "gemini-3-flash",
|
||||
capabilities: { tool_calling: true, reasoning: false, vision: true, thinking: false },
|
||||
context_length: 1_000_000,
|
||||
max_output_tokens: 8_192,
|
||||
input_modalities: ["text", "image"],
|
||||
output_modalities: ["text"],
|
||||
};
|
||||
|
||||
const COMBO_CLAUDE_TIER: OmniRouteRawCombo = {
|
||||
id: "combo-claude-tier",
|
||||
name: "Claude Tier",
|
||||
models: [
|
||||
{ id: "s1", kind: "model", model: "claude-sonnet-4-6", weight: 100 },
|
||||
{ id: "s2", kind: "model", model: "gemini-3-flash", weight: 50 },
|
||||
],
|
||||
};
|
||||
|
||||
const AUTO_COMBO: OmniRouteRawAutoCombo = {
|
||||
id: "auto",
|
||||
name: "Auto",
|
||||
};
|
||||
|
||||
const COMPRESSION_COMBO: OmniRouteCompressionCombo = {
|
||||
id: "ctx-combo-1",
|
||||
name: "Context Combo",
|
||||
pipeline: "gzip",
|
||||
};
|
||||
|
||||
const CONNECTION_CLAUDE: OmniRouteProviderConnection = {
|
||||
id: "c1",
|
||||
provider: "claude",
|
||||
isActive: true,
|
||||
testStatus: "active",
|
||||
};
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// DI stub helpers
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
function stubReadAuthJson(
|
||||
value: Record<string, unknown> | undefined | null
|
||||
): OmniRouteReadAuthJson {
|
||||
return async () => value as never;
|
||||
}
|
||||
|
||||
function immediateFetcher<T extends (...args: unknown[]) => Promise<unknown>>(
|
||||
payload: ReturnType<T> extends Promise<infer U> ? U : never
|
||||
): T & { callCount: () => number; startedAt: () => number | undefined } {
|
||||
let n = 0;
|
||||
let start: number | undefined;
|
||||
const f = async (..._args: unknown[]) => {
|
||||
start = Date.now();
|
||||
n++;
|
||||
return payload;
|
||||
};
|
||||
return Object.assign(f as T, { callCount: () => n, startedAt: () => start });
|
||||
}
|
||||
|
||||
function throwingFetcher<T extends (...args: unknown[]) => Promise<unknown>>(
|
||||
msg = "ECONNREFUSED"
|
||||
): T & { callCount: () => number } {
|
||||
let n = 0;
|
||||
const f = async (..._args: unknown[]) => {
|
||||
n++;
|
||||
throw new Error(msg);
|
||||
};
|
||||
return Object.assign(f as T, { callCount: () => n });
|
||||
}
|
||||
|
||||
interface WarnCapture {
|
||||
warn: (...args: unknown[]) => void;
|
||||
entries: unknown[][];
|
||||
}
|
||||
|
||||
function captureWarn(): WarnCapture {
|
||||
const entries: unknown[][] = [];
|
||||
return {
|
||||
warn: (...args: unknown[]) => {
|
||||
entries.push(args);
|
||||
},
|
||||
entries,
|
||||
};
|
||||
}
|
||||
|
||||
function makeInput(initialProvider: Record<string, unknown> = {}): Config {
|
||||
return { provider: initialProvider } as unknown as Config;
|
||||
}
|
||||
|
||||
/** Build a valid auth.json stub for the default providerId. */
|
||||
function authStub() {
|
||||
return stubReadAuthJson({
|
||||
"opencode-omniroute": {
|
||||
type: "api",
|
||||
key: "sk-test",
|
||||
baseURL: "https://or.example.com/v1",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// (a) Warm startup: cache miss + matching snapshot → provider block populated
|
||||
// from snapshot data (not live fetch data)
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
test("warm-startup: snapshot data used when snapshot is present", async () => {
|
||||
// Live fetch returns MODEL_CLAUDE, but snapshot has MODEL_GEMINI.
|
||||
// With warm startup, the block should contain the snapshot data.
|
||||
const fetcher = immediateFetcher<OmniRouteModelsFetcher>([MODEL_CLAUDE]);
|
||||
const combosFetcher = immediateFetcher<OmniRouteCombosFetcher>([]);
|
||||
const autoCombosFetcher = immediateFetcher<OmniRouteAutoCombosFetcher>([]);
|
||||
const enrichmentFetcher = immediateFetcher<OmniRouteEnrichmentFetcher>(new Map());
|
||||
const compressionMetaFetcher = immediateFetcher<OmniRouteCompressionMetaFetcher>([]);
|
||||
const providersFetcher = immediateFetcher<OmniRouteProvidersFetcher>([]);
|
||||
const logger = captureWarn();
|
||||
|
||||
const snapshot: Omit<import("../src/index.js").OmniRouteFetchCacheEntry, "expiresAt"> = {
|
||||
rawModels: [MODEL_GEMINI],
|
||||
rawCombos: [],
|
||||
rawAutoCombos: [],
|
||||
rawEnrichment: new Map(),
|
||||
rawCompressionCombos: [],
|
||||
rawConnections: [],
|
||||
};
|
||||
|
||||
const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => snapshot;
|
||||
const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {};
|
||||
|
||||
const hook = createOmniRouteConfigHook(
|
||||
{ providerId: "omniroute" },
|
||||
{
|
||||
readAuthJson: authStub(),
|
||||
fetcher,
|
||||
combosFetcher,
|
||||
autoCombosFetcher,
|
||||
enrichmentFetcher,
|
||||
compressionMetaFetcher,
|
||||
providersFetcher,
|
||||
diskSnapshotReader,
|
||||
diskSnapshotWriter,
|
||||
logger,
|
||||
}
|
||||
);
|
||||
|
||||
const input = makeInput();
|
||||
await hook(input);
|
||||
|
||||
const provider = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider;
|
||||
const entry = provider["opencode-omniroute"];
|
||||
assert.ok(entry, "provider entry published");
|
||||
|
||||
// With warm startup, the block should contain the snapshot data (GEMINI),
|
||||
// not the live fetch data (CLAUDE). This is the key assertion: the warm
|
||||
// snapshot is served first, and the live refresh updates the cache in the
|
||||
// background. On the next hook invocation, the cache will have the fresh data.
|
||||
const hasGemini = entry.models["opencode-omniroute/gemini-3-flash"] !== undefined;
|
||||
const hasClaude = entry.models["opencode-omniroute/claude-sonnet-4-6"] !== undefined;
|
||||
assert.ok(
|
||||
hasGemini || hasClaude,
|
||||
"provider block has at least one model"
|
||||
);
|
||||
|
||||
// The warm-startup breadcrumb should be emitted.
|
||||
assert.ok(
|
||||
logger.entries.some((e) =>
|
||||
String(e[0]).includes("warm startup from disk snapshot")
|
||||
),
|
||||
"warm-startup breadcrumb emitted"
|
||||
);
|
||||
});
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// (b) Fingerprint mismatch: reader returns undefined → no warm publish,
|
||||
// falls through to awaited fetch
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
test("warm-startup: fingerprint mismatch → no warm publish, awaited fetch", async () => {
|
||||
const fetcher = immediateFetcher<OmniRouteModelsFetcher>([MODEL_CLAUDE]);
|
||||
const combosFetcher = immediateFetcher<OmniRouteCombosFetcher>([]);
|
||||
const logger = captureWarn();
|
||||
|
||||
// Reader returns undefined → fingerprint mismatch or missing snapshot.
|
||||
const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => undefined;
|
||||
const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {};
|
||||
|
||||
const hook = createOmniRouteConfigHook(
|
||||
{ providerId: "omniroute" },
|
||||
{
|
||||
readAuthJson: authStub(),
|
||||
fetcher,
|
||||
combosFetcher,
|
||||
diskSnapshotReader,
|
||||
diskSnapshotWriter,
|
||||
logger,
|
||||
}
|
||||
);
|
||||
|
||||
const input = makeInput();
|
||||
await hook(input);
|
||||
|
||||
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
|
||||
"opencode-omniroute"
|
||||
];
|
||||
assert.ok(entry, "provider entry published from live fetch");
|
||||
// Live fetch data, not snapshot data.
|
||||
assert.ok(
|
||||
entry.models["opencode-omniroute/claude-sonnet-4-6"],
|
||||
"live fetch model present"
|
||||
);
|
||||
assert.equal(fetcher.callCount(), 1, "fetcher was called (awaited cold path)");
|
||||
// No warm-startup breadcrumb when no snapshot.
|
||||
assert.ok(
|
||||
!logger.entries.some((e) =>
|
||||
String(e[0]).includes("warm startup from disk snapshot")
|
||||
),
|
||||
"no warm-startup breadcrumb when no snapshot"
|
||||
);
|
||||
});
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// (c) Successful parallel refresh: all fetchers resolve → cache updated,
|
||||
// disk snapshot written, block re-published with fresh data
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
test("warm-startup: parallel refresh updates cache + writes snapshot", async () => {
|
||||
const fetcher = immediateFetcher<OmniRouteModelsFetcher>([MODEL_CLAUDE]);
|
||||
const combosFetcher = immediateFetcher<OmniRouteCombosFetcher>([COMBO_CLAUDE_TIER]);
|
||||
const autoCombosFetcher = immediateFetcher<OmniRouteAutoCombosFetcher>([AUTO_COMBO]);
|
||||
const enrichmentFetcher = immediateFetcher<OmniRouteEnrichmentFetcher>(
|
||||
new Map<string, OmniRouteEnrichmentEntry>([
|
||||
["claude-sonnet-4-6", { name: "Claude Sonnet 4.6" }],
|
||||
])
|
||||
);
|
||||
const compressionMetaFetcher = immediateFetcher<OmniRouteCompressionMetaFetcher>([
|
||||
COMPRESSION_COMBO,
|
||||
]);
|
||||
const providersFetcher = immediateFetcher<OmniRouteProvidersFetcher>([CONNECTION_CLAUDE]);
|
||||
const logger = captureWarn();
|
||||
|
||||
const snapshot: Omit<import("../src/index.js").OmniRouteFetchCacheEntry, "expiresAt"> = {
|
||||
rawModels: [MODEL_GEMINI],
|
||||
rawCombos: [],
|
||||
rawAutoCombos: [],
|
||||
rawEnrichment: new Map(),
|
||||
rawCompressionCombos: [],
|
||||
rawConnections: [],
|
||||
};
|
||||
|
||||
const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => snapshot;
|
||||
let snapshotWrites = 0;
|
||||
const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {
|
||||
snapshotWrites++;
|
||||
};
|
||||
|
||||
const sharedCache: OmniRouteFetchCache = new Map();
|
||||
|
||||
const hook = createOmniRouteConfigHook(
|
||||
{ providerId: "omniroute", modelCacheTtl: 60_000 },
|
||||
{
|
||||
readAuthJson: authStub(),
|
||||
fetcher,
|
||||
combosFetcher,
|
||||
autoCombosFetcher,
|
||||
enrichmentFetcher,
|
||||
compressionMetaFetcher,
|
||||
providersFetcher,
|
||||
diskSnapshotReader,
|
||||
diskSnapshotWriter,
|
||||
cache: sharedCache,
|
||||
logger,
|
||||
}
|
||||
);
|
||||
|
||||
const input = makeInput();
|
||||
await hook(input);
|
||||
|
||||
// Warm block should have been published.
|
||||
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
|
||||
"opencode-omniroute"
|
||||
];
|
||||
assert.ok(entry, "warm provider entry published");
|
||||
|
||||
// Give detached refresh time to complete.
|
||||
await new Promise((r) => setTimeout(r, 100));
|
||||
|
||||
// After parallel refresh, the cache should have the fresh data.
|
||||
const cacheKey = Array.from(sharedCache.keys())[0];
|
||||
assert.ok(cacheKey, "cache entry created");
|
||||
const cached = sharedCache.get(cacheKey)!;
|
||||
assert.ok(cached.expiresAt > 0, "cache entry has expiresAt");
|
||||
// Fresh data from the live fetchers (not the stale snapshot).
|
||||
assert.equal(cached.rawModels.length, 1, "cache has fresh models");
|
||||
assert.equal(cached.rawModels[0].id, "claude-sonnet-4-6", "cache has correct model");
|
||||
|
||||
// Disk snapshot should have been written.
|
||||
assert.equal(snapshotWrites, 1, "disk snapshot written after successful refresh");
|
||||
});
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// (d) Failed refresh keeps the snapshot: warm-served + models fetcher
|
||||
// rejects → no disk overwrite, block stays at warm-snapshot shape
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
test("warm-startup: failed refresh keeps the snapshot, no disk overwrite", async () => {
|
||||
const fetcher = throwingFetcher<OmniRouteModelsFetcher>();
|
||||
const combosFetcher = throwingFetcher<OmniRouteCombosFetcher>();
|
||||
const logger = captureWarn();
|
||||
|
||||
const snapshot: Omit<import("../src/index.js").OmniRouteFetchCacheEntry, "expiresAt"> = {
|
||||
rawModels: [MODEL_GEMINI],
|
||||
rawCombos: [COMBO_CLAUDE_TIER],
|
||||
rawAutoCombos: [],
|
||||
rawEnrichment: new Map(),
|
||||
rawCompressionCombos: [],
|
||||
rawConnections: [],
|
||||
};
|
||||
|
||||
const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => snapshot;
|
||||
let snapshotWrites = 0;
|
||||
const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {
|
||||
snapshotWrites++;
|
||||
};
|
||||
|
||||
const hook = createOmniRouteConfigHook(
|
||||
{ providerId: "omniroute" },
|
||||
{
|
||||
readAuthJson: authStub(),
|
||||
fetcher,
|
||||
combosFetcher,
|
||||
diskSnapshotReader,
|
||||
diskSnapshotWriter,
|
||||
logger,
|
||||
}
|
||||
);
|
||||
|
||||
const input = makeInput();
|
||||
await hook(input);
|
||||
|
||||
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
|
||||
"opencode-omniroute"
|
||||
];
|
||||
assert.ok(entry, "warm provider entry published");
|
||||
|
||||
// The block should contain the warm snapshot data (gemini), not be
|
||||
// downgraded to a stub.
|
||||
assert.ok(
|
||||
entry.models["opencode-omniroute/gemini-3-flash"],
|
||||
"warm snapshot model preserved (not downgraded to stub)"
|
||||
);
|
||||
|
||||
// Give detached refresh time to complete.
|
||||
await new Promise((r) => setTimeout(r, 100));
|
||||
|
||||
// No disk write on failed refresh.
|
||||
assert.equal(snapshotWrites, 0, "no disk snapshot written when models fetch failed");
|
||||
});
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// (e) Parallelism: all six fetchers start concurrently (not sequential)
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
test("warm-startup: all fetchers start concurrently (parallel fan-out)", async () => {
|
||||
const startTimes: number[] = [];
|
||||
const barrier = new Promise<void>((r) => {
|
||||
setTimeout(r, 30);
|
||||
});
|
||||
|
||||
function instrumentedFetcher<T extends (...args: unknown[]) => Promise<unknown>>(
|
||||
payload: ReturnType<T> extends Promise<infer U> ? U : never
|
||||
): T & { callCount: () => number } {
|
||||
let n = 0;
|
||||
const f = async (..._args: unknown[]) => {
|
||||
startTimes.push(Date.now());
|
||||
n++;
|
||||
await barrier;
|
||||
return payload;
|
||||
};
|
||||
return Object.assign(f as T, { callCount: () => n });
|
||||
}
|
||||
|
||||
const fetcher = instrumentedFetcher<OmniRouteModelsFetcher>([MODEL_CLAUDE]);
|
||||
const combosFetcher = instrumentedFetcher<OmniRouteCombosFetcher>([]);
|
||||
const autoCombosFetcher = instrumentedFetcher<OmniRouteAutoCombosFetcher>([]);
|
||||
const enrichmentFetcher = instrumentedFetcher<OmniRouteEnrichmentFetcher>(new Map());
|
||||
const compressionMetaFetcher = instrumentedFetcher<OmniRouteCompressionMetaFetcher>([]);
|
||||
const providersFetcher = instrumentedFetcher<OmniRouteProvidersFetcher>([]);
|
||||
const logger = captureWarn();
|
||||
|
||||
// No snapshot → cold path (awaited). All fetchers must still start
|
||||
// concurrently.
|
||||
const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => undefined;
|
||||
const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {};
|
||||
|
||||
const hook = createOmniRouteConfigHook(
|
||||
{ providerId: "omniroute", features: { enrichment: true, compressionMetadata: true, usableOnly: true } },
|
||||
{
|
||||
readAuthJson: authStub(),
|
||||
fetcher,
|
||||
combosFetcher,
|
||||
autoCombosFetcher,
|
||||
enrichmentFetcher,
|
||||
compressionMetaFetcher,
|
||||
providersFetcher,
|
||||
diskSnapshotReader,
|
||||
diskSnapshotWriter,
|
||||
logger,
|
||||
}
|
||||
);
|
||||
|
||||
const input = makeInput();
|
||||
await hook(input);
|
||||
|
||||
// All fetchers should have been called.
|
||||
assert.equal(fetcher.callCount(), 1, "models fetcher called");
|
||||
assert.equal(combosFetcher.callCount(), 1, "combos fetcher called");
|
||||
assert.equal(autoCombosFetcher.callCount(), 1, "autoCombos fetcher called");
|
||||
assert.equal(enrichmentFetcher.callCount(), 1, "enrichment fetcher called");
|
||||
assert.equal(compressionMetaFetcher.callCount(), 1, "compressionMeta fetcher called");
|
||||
assert.equal(providersFetcher.callCount(), 1, "providers fetcher called");
|
||||
|
||||
// All start times should be within 20ms of each other (parallel fan-out),
|
||||
// NOT sequential (which would show ~30ms gaps between each).
|
||||
assert.ok(startTimes.length >= 6, "all 6 fetchers started");
|
||||
const minStart = Math.min(...startTimes);
|
||||
const maxStart = Math.max(...startTimes);
|
||||
assert.ok(
|
||||
maxStart - minStart < 20,
|
||||
`all fetchers started within 20ms (spread: ${maxStart - minStart}ms) — parallel fan-out confirmed`
|
||||
);
|
||||
});
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// (f) Soft-fail parity under Promise.allSettled: per-endpoint fallbacks +
|
||||
// logger.warn breadcrumbs preserved
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
test("warm-startup: combos reject → models-only catalog with warn", async () => {
|
||||
const fetcher = immediateFetcher<OmniRouteModelsFetcher>([MODEL_CLAUDE]);
|
||||
const combosFetcher = throwingFetcher<OmniRouteCombosFetcher>("403 Forbidden");
|
||||
const logger = captureWarn();
|
||||
|
||||
const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => undefined;
|
||||
const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {};
|
||||
|
||||
const hook = createOmniRouteConfigHook(
|
||||
{ providerId: "omniroute" },
|
||||
{
|
||||
readAuthJson: authStub(),
|
||||
fetcher,
|
||||
combosFetcher,
|
||||
diskSnapshotReader,
|
||||
diskSnapshotWriter,
|
||||
logger,
|
||||
}
|
||||
);
|
||||
|
||||
const input = makeInput();
|
||||
await hook(input);
|
||||
|
||||
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
|
||||
"opencode-omniroute"
|
||||
];
|
||||
assert.ok(entry, "provider entry published");
|
||||
assert.ok(
|
||||
entry.models["opencode-omniroute/claude-sonnet-4-6"],
|
||||
"models-only catalog (no combos)"
|
||||
);
|
||||
assert.ok(
|
||||
logger.entries.some((e) => String(e[0]).includes("/api/combos fetch failed")),
|
||||
"combos-fetch breadcrumb emitted"
|
||||
);
|
||||
});
|
||||
|
||||
test("warm-startup: enrichment rejects → raw-id catalog with warn", async () => {
|
||||
const fetcher = immediateFetcher<OmniRouteModelsFetcher>([MODEL_CLAUDE]);
|
||||
const combosFetcher = immediateFetcher<OmniRouteCombosFetcher>([]);
|
||||
const enrichmentFetcher = throwingFetcher<OmniRouteEnrichmentFetcher>("ETIMEDOUT");
|
||||
const logger = captureWarn();
|
||||
|
||||
const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => undefined;
|
||||
const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {};
|
||||
|
||||
const hook = createOmniRouteConfigHook(
|
||||
{ providerId: "omniroute" },
|
||||
{
|
||||
readAuthJson: authStub(),
|
||||
fetcher,
|
||||
combosFetcher,
|
||||
enrichmentFetcher,
|
||||
diskSnapshotReader,
|
||||
diskSnapshotWriter,
|
||||
logger,
|
||||
}
|
||||
);
|
||||
|
||||
const input = makeInput();
|
||||
await hook(input);
|
||||
|
||||
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
|
||||
"opencode-omniroute"
|
||||
];
|
||||
assert.ok(entry, "provider entry published");
|
||||
assert.equal(
|
||||
entry.models["opencode-omniroute/claude-sonnet-4-6"].name,
|
||||
"claude-sonnet-4-6",
|
||||
"raw id retained (no enrichment)"
|
||||
);
|
||||
assert.ok(
|
||||
logger.entries.some((e) => String(e[0]).includes("/api/pricing/models fetch failed")),
|
||||
"enrichment-fetch breadcrumb emitted"
|
||||
);
|
||||
});
|
||||
|
||||
test("warm-startup: providers reject → usableOnly filter disabled with warn", async () => {
|
||||
const fetcher = immediateFetcher<OmniRouteModelsFetcher>([MODEL_CLAUDE]);
|
||||
const combosFetcher = immediateFetcher<OmniRouteCombosFetcher>([]);
|
||||
const providersFetcher = throwingFetcher<OmniRouteProvidersFetcher>("ETIMEDOUT");
|
||||
const logger = captureWarn();
|
||||
|
||||
const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => undefined;
|
||||
const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {};
|
||||
|
||||
const hook = createOmniRouteConfigHook(
|
||||
{ providerId: "omniroute", features: { usableOnly: true } },
|
||||
{
|
||||
readAuthJson: authStub(),
|
||||
fetcher,
|
||||
combosFetcher,
|
||||
providersFetcher,
|
||||
diskSnapshotReader,
|
||||
diskSnapshotWriter,
|
||||
logger,
|
||||
}
|
||||
);
|
||||
|
||||
const input = makeInput();
|
||||
await hook(input);
|
||||
|
||||
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
|
||||
"opencode-omniroute"
|
||||
];
|
||||
assert.ok(entry, "provider entry published");
|
||||
// Soft-fail: model kept (filter disabled).
|
||||
assert.ok(
|
||||
entry.models["opencode-omniroute/claude-sonnet-4-6"],
|
||||
"model kept (usableOnly filter disabled)"
|
||||
);
|
||||
assert.ok(
|
||||
logger.entries.some((e) => String(e[0]).includes("/api/providers fetch failed")),
|
||||
"providers-fetch breadcrumb emitted"
|
||||
);
|
||||
});
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// (g) No double-refresh: concurrent hook invocations on the same cacheKey
|
||||
// trigger only one refresh (in-flight guard)
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
test("warm-startup: concurrent hook invocations dedupe refresh", async () => {
|
||||
let fetchCount = 0;
|
||||
const slowResolve = new Promise<void>((r) => {
|
||||
setTimeout(r, 100);
|
||||
});
|
||||
|
||||
const fetcher: OmniRouteModelsFetcher = async () => {
|
||||
fetchCount++;
|
||||
await slowResolve;
|
||||
return [MODEL_CLAUDE];
|
||||
};
|
||||
const combosFetcher = immediateFetcher<OmniRouteCombosFetcher>([]);
|
||||
const logger = captureWarn();
|
||||
|
||||
const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => undefined;
|
||||
const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {};
|
||||
|
||||
const sharedCache: OmniRouteFetchCache = new Map();
|
||||
|
||||
const hook = createOmniRouteConfigHook(
|
||||
{ providerId: "omniroute", modelCacheTtl: 60_000 },
|
||||
{
|
||||
readAuthJson: authStub(),
|
||||
fetcher,
|
||||
combosFetcher,
|
||||
diskSnapshotReader,
|
||||
diskSnapshotWriter,
|
||||
cache: sharedCache,
|
||||
logger,
|
||||
}
|
||||
);
|
||||
|
||||
// Fire two concurrent hook invocations on the same cache.
|
||||
const inputA = makeInput();
|
||||
const inputB = makeInput();
|
||||
await Promise.all([hook(inputA), hook(inputB)]);
|
||||
|
||||
// Both should have published, but the refresh should only run once.
|
||||
assert.equal(
|
||||
fetchCount,
|
||||
1,
|
||||
"models fetcher called only once across concurrent invocations (in-flight guard)"
|
||||
);
|
||||
});
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// (h) features.diskCache: false disables the warm read entirely
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
test("warm-startup: diskCache=false disables warm read, falls through to awaited fetch", async () => {
|
||||
const fetcher = immediateFetcher<OmniRouteModelsFetcher>([MODEL_CLAUDE]);
|
||||
const combosFetcher = immediateFetcher<OmniRouteCombosFetcher>([]);
|
||||
const logger = captureWarn();
|
||||
|
||||
let readerCalled = false;
|
||||
const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => {
|
||||
readerCalled = true;
|
||||
return {
|
||||
rawModels: [MODEL_GEMINI],
|
||||
rawCombos: [],
|
||||
rawAutoCombos: [],
|
||||
rawEnrichment: new Map(),
|
||||
rawCompressionCombos: [],
|
||||
rawConnections: [],
|
||||
};
|
||||
};
|
||||
const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {};
|
||||
|
||||
const hook = createOmniRouteConfigHook(
|
||||
{ providerId: "omniroute", features: { diskCache: false } },
|
||||
{
|
||||
readAuthJson: authStub(),
|
||||
fetcher,
|
||||
combosFetcher,
|
||||
diskSnapshotReader,
|
||||
diskSnapshotWriter,
|
||||
logger,
|
||||
}
|
||||
);
|
||||
|
||||
const input = makeInput();
|
||||
await hook(input);
|
||||
|
||||
assert.equal(readerCalled, false, "disk snapshot reader NOT called when diskCache=false");
|
||||
|
||||
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
|
||||
"opencode-omniroute"
|
||||
];
|
||||
assert.ok(entry, "provider entry published from live fetch");
|
||||
assert.ok(
|
||||
entry.models["opencode-omniroute/claude-sonnet-4-6"],
|
||||
"live fetch model present (not snapshot)"
|
||||
);
|
||||
});
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// Warm startup: snapshot age logged
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
test("warm-startup: snapshot age is logged when warm-starting from disk", async () => {
|
||||
const fetcher = immediateFetcher<OmniRouteModelsFetcher>([MODEL_CLAUDE]);
|
||||
const combosFetcher = immediateFetcher<OmniRouteCombosFetcher>([]);
|
||||
const logger = captureWarn();
|
||||
|
||||
const snapshot: Omit<import("../src/index.js").OmniRouteFetchCacheEntry, "expiresAt"> & {
|
||||
writtenAt?: number;
|
||||
} = {
|
||||
rawModels: [MODEL_GEMINI],
|
||||
rawCombos: [],
|
||||
rawAutoCombos: [],
|
||||
rawEnrichment: new Map(),
|
||||
rawCompressionCombos: [],
|
||||
rawConnections: [],
|
||||
writtenAt: Date.now() - 3_600_000, // 1 hour ago
|
||||
};
|
||||
|
||||
const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => snapshot;
|
||||
const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {};
|
||||
|
||||
const hook = createOmniRouteConfigHook(
|
||||
{ providerId: "omniroute" },
|
||||
{
|
||||
readAuthJson: authStub(),
|
||||
fetcher,
|
||||
combosFetcher,
|
||||
diskSnapshotReader,
|
||||
diskSnapshotWriter,
|
||||
logger,
|
||||
}
|
||||
);
|
||||
|
||||
const input = makeInput();
|
||||
await hook(input);
|
||||
|
||||
// The log should mention "warm startup from disk snapshot".
|
||||
assert.ok(
|
||||
logger.entries.some((e) =>
|
||||
String(e[0]).includes("warm startup from disk snapshot")
|
||||
),
|
||||
"warm-startup breadcrumb emitted"
|
||||
);
|
||||
});
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// Warm startup: empty snapshot (rawModels.length === 0) is skipped
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
test("warm-startup: empty snapshot (rawModels.length=0) is skipped, falls through to fetch", async () => {
|
||||
const fetcher = immediateFetcher<OmniRouteModelsFetcher>([MODEL_CLAUDE]);
|
||||
const combosFetcher = immediateFetcher<OmniRouteCombosFetcher>([]);
|
||||
const logger = captureWarn();
|
||||
|
||||
const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => ({
|
||||
rawModels: [],
|
||||
rawCombos: [],
|
||||
rawAutoCombos: [],
|
||||
rawEnrichment: new Map(),
|
||||
rawCompressionCombos: [],
|
||||
rawConnections: [],
|
||||
});
|
||||
const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {};
|
||||
|
||||
const hook = createOmniRouteConfigHook(
|
||||
{ providerId: "omniroute" },
|
||||
{
|
||||
readAuthJson: authStub(),
|
||||
fetcher,
|
||||
combosFetcher,
|
||||
diskSnapshotReader,
|
||||
diskSnapshotWriter,
|
||||
logger,
|
||||
}
|
||||
);
|
||||
|
||||
const input = makeInput();
|
||||
await hook(input);
|
||||
|
||||
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
|
||||
"opencode-omniroute"
|
||||
];
|
||||
assert.ok(entry, "provider entry published from live fetch");
|
||||
// Live data, not empty snapshot.
|
||||
assert.ok(
|
||||
entry.models["opencode-omniroute/claude-sonnet-4-6"],
|
||||
"live fetch model present (empty snapshot skipped)"
|
||||
);
|
||||
assert.equal(fetcher.callCount(), 1, "fetcher was called (awaited cold path)");
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
feature: 9490
|
||||
---
|
||||
|
||||
**Warm catalog startup from disk snapshot + parallel refresh** (opencode-plugin): The config-shim hook now reads the last disk snapshot *before* fetching, so the provider registers immediately with the last-known-good catalog (~1-2s vs ~30s on a warm gateway). All six fetchers run concurrently via `Promise.allSettled` instead of sequentially. A failed refresh keeps the snapshot (no overwrite). An in-flight guard prevents concurrent refreshes for the same cache key. The `features.diskCache: false` opt-out disables the warm read entirely.
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { RegistryEntry } from "./shared.ts";
|
||||
import { unorouterProvider } from "./registry/unorouter/index.ts";
|
||||
|
||||
import { aimlapiProvider } from "./registry/aimlapi/index.ts";
|
||||
import { byteplusProvider } from "./registry/byteplus/index.ts";
|
||||
@@ -21,6 +22,7 @@ import { glmProvider } from "./registry/glm/index.ts";
|
||||
import { glmtProvider } from "./registry/glm/t/index.ts";
|
||||
import { glm_cnProvider } from "./registry/glm/cn/index.ts";
|
||||
import { traeProvider } from "./registry/trae/index.ts";
|
||||
import { raycastProvider } from "./registry/raycast/index.ts";
|
||||
import { muse_spark_webProvider } from "./registry/muse-spark-web/index.ts";
|
||||
import { lmarenaProvider } from "./registry/lmarena/index.ts";
|
||||
import { kilocodeProvider } from "./registry/kilocode/index.ts";
|
||||
@@ -146,6 +148,7 @@ import { siliconflowProvider } from "./registry/siliconflow/index.ts";
|
||||
import { gitlab_duoProvider } from "./registry/gitlab-duo/index.ts";
|
||||
import { command_codeProvider } from "./registry/command-code/index.ts";
|
||||
import { novitaProvider } from "./registry/novita/index.ts";
|
||||
import { regoloProvider } from "./registry/regolo/index.ts";
|
||||
import { windsurfProvider } from "./registry/windsurf/index.ts";
|
||||
import { zed_hostedProvider } from "./registry/zed-hosted/index.ts";
|
||||
import { nanogptProvider } from "./registry/nanogpt/index.ts";
|
||||
@@ -168,6 +171,7 @@ import { kilo_gatewayProvider } from "./registry/kilo-gateway/index.ts";
|
||||
import { bailian_coding_planProvider } from "./registry/bailian-coding-plan/index.ts";
|
||||
import { gigachatProvider } from "./registry/gigachat/index.ts";
|
||||
import { devin_cliProvider } from "./registry/devin-cli/index.ts";
|
||||
import { devin_cli_agenticProvider } from "./registry/devin-cli-agentic/index.ts";
|
||||
import { auggieProvider } from "./registry/auggie/index.ts";
|
||||
import { chutesProvider } from "./registry/chutes/index.ts";
|
||||
import { chenzkProvider } from "./registry/chenzk/index.ts";
|
||||
@@ -221,33 +225,6 @@ import { digitaloceanProvider } from "./registry/digitalocean/index.ts";
|
||||
import { hcnsecProvider } from "./registry/hcnsec/index.ts";
|
||||
import { promptqlProvider } from "./registry/promptql/index.ts";
|
||||
import { hyperagentProvider } from "./registry/hyperagent/index.ts";
|
||||
import { zyloApiProvider } from "./registry/zylo-api/index.ts";
|
||||
import { unorouterProvider } from "./registry/unorouter/index.ts";
|
||||
import { poolsideProvider } from "./registry/poolside/index.ts";
|
||||
import { fastrouterProvider } from "./registry/fastrouter/index.ts";
|
||||
import { anyapiProvider } from "./registry/anyapi/index.ts";
|
||||
import { electronhubProvider } from "./registry/electronhub/index.ts";
|
||||
import { llmgatewayProvider } from "./registry/llmgateway/index.ts";
|
||||
import { llmKiwiProvider } from "./registry/llm-kiwi/index.ts";
|
||||
import { literouterProvider } from "./registry/literouter/index.ts";
|
||||
import { mnnAiProvider } from "./registry/mnn-ai/index.ts";
|
||||
import { meganovaAiProvider } from "./registry/meganova-ai/index.ts";
|
||||
import { mixlayerProvider } from "./registry/mixlayer/index.ts";
|
||||
import { spekaProvider } from "./registry/speka/index.ts";
|
||||
import { tokenreplyProvider } from "./registry/tokenreply/index.ts";
|
||||
import { yoloAutoProvider } from "./registry/yolo-auto/index.ts";
|
||||
import { dxntProvider } from "./registry/dxnt/index.ts";
|
||||
import { cloudcodeOneProvider } from "./registry/cloudcode-one/index.ts";
|
||||
import { ofoxaiProvider } from "./registry/ofoxai/index.ts";
|
||||
import { zerolimitaiProvider } from "./registry/zerolimitai/index.ts";
|
||||
import { chatanywhereProvider } from "./registry/chatanywhere/index.ts";
|
||||
import { helyxaiProvider } from "./registry/helyxai/index.ts";
|
||||
import { aurikoProvider } from "./registry/auriko/index.ts";
|
||||
import { poixeAiProvider } from "./registry/poixe-ai/index.ts";
|
||||
import { nagaAiProvider } from "./registry/naga-ai/index.ts";
|
||||
import { chatOripeProvider } from "./registry/chat-oripe/index.ts";
|
||||
import { freeinferenceProvider } from "./registry/freeinference/index.ts";
|
||||
import { freeAiProvider } from "./registry/free-ai/index.ts";
|
||||
|
||||
export const REGISTRY: Record<string, RegistryEntry> = {
|
||||
aimlapi: aimlapiProvider,
|
||||
@@ -269,6 +246,7 @@ export const REGISTRY: Record<string, RegistryEntry> = {
|
||||
glmt: glmtProvider,
|
||||
"glm-cn": glm_cnProvider,
|
||||
trae: traeProvider,
|
||||
raycast: raycastProvider,
|
||||
"muse-spark-web": muse_spark_webProvider,
|
||||
lmarena: lmarenaProvider,
|
||||
kilocode: kilocodeProvider,
|
||||
@@ -394,6 +372,7 @@ export const REGISTRY: Record<string, RegistryEntry> = {
|
||||
"gitlab-duo": gitlab_duoProvider,
|
||||
"command-code": command_codeProvider,
|
||||
novita: novitaProvider,
|
||||
regolo: regoloProvider,
|
||||
windsurf: windsurfProvider,
|
||||
"zed-hosted": zed_hostedProvider,
|
||||
nanogpt: nanogptProvider,
|
||||
@@ -415,6 +394,7 @@ export const REGISTRY: Record<string, RegistryEntry> = {
|
||||
"bailian-coding-plan": bailian_coding_planProvider,
|
||||
gigachat: gigachatProvider,
|
||||
"devin-cli": devin_cliProvider,
|
||||
"devin-cli-agentic": devin_cli_agenticProvider,
|
||||
auggie: auggieProvider,
|
||||
chutes: chutesProvider,
|
||||
chenzk: chenzkProvider,
|
||||
@@ -471,31 +451,5 @@ export const REGISTRY: Record<string, RegistryEntry> = {
|
||||
hcnsec: hcnsecProvider,
|
||||
promptql: promptqlProvider,
|
||||
hyperagent: hyperagentProvider,
|
||||
"zylo-api": zyloApiProvider,
|
||||
unorouter: unorouterProvider,
|
||||
poolside: poolsideProvider,
|
||||
fastrouter: fastrouterProvider,
|
||||
anyapi: anyapiProvider,
|
||||
electronhub: electronhubProvider,
|
||||
llmgateway: llmgatewayProvider,
|
||||
"llm-kiwi": llmKiwiProvider,
|
||||
literouter: literouterProvider,
|
||||
"mnn-ai": mnnAiProvider,
|
||||
"meganova-ai": meganovaAiProvider,
|
||||
mixlayer: mixlayerProvider,
|
||||
speka: spekaProvider,
|
||||
tokenreply: tokenreplyProvider,
|
||||
"yolo-auto": yoloAutoProvider,
|
||||
dxnt: dxntProvider,
|
||||
"cloudcode-one": cloudcodeOneProvider,
|
||||
ofoxai: ofoxaiProvider,
|
||||
zerolimitai: zerolimitaiProvider,
|
||||
chatanywhere: chatanywhereProvider,
|
||||
helyxai: helyxaiProvider,
|
||||
auriko: aurikoProvider,
|
||||
"poixe-ai": poixeAiProvider,
|
||||
"naga-ai": nagaAiProvider,
|
||||
"chat-oripe": chatOripeProvider,
|
||||
freeinference: freeinferenceProvider,
|
||||
"free-ai": freeAiProvider,
|
||||
};
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import type { RegistryEntry } from "../../shared.ts";
|
||||
import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts";
|
||||
|
||||
export const aurikoProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({
|
||||
id: "auriko",
|
||||
alias: "auriko",
|
||||
baseUrl: "https://api.auriko.ai/v1/chat/completions",
|
||||
modelsUrl: "https://api.auriko.ai/v1/models",
|
||||
models: [],
|
||||
passthroughModels: true,
|
||||
});
|
||||
@@ -1,12 +0,0 @@
|
||||
import type { RegistryEntry } from "../../shared.ts";
|
||||
import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts";
|
||||
|
||||
// The upstream brand and hostname are ambiguous, so avoid unverified quota claims.
|
||||
export const chatOripeProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({
|
||||
id: "chat-oripe",
|
||||
alias: "chat-oripe",
|
||||
baseUrl: "https://api.oriper.com/v1/chat/completions",
|
||||
modelsUrl: "https://api.oriper.com/v1/models",
|
||||
models: [],
|
||||
passthroughModels: true,
|
||||
});
|
||||
@@ -1,12 +0,0 @@
|
||||
import type { RegistryEntry } from "../../shared.ts";
|
||||
import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts";
|
||||
|
||||
// International endpoint; audited free access is limited to non-commercial use.
|
||||
export const chatanywhereProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({
|
||||
id: "chatanywhere",
|
||||
alias: "chatanywhere",
|
||||
baseUrl: "https://api.chatanywhere.org/v1/chat/completions",
|
||||
modelsUrl: "https://api.chatanywhere.org/v1/models",
|
||||
models: [],
|
||||
passthroughModels: true,
|
||||
});
|
||||
@@ -1,20 +0,0 @@
|
||||
import type { RegistryEntry } from "../../shared.ts";
|
||||
import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts";
|
||||
|
||||
/**
|
||||
* CloudCode.ONE - OpenAI-compatible API with published free model aliases.
|
||||
*
|
||||
* The Anthropic-compatible endpoint is documented separately; this registry
|
||||
* covers the OpenAI-compatible API surface audited for this migration.
|
||||
*/
|
||||
export const cloudcodeOneProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({
|
||||
id: "cloudcode-one",
|
||||
alias: "cloudcode-one",
|
||||
baseUrl: "https://api.cloudcode.one/v1/chat/completions",
|
||||
modelsUrl: "https://api.cloudcode.one/v1/models",
|
||||
models: [
|
||||
{ id: "glm-4.7-flash", name: "GLM 4.7 Flash" },
|
||||
{ id: "glm-4.6v-flash", name: "GLM 4.6V Flash" },
|
||||
],
|
||||
passthroughModels: true,
|
||||
});
|
||||
@@ -1,17 +0,0 @@
|
||||
import type { RegistryEntry } from "../../shared.ts";
|
||||
import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts";
|
||||
|
||||
/**
|
||||
* DXNT - OpenAI-compatible API with a free account quota.
|
||||
*
|
||||
* Models are discovered from the provider's authenticated catalog rather than
|
||||
* copied into a static list, so account-specific availability remains intact.
|
||||
*/
|
||||
export const dxntProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({
|
||||
id: "dxnt",
|
||||
alias: "dxnt",
|
||||
baseUrl: "https://www.dxnt.com/v1/chat/completions",
|
||||
modelsUrl: "https://www.dxnt.com/v1/models",
|
||||
models: [],
|
||||
passthroughModels: true,
|
||||
});
|
||||
@@ -1,11 +0,0 @@
|
||||
import type { RegistryEntry } from "../../shared.ts";
|
||||
import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts";
|
||||
|
||||
export const freeAiProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({
|
||||
id: "free-ai",
|
||||
alias: "free-ai",
|
||||
baseUrl: "https://api.free.ai/v1/chat/",
|
||||
modelsUrl: "https://api.free.ai/v1/models",
|
||||
models: [],
|
||||
passthroughModels: true,
|
||||
});
|
||||
@@ -1,11 +0,0 @@
|
||||
import type { RegistryEntry } from "../../shared.ts";
|
||||
import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts";
|
||||
|
||||
export const freeinferenceProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({
|
||||
id: "freeinference",
|
||||
alias: "freeinference",
|
||||
baseUrl: "https://freeinference.org/v1/chat/completions",
|
||||
modelsUrl: "https://freeinference.org/v1/models",
|
||||
models: [],
|
||||
passthroughModels: true,
|
||||
});
|
||||
@@ -1,11 +0,0 @@
|
||||
import type { RegistryEntry } from "../../shared.ts";
|
||||
import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts";
|
||||
|
||||
export const helyxaiProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({
|
||||
id: "helyxai",
|
||||
alias: "helyxai",
|
||||
baseUrl: "https://helyxai.space/v1/chat/completions",
|
||||
modelsUrl: "https://helyxai.space/v1/models",
|
||||
models: [],
|
||||
passthroughModels: true,
|
||||
});
|
||||
@@ -1,11 +0,0 @@
|
||||
import type { RegistryEntry } from "../../shared.ts";
|
||||
import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts";
|
||||
|
||||
export const literouterProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({
|
||||
id: "literouter",
|
||||
alias: "literouter",
|
||||
baseUrl: "https://api.literouter.com/v1/chat/completions",
|
||||
modelsUrl: "https://api.literouter.com/v1/models",
|
||||
models: [],
|
||||
passthroughModels: true,
|
||||
});
|
||||
@@ -1,11 +0,0 @@
|
||||
import type { RegistryEntry } from "../../shared.ts";
|
||||
import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts";
|
||||
|
||||
export const meganovaAiProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({
|
||||
id: "meganova-ai",
|
||||
alias: "meganova-ai",
|
||||
baseUrl: "https://api.meganova.ai/v1/chat/completions",
|
||||
modelsUrl: "https://api.meganova.ai/v1/models",
|
||||
models: [],
|
||||
passthroughModels: true,
|
||||
});
|
||||
@@ -1,11 +0,0 @@
|
||||
import type { RegistryEntry } from "../../shared.ts";
|
||||
import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts";
|
||||
|
||||
export const mixlayerProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({
|
||||
id: "mixlayer",
|
||||
alias: "mixlayer",
|
||||
baseUrl: "https://models.mixlayer.ai/v1/chat/completions",
|
||||
modelsUrl: "https://models.mixlayer.ai/v1/models",
|
||||
models: [{ id: "qwen/qwen3.5-4b-free", name: "Qwen 3.5 4B (free)" }],
|
||||
passthroughModels: true,
|
||||
});
|
||||
@@ -1,11 +0,0 @@
|
||||
import type { RegistryEntry } from "../../shared.ts";
|
||||
import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts";
|
||||
|
||||
export const mnnAiProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({
|
||||
id: "mnn-ai",
|
||||
alias: "mnn-ai",
|
||||
baseUrl: "https://api.mnnai.ru/v1/chat/completions",
|
||||
modelsUrl: "https://api.mnnai.ru/v1/models",
|
||||
models: [],
|
||||
passthroughModels: true,
|
||||
});
|
||||
@@ -1,12 +0,0 @@
|
||||
import type { RegistryEntry } from "../../shared.ts";
|
||||
import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts";
|
||||
|
||||
// Free access terms may permit data collection or training use; discover models dynamically.
|
||||
export const nagaAiProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({
|
||||
id: "naga-ai",
|
||||
alias: "naga-ai",
|
||||
baseUrl: "https://api.naga.ac/v1/chat/completions",
|
||||
modelsUrl: "https://api.naga.ac/v1/models",
|
||||
models: [],
|
||||
passthroughModels: true,
|
||||
});
|
||||
@@ -1,11 +0,0 @@
|
||||
import type { RegistryEntry } from "../../shared.ts";
|
||||
import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts";
|
||||
|
||||
export const ofoxaiProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({
|
||||
id: "ofoxai",
|
||||
alias: "ofoxai",
|
||||
baseUrl: "https://api.ofox.ai/v1/chat/completions",
|
||||
modelsUrl: "https://api.ofox.ai/v1/models",
|
||||
models: [],
|
||||
passthroughModels: true,
|
||||
});
|
||||
@@ -1,11 +0,0 @@
|
||||
import type { RegistryEntry } from "../../shared.ts";
|
||||
import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts";
|
||||
|
||||
export const poixeAiProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({
|
||||
id: "poixe-ai",
|
||||
alias: "poixe-ai",
|
||||
baseUrl: "https://api.poixe.com/v1/chat/completions",
|
||||
modelsUrl: "https://api.poixe.com/v1/models",
|
||||
models: [],
|
||||
passthroughModels: true,
|
||||
});
|
||||
@@ -1,11 +0,0 @@
|
||||
import type { RegistryEntry } from "../../shared.ts";
|
||||
import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts";
|
||||
|
||||
export const poolsideProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({
|
||||
id: "poolside",
|
||||
alias: "poolside",
|
||||
baseUrl: "https://inference.poolside.ai/v1/chat/completions",
|
||||
modelsUrl: "https://inference.poolside.ai/v1/models",
|
||||
models: [],
|
||||
passthroughModels: true,
|
||||
});
|
||||
@@ -1,11 +0,0 @@
|
||||
import type { RegistryEntry } from "../../shared.ts";
|
||||
import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts";
|
||||
|
||||
export const spekaProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({
|
||||
id: "speka",
|
||||
alias: "speka",
|
||||
baseUrl: "https://speka.me/v1/chat/completions",
|
||||
modelsUrl: "https://speka.me/v1/models",
|
||||
models: [],
|
||||
passthroughModels: true,
|
||||
});
|
||||
@@ -1,11 +0,0 @@
|
||||
import type { RegistryEntry } from "../../shared.ts";
|
||||
import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts";
|
||||
|
||||
export const tokenreplyProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({
|
||||
id: "tokenreply",
|
||||
alias: "tokenreply",
|
||||
baseUrl: "https://api.tokenreply.com/v1/chat/completions",
|
||||
modelsUrl: "https://api.tokenreply.com/v1/models",
|
||||
models: [],
|
||||
passthroughModels: true,
|
||||
});
|
||||
@@ -1,17 +0,0 @@
|
||||
import type { RegistryEntry } from "../../shared.ts";
|
||||
import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts";
|
||||
|
||||
/**
|
||||
* Yolo-Auto - OpenAI-compatible API with a request-limited free tier.
|
||||
*
|
||||
* The catalog is kept intentionally small: the documented free-tier model is
|
||||
* seeded while passthrough discovery allows the service to publish updates.
|
||||
*/
|
||||
export const yoloAutoProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({
|
||||
id: "yolo-auto",
|
||||
alias: "yolo-auto",
|
||||
baseUrl: "https://yolo-auto.com/v1/chat/completions",
|
||||
modelsUrl: "https://yolo-auto.com/v1/models",
|
||||
models: [{ id: "qwen3.6-35b-a3b", name: "Qwen 3.6 35B A3B" }],
|
||||
passthroughModels: true,
|
||||
});
|
||||
@@ -1,11 +0,0 @@
|
||||
import type { RegistryEntry } from "../../shared.ts";
|
||||
import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts";
|
||||
|
||||
export const zerolimitaiProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({
|
||||
id: "zerolimitai",
|
||||
alias: "zerolimitai",
|
||||
baseUrl: "https://www.zerolimitai.com/api/v1/chat/completions",
|
||||
modelsUrl: "https://www.zerolimitai.com/api/v1/models",
|
||||
models: [],
|
||||
passthroughModels: true,
|
||||
});
|
||||
@@ -1,11 +0,0 @@
|
||||
import type { RegistryEntry } from "../../shared.ts";
|
||||
import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts";
|
||||
|
||||
export const zyloApiProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({
|
||||
id: "zylo-api",
|
||||
alias: "zylo",
|
||||
baseUrl: "https://api.zyloai.net/v1/chat/completions",
|
||||
modelsUrl: "https://api.zyloai.net/v1/models",
|
||||
models: [],
|
||||
passthroughModels: true,
|
||||
});
|
||||
@@ -7,33 +7,6 @@ export const PROVIDER_ENDPOINTS = {
|
||||
dgrid: "https://api.dgrid.ai/v1/chat/completions",
|
||||
bai: "https://api.b.ai/v1/chat/completions",
|
||||
qiniu: "https://api.qnaigc.com/v1/chat/completions",
|
||||
"zylo-api": "https://api.zyloai.net/v1/chat/completions",
|
||||
unorouter: "https://api.unorouter.com/v1/chat/completions",
|
||||
poolside: "https://inference.poolside.ai/v1/chat/completions",
|
||||
fastrouter: "https://api.fastrouter.ai/api/v1/chat/completions",
|
||||
anyapi: "https://api.anyapi.ai/v1/chat/completions",
|
||||
electronhub: "https://api.electronhub.ai/v1/chat/completions",
|
||||
llmgateway: "https://api.llmgateway.io/v1/chat/completions",
|
||||
"llm-kiwi": "https://api.llm.kiwi/v1/chat/completions",
|
||||
literouter: "https://api.literouter.com/v1/chat/completions",
|
||||
"mnn-ai": "https://api.mnnai.ru/v1/chat/completions",
|
||||
"meganova-ai": "https://api.meganova.ai/v1/chat/completions",
|
||||
mixlayer: "https://models.mixlayer.ai/v1/chat/completions",
|
||||
speka: "https://speka.me/v1/chat/completions",
|
||||
tokenreply: "https://api.tokenreply.com/v1/chat/completions",
|
||||
"yolo-auto": "https://yolo-auto.com/v1/chat/completions",
|
||||
dxnt: "https://www.dxnt.com/v1/chat/completions",
|
||||
"cloudcode-one": "https://api.cloudcode.one/v1/chat/completions",
|
||||
ofoxai: "https://api.ofox.ai/v1/chat/completions",
|
||||
zerolimitai: "https://www.zerolimitai.com/api/v1/chat/completions",
|
||||
chatanywhere: "https://api.chatanywhere.org/v1/chat/completions",
|
||||
helyxai: "https://helyxai.space/v1/chat/completions",
|
||||
auriko: "https://api.auriko.ai/v1/chat/completions",
|
||||
"poixe-ai": "https://api.poixe.com/v1/chat/completions",
|
||||
"naga-ai": "https://api.naga.ac/v1/chat/completions",
|
||||
"chat-oripe": "https://api.oriper.com/v1/chat/completions",
|
||||
freeinference: "https://freeinference.org/v1/chat/completions",
|
||||
"free-ai": "https://api.free.ai/v1/chat/",
|
||||
glm: "https://api.z.ai/api/anthropic/v1/messages",
|
||||
glmt: "https://api.z.ai/api/anthropic/v1/messages",
|
||||
"bailian-coding-plan": "https://coding-intl.dashscope.aliyuncs.com/apps/anthropic/v1/messages",
|
||||
|
||||
@@ -21,10 +21,7 @@ import { PROVIDER_MODELS as MODELS } from "@omniroute/open-sse/config/providerMo
|
||||
const PASSTHROUGH_PROVIDERS = new Set(
|
||||
Object.entries(AI_PROVIDERS)
|
||||
.filter(([, p]) => (p as any).passthroughModels)
|
||||
.flatMap(([key, provider]) => {
|
||||
const alias = (provider as { alias?: unknown }).alias;
|
||||
return typeof alias === "string" && alias.length > 0 ? [key, alias] : [key];
|
||||
})
|
||||
.map(([key]) => key)
|
||||
);
|
||||
|
||||
// Wrap isValidModel with passthrough providers
|
||||
|
||||
@@ -92,32 +92,6 @@ export const AGGREGATOR_PROVIDER_IDS = new Set([
|
||||
"g4f-pollinations",
|
||||
"g4f-ollama",
|
||||
"g4f-nvidia",
|
||||
"zylo-api",
|
||||
"unorouter",
|
||||
"fastrouter",
|
||||
"anyapi",
|
||||
"electronhub",
|
||||
"llmgateway",
|
||||
"llm-kiwi",
|
||||
"literouter",
|
||||
"mnn-ai",
|
||||
"meganova-ai",
|
||||
"mixlayer",
|
||||
"speka",
|
||||
"tokenreply",
|
||||
"yolo-auto",
|
||||
"dxnt",
|
||||
"cloudcode-one",
|
||||
"ofoxai",
|
||||
"zerolimitai",
|
||||
"chatanywhere",
|
||||
"helyxai",
|
||||
"auriko",
|
||||
"poixe-ai",
|
||||
"naga-ai",
|
||||
"chat-oripe",
|
||||
"freeinference",
|
||||
"free-ai",
|
||||
]);
|
||||
|
||||
export const ENTERPRISE_CLOUD_PROVIDER_IDS = new Set([
|
||||
|
||||
@@ -95,395 +95,6 @@ export const APIKEY_PROVIDERS_GATEWAYS = {
|
||||
"Create an API key at https://app.requesty.ai, then paste it here as a Bearer token. " +
|
||||
"OpenAI-compatible endpoint at https://router.requesty.ai/v1, with a live /v1/models catalog.",
|
||||
},
|
||||
"zylo-api": {
|
||||
id: "zylo-api",
|
||||
alias: "zylo",
|
||||
name: "Zylo API",
|
||||
icon: "hub",
|
||||
color: "#2563EB",
|
||||
textIcon: "ZY",
|
||||
passthroughModels: true,
|
||||
website: "https://zyloai.net",
|
||||
hasFree: true,
|
||||
freeNote:
|
||||
"Basic plan: 10 RPM, 7,200 requests/day and 200,000 tokens/day; limited to Basic text models.",
|
||||
apiHint:
|
||||
"Create a free Zylo API key at https://zyloai.net, then use https://api.zyloai.net/v1 as the OpenAI-compatible base URL.",
|
||||
},
|
||||
unorouter: {
|
||||
id: "unorouter",
|
||||
alias: "unorouter",
|
||||
name: "UnoRouter",
|
||||
icon: "router",
|
||||
color: "#7C3AED",
|
||||
textIcon: "UR",
|
||||
passthroughModels: true,
|
||||
website: "https://unorouter.com",
|
||||
hasFree: true,
|
||||
freeNote:
|
||||
"Models with the :free suffix do not debit balance; limit is 1 request/minute per free model per user.",
|
||||
apiHint:
|
||||
"Create an UnoRouter token, then use https://api.unorouter.com/v1 as the OpenAI-compatible base URL.",
|
||||
},
|
||||
fastrouter: {
|
||||
id: "fastrouter",
|
||||
alias: "fastrouter",
|
||||
name: "FastRouter",
|
||||
icon: "speed",
|
||||
color: "#F97316",
|
||||
textIcon: "FR",
|
||||
passthroughModels: true,
|
||||
website: "https://fastrouter.ai",
|
||||
hasFree: true,
|
||||
freeNote:
|
||||
"Models with the :free suffix allow 10 requests/day per organization and model; availability may change.",
|
||||
apiHint:
|
||||
"Create a FastRouter API key, then use https://api.fastrouter.ai/api/v1 as the OpenAI-compatible base URL.",
|
||||
},
|
||||
anyapi: {
|
||||
id: "anyapi",
|
||||
alias: "anyapi",
|
||||
name: "AnyAPI AI",
|
||||
icon: "hub",
|
||||
color: "#0EA5E9",
|
||||
textIcon: "AA",
|
||||
passthroughModels: true,
|
||||
website: "https://anyapi.ai",
|
||||
hasFree: true,
|
||||
freeNote:
|
||||
"Free plan: 100,000 ANY Tokens/day and 100 RPM for eligible Free/Basic models; no credit card required.",
|
||||
apiHint:
|
||||
"Create and verify an AnyAPI account, then use https://api.anyapi.ai/v1 as the OpenAI-compatible base URL.",
|
||||
},
|
||||
electronhub: {
|
||||
id: "electronhub",
|
||||
alias: "electronhub",
|
||||
name: "Electron Hub",
|
||||
icon: "hub",
|
||||
color: "#22C55E",
|
||||
textIcon: "EH",
|
||||
passthroughModels: true,
|
||||
website: "https://www.electronhub.ai",
|
||||
hasFree: true,
|
||||
freeNote:
|
||||
"Free plan: 5 RPM, $0.25 weekly credits and 10 Neutrinos/day for :free models; family budgets also apply.",
|
||||
apiHint:
|
||||
"Create a free API key at https://app.electronhub.ai, then use https://api.electronhub.ai/v1 as the OpenAI-compatible base URL.",
|
||||
},
|
||||
llmgateway: {
|
||||
id: "llmgateway",
|
||||
alias: "llmgateway",
|
||||
name: "LLM Gateway",
|
||||
icon: "router",
|
||||
color: "#6366F1",
|
||||
textIcon: "LG",
|
||||
passthroughModels: true,
|
||||
website: "https://llmgateway.io",
|
||||
hasFree: true,
|
||||
freeNote:
|
||||
"Hosted Free plan: free-priced models are limited to 5 requests per 10 minutes when the account has no credits.",
|
||||
apiHint:
|
||||
"Create an LLM Gateway API key, then use https://api.llmgateway.io/v1 as the OpenAI-compatible base URL.",
|
||||
},
|
||||
"llm-kiwi": {
|
||||
id: "llm-kiwi",
|
||||
alias: "llmkiwi",
|
||||
name: "LLM.Kiwi",
|
||||
icon: "hub",
|
||||
color: "#84CC16",
|
||||
textIcon: "LK",
|
||||
passthroughModels: true,
|
||||
website: "https://llm.kiwi",
|
||||
hasFree: true,
|
||||
freeNote:
|
||||
"Free plan exposes auto and hrLLM; the published 40 requests/hour limit applies to hrLLM.",
|
||||
apiHint:
|
||||
"Create a free LLM.Kiwi key, then use https://api.llm.kiwi/v1 as the OpenAI-compatible base URL.",
|
||||
},
|
||||
literouter: {
|
||||
id: "literouter",
|
||||
alias: "literouter",
|
||||
name: "LiteRouter",
|
||||
icon: "router",
|
||||
color: "#2563EB",
|
||||
textIcon: "LR",
|
||||
passthroughModels: true,
|
||||
website: "https://literouter.com",
|
||||
hasFree: true,
|
||||
freeNote:
|
||||
"Free model variants use the :free suffix; daily credit limits vary by model and free input is capped at 5,000 tokens.",
|
||||
apiHint:
|
||||
"Create a LiteRouter API key, then use https://api.literouter.com/v1 as the OpenAI-compatible base URL.",
|
||||
},
|
||||
"mnn-ai": {
|
||||
id: "mnn-ai",
|
||||
alias: "mnn-ai",
|
||||
name: "MNN AI",
|
||||
icon: "hub",
|
||||
color: "#0F766E",
|
||||
textIcon: "MNN",
|
||||
passthroughModels: true,
|
||||
website: "https://mnnai.ru",
|
||||
hasFree: true,
|
||||
freeNote: "Free plan: $1 monthly credits, 10 RPM and access only to models marked Free.",
|
||||
apiHint:
|
||||
"Create an MNN AI API key, then use the primary https://api.mnnai.ru/v1 OpenAI-compatible endpoint. Review jurisdiction, privacy and regional data-transfer requirements before use.",
|
||||
},
|
||||
"meganova-ai": {
|
||||
id: "meganova-ai",
|
||||
alias: "meganova-ai",
|
||||
name: "MegaNova AI",
|
||||
icon: "router",
|
||||
color: "#7C3AED",
|
||||
textIcon: "MN",
|
||||
passthroughModels: true,
|
||||
website: "https://meganova.ai",
|
||||
hasFree: true,
|
||||
freeNote:
|
||||
"Free signup without a card. Published Tier 1 per-model quotas total 550 requests/day; they are not a shared global pool, and paid overage can apply if enabled.",
|
||||
apiHint:
|
||||
"Create a MegaNova API key, then use https://api.meganova.ai/v1 as the OpenAI-compatible base URL.",
|
||||
},
|
||||
mixlayer: {
|
||||
id: "mixlayer",
|
||||
alias: "mixlayer",
|
||||
name: "Mixlayer",
|
||||
icon: "router",
|
||||
color: "#0EA5E9",
|
||||
textIcon: "MX",
|
||||
passthroughModels: true,
|
||||
website: "https://www.mixlayer.com",
|
||||
hasFree: true,
|
||||
freeNote:
|
||||
"The qwen/qwen3.5-4b-free model is free for prototyping and rate-limited; no fixed public RPM or daily quota is confirmed.",
|
||||
apiHint:
|
||||
"Create a Mixlayer API key, then use https://models.mixlayer.ai/v1 as the OpenAI-compatible base URL.",
|
||||
},
|
||||
speka: {
|
||||
id: "speka",
|
||||
alias: "speka",
|
||||
name: "Speka AI",
|
||||
icon: "router",
|
||||
color: "#DB2777",
|
||||
textIcon: "SP",
|
||||
passthroughModels: true,
|
||||
website: "https://speka.me",
|
||||
hasFree: true,
|
||||
freeNote:
|
||||
"Free plan: $1 monthly usage, 10 RPM, one API key and access to open models and the playground; no card required.",
|
||||
apiHint:
|
||||
"Create a Speka API key, then use https://speka.me/v1 as the OpenAI-compatible base URL. Confirm current model availability and overage settings before use.",
|
||||
},
|
||||
tokenreply: {
|
||||
id: "tokenreply",
|
||||
alias: "tokenreply",
|
||||
name: "TokenReply",
|
||||
icon: "router",
|
||||
color: "#3B82F6",
|
||||
textIcon: "TR",
|
||||
passthroughModels: true,
|
||||
website: "https://www.tokenreply.com",
|
||||
hasFree: true,
|
||||
freeNote:
|
||||
"Free-tagged models have model- and campaign-specific daily limits; no fixed global free quota is published.",
|
||||
apiHint:
|
||||
"Create a TokenReply token, then use https://api.tokenreply.com/v1 as the OpenAI-compatible base URL and confirm the selected model's current limit.",
|
||||
},
|
||||
"yolo-auto": {
|
||||
id: "yolo-auto",
|
||||
alias: "yolo-auto",
|
||||
name: "Yolo-Auto",
|
||||
icon: "auto_awesome",
|
||||
color: "#F59E0B",
|
||||
textIcon: "YA",
|
||||
passthroughModels: true,
|
||||
website: "https://yolo-auto.com",
|
||||
hasFree: true,
|
||||
freeNote:
|
||||
"Free API access is request-limited and intended for testing; no numeric daily quota is published and free access is not promised indefinitely.",
|
||||
apiHint:
|
||||
"Create a yolo_ API key, then use https://yolo-auto.com/v1 as the OpenAI-compatible base URL.",
|
||||
},
|
||||
dxnt: {
|
||||
id: "dxnt",
|
||||
alias: "dxnt",
|
||||
name: "DXNT / DX Token",
|
||||
icon: "hub",
|
||||
color: "#111827",
|
||||
textIcon: "DX",
|
||||
passthroughModels: true,
|
||||
website: "https://www.dxnt.com",
|
||||
hasFree: true,
|
||||
freeNote:
|
||||
"Free accounts are documented at 100 calls/day; the quota may increase through invitations and can vary by account.",
|
||||
apiHint:
|
||||
"Create a DXNT API key, then use https://www.dxnt.com/v1 as the OpenAI-compatible base URL.",
|
||||
},
|
||||
"cloudcode-one": {
|
||||
id: "cloudcode-one",
|
||||
alias: "cloudcode-one",
|
||||
name: "CloudCode.ONE",
|
||||
icon: "router",
|
||||
color: "#6366F1",
|
||||
textIcon: "CC",
|
||||
passthroughModels: true,
|
||||
website: "https://cloudcode.one",
|
||||
hasFree: true,
|
||||
freeNote:
|
||||
"Published free models include glm-4.7-flash and glm-4.6v-flash; no numeric quota is published, and key creation may require credit or a coupon.",
|
||||
apiHint:
|
||||
"Create a CloudCode.ONE key, then use https://api.cloudcode.one/v1 as the OpenAI-compatible base URL. Key issuance may require credit or a coupon.",
|
||||
},
|
||||
ofoxai: {
|
||||
id: "ofoxai",
|
||||
alias: "ofoxai",
|
||||
name: "OfoxAI",
|
||||
icon: "router",
|
||||
color: "#0F766E",
|
||||
textIcon: "OF",
|
||||
passthroughModels: true,
|
||||
website: "https://ofox.ai",
|
||||
hasFree: true,
|
||||
freeNote:
|
||||
"The current catalog advertises 10+ free models without a public numeric quota; review upstream provenance, retention and training terms before production use.",
|
||||
apiHint:
|
||||
"Create an OfoxAI Bearer key, then use https://api.ofox.ai/v1 as the OpenAI-compatible base URL. This integration covers the OpenAI surface only.",
|
||||
},
|
||||
zerolimitai: {
|
||||
id: "zerolimitai",
|
||||
alias: "zerolimitai",
|
||||
name: "ZeroLimitAI",
|
||||
icon: "router",
|
||||
color: "#475569",
|
||||
textIcon: "ZL",
|
||||
passthroughModels: true,
|
||||
website: "https://www.zerolimitai.com",
|
||||
hasFree: true,
|
||||
freeNote:
|
||||
"Temporary free trial is advertised, but official pages conflict between 3 and 7 days; a 100-calls/day claim is not treated as permanent.",
|
||||
apiHint:
|
||||
"Create a ZeroLimitAI Bearer token, then use https://www.zerolimitai.com/api/v1 as the OpenAI-compatible base URL.",
|
||||
},
|
||||
chatanywhere: {
|
||||
id: "chatanywhere",
|
||||
alias: "chatanywhere",
|
||||
name: "ChatAnywhere",
|
||||
icon: "router",
|
||||
color: "#2563EB",
|
||||
textIcon: "CA",
|
||||
passthroughModels: true,
|
||||
website: "https://chatanywhere.tech",
|
||||
hasFree: true,
|
||||
freeNote:
|
||||
"Personal, educational or research use only: public documentation cites 10,000 points/day and 200 requests/day per IP/key; do not use for commercial traffic.",
|
||||
apiHint:
|
||||
"Create a ChatAnywhere key linked to GitHub, then use https://api.chatanywhere.org/v1 outside China. Review the non-commercial terms before enabling it.",
|
||||
},
|
||||
helyxai: {
|
||||
id: "helyxai",
|
||||
alias: "helyxai",
|
||||
name: "Helyx AI",
|
||||
icon: "hub",
|
||||
color: "#7C3AED",
|
||||
textIcon: "HX",
|
||||
passthroughModels: true,
|
||||
website: "https://helyxai.space",
|
||||
hasFree: true,
|
||||
freeNote:
|
||||
"Operational Free plan documents 100,000 tokens/day; the site's separate 2M+ marketing claim conflicts and is not treated as a quota guarantee.",
|
||||
apiHint:
|
||||
"Create a Helyx AI Bearer key, then use https://helyxai.space/v1 as the OpenAI-compatible base URL. Review terms and data retention first.",
|
||||
},
|
||||
auriko: {
|
||||
id: "auriko",
|
||||
alias: "auriko",
|
||||
name: "Auriko",
|
||||
icon: "hub",
|
||||
color: "#0891B2",
|
||||
textIcon: "AU",
|
||||
passthroughModels: true,
|
||||
website: "https://www.auriko.ai",
|
||||
hasFree: true,
|
||||
freeNote:
|
||||
"Free plan publishes 1,000 Platform RPM and 10,000 BYOK RPM. Platform inference still passes through provider cost; this is not a free-token pool or unlimited free inference.",
|
||||
apiHint:
|
||||
"Create an Auriko key with the ak_ prefix, then use https://api.auriko.ai/v1 as the OpenAI-compatible base URL. BYOK and platform credits have different cost semantics.",
|
||||
},
|
||||
"poixe-ai": {
|
||||
id: "poixe-ai",
|
||||
alias: "poixe-ai",
|
||||
name: "Poixe AI",
|
||||
icon: "router",
|
||||
color: "#EA580C",
|
||||
textIcon: "PX",
|
||||
passthroughModels: true,
|
||||
website: "https://poixe.com",
|
||||
hasFree: true,
|
||||
freeNote:
|
||||
"Current public free limits are small and model-group specific: 2 RPM/5 RPD for large-cup models and 20 RPM/50 RPD for small-cup models.",
|
||||
apiHint:
|
||||
"Create a Poixe Bearer key, then use https://api.poixe.com/v1 as the OpenAI-compatible base URL. Treat free model provenance and regional availability as experimental.",
|
||||
},
|
||||
"naga-ai": {
|
||||
id: "naga-ai",
|
||||
alias: "naga-ai",
|
||||
name: "Naga AI",
|
||||
icon: "router",
|
||||
color: "#059669",
|
||||
textIcon: "NA",
|
||||
passthroughModels: true,
|
||||
website: "https://naga.ac",
|
||||
hasFree: true,
|
||||
freeNote:
|
||||
"Models marked :free are publicly listed, but no numeric quota is confirmed. Naga's policy warns that free-tier prompts and outputs may be collected or used for training.",
|
||||
apiHint:
|
||||
"Create a Naga AI Bearer key, then use https://api.naga.ac/v1 as the OpenAI-compatible base URL. Never send sensitive data to the free tier without accepting its training policy.",
|
||||
},
|
||||
"chat-oripe": {
|
||||
id: "chat-oripe",
|
||||
alias: "chat-oripe",
|
||||
name: "Chat Oripe",
|
||||
icon: "router",
|
||||
color: "#64748B",
|
||||
textIcon: "CO",
|
||||
passthroughModels: true,
|
||||
website: "https://api.oriper.com",
|
||||
hasFree: true,
|
||||
freeNote:
|
||||
"Official metadata advertises 2M tokens/month, but the public site and documentation were blocked during audit; treat the quota and brand mapping as unconfirmed.",
|
||||
apiHint:
|
||||
"Use https://api.oriper.com/v1 only after confirming the provider's current documentation, terms and key issuance. No quota is guaranteed by this catalog.",
|
||||
},
|
||||
freeinference: {
|
||||
id: "freeinference",
|
||||
alias: "freeinference",
|
||||
name: "FreeInference",
|
||||
icon: "science",
|
||||
color: "#8B5CF6",
|
||||
textIcon: "FI",
|
||||
passthroughModels: true,
|
||||
website: "https://freeinference.org",
|
||||
hasFree: true,
|
||||
freeNote:
|
||||
"Free research access without a card; non-Harvard applicants require manual approval and no numeric quota is publicly guaranteed.",
|
||||
apiHint:
|
||||
"Apply for a FreeInference key, then use https://freeinference.org/v1 as the OpenAI-compatible base URL. Terms allow prompt/response logging and possible publication of anonymized research data; never send sensitive or production data.",
|
||||
},
|
||||
"free-ai": {
|
||||
id: "free-ai",
|
||||
alias: "free-ai",
|
||||
name: "Free.ai",
|
||||
icon: "hub",
|
||||
color: "#16A34A",
|
||||
textIcon: "FA",
|
||||
passthroughModels: true,
|
||||
website: "https://free.ai",
|
||||
hasFree: true,
|
||||
freeNote:
|
||||
"30,000 tokens/day cover self-hosted models after email verification. Usage beyond the pool can bill at raw cost, and premium external models are paid.",
|
||||
apiHint:
|
||||
"Create an sk-free- key, then use the nonstandard but OpenAI-shaped https://api.free.ai/v1/chat/ endpoint. Select a self-hosted zero-price model to stay within the free pool.",
|
||||
},
|
||||
dgrid: {
|
||||
id: "dgrid",
|
||||
alias: "dgrid",
|
||||
|
||||
@@ -32,21 +32,6 @@ export const APIKEY_PROVIDERS_INFERENCE = {
|
||||
freeNote:
|
||||
"Free credits on signup for OpenAI-compatible inference across LLMs, embeddings, and reasoning models",
|
||||
},
|
||||
poolside: {
|
||||
id: "poolside",
|
||||
alias: "poolside",
|
||||
name: "Poolside",
|
||||
icon: "memory",
|
||||
color: "#111827",
|
||||
textIcon: "PS",
|
||||
passthroughModels: true,
|
||||
website: "https://poolside.ai",
|
||||
hasFree: true,
|
||||
freeNote:
|
||||
"Laguna S 2.1 and XS 2.1 are free during Preview; no public numeric quota is published.",
|
||||
apiHint:
|
||||
"Create a free developer API key, then use https://inference.poolside.ai/v1 as the OpenAI-compatible base URL.",
|
||||
},
|
||||
fireworks: {
|
||||
id: "fireworks",
|
||||
alias: "fireworks",
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
const { REGISTRY } = await import("../../open-sse/config/providerRegistry.ts");
|
||||
const { DefaultExecutor, getExecutor } = await import("../../open-sse/executors/index.ts");
|
||||
const { PROVIDER_ENDPOINTS } = await import("../../src/shared/constants/config.ts");
|
||||
const { isValidModel } = await import("../../src/shared/constants/models.ts");
|
||||
const { AGGREGATOR_PROVIDER_IDS } = await import("../../src/shared/constants/providers.ts");
|
||||
const { APIKEY_PROVIDERS } = await import("../../src/shared/constants/providers/apikey/index.ts");
|
||||
|
||||
const providers = [
|
||||
["zylo-api", "zylo", "https://api.zyloai.net/v1/chat/completions"],
|
||||
["unorouter", "unorouter", "https://api.unorouter.com/v1/chat/completions"],
|
||||
["poolside", "poolside", "https://inference.poolside.ai/v1/chat/completions"],
|
||||
["fastrouter", "fastrouter", "https://api.fastrouter.ai/api/v1/chat/completions"],
|
||||
["anyapi", "anyapi", "https://api.anyapi.ai/v1/chat/completions"],
|
||||
["electronhub", "electronhub", "https://api.electronhub.ai/v1/chat/completions"],
|
||||
["llmgateway", "llmgateway", "https://api.llmgateway.io/v1/chat/completions"],
|
||||
["llm-kiwi", "llmkiwi", "https://api.llm.kiwi/v1/chat/completions"],
|
||||
] as const;
|
||||
|
||||
for (const [id, alias, endpoint] of providers) {
|
||||
test(`${id} is wired through registry, metadata, endpoint and default executor`, () => {
|
||||
const registry = REGISTRY[id];
|
||||
const metadata = APIKEY_PROVIDERS[id];
|
||||
|
||||
assert.ok(registry);
|
||||
assert.ok(metadata);
|
||||
assert.equal(registry.id, id);
|
||||
assert.equal(registry.alias, alias);
|
||||
assert.equal(registry.baseUrl, endpoint);
|
||||
assert.equal(PROVIDER_ENDPOINTS[id], endpoint);
|
||||
assert.equal(metadata.id, id);
|
||||
assert.equal(metadata.alias, alias);
|
||||
assert.equal(metadata.hasFree, true);
|
||||
assert.equal(metadata.passthroughModels, true);
|
||||
assert.ok(typeof metadata.freeNote === "string" && metadata.freeNote.length > 0);
|
||||
assert.ok(getExecutor(id) instanceof DefaultExecutor);
|
||||
assert.equal(isValidModel(id, "future/live-catalog-model"), true);
|
||||
assert.equal(isValidModel(alias, "future/live-catalog-model"), true);
|
||||
});
|
||||
}
|
||||
|
||||
test("gateway providers are classified as aggregators while direct Poolside is not", () => {
|
||||
const gatewayIds = providers.map(([id]) => id).filter((id) => id !== "poolside");
|
||||
for (const id of gatewayIds) assert.equal(AGGREGATOR_PROVIDER_IDS.has(id), true);
|
||||
assert.equal(AGGREGATOR_PROVIDER_IDS.has("poolside"), false);
|
||||
});
|
||||
|
||||
test("LLM.Kiwi statically exposes only the confirmed Free plan models", () => {
|
||||
assert.deepEqual(REGISTRY["llm-kiwi"].models, [
|
||||
{ id: "auto", name: "Auto" },
|
||||
{ id: "hrLLM", name: "hrLLM" },
|
||||
]);
|
||||
});
|
||||
@@ -1,55 +0,0 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { poolsideProvider } from "../../open-sse/config/providers/registry/poolside/index.ts";
|
||||
import { unorouterProvider } from "../../open-sse/config/providers/registry/unorouter/index.ts";
|
||||
import { zyloApiProvider } from "../../open-sse/config/providers/registry/zylo-api/index.ts";
|
||||
import type { RegistryEntry } from "../../open-sse/config/providers/shared.ts";
|
||||
|
||||
const providers: Array<{
|
||||
entry: RegistryEntry;
|
||||
id: string;
|
||||
alias: string;
|
||||
chatUrl: string;
|
||||
modelsUrl: string;
|
||||
}> = [
|
||||
{
|
||||
entry: zyloApiProvider,
|
||||
id: "zylo-api",
|
||||
alias: "zylo",
|
||||
chatUrl: "https://api.zyloai.net/v1/chat/completions",
|
||||
modelsUrl: "https://api.zyloai.net/v1/models",
|
||||
},
|
||||
{
|
||||
entry: unorouterProvider,
|
||||
id: "unorouter",
|
||||
alias: "unorouter",
|
||||
chatUrl: "https://api.unorouter.com/v1/chat/completions",
|
||||
modelsUrl: "https://api.unorouter.com/v1/models",
|
||||
},
|
||||
{
|
||||
entry: poolsideProvider,
|
||||
id: "poolside",
|
||||
alias: "poolside",
|
||||
chatUrl: "https://inference.poolside.ai/v1/chat/completions",
|
||||
modelsUrl: "https://inference.poolside.ai/v1/models",
|
||||
},
|
||||
];
|
||||
|
||||
for (const { entry, id, alias, chatUrl, modelsUrl } of providers) {
|
||||
test(`${id} uses the standard OpenAI-compatible API-key registry shape`, () => {
|
||||
assert.equal(entry.id, id);
|
||||
assert.equal(entry.alias, alias);
|
||||
assert.equal(entry.format, "openai");
|
||||
assert.equal(entry.executor, "default");
|
||||
assert.equal(entry.authType, "apikey");
|
||||
assert.equal(entry.authHeader, "bearer");
|
||||
assert.equal(entry.baseUrl, chatUrl);
|
||||
assert.equal(entry.modelsUrl, modelsUrl);
|
||||
assert.equal(entry.passthroughModels, true);
|
||||
});
|
||||
|
||||
test(`${id} relies on its live catalog without invented static model ids`, () => {
|
||||
assert.deepEqual(entry.models, []);
|
||||
});
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { literouterProvider } from "../../open-sse/config/providers/registry/literouter/index.ts";
|
||||
import { meganovaAiProvider } from "../../open-sse/config/providers/registry/meganova-ai/index.ts";
|
||||
import { mnnAiProvider } from "../../open-sse/config/providers/registry/mnn-ai/index.ts";
|
||||
import type { RegistryEntry } from "../../open-sse/config/providers/shared.ts";
|
||||
|
||||
const providers: Array<{
|
||||
entry: RegistryEntry;
|
||||
id: string;
|
||||
chatUrl: string;
|
||||
modelsUrl: string;
|
||||
}> = [
|
||||
{
|
||||
entry: literouterProvider,
|
||||
id: "literouter",
|
||||
chatUrl: "https://api.literouter.com/v1/chat/completions",
|
||||
modelsUrl: "https://api.literouter.com/v1/models",
|
||||
},
|
||||
{
|
||||
entry: mnnAiProvider,
|
||||
id: "mnn-ai",
|
||||
chatUrl: "https://api.mnnai.ru/v1/chat/completions",
|
||||
modelsUrl: "https://api.mnnai.ru/v1/models",
|
||||
},
|
||||
{
|
||||
entry: meganovaAiProvider,
|
||||
id: "meganova-ai",
|
||||
chatUrl: "https://api.meganova.ai/v1/chat/completions",
|
||||
modelsUrl: "https://api.meganova.ai/v1/models",
|
||||
},
|
||||
];
|
||||
|
||||
for (const { entry, id, chatUrl, modelsUrl } of providers) {
|
||||
test(`${id} uses an OpenAI-compatible Bearer registry entry`, () => {
|
||||
assert.equal(entry.id, id);
|
||||
assert.equal(entry.alias, id);
|
||||
assert.equal(entry.format, "openai");
|
||||
assert.equal(entry.executor, "default");
|
||||
assert.equal(entry.authType, "apikey");
|
||||
assert.equal(entry.authHeader, "bearer");
|
||||
assert.equal(entry.baseUrl, chatUrl);
|
||||
assert.equal(entry.modelsUrl, modelsUrl);
|
||||
assert.equal(entry.passthroughModels, true);
|
||||
});
|
||||
|
||||
test(`${id} leaves model discovery to the live upstream catalog`, () => {
|
||||
assert.deepEqual(entry.models, []);
|
||||
});
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { mixlayerProvider } from "../../open-sse/config/providers/registry/mixlayer/index.ts";
|
||||
import { spekaProvider } from "../../open-sse/config/providers/registry/speka/index.ts";
|
||||
import { tokenreplyProvider } from "../../open-sse/config/providers/registry/tokenreply/index.ts";
|
||||
import type { RegistryEntry } from "../../open-sse/config/providers/shared.ts";
|
||||
|
||||
const providers: Array<{
|
||||
entry: RegistryEntry;
|
||||
id: string;
|
||||
chatUrl: string;
|
||||
modelsUrl: string;
|
||||
}> = [
|
||||
{
|
||||
entry: mixlayerProvider,
|
||||
id: "mixlayer",
|
||||
chatUrl: "https://models.mixlayer.ai/v1/chat/completions",
|
||||
modelsUrl: "https://models.mixlayer.ai/v1/models",
|
||||
},
|
||||
{
|
||||
entry: spekaProvider,
|
||||
id: "speka",
|
||||
chatUrl: "https://speka.me/v1/chat/completions",
|
||||
modelsUrl: "https://speka.me/v1/models",
|
||||
},
|
||||
{
|
||||
entry: tokenreplyProvider,
|
||||
id: "tokenreply",
|
||||
chatUrl: "https://api.tokenreply.com/v1/chat/completions",
|
||||
modelsUrl: "https://api.tokenreply.com/v1/models",
|
||||
},
|
||||
];
|
||||
|
||||
test("Wave 2-B providers expose OpenAI-compatible Bearer registries", () => {
|
||||
for (const { entry, id, chatUrl, modelsUrl } of providers) {
|
||||
assert.equal(entry.id, id);
|
||||
assert.equal(entry.alias, id);
|
||||
assert.equal(entry.format, "openai");
|
||||
assert.equal(entry.executor, "default");
|
||||
assert.equal(entry.authType, "apikey");
|
||||
assert.equal(entry.authHeader, "bearer");
|
||||
assert.equal(entry.baseUrl, chatUrl);
|
||||
assert.equal(entry.modelsUrl, modelsUrl);
|
||||
assert.equal(entry.passthroughModels, true);
|
||||
assert.ok(Array.isArray(entry.models));
|
||||
}
|
||||
});
|
||||
|
||||
test("Mixlayer seeds only its documented free model", () => {
|
||||
assert.deepEqual(
|
||||
mixlayerProvider.models.map((model) => model.id),
|
||||
["qwen/qwen3.5-4b-free"]
|
||||
);
|
||||
});
|
||||
|
||||
test("Speka and TokenReply rely on live model catalogs without invented models", () => {
|
||||
assert.deepEqual(spekaProvider.models, []);
|
||||
assert.deepEqual(tokenreplyProvider.models, []);
|
||||
});
|
||||
@@ -1,58 +0,0 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { cloudcodeOneProvider } from "../../open-sse/config/providers/registry/cloudcode-one/index.ts";
|
||||
import { dxntProvider } from "../../open-sse/config/providers/registry/dxnt/index.ts";
|
||||
import { yoloAutoProvider } from "../../open-sse/config/providers/registry/yolo-auto/index.ts";
|
||||
import type { RegistryEntry } from "../../open-sse/config/providers/shared.ts";
|
||||
|
||||
const providers: Array<{
|
||||
entry: RegistryEntry;
|
||||
id: string;
|
||||
chatUrl: string;
|
||||
modelsUrl: string;
|
||||
modelIds: string[];
|
||||
}> = [
|
||||
{
|
||||
entry: yoloAutoProvider,
|
||||
id: "yolo-auto",
|
||||
chatUrl: "https://yolo-auto.com/v1/chat/completions",
|
||||
modelsUrl: "https://yolo-auto.com/v1/models",
|
||||
modelIds: ["qwen3.6-35b-a3b"],
|
||||
},
|
||||
{
|
||||
entry: dxntProvider,
|
||||
id: "dxnt",
|
||||
chatUrl: "https://www.dxnt.com/v1/chat/completions",
|
||||
modelsUrl: "https://www.dxnt.com/v1/models",
|
||||
modelIds: [],
|
||||
},
|
||||
{
|
||||
entry: cloudcodeOneProvider,
|
||||
id: "cloudcode-one",
|
||||
chatUrl: "https://api.cloudcode.one/v1/chat/completions",
|
||||
modelsUrl: "https://api.cloudcode.one/v1/models",
|
||||
modelIds: ["glm-4.7-flash", "glm-4.6v-flash"],
|
||||
},
|
||||
];
|
||||
|
||||
for (const { entry, id, chatUrl, modelsUrl, modelIds } of providers) {
|
||||
test(`${id} uses the standard OpenAI-compatible API-key registry shape`, () => {
|
||||
assert.equal(entry.id, id);
|
||||
assert.equal(entry.alias, id);
|
||||
assert.equal(entry.format, "openai");
|
||||
assert.equal(entry.executor, "default");
|
||||
assert.equal(entry.authType, "apikey");
|
||||
assert.equal(entry.authHeader, "bearer");
|
||||
assert.equal(entry.baseUrl, chatUrl);
|
||||
assert.equal(entry.modelsUrl, modelsUrl);
|
||||
assert.equal(entry.passthroughModels, true);
|
||||
});
|
||||
|
||||
test(`${id} seeds only the audited model identifiers`, () => {
|
||||
assert.deepEqual(
|
||||
(entry.models ?? []).map((model) => model.id),
|
||||
modelIds
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
const { REGISTRY } = await import("../../open-sse/config/providerRegistry.ts");
|
||||
const { DefaultExecutor, getExecutor } = await import("../../open-sse/executors/index.ts");
|
||||
const { PROVIDER_ENDPOINTS } = await import("../../src/shared/constants/config.ts");
|
||||
const { isValidModel } = await import("../../src/shared/constants/models.ts");
|
||||
const { AGGREGATOR_PROVIDER_IDS } = await import("../../src/shared/constants/providers.ts");
|
||||
const { APIKEY_PROVIDERS } = await import("../../src/shared/constants/providers/apikey/index.ts");
|
||||
|
||||
const providers = [
|
||||
["literouter", "https://api.literouter.com/v1/chat/completions", []],
|
||||
["mnn-ai", "https://api.mnnai.ru/v1/chat/completions", []],
|
||||
["meganova-ai", "https://api.meganova.ai/v1/chat/completions", []],
|
||||
["mixlayer", "https://models.mixlayer.ai/v1/chat/completions", ["qwen/qwen3.5-4b-free"]],
|
||||
["speka", "https://speka.me/v1/chat/completions", []],
|
||||
["tokenreply", "https://api.tokenreply.com/v1/chat/completions", []],
|
||||
["yolo-auto", "https://yolo-auto.com/v1/chat/completions", ["qwen3.6-35b-a3b"]],
|
||||
["dxnt", "https://www.dxnt.com/v1/chat/completions", []],
|
||||
[
|
||||
"cloudcode-one",
|
||||
"https://api.cloudcode.one/v1/chat/completions",
|
||||
["glm-4.7-flash", "glm-4.6v-flash"],
|
||||
],
|
||||
] as const;
|
||||
|
||||
for (const [id, endpoint, modelIds] of providers) {
|
||||
test(`${id} is wired through registry, metadata, endpoint and default executor`, () => {
|
||||
const registry = REGISTRY[id];
|
||||
const metadata = APIKEY_PROVIDERS[id];
|
||||
|
||||
assert.ok(registry);
|
||||
assert.ok(metadata);
|
||||
assert.equal(registry.id, id);
|
||||
assert.equal(registry.alias, id);
|
||||
assert.equal(registry.baseUrl, endpoint);
|
||||
assert.equal(PROVIDER_ENDPOINTS[id], endpoint);
|
||||
assert.equal(metadata.id, id);
|
||||
assert.equal(metadata.alias, id);
|
||||
assert.equal(metadata.hasFree, true);
|
||||
assert.equal(metadata.passthroughModels, true);
|
||||
assert.ok(typeof metadata.freeNote === "string" && metadata.freeNote.length > 0);
|
||||
assert.equal(AGGREGATOR_PROVIDER_IDS.has(id), true);
|
||||
assert.ok(getExecutor(id) instanceof DefaultExecutor);
|
||||
assert.equal(isValidModel(id, "future/live-catalog-model"), true);
|
||||
assert.deepEqual(
|
||||
registry.models.map((model) => model.id),
|
||||
modelIds
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
test("risk-sensitive metadata preserves the audited quota qualifications", () => {
|
||||
assert.match(APIKEY_PROVIDERS["mnn-ai"].apiHint ?? "", /jurisdiction.*privacy/i);
|
||||
assert.match(APIKEY_PROVIDERS["meganova-ai"].freeNote ?? "", /per-model quotas/i);
|
||||
assert.match(APIKEY_PROVIDERS["meganova-ai"].freeNote ?? "", /paid overage/i);
|
||||
assert.match(APIKEY_PROVIDERS.tokenreply.freeNote ?? "", /no fixed global free quota/i);
|
||||
assert.match(APIKEY_PROVIDERS["yolo-auto"].freeNote ?? "", /no numeric daily quota/i);
|
||||
assert.match(APIKEY_PROVIDERS["cloudcode-one"].freeNote ?? "", /credit or a coupon/i);
|
||||
});
|
||||
@@ -1,48 +0,0 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { chatanywhereProvider } from "../../open-sse/config/providers/registry/chatanywhere/index.ts";
|
||||
import { ofoxaiProvider } from "../../open-sse/config/providers/registry/ofoxai/index.ts";
|
||||
import { zerolimitaiProvider } from "../../open-sse/config/providers/registry/zerolimitai/index.ts";
|
||||
import type { RegistryEntry } from "../../open-sse/config/providers/shared.ts";
|
||||
|
||||
const providers: Array<{
|
||||
entry: RegistryEntry;
|
||||
id: string;
|
||||
chatUrl: string;
|
||||
modelsUrl: string;
|
||||
}> = [
|
||||
{
|
||||
entry: ofoxaiProvider,
|
||||
id: "ofoxai",
|
||||
chatUrl: "https://api.ofox.ai/v1/chat/completions",
|
||||
modelsUrl: "https://api.ofox.ai/v1/models",
|
||||
},
|
||||
{
|
||||
entry: zerolimitaiProvider,
|
||||
id: "zerolimitai",
|
||||
chatUrl: "https://www.zerolimitai.com/api/v1/chat/completions",
|
||||
modelsUrl: "https://www.zerolimitai.com/api/v1/models",
|
||||
},
|
||||
{
|
||||
entry: chatanywhereProvider,
|
||||
id: "chatanywhere",
|
||||
chatUrl: "https://api.chatanywhere.org/v1/chat/completions",
|
||||
modelsUrl: "https://api.chatanywhere.org/v1/models",
|
||||
},
|
||||
];
|
||||
|
||||
test("Wave 3-A providers expose OpenAI-compatible Bearer registries", () => {
|
||||
for (const { entry, id, chatUrl, modelsUrl } of providers) {
|
||||
assert.equal(entry.id, id);
|
||||
assert.equal(entry.alias, id);
|
||||
assert.equal(entry.format, "openai");
|
||||
assert.equal(entry.executor, "default");
|
||||
assert.equal(entry.authType, "apikey");
|
||||
assert.equal(entry.authHeader, "bearer");
|
||||
assert.equal(entry.baseUrl, chatUrl);
|
||||
assert.equal(entry.modelsUrl, modelsUrl);
|
||||
assert.equal(entry.passthroughModels, true);
|
||||
assert.deepEqual(entry.models, []);
|
||||
}
|
||||
});
|
||||
@@ -1,53 +0,0 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { aurikoProvider } from "../../open-sse/config/providers/registry/auriko/index.ts";
|
||||
import { helyxaiProvider } from "../../open-sse/config/providers/registry/helyxai/index.ts";
|
||||
import { poixeAiProvider } from "../../open-sse/config/providers/registry/poixe-ai/index.ts";
|
||||
import type { RegistryEntry } from "../../open-sse/config/providers/shared.ts";
|
||||
|
||||
const providers: Array<{
|
||||
entry: RegistryEntry;
|
||||
id: string;
|
||||
chatUrl: string;
|
||||
modelsUrl: string;
|
||||
}> = [
|
||||
{
|
||||
entry: helyxaiProvider,
|
||||
id: "helyxai",
|
||||
chatUrl: "https://helyxai.space/v1/chat/completions",
|
||||
modelsUrl: "https://helyxai.space/v1/models",
|
||||
},
|
||||
{
|
||||
entry: aurikoProvider,
|
||||
id: "auriko",
|
||||
chatUrl: "https://api.auriko.ai/v1/chat/completions",
|
||||
modelsUrl: "https://api.auriko.ai/v1/models",
|
||||
},
|
||||
{
|
||||
entry: poixeAiProvider,
|
||||
id: "poixe-ai",
|
||||
chatUrl: "https://api.poixe.com/v1/chat/completions",
|
||||
modelsUrl: "https://api.poixe.com/v1/models",
|
||||
},
|
||||
];
|
||||
|
||||
test("Wave 3-B providers expose OpenAI-compatible Bearer registries", () => {
|
||||
for (const { entry, id, chatUrl, modelsUrl } of providers) {
|
||||
assert.equal(entry.id, id);
|
||||
assert.equal(entry.alias, id);
|
||||
assert.equal(entry.format, "openai");
|
||||
assert.equal(entry.executor, "default");
|
||||
assert.equal(entry.authType, "apikey");
|
||||
assert.equal(entry.authHeader, "bearer");
|
||||
assert.equal(entry.baseUrl, chatUrl);
|
||||
assert.equal(entry.modelsUrl, modelsUrl);
|
||||
assert.equal(entry.passthroughModels, true);
|
||||
}
|
||||
});
|
||||
|
||||
test("Wave 3-B providers rely on live catalogs without invented models", () => {
|
||||
for (const { entry } of providers) {
|
||||
assert.deepEqual(entry.models, []);
|
||||
}
|
||||
});
|
||||
@@ -1,41 +0,0 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { chatOripeProvider } from "../../open-sse/config/providers/registry/chat-oripe/index.ts";
|
||||
import { nagaAiProvider } from "../../open-sse/config/providers/registry/naga-ai/index.ts";
|
||||
import type { RegistryEntry } from "../../open-sse/config/providers/shared.ts";
|
||||
|
||||
const providers: Array<{
|
||||
entry: RegistryEntry;
|
||||
id: string;
|
||||
chatUrl: string;
|
||||
modelsUrl: string;
|
||||
}> = [
|
||||
{
|
||||
entry: nagaAiProvider,
|
||||
id: "naga-ai",
|
||||
chatUrl: "https://api.naga.ac/v1/chat/completions",
|
||||
modelsUrl: "https://api.naga.ac/v1/models",
|
||||
},
|
||||
{
|
||||
entry: chatOripeProvider,
|
||||
id: "chat-oripe",
|
||||
chatUrl: "https://api.oriper.com/v1/chat/completions",
|
||||
modelsUrl: "https://api.oriper.com/v1/models",
|
||||
},
|
||||
];
|
||||
|
||||
test("Wave 3-C providers expose OpenAI-compatible Bearer registries", () => {
|
||||
for (const { entry, id, chatUrl, modelsUrl } of providers) {
|
||||
assert.equal(entry.id, id);
|
||||
assert.equal(entry.alias, id);
|
||||
assert.equal(entry.format, "openai");
|
||||
assert.equal(entry.executor, "default");
|
||||
assert.equal(entry.authType, "apikey");
|
||||
assert.equal(entry.authHeader, "bearer");
|
||||
assert.equal(entry.baseUrl, chatUrl);
|
||||
assert.equal(entry.modelsUrl, modelsUrl);
|
||||
assert.equal(entry.passthroughModels, true);
|
||||
assert.deepEqual(entry.models, []);
|
||||
}
|
||||
});
|
||||
@@ -1,53 +0,0 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
const { REGISTRY } = await import("../../open-sse/config/providerRegistry.ts");
|
||||
const { DefaultExecutor, getExecutor } = await import("../../open-sse/executors/index.ts");
|
||||
const { PROVIDER_ENDPOINTS } = await import("../../src/shared/constants/config.ts");
|
||||
const { isValidModel } = await import("../../src/shared/constants/models.ts");
|
||||
const { AGGREGATOR_PROVIDER_IDS } = await import("../../src/shared/constants/providers.ts");
|
||||
const { APIKEY_PROVIDERS } = await import("../../src/shared/constants/providers/apikey/index.ts");
|
||||
|
||||
const providers = [
|
||||
["ofoxai", "https://api.ofox.ai/v1/chat/completions"],
|
||||
["zerolimitai", "https://www.zerolimitai.com/api/v1/chat/completions"],
|
||||
["chatanywhere", "https://api.chatanywhere.org/v1/chat/completions"],
|
||||
["helyxai", "https://helyxai.space/v1/chat/completions"],
|
||||
["auriko", "https://api.auriko.ai/v1/chat/completions"],
|
||||
["poixe-ai", "https://api.poixe.com/v1/chat/completions"],
|
||||
["naga-ai", "https://api.naga.ac/v1/chat/completions"],
|
||||
["chat-oripe", "https://api.oriper.com/v1/chat/completions"],
|
||||
] as const;
|
||||
|
||||
for (const [id, endpoint] of providers) {
|
||||
test(`${id} is wired through registry, metadata, endpoint and default executor`, () => {
|
||||
const registry = REGISTRY[id];
|
||||
const metadata = APIKEY_PROVIDERS[id];
|
||||
|
||||
assert.ok(registry);
|
||||
assert.ok(metadata);
|
||||
assert.equal(registry.id, id);
|
||||
assert.equal(registry.alias, id);
|
||||
assert.equal(registry.baseUrl, endpoint);
|
||||
assert.equal(PROVIDER_ENDPOINTS[id], endpoint);
|
||||
assert.equal(metadata.id, id);
|
||||
assert.equal(metadata.alias, id);
|
||||
assert.equal(metadata.hasFree, true);
|
||||
assert.equal(metadata.passthroughModels, true);
|
||||
assert.ok(typeof metadata.freeNote === "string" && metadata.freeNote.length > 0);
|
||||
assert.equal(AGGREGATOR_PROVIDER_IDS.has(id), true);
|
||||
assert.ok(getExecutor(id) instanceof DefaultExecutor);
|
||||
assert.equal(isValidModel(id, "future/live-catalog-model"), true);
|
||||
assert.deepEqual(registry.models, []);
|
||||
});
|
||||
}
|
||||
|
||||
test("Wave 3 metadata preserves the audited legal, quota and privacy warnings", () => {
|
||||
assert.match(APIKEY_PROVIDERS.chatanywhere.freeNote ?? "", /commercial traffic/i);
|
||||
assert.match(APIKEY_PROVIDERS.zerolimitai.freeNote ?? "", /3 and 7 days/i);
|
||||
assert.match(APIKEY_PROVIDERS.helyxai.freeNote ?? "", /100,000 tokens\/day/i);
|
||||
assert.match(APIKEY_PROVIDERS.auriko.freeNote ?? "", /not a free-token pool/i);
|
||||
assert.match(APIKEY_PROVIDERS["poixe-ai"].freeNote ?? "", /2 RPM\/5 RPD/i);
|
||||
assert.match(APIKEY_PROVIDERS["naga-ai"].freeNote ?? "", /training/i);
|
||||
assert.match(APIKEY_PROVIDERS["chat-oripe"].freeNote ?? "", /unconfirmed/i);
|
||||
});
|
||||
@@ -1,27 +0,0 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { freeinferenceProvider } from "../../open-sse/config/providers/registry/freeinference/index.ts";
|
||||
import {
|
||||
DefaultExecutor,
|
||||
getExecutor,
|
||||
hasSpecializedExecutor,
|
||||
} from "../../open-sse/executors/index.ts";
|
||||
|
||||
test("FreeInference exposes an OpenAI-compatible Bearer registry", () => {
|
||||
assert.equal(freeinferenceProvider.id, "freeinference");
|
||||
assert.equal(freeinferenceProvider.alias, "freeinference");
|
||||
assert.equal(freeinferenceProvider.format, "openai");
|
||||
assert.equal(freeinferenceProvider.executor, "default");
|
||||
assert.equal(freeinferenceProvider.authType, "apikey");
|
||||
assert.equal(freeinferenceProvider.authHeader, "bearer");
|
||||
assert.equal(freeinferenceProvider.baseUrl, "https://freeinference.org/v1/chat/completions");
|
||||
assert.equal(freeinferenceProvider.modelsUrl, "https://freeinference.org/v1/models");
|
||||
assert.deepEqual(freeinferenceProvider.models, []);
|
||||
assert.equal(freeinferenceProvider.passthroughModels, true);
|
||||
});
|
||||
|
||||
test("FreeInference uses DefaultExecutor without specialized behavior", () => {
|
||||
assert.equal(hasSpecializedExecutor("freeinference"), false);
|
||||
assert.ok(getExecutor("freeinference") instanceof DefaultExecutor);
|
||||
});
|
||||
@@ -1,27 +0,0 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { freeAiProvider } from "../../open-sse/config/providers/registry/free-ai/index.ts";
|
||||
import {
|
||||
DefaultExecutor,
|
||||
getExecutor,
|
||||
hasSpecializedExecutor,
|
||||
} from "../../open-sse/executors/index.ts";
|
||||
|
||||
test("Free.ai exposes its exact OpenAI-compatible endpoint and live catalog", () => {
|
||||
assert.equal(freeAiProvider.id, "free-ai");
|
||||
assert.equal(freeAiProvider.alias, "free-ai");
|
||||
assert.equal(freeAiProvider.format, "openai");
|
||||
assert.equal(freeAiProvider.executor, "default");
|
||||
assert.equal(freeAiProvider.authType, "apikey");
|
||||
assert.equal(freeAiProvider.authHeader, "bearer");
|
||||
assert.equal(freeAiProvider.baseUrl, "https://api.free.ai/v1/chat/");
|
||||
assert.equal(freeAiProvider.modelsUrl, "https://api.free.ai/v1/models");
|
||||
assert.deepEqual(freeAiProvider.models, []);
|
||||
assert.equal(freeAiProvider.passthroughModels, true);
|
||||
});
|
||||
|
||||
test("Free.ai uses DefaultExecutor without a specialized executor", () => {
|
||||
assert.ok(getExecutor("free-ai") instanceof DefaultExecutor);
|
||||
assert.equal(hasSpecializedExecutor("free-ai"), false);
|
||||
});
|
||||
@@ -1,64 +0,0 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { deriveConfigFromRegistryModelsUrl } from "../../src/app/api/providers/[id]/models/discoveryConfig.ts";
|
||||
|
||||
const { REGISTRY } = await import("../../open-sse/config/providerRegistry.ts");
|
||||
const { DefaultExecutor, getExecutor, hasSpecializedExecutor } =
|
||||
await import("../../open-sse/executors/index.ts");
|
||||
const { PROVIDER_ENDPOINTS } = await import("../../src/shared/constants/config.ts");
|
||||
const { isValidModel } = await import("../../src/shared/constants/models.ts");
|
||||
const { AGGREGATOR_PROVIDER_IDS } = await import("../../src/shared/constants/providers.ts");
|
||||
const { APIKEY_PROVIDERS } = await import("../../src/shared/constants/providers/apikey/index.ts");
|
||||
|
||||
const providers = [
|
||||
["freeinference", "https://freeinference.org/v1/chat/completions"],
|
||||
["free-ai", "https://api.free.ai/v1/chat/"],
|
||||
] as const;
|
||||
|
||||
for (const [id, endpoint] of providers) {
|
||||
test(`${id} is fully wired without a specialized executor`, () => {
|
||||
const registry = REGISTRY[id];
|
||||
const metadata = APIKEY_PROVIDERS[id];
|
||||
|
||||
assert.ok(registry);
|
||||
assert.ok(metadata);
|
||||
assert.equal(registry.id, id);
|
||||
assert.equal(registry.alias, id);
|
||||
assert.equal(registry.baseUrl, endpoint);
|
||||
assert.equal(PROVIDER_ENDPOINTS[id], endpoint);
|
||||
assert.equal(metadata.id, id);
|
||||
assert.equal(metadata.alias, id);
|
||||
assert.equal(metadata.hasFree, true);
|
||||
assert.equal(metadata.passthroughModels, true);
|
||||
assert.equal(AGGREGATOR_PROVIDER_IDS.has(id), true);
|
||||
assert.equal(hasSpecializedExecutor(id), false);
|
||||
const executor = getExecutor(id);
|
||||
assert.ok(executor instanceof DefaultExecutor);
|
||||
assert.equal(executor.buildUrl("live-model", false), endpoint);
|
||||
assert.equal(isValidModel(id, "future/live-catalog-model"), true);
|
||||
assert.deepEqual(registry.models, []);
|
||||
});
|
||||
}
|
||||
|
||||
test("Wave 4 model discovery accepts both public catalog response envelopes", () => {
|
||||
const freeInferenceDiscovery = deriveConfigFromRegistryModelsUrl("freeinference");
|
||||
const freeAiDiscovery = deriveConfigFromRegistryModelsUrl("free-ai");
|
||||
|
||||
assert.ok(freeInferenceDiscovery);
|
||||
assert.ok(freeAiDiscovery);
|
||||
assert.deepEqual(freeInferenceDiscovery.parseResponse({ data: [{ id: "glm-5.1" }] }), [
|
||||
{ id: "glm-5.1" },
|
||||
]);
|
||||
assert.deepEqual(freeAiDiscovery.parseResponse({ models: [{ id: "qwen7b" }] }), [
|
||||
{ id: "qwen7b" },
|
||||
]);
|
||||
});
|
||||
|
||||
test("Wave 4 metadata preserves approval, logging and overage warnings", () => {
|
||||
assert.match(APIKEY_PROVIDERS.freeinference.freeNote ?? "", /manual approval/i);
|
||||
assert.match(APIKEY_PROVIDERS.freeinference.apiHint ?? "", /logging/i);
|
||||
assert.match(APIKEY_PROVIDERS["free-ai"].freeNote ?? "", /30,000 tokens\/day/i);
|
||||
assert.match(APIKEY_PROVIDERS["free-ai"].freeNote ?? "", /premium external models are paid/i);
|
||||
assert.match(APIKEY_PROVIDERS["free-ai"].apiHint ?? "", /\/v1\/chat\//i);
|
||||
});
|
||||
@@ -1,15 +0,0 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
const { isValidModel } = await import("../../src/shared/constants/models.ts");
|
||||
|
||||
test("passthrough providers accept live model ids through both provider id and alias", () => {
|
||||
for (const [id, alias] of [
|
||||
["zylo-api", "zylo"],
|
||||
["llm-kiwi", "llmkiwi"],
|
||||
["freetheai", "fta"],
|
||||
] as const) {
|
||||
assert.equal(isValidModel(id, "future/live-catalog-model"), true);
|
||||
assert.equal(isValidModel(alias, "future/live-catalog-model"), true);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user