fix(earn): collapse catalog variants to one model per family#417
Conversation
The calculator listed every catalog build — gemma-4-26b, gemma-4-26b-qat-4bit and gemma-4-26b-8bit (rollback) all showed as separate rows alongside gpt-oss-20b (4 entries). Dedupe by a normalized base-model key (strips quant/rollback suffixes) and keep the canonical build, so only the two real models show: Gemma 4 26B and GPT-OSS 20B. No hardcoded model list — derived from the live catalog. Mirrored across console /earn and landing; added unit tests.
|
@Gajesh2007 is attempting to deploy a commit to the EigenLabs Team on Vercel. A member of the Team first needs to authorize it. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: dc8bed2813
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const text = `${m.display_name || ""} ${m.id || ""}`.toLowerCase(); | ||
| let p = 0; | ||
| if (/\(|rollback|preview|\brc\b/.test(text)) p += 100; | ||
| if (/qat|int4|int8|fp16|bf16|mxfp4|nf4|\d\s*-?bit/.test(text)) p += 10; |
There was a problem hiding this comment.
Prefer alias primary builds when collapsing variants
When the landing calculator consumes /v1/models/catalog during an alias takeover, the catalog can contain both the public-name previous build (gemma-4-26b) and the quantized desired build (gemma-4-26b-qat-4bit), while the coordinator routes the public alias to the desired build. This penalty makes the desired build lose solely because its id/name contains qat/4bit, so the single displayed Gemma row can use the old build's size and prices even though requests are routed to the new build; use the alias primary_build metadata or otherwise prefer the routed/desired build before penalizing quant suffixes.
Useful? React with 👍 / 👎.
| export function buildCatalogModels(models: Model[], pricing: PricingResponse | null): CatalogModel[] { | ||
| const prices = buildPricingLookup(pricing); | ||
| return models | ||
| return dedupeModelVariants(models) |
There was a problem hiding this comment.
Preserve lower-memory variants during dedupe
Because variants are collapsed before the page applies its RAM eligibility filter, a clean/base row with a higher min_ram_gb or larger size_gb can replace a quantized sibling that would fit on smaller hardware. For those RAM selections, rankedModels later filters out the retained base row and the fitting variant is already gone, so users can see no compatible model even though the catalog has one; the dedupe needs to account for eligibility or retain the lowest-memory representative when variants differ in fit.
Useful? React with 👍 / 👎.
| */ | ||
| export function baseModelKey(id: string): string { | ||
| let k = id.toLowerCase().trim(); | ||
| const suffix = /-(qat|q4|q8|int4|int8|4bit|8bit|4-bit|8-bit|bf16|fp16|mxfp4|nf4|gguf|rollback|preview|beta|rc\d*)$/; |
There was a problem hiding this comment.
Strip fp8 variants in base model keys
The alias migration paths in this repo use previous builds ending in -fp8, but this suffix list strips -qat-4bit while leaving -fp8 intact. In a catalog containing an FP8 previous build and a QAT desired build for the same family, they get different base keys and both remain in the earnings picker, so the original duplicate-variant problem still appears for that rollout shape; include FP8 (and related fp quant suffixes) in the normalized key.
Useful? React with 👍 / 👎.
ethenotethan
left a comment
There was a problem hiding this comment.
Automated Code Review — Layr-Labs/d-inference#
Verdict: REQUEST_CHANGES
Security — ✅ No issues found
Performance — 2 finding(s) (2 blocking)
- 🟡 [MEDIUM]
console-ui/src/app/earn/calc.ts:245-252— Map lookup in tight loop for model deduplication- Suggestion: Consider pre-sorting models by penalty score to avoid repeated Map.get() calls, or use a single pass with array sorting if the number of models is small
- 🟡 [MEDIUM]
landing/earn-calculator.js:133-140— Duplicate Map lookup pattern in dedupeModelVariants- Suggestion: Same optimization as TypeScript version - consider pre-sorting or single-pass approach
Type_diligence — ✅ No issues found
Additive_complexity — 3 finding(s) (1 blocking)
- 🔵 [INFO]
console-ui/src/app/earn/calc.ts:229— variantPenalty function could be simplified with a single regex- Suggestion: Combine the two regex checks into one pattern or use a simple scoring array instead of complex penalty calculation
- 🟡 [MEDIUM]
landing/earn-calculator.js:118-141— Exact duplication of baseModelKey, variantPenalty, and dedupeModelVariants logic- Suggestion: Extract this logic into a shared utility module or consider if the landing page really needs this complexity vs a simpler hardcoded list
- 🔵 [INFO]
console-ui/src/app/earn/calc.ts:218-227— Complex regex pattern with many quantization suffixes may be overkill- Suggestion: Consider if a simpler approach like checking for common suffixes (-4bit, -8bit, -rollback) would suffice for the current use case
5 finding(s) total, 3 blocking. Verdict: REQUEST_CHANGES.
🤖 Automated review by Centaur · DAR-186
| export function dedupeModelVariants(models: Model[]): Model[] { | ||
| const byBase = new Map<string, Model>(); | ||
| for (const m of models) { | ||
| const key = baseModelKey(m.id); | ||
| const cur = byBase.get(key); | ||
| if (!cur || variantPenalty(m) < variantPenalty(cur)) byBase.set(key, m); | ||
| } | ||
| return [...byBase.values()]; |
There was a problem hiding this comment.
🟡 [MEDIUM] ⚡ Map lookup in tight loop for model deduplication
💡 Suggestion: Consider pre-sorting models by penalty score to avoid repeated Map.get() calls, or use a single pass with array sorting if the number of models is small
📊 Score: 2×4 = 8 · Category: Inefficient data structures
| return k; | ||
| } | ||
|
|
||
| /** Lower is more canonical — prefer clean names / base ids over quant/rollback builds. */ |
There was a problem hiding this comment.
🔵 [INFO] 🧩 variantPenalty function could be simplified with a single regex
💡 Suggestion: Combine the two regex checks into one pattern or use a simple scoring array instead of complex penalty calculation
📊 Score: 2×3 = 6 · Category: over-abstraction
| function baseModelKey(id) { | ||
| let k = String(id || "").toLowerCase().trim(); | ||
| const suffix = /-(qat|q4|q8|int4|int8|4bit|8bit|4-bit|8-bit|bf16|fp16|mxfp4|nf4|gguf|rollback|preview|beta|rc\d*)$/; | ||
| let prev = ""; | ||
| while (k !== prev) { prev = k; k = k.replace(suffix, ""); } | ||
| return k; | ||
| } | ||
| function variantPenalty(m) { | ||
| const text = `${m.display_name || ""} ${m.id || ""}`.toLowerCase(); | ||
| let p = 0; | ||
| if (/\(|rollback|preview|\brc\b/.test(text)) p += 100; | ||
| if (/qat|int4|int8|fp16|bf16|mxfp4|nf4|\d\s*-?bit/.test(text)) p += 10; | ||
| p += String(m.id || "").length * 0.01; | ||
| return p; | ||
| } | ||
| function dedupeModelVariants(models) { | ||
| const byBase = new Map(); | ||
| for (const m of models) { | ||
| const key = baseModelKey(m.id); | ||
| const cur = byBase.get(key); | ||
| if (!cur || variantPenalty(m) < variantPenalty(cur)) byBase.set(key, m); | ||
| } | ||
| return [...byBase.values()]; | ||
| } |
There was a problem hiding this comment.
🟡 [MEDIUM] 🧩 Exact duplication of baseModelKey, variantPenalty, and dedupeModelVariants logic
💡 Suggestion: Extract this logic into a shared utility module or consider if the landing page really needs this complexity vs a simpler hardcoded list
📊 Score: 3×4 = 12 · Category: duplicate logic
| export function baseModelKey(id: string): string { | ||
| let k = id.toLowerCase().trim(); | ||
| const suffix = /-(qat|q4|q8|int4|int8|4bit|8bit|4-bit|8-bit|bf16|fp16|mxfp4|nf4|gguf|rollback|preview|beta|rc\d*)$/; | ||
| let prev = ""; | ||
| while (k !== prev) { | ||
| prev = k; | ||
| k = k.replace(suffix, ""); | ||
| } | ||
| return k; | ||
| } |
There was a problem hiding this comment.
🔵 [INFO] 🧩 Complex regex pattern with many quantization suffixes may be overkill
💡 Suggestion: Consider if a simpler approach like checking for common suffixes (-4bit, -8bit, -rollback) would suffice for the current use case
📊 Score: 2×2 = 4 · Category: over-configuration
ethenotethan
left a comment
There was a problem hiding this comment.
Automated Code Review — Layr-Labs/d-inference#
Verdict: REQUEST_CHANGES
Security — ✅ No issues found
Performance — 4 finding(s) (1 blocking)
- 🟡 [MEDIUM]
console-ui/src/app/earn/calc.ts:246-250— O(n²) nested loop in dedupeModelVariants due to Map.get() calls inside loop- Suggestion: The current implementation is already efficient with O(n) complexity using Map operations. No optimization needed.
- 🔵 [INFO]
landing/earn-calculator.js:133-140— Redundant regex compilation and string operations in baseModelKey loop- Suggestion: Pre-compile the regex outside the function and consider caching results for repeated calls with same input
- 🔵 [INFO]
console-ui/src/app/earn/calc.ts:218-227— Regex compilation happens on every baseModelKey call- Suggestion: Move regex compilation outside function scope: const SUFFIX_REGEX = /-(qat|q4|q8|int4|int8|4bit|8bit|4-bit|8-bit|bf16|fp16|mxfp4|nf4|gguf|rollback|preview|beta|rc\d*)$/;
- 🔵 [INFO]
landing/earn-calculator.js:118-122— Regex compilation happens on every baseModelKey call- Suggestion: Move regex compilation outside function scope to avoid recompiling on each call
Type_diligence — ✅ No issues found
Additive_complexity — 2 finding(s) (1 blocking)
- 🔵 [INFO]
console-ui/src/app/earn/calc.ts:219-227— Complex regex pattern for suffix stripping could be simplified- Suggestion: Consider using a simple array of known suffixes and string.endsWith() checks instead of a complex regex with while loop
- 🟡 [MEDIUM]
landing/earn-calculator.js:118-140— Exact duplication of baseModelKey, variantPenalty, and dedupeModelVariants logic- Suggestion: Extract this logic into a shared utility module that both console-ui and landing can import, or generate the landing code from the TypeScript source
6 finding(s) total, 2 blocking. Verdict: REQUEST_CHANGES.
🤖 Automated review by Centaur · DAR-186
| const byBase = new Map<string, Model>(); | ||
| for (const m of models) { | ||
| const key = baseModelKey(m.id); | ||
| const cur = byBase.get(key); | ||
| if (!cur || variantPenalty(m) < variantPenalty(cur)) byBase.set(key, m); |
There was a problem hiding this comment.
🟡 [MEDIUM] ⚡ O(n²) nested loop in dedupeModelVariants due to Map.get() calls inside loop
💡 Suggestion: The current implementation is already efficient with O(n) complexity using Map operations. No optimization needed.
📊 Score: 2×4 = 8 · Category: Inefficient data structures
| function dedupeModelVariants(models) { | ||
| const byBase = new Map(); | ||
| for (const m of models) { | ||
| const key = baseModelKey(m.id); | ||
| const cur = byBase.get(key); | ||
| if (!cur || variantPenalty(m) < variantPenalty(cur)) byBase.set(key, m); | ||
| } | ||
| return [...byBase.values()]; |
There was a problem hiding this comment.
🔵 [INFO] ⚡ Redundant regex compilation and string operations in baseModelKey loop
💡 Suggestion: Pre-compile the regex outside the function and consider caching results for repeated calls with same input
📊 Score: 2×3 = 6 · Category: Repeated work
| export function baseModelKey(id: string): string { | ||
| let k = id.toLowerCase().trim(); | ||
| const suffix = /-(qat|q4|q8|int4|int8|4bit|8bit|4-bit|8-bit|bf16|fp16|mxfp4|nf4|gguf|rollback|preview|beta|rc\d*)$/; | ||
| let prev = ""; | ||
| while (k !== prev) { | ||
| prev = k; | ||
| k = k.replace(suffix, ""); | ||
| } | ||
| return k; | ||
| } |
There was a problem hiding this comment.
🔵 [INFO] ⚡ Regex compilation happens on every baseModelKey call
💡 Suggestion: Move regex compilation outside function scope: const SUFFIX_REGEX = /-(qat|q4|q8|int4|int8|4bit|8bit|4-bit|8-bit|bf16|fp16|mxfp4|nf4|gguf|rollback|preview|beta|rc\d*)$/;
📊 Score: 1×2 = 2 · Category: Repeated work
| function baseModelKey(id) { | ||
| let k = String(id || "").toLowerCase().trim(); | ||
| const suffix = /-(qat|q4|q8|int4|int8|4bit|8bit|4-bit|8-bit|bf16|fp16|mxfp4|nf4|gguf|rollback|preview|beta|rc\d*)$/; | ||
| let prev = ""; | ||
| while (k !== prev) { prev = k; k = k.replace(suffix, ""); } |
There was a problem hiding this comment.
🔵 [INFO] ⚡ Regex compilation happens on every baseModelKey call
💡 Suggestion: Move regex compilation outside function scope to avoid recompiling on each call
📊 Score: 1×2 = 2 · Category: Repeated work
| let k = id.toLowerCase().trim(); | ||
| const suffix = /-(qat|q4|q8|int4|int8|4bit|8bit|4-bit|8-bit|bf16|fp16|mxfp4|nf4|gguf|rollback|preview|beta|rc\d*)$/; | ||
| let prev = ""; | ||
| while (k !== prev) { | ||
| prev = k; | ||
| k = k.replace(suffix, ""); | ||
| } | ||
| return k; | ||
| } |
There was a problem hiding this comment.
🔵 [INFO] 🧩 Complex regex pattern for suffix stripping could be simplified
💡 Suggestion: Consider using a simple array of known suffixes and string.endsWith() checks instead of a complex regex with while loop
📊 Score: 2×3 = 6 · Category: over-abstraction
| function baseModelKey(id) { | ||
| let k = String(id || "").toLowerCase().trim(); | ||
| const suffix = /-(qat|q4|q8|int4|int8|4bit|8bit|4-bit|8-bit|bf16|fp16|mxfp4|nf4|gguf|rollback|preview|beta|rc\d*)$/; | ||
| let prev = ""; | ||
| while (k !== prev) { prev = k; k = k.replace(suffix, ""); } | ||
| return k; | ||
| } | ||
| function variantPenalty(m) { | ||
| const text = `${m.display_name || ""} ${m.id || ""}`.toLowerCase(); | ||
| let p = 0; | ||
| if (/\(|rollback|preview|\brc\b/.test(text)) p += 100; | ||
| if (/qat|int4|int8|fp16|bf16|mxfp4|nf4|\d\s*-?bit/.test(text)) p += 10; | ||
| p += String(m.id || "").length * 0.01; | ||
| return p; | ||
| } | ||
| function dedupeModelVariants(models) { | ||
| const byBase = new Map(); | ||
| for (const m of models) { | ||
| const key = baseModelKey(m.id); | ||
| const cur = byBase.get(key); | ||
| if (!cur || variantPenalty(m) < variantPenalty(cur)) byBase.set(key, m); | ||
| } | ||
| return [...byBase.values()]; |
There was a problem hiding this comment.
🟡 [MEDIUM] 🧩 Exact duplication of baseModelKey, variantPenalty, and dedupeModelVariants logic
💡 Suggestion: Extract this logic into a shared utility module that both console-ui and landing can import, or generate the landing code from the TypeScript source
📊 Score: 3×4 = 12 · Category: duplicate logic
Summary
Follow-up to #414. The earnings calculator was listing every catalog build of a model as a separate row. The live catalog returns four entries:
gpt-oss-20b→ "GPT-OSS 20B"gemma-4-26b→ "Gemma 4 26B" (active)gemma-4-26b-qat-4bit→ "Gemma 4 26B" (beta, duplicate name)gemma-4-26b-8bit→ "Gemma 4 26B 8-bit (rollback)"…so the picker showed 4 rows (two identical "Gemma 4 26B", an 8-bit rollback, and GPT-OSS). It should show the two real models: Gemma 4 26B and GPT-OSS 20B.
Fix
Dedupe the catalog by a normalized base-model key (
baseModelKeystrips quantization / build suffixes like-qat-4bit,-8bit,-rollback,-bf16, …) and keep the most canonical build per family (variantPenaltyfavors clean names and shorter base ids over quant/rollback builds).console-ui/src/app/earn/calc.ts(dedupeModelVariants, used inbuildCatalogModels) andlanding/earn-calculator.js.baseModelKeysuffix stripping,dedupeModelVariants→ one entry per base, andbuildCatalogModels→ exactly["GPT-OSS 20B", "Gemma 4 26B"].Before / after
Verification
npx vitest run: ✅ (earn suite 6/6, incl. 3 new dedupe tests)npx eslint src/app/earn/calc.ts: ✅ 0 errorsnode --check landing/earn-calculator.js: ✅Need help on this PR? Tag
/codesmithwith what you need. Autofix is disabled.