From 66a38b74df6ab30a9250d86fd55adc658c8bc796 Mon Sep 17 00:00:00 2001 From: Adam Eivy Date: Thu, 3 Sep 2026 22:46:57 +0000 Subject: [PATCH] refactor: compose the video registry upgraders from one ordered list (#6056) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `normalizeRegistry`'s `videoEntries` hand-nested six load-time registry upgraders four parens deep, plus a conditional pair for the CUDA bucket. Every addition since has cost a nesting level, and the ordering — which is load-bearing, since `upgradeFastMetalDownloadSizes` has to run ahead of `applyVideoDisclosures` (that decorator only fills an ABSENT disclosure) and `dropRetiredEntries` has to run first so a withdrawn model isn't decorated on its way out — read as arbitrary nesting. Replace the nest with an ordered `VIDEO_REGISTRY_UPGRADES` list reduced in one place. Each row names its upgrader and states its ordering constraint beside it; a `buckets` field replaces the `upgradeLegacyCudaLtx` flag for the two CUDA-only rows. Adding the seventh upgrader is now appending a row. Behavior-preserving: every upgrader keeps its own shipped-value guard, the chain applies in exactly the prior order, and the exported upgraders the registry migrations call are unchanged. Adds two guards: the chain order is pinned by name, so a reorder becomes a deliberate edit rather than an invisible re-nesting, and the CUDA-only rows are asserted not to reach the MLX bucket. Claude-Session: https://claude.ai/code/session_01RA3pD5YM2dukQwbZ3pC6WA --- server/lib/mediaModels.js | 78 +++++++++++++++++++++++++++------- server/lib/mediaModels.test.js | 38 +++++++++++++++++ 2 files changed, 101 insertions(+), 15 deletions(-) diff --git a/server/lib/mediaModels.js b/server/lib/mediaModels.js index aeea7ffb99..fc36a86098 100644 --- a/server/lib/mediaModels.js +++ b/server/lib/mediaModels.js @@ -65,6 +65,7 @@ import { PATHS, expandHome } from './fileUtils.js'; import { isPlainObject } from './objects.js'; import { LEGACY_VIDEO_KEYS, + VIDEO_BUCKET_CUDA, VIDEO_BUCKETS, VIDEO_DEFAULT_KEYS, activeVideoBucket, @@ -1372,6 +1373,57 @@ const dropRetiredEntries = (list) => ( Array.isArray(list) ? list.filter((entry) => !isRetired(entry)) : list ); +// The ordered load-time upgrade chain a video bucket's persisted entries run +// through before the apply*/sanitize* decorators in `videoEntries` touch them. +// +// This list IS the order. Every row is the same shape — a whole-list rewrite +// that only touches an entry still matching what PortOS itself shipped — so +// adding the next upgrader is appending a row rather than adding a paren level +// to one nested expression, and the ordering constraints below are stated where +// a reader — and the ordering guard in `mediaModels.test.js` — can see them, +// instead of being implied by nesting depth. +// +// `buckets` limits a row to the video buckets it applies to; a row without one +// runs for every bucket. +const VIDEO_REGISTRY_UPGRADES = Object.freeze([ + // FIRST, unconditionally: a withdrawn model has to leave before any later row + // spends work on it, and before the apply* decorators would hand it a + // disclosure, a Finish edge, a speed profile or a draft decoder on its way out. + Object.freeze({ name: 'dropRetiredEntries', apply: dropRetiredEntries }), + Object.freeze({ name: 'upgradeMiniMaxH3OutputControls', apply: upgradeMiniMaxH3OutputControls }), + Object.freeze({ name: 'upgradeLtx25AudioControls', apply: upgradeLtx25AudioControls }), + // MUST stay ahead of `applyVideoDisclosures`, which only fills an ABSENT + // disclosure block: this row corrects the stale `estimatedDownloadGb` inside + // an already-persisted one, and the decorator would never revisit it. + Object.freeze({ name: 'upgradeFastMetalDownloadSizes', apply: upgradeFastMetalDownloadSizes }), + Object.freeze({ name: 'backfillRuntime', apply: backfillRuntime }), + // CUDA-only, and in this order: the legacy `ltx_video` row is repointed at the + // `cuda_video` runtime first, then the LTX-2.5 memory floor is raised. + Object.freeze({ + name: 'upgradeLegacyCudaLtxRuntime', + apply: upgradeLegacyCudaLtxRuntime, + buckets: [VIDEO_BUCKET_CUDA], + }), + Object.freeze({ + name: 'upgradeLtx25CudaMemoryFloor', + apply: upgradeLtx25CudaMemoryFloor, + buckets: [VIDEO_BUCKET_CUDA], + }), +]); + +// Exported for the ordering guard in mediaModels.test.js: the chain's +// order is load-bearing (see the constraints above), so a reorder has to be a +// deliberate edit to a named list rather than an invisible re-nesting. +export const VIDEO_REGISTRY_UPGRADE_NAMES = Object.freeze( + VIDEO_REGISTRY_UPGRADES.map((upgrade) => upgrade.name), +); + +// Run one bucket's persisted entries through the chain above. +const applyVideoRegistryUpgrades = (entries, bucket) => VIDEO_REGISTRY_UPGRADES.reduce( + (list, { apply, buckets }) => (buckets && !buckets.includes(bucket) ? list : apply(list)), + entries, +); + // Repoint a platform default that named a model this load just retired. Falls // through to the original id when the successor isn't installed either — then // getDefaultVideoModelId()'s "unknown default → first available" warning is the @@ -1471,24 +1523,22 @@ const normalizeRegistry = (parsed) => { list: [...shippedImageIds, ...imageResult.newlyShipped], }; + // VIDEO_REGISTRY_UPGRADES runs first: it is the ordered chain of load-time + // twins of the registry migrations, and its own ordering constraints are + // stated row by row up there. + // // applyVideoDisclosures is the load-time twin of migration 237: installs // that persisted their registry before `disclosure` existed pick it up // here without waiting for the migration, and both paths share the same - // preservation guards (user value wins, forked repo keeps Unknown). - // dropRetiredEntries is the same arrangement for the retirement migrations, - // and runs - // FIRST so a withdrawn model isn't handed a disclosure or a Finish edge on - // its way out. sanitizeFinishProfiles runs LAST (after the backfill and + // preservation guards (user value wins, forked repo keeps Unknown). It only + // fills an ABSENT block, which is why the upgrade chain — not a decorator — + // is where a stale persisted disclosure gets corrected. + // sanitizeFinishProfiles runs LAST (after the backfill and // after the user's own entries are merged in) so an edge that points at a // model this install deleted — or a hand-edited typo — is dropped with a // warning instead of surfacing a Finish button targeting nothing. - const videoEntries = (entries, { upgradeLegacyCudaLtx = false } = {}) => { - const normalized = backfillRuntime(upgradeFastMetalDownloadSizes(upgradeLtx25AudioControls( - upgradeMiniMaxH3OutputControls(dropRetiredEntries(entries)), - ))); - const upgraded = upgradeLegacyCudaLtx - ? upgradeLtx25CudaMemoryFloor(upgradeLegacyCudaLtxRuntime(normalized)) - : normalized; + const videoEntries = (entries, bucket) => { + const upgraded = applyVideoRegistryUpgrades(entries, bucket); const decorated = sanitizeFinishProfiles(applyVideoFinishProfiles(applyVideoDisclosures(upgraded))); // applyVideoSpeedProfiles is the load-time twin of migration 295, and // sanitizeSpeedProfiles is its sibling of sanitizeFinishProfiles: a @@ -1510,9 +1560,7 @@ const normalizeRegistry = (parsed) => { return sanitizeMiniMaxH3MemoryProfiles(applyMiniMaxH3MemoryProfiles(withDraftDecode)); }; const normalizedBuckets = Object.fromEntries( - VIDEO_BUCKETS.map((bucket) => [bucket, videoEntries(bucketResults[bucket].entries, { - upgradeLegacyCudaLtx: bucket === 'cuda', - })]), + VIDEO_BUCKETS.map((bucket) => [bucket, videoEntries(bucketResults[bucket].entries, bucket)]), ); // Spread the user's own `video` keys but NOT the legacy bucket spellings: the diff --git a/server/lib/mediaModels.test.js b/server/lib/mediaModels.test.js index 075fac0c90..63908962f2 100644 --- a/server/lib/mediaModels.test.js +++ b/server/lib/mediaModels.test.js @@ -1591,3 +1591,41 @@ describe('video bucket selection is an MLX/CUDA axis, not an OS one', () => { expect(reg.video.mlx.map((m) => m.id)).not.toContain('user-cuda'); }); }); + +// The load-time upgrade chain's ORDER is load-bearing — `upgradeFastMetalDownloadSizes` +// has to precede `applyVideoDisclosures` (which only fills an ABSENT disclosure), and +// `dropRetiredEntries` has to run first so a withdrawn model is not decorated on its +// way out. Pinning the list here makes a reorder a deliberate two-file edit rather +// than a silently-shipped behavior change. +describe('video registry upgrade chain', () => { + it('runs the upgraders in the order the constraints require', async () => { + const { VIDEO_REGISTRY_UPGRADE_NAMES } = await import('./mediaModels.js'); + expect([...VIDEO_REGISTRY_UPGRADE_NAMES]).toEqual([ + 'dropRetiredEntries', + 'upgradeMiniMaxH3OutputControls', + 'upgradeLtx25AudioControls', + 'upgradeFastMetalDownloadSizes', + 'backfillRuntime', + 'upgradeLegacyCudaLtxRuntime', + 'upgradeLtx25CudaMemoryFloor', + ]); + }); + + // The two CUDA-only rows must stay off the MLX bucket: `ltx_video` is a legacy + // diffusers CUDA row, and re-pointing an MLX entry of the same id at the + // `cuda_video` runtime would break its dispatch. + it('applies the CUDA-only upgraders to the cuda bucket only', async () => { + writeFileSync(registryFile, JSON.stringify({ + video: { + mlx: [{ id: 'ltx_video', name: 'LTX Video' }], + cuda: [{ id: 'ltx_video', name: 'LTX Video' }], + }, + }, null, 2)); + + const { loadMediaModels } = await import('./mediaModels.js'); + const registry = loadMediaModels(); + + expect(registry.video.mlx.find((m) => m.id === 'ltx_video').runtime).toBeUndefined(); + expect(registry.video.cuda.find((m) => m.id === 'ltx_video').runtime).toBe('cuda_video'); + }); +});