Skip to content

fix(earn): collapse catalog variants to one model per family#417

Merged
Gajesh2007 merged 2 commits into
Layr-Labs:masterfrom
Gajesh2007:fix/earn-dedupe-model-variants
Jun 20, 2026
Merged

fix(earn): collapse catalog variants to one model per family#417
Gajesh2007 merged 2 commits into
Layr-Labs:masterfrom
Gajesh2007:fix/earn-dedupe-model-variants

Conversation

@Gajesh2007

@Gajesh2007 Gajesh2007 commented Jun 20, 2026

Copy link
Copy Markdown
Member

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 (baseModelKey strips quantization / build suffixes like -qat-4bit, -8bit, -rollback, -bf16, …) and keep the most canonical build per family (variantPenalty favors clean names and shorter base ids over quant/rollback builds).

  • No hardcoded model list — derived from the live catalog, so a genuinely new model still appears; only quant/rollback variants of the same base collapse.
  • Mirrored across console-ui/src/app/earn/calc.ts (dedupeModelVariants, used in buildCatalogModels) and landing/earn-calculator.js.
  • Added unit tests: baseModelKey suffix stripping, dedupeModelVariants → one entry per base, and buildCatalogModels → exactly ["GPT-OSS 20B", "Gemma 4 26B"].

Before / after

Models shown
Before Gemma 4 26B · Gemma 4 26B · Gemma 4 26B 8-bit (rollback) · GPT-OSS 20B
After Gemma 4 26B · GPT-OSS 20B

Verification

  • npx vitest run: ✅ (earn suite 6/6, incl. 3 new dedupe tests)
  • npx eslint src/app/earn/calc.ts: ✅ 0 errors
  • node --check landing/earn-calculator.js: ✅

View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.

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

vercel Bot commented Jun 20, 2026

Copy link
Copy Markdown

@Gajesh2007 is attempting to deploy a commit to the EigenLabs Team on Vercel.

A member of the Team first needs to authorize it.

@Gajesh2007
Gajesh2007 merged commit 916de68 into Layr-Labs:master Jun 20, 2026
4 of 11 checks passed

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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*)$/;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 ethenotethan left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +245 to +252
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()];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 [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

Comment thread landing/earn-calculator.js
return k;
}

/** Lower is more canonical — prefer clean names / base ids over quant/rollback builds. */

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 [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

Comment on lines +118 to +141
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()];
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 [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

Comment on lines +218 to +227
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;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 [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 ethenotethan left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +246 to +250
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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 [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

Comment on lines +133 to +140
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()];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 [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

Comment on lines +218 to +227
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;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 [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

Comment on lines +118 to +122
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, ""); }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 [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

Comment on lines +219 to +227
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;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 [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

Comment on lines +118 to +140
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()];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 [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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants