mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-23 15:42:12 +03:00
The Build CI job is advisory, so eight module-level defects from eight different PRs accumulated on release/v3.8.50 until `npm run build` failed with 7 Turbopack errors and `npm run lint` with 14. Build (link-time): - modelSelectModalHelpers.ts: a lost `}` swallowed PROVIDER_TEST_CHUNK_SIZE into isProviderModelHidden's body (#9011). - videoGeneration.ts: handleFalVideoGeneration imported twice; the standalone falHandler.ts is superseded by the provider-neutral mediaGeneration/fal.ts and is removed here (#9982 over #9969). - catalog.ts: re-exported and called the injectable SWR policy that #9199 deliberately replaced with a fixed 30s bound. Fixed on the consumer side — restoring the accessor would resurrect the unbounded window #9199 removed after measuring a 41s catalog build in production. - tinycmsSigner.ts: generated wasm-bindgen glue kept a sidecar `new URL('wasm_signer_bg.wasm', import.meta.url)` that no file backs; Turbopack resolves it statically. The module ships inlined as WASM_BASE64 and the only caller always passes it explicitly (#8736/#10087). - conolDiscovery.ts: imported getProviderOutboundGuard from outboundUrlGuard, which does not export it. Fixed on the consumer side: outboundUrlGuard.ts is loaded by the packaged CLI without a tsconfig, so it must stay free of `@/`-aliased imports (#7682). Runtime (the build never caught this one): - catalogCache.ts::scheduleBackgroundRefresh had two dangling statements referencing undeclared `inFlight`/`promise`, so EVERY stale-while-revalidate read threw a ReferenceError. Surfaced by realigning the #8728 suite, which #9199 left asserting a removed contract. Lint: - driverFactory.test.ts: a case inserted between the preceding test's `finally` and its `});` left the file unparseable, so the SQLite driver-cascade suite (26 tests) had not run since 2026-08-11 (#9173). - providerModelsConfig.ts: imported an executor directly, crossing the G14 boundary; routed through a new open-sse/services/zaiWebCredentials.ts (#8451). - image-combo.test.ts: 11 `any` violations, now typed (#9499). Validation: npm run build exit 0, npm run lint clean, typecheck:core clean, 41/41 tests green across the affected suites. Refs #9011 #9982 #9199 #8728 #8736 #10087 #8974 #9173 #8451 #9499 Co-authored-by: backryun <bakryun0718@proton.me>
312 lines
11 KiB
TypeScript
312 lines
11 KiB
TypeScript
// Pure helpers extracted from ModelSelectModal so the grouping logic is
|
|
// unit-testable (the component itself lives inside a useMemo and is not
|
|
// directly exercisable by node:test). Keep these free of React imports.
|
|
|
|
export type PassthroughAliasModel = {
|
|
id: string;
|
|
name: string;
|
|
value: string;
|
|
source: "alias";
|
|
};
|
|
|
|
/**
|
|
* Build the alias-derived model rows for a passthrough provider.
|
|
*
|
|
* `modelAliases` maps an alias name → the fully-qualified model string, which
|
|
* is prefixed by the provider's *canonical id* (e.g. `github/gpt-4`), NOT by
|
|
* its public alias (e.g. `gh`). Filtering/stripping must therefore use the
|
|
* `providerId`, mirroring the sibling custom-provider branch. Using the alias
|
|
* here meant aliases registered under a providerId whose alias differs (the
|
|
* common case) never resolved.
|
|
*
|
|
* Inspired by upstream PR decolua/9router#485 (Anurag Saxena).
|
|
*/
|
|
export function buildPassthroughAliasModels(
|
|
modelAliases: Record<string, string>,
|
|
providerId: string
|
|
): PassthroughAliasModel[] {
|
|
const prefix = `${providerId}/`;
|
|
return Object.entries(modelAliases || {})
|
|
.filter(([, fullModel]) => typeof fullModel === "string" && fullModel.startsWith(prefix))
|
|
.map(([aliasName, fullModel]) => ({
|
|
id: fullModel.replace(prefix, ""),
|
|
name: aliasName,
|
|
value: fullModel,
|
|
source: "alias" as const,
|
|
}));
|
|
}
|
|
|
|
export type NodeAliasModel = {
|
|
id: string;
|
|
name: string;
|
|
value: string;
|
|
source: "alias";
|
|
};
|
|
|
|
/**
|
|
* Build the alias-derived model rows for a custom-provider ("node") entry.
|
|
*
|
|
* Mirrors `buildPassthroughAliasModels` above but rewrites `value` using the
|
|
* node's display `nodePrefix` instead of the raw `providerId` (custom
|
|
* providers are keyed by their canonical id/UUID in `modelAliases`, but
|
|
* displayed/selected using a node-specific prefix).
|
|
*
|
|
* `modelAliases` values can be `null`/`undefined` for stale or partial
|
|
* entries persisted to settings, so this guards with `typeof fullModel ===
|
|
* "string"` before calling `.startsWith` — without it, opening Create Combo
|
|
* for a custom provider node throws a TypeError.
|
|
*
|
|
* Inspired by upstream PR decolua/9router#2247 (wahyuzero).
|
|
*/
|
|
export function buildNodeAliasModels(
|
|
modelAliases: Record<string, string>,
|
|
providerId: string,
|
|
nodePrefix: string
|
|
): NodeAliasModel[] {
|
|
const prefix = `${providerId}/`;
|
|
return Object.entries(modelAliases || {})
|
|
.filter(([, fullModel]) => typeof fullModel === "string" && fullModel.startsWith(prefix))
|
|
.map(([aliasName, fullModel]) => ({
|
|
id: fullModel.replace(prefix, ""),
|
|
name: aliasName,
|
|
value: `${nodePrefix}/${fullModel.replace(prefix, "")}`,
|
|
source: "alias" as const,
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* "Select all" adds every currently-visible model to the combo in one click,
|
|
* with no cap — turning off "Show configured only" (or just having a large
|
|
* provider catalog) can put hundreds of candidates behind a single click.
|
|
* Above this threshold the caller must confirm before batch-adding (#8526).
|
|
*
|
|
* A native `confirm()` — not a bespoke modal — matches the existing bulk /
|
|
* destructive-action pattern already used in this codebase (e.g.
|
|
* `ReasoningRoutingRules.tsx::deleteConfirm`, `combos/page.tsx::deleteConfirm`),
|
|
* so no new UI primitive is needed for this one interaction.
|
|
*/
|
|
export const SELECT_ALL_CONFIRM_THRESHOLD = 20;
|
|
|
|
export function shouldConfirmSelectAll(
|
|
candidateCount: number,
|
|
threshold: number = SELECT_ALL_CONFIRM_THRESHOLD
|
|
): boolean {
|
|
return Number.isFinite(candidateCount) && candidateCount > threshold;
|
|
}
|
|
|
|
/**
|
|
* #9203 — hidden-model filtering for the Combo "Add model" picker.
|
|
*
|
|
* `/api/provider-models` returns the unified hidden-model map as a plain
|
|
* `Record<providerId, string[]>` (serialized from the DB's
|
|
* `Map<string, Set<string>>`, which covers both `customModels.isHidden` and
|
|
* `modelCompatOverrides.isHidden`). This normalizes that response into a
|
|
* lookup-friendly map, defensively skipping malformed entries so an unknown or
|
|
* partially-shipped API shape can never crash the picker.
|
|
*
|
|
* Keys are canonical provider ids; values are the hidden model ids for that
|
|
* provider. Matching happens on the canonical provider id and the *raw* model
|
|
* id (before any passthrough/node alias prefixing), so a single helper covers
|
|
* system, fallback, alias, node-alias, custom and auto-fetched rows alike.
|
|
*/
|
|
export function parseHiddenModelsByProvider(raw: unknown): Map<string, Set<string>> {
|
|
const result = new Map<string, Set<string>>();
|
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return result;
|
|
|
|
for (const [providerId, modelIds] of Object.entries(raw as Record<string, unknown>)) {
|
|
if (typeof providerId !== "string" || providerId.length === 0) continue;
|
|
if (!Array.isArray(modelIds)) continue;
|
|
const hidden = new Set<string>();
|
|
for (const modelId of modelIds) {
|
|
if (typeof modelId === "string" && modelId.length > 0) hidden.add(modelId);
|
|
}
|
|
if (hidden.size > 0) result.set(providerId, hidden);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Whether `modelId` is hidden for `providerId` under the parsed hidden map.
|
|
* Uses the canonical provider id and raw model id — callers pass the same ids
|
|
* they pass to `buildPassthroughAliasModels` / `buildNodeAliasModels`.
|
|
*/
|
|
export function isProviderModelHidden(
|
|
hiddenModelsByProvider: Map<string, Set<string>>,
|
|
providerId: string,
|
|
modelId: string
|
|
): boolean {
|
|
if (typeof providerId !== "string" || typeof modelId !== "string" || modelId.length === 0) {
|
|
return false;
|
|
}
|
|
return hiddenModelsByProvider.get(providerId)?.has(modelId) ?? false;
|
|
}
|
|
|
|
/** Matches the provider-page "Test All Models" concurrency (#chunks of 3). */
|
|
export const PROVIDER_TEST_CHUNK_SIZE = 3;
|
|
|
|
export type ProviderConnectionLike = {
|
|
id?: string | number | null;
|
|
provider?: string | null;
|
|
};
|
|
|
|
export type ProviderTestModelLike = {
|
|
value?: string | null;
|
|
id?: string | null;
|
|
};
|
|
|
|
export type ProviderTestTarget = {
|
|
providerId: string;
|
|
connectionId?: string;
|
|
modelIds: string[];
|
|
};
|
|
|
|
/**
|
|
* Resolve the first active connection id for a provider catalog key.
|
|
* Mirrors the live-/models lookup in ModelSelectModal.
|
|
*/
|
|
export function resolveConnectionIdForProvider(
|
|
providerId: string,
|
|
activeProviders: ProviderConnectionLike[] | null | undefined
|
|
): string | undefined {
|
|
if (!providerId || !Array.isArray(activeProviders)) return undefined;
|
|
const match = activeProviders.find((p) => p?.provider === providerId);
|
|
const raw = match?.id;
|
|
const id =
|
|
typeof raw === "string"
|
|
? raw.trim()
|
|
: typeof raw === "number" && Number.isFinite(raw)
|
|
? String(raw)
|
|
: "";
|
|
return id || undefined;
|
|
}
|
|
|
|
/**
|
|
* Build /api/models/test-all targets for the providers the user selected
|
|
* in the combo "Add Model" modal. Only models currently visible in the
|
|
* grouped catalog are included (same set Select All would touch).
|
|
*/
|
|
export function buildProviderTestTargets(opts: {
|
|
selectedProviderIds: Iterable<string>;
|
|
groups: Array<[string, ProviderTestModelLike[]]>;
|
|
activeProviders?: ProviderConnectionLike[] | null;
|
|
}): ProviderTestTarget[] {
|
|
const selected = new Set(
|
|
Array.from(opts.selectedProviderIds || []).filter(
|
|
(id) => typeof id === "string" && id.trim().length > 0
|
|
)
|
|
);
|
|
if (selected.size === 0) return [];
|
|
|
|
const byProvider = new Map<string, ProviderTestModelLike[]>();
|
|
for (const [providerId, models] of opts.groups || []) {
|
|
if (!selected.has(providerId)) continue;
|
|
byProvider.set(providerId, Array.isArray(models) ? models : []);
|
|
}
|
|
|
|
const targets: ProviderTestTarget[] = [];
|
|
for (const providerId of selected) {
|
|
const models = byProvider.get(providerId) || [];
|
|
const modelIds = models
|
|
.map((m) => {
|
|
const value = typeof m?.value === "string" ? m.value.trim() : "";
|
|
if (value) return value;
|
|
const id = typeof m?.id === "string" ? m.id.trim() : "";
|
|
return id;
|
|
})
|
|
.filter((id) => id.length > 0);
|
|
if (modelIds.length === 0) continue;
|
|
targets.push({
|
|
providerId,
|
|
connectionId: resolveConnectionIdForProvider(providerId, opts.activeProviders),
|
|
modelIds,
|
|
});
|
|
}
|
|
return targets;
|
|
}
|
|
|
|
export function chunkItems<T>(items: T[], size: number = PROVIDER_TEST_CHUNK_SIZE): T[][] {
|
|
const chunkSize = Number.isFinite(size) && size > 0 ? Math.floor(size) : PROVIDER_TEST_CHUNK_SIZE;
|
|
const out: T[][] = [];
|
|
for (let i = 0; i < items.length; i += chunkSize) {
|
|
out.push(items.slice(i, i + chunkSize));
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/** Toggle a provider id in a selection set (immutable). */
|
|
export function toggleProviderSelection(
|
|
selected: ReadonlySet<string>,
|
|
providerId: string
|
|
): Set<string> {
|
|
const next = new Set(selected);
|
|
if (!providerId) return next;
|
|
if (next.has(providerId)) next.delete(providerId);
|
|
else next.add(providerId);
|
|
return next;
|
|
}
|
|
|
|
/**
|
|
* Classify a /api/models/test-all entry as working (ok|slow) vs failed.
|
|
* Combo modal never auto-hides — keep the check intentionally thin.
|
|
*/
|
|
export function isProviderTestEntryWorking(
|
|
entry: {
|
|
status?: string | null;
|
|
} | null
|
|
): boolean {
|
|
const status = typeof entry?.status === "string" ? entry.status : "";
|
|
return status === "ok" || status === "slow";
|
|
}
|
|
|
|
export function formatProviderTestResults(ok: number, total: number): string {
|
|
const safeOk = Number.isFinite(ok) ? Math.max(0, Math.floor(ok)) : 0;
|
|
const safeTotal = Number.isFinite(total) ? Math.max(0, Math.floor(total)) : 0;
|
|
return `${safeOk} of ${safeTotal} models working`;
|
|
}
|
|
|
|
/**
|
|
* Models that passed the last provider test.
|
|
* `alreadyAdded: false` → ready to add to the combo.
|
|
* `alreadyAdded: true` → already in the combo (for Unselect working).
|
|
*/
|
|
export function collectWorkingModelsToSelect<T extends { value?: string | null }>(opts: {
|
|
models: T[];
|
|
modelTestStatus: Record<string, "ok" | "error" | string>;
|
|
addedModelValues?: Iterable<string> | null;
|
|
alreadyAdded?: boolean;
|
|
}): T[] {
|
|
const wantAdded = opts.alreadyAdded === true;
|
|
const added = new Set(
|
|
Array.from(opts.addedModelValues || []).filter(
|
|
(v) => typeof v === "string" && v.trim().length > 0
|
|
)
|
|
);
|
|
const models = Array.isArray(opts.models) ? opts.models : [];
|
|
const status = opts.modelTestStatus || {};
|
|
return models.filter((model) => {
|
|
const value = typeof model?.value === "string" ? model.value.trim() : "";
|
|
if (!value) return false;
|
|
if (status[value] !== "ok") return false;
|
|
return wantAdded ? added.has(value) : !added.has(value);
|
|
});
|
|
}
|
|
|
|
/** True once at least one model reported ok from a provider test run. */
|
|
export function hasWorkingTestResults(
|
|
modelTestStatus: Record<string, "ok" | "error" | string> | null | undefined
|
|
): boolean {
|
|
if (!modelTestStatus) return false;
|
|
return Object.values(modelTestStatus).some((status) => status === "ok");
|
|
}
|
|
|
|
/** Visible provider ids for Select-all / Clear provider checkboxes. */
|
|
export function listVisibleProviderIds(
|
|
groups: Array<[string, unknown]> | Record<string, unknown> | null | undefined
|
|
): string[] {
|
|
if (!groups) return [];
|
|
if (Array.isArray(groups)) {
|
|
return groups.map(([id]) => id).filter((id) => typeof id === "string" && id.trim().length > 0);
|
|
}
|
|
return Object.keys(groups).filter((id) => id.trim().length > 0);
|
|
}
|