Skip to content

feat: normalize model IDs for display aggregation (rule-based) - #131

Closed
hellosunghyun wants to merge 3 commits into
junhoyeo:mainfrom
hellosunghyun:feat/normalize-model-ids-v2
Closed

feat: normalize model IDs for display aggregation (rule-based)#131
hellosunghyun wants to merge 3 commits into
junhoyeo:mainfrom
hellosunghyun:feat/normalize-model-ids-v2

Conversation

@hellosunghyun

@hellosunghyun hellosunghyun commented Jan 26, 2026

Copy link
Copy Markdown
Contributor

Summary

Model IDs from different tools/configs come in many inconsistent forms that refer to the same underlying model. This PR replaces the old hardcoded DISPLAY_ALIASES map with a rule-based normalization pipeline (normalize_display_model_id) in both Rust core and a TypeScript frontend port, so every display/aggregation surface shows unified, human-readable model names.

Before → After

Raw model ID Normalized Display
claude-opus-4-1-20250805 claude-opus-4.1 Claude Opus 4.1
antigravity-claude-opus-4-5-thinking claude-opus-4.5 Claude Opus 4.5
gemini-claude-opus-4.5-thinking-13 claude-opus-4.5 Claude Opus 4.5
claude-4-sonnet-thinking claude-sonnet-4 Claude Sonnet 4
claude-4.5-opus-high-thinking claude-opus-4.5 Claude Opus 4.5
gemini-2.5-pro-preview-05-06 gemini-2.5-pro Gemini 2.5 Pro
gemini-2.5-flash-latest gemini-2.5-flash Gemini 2.5 Flash
qwen/qwen3-32b qwen3-32b Qwen3 32b
gpt-5-1-codex-max-0 gpt-5.1-codex-max GPT 5.1 Codex Max
gpt-5.2-codex-fast gpt-5.2-codex GPT 5.2 Codex
gpt-5.2-extra-high-fast gpt-5.2 GPT 5.2
minimax-m2.1-free minimax-m2.1 Minimax M2.1
claude-3-5-sonnet-20241022 claude-3.5-sonnet Claude 3.5 Sonnet

Normalization rules (applied in order)

  1. Lowercase the entire ID
  2. Strip routing prefix (antigravity-)
  3. Strip outer model-family routing (e.g. gemini-claude-opus-4.5claude-opus-4.5)
  4. Strip provider prefixes (qwen/, accounts/fireworks/models/, etc.)
  5. Strip date suffixes (-YYYYMMDD)
  6. Strip -preview/-exp suffixes (including -preview-DD-DD)
  7. Strip -latest
  8. Strip tier suffixes (-low, -high, -fast, -free, -xhigh, -extra-high-fast, -medium-fast, etc.)
  9. Strip trailing iteration numbers (2+ digits on claude/gemini, any digit after -max)
  10. Strip -thinking/-high-thinking from Claude models
  11. Strip -max (except codex-max which is a real model)
  12. Fix Claude wrong naming order (claude-4-sonnetclaude-sonnet-4, only version ≥ 4)
  13. Normalize version separator (4-54.5 between single digits)

Design decisions

  • No hardcoded maps. The old DISPLAY_ALIASES HashMap and the 40+ entry MODEL_DISPLAY_NAMES map in wrapped.ts are removed. All normalization is rule-based.
  • codex-max is preserved. It's the only -max variant that is a real distinct model.
  • o1/o3 stay lowercase. These are official branding.
  • Pricing lookup is NOT affected. The normalization is for display/aggregation only.

Changes

Rust core

  • packages/core/src/pricing/aliases.rs: Removed DISPLAY_ALIASES HashMap. Added normalize_display_model_id() with 13 rule-based helper functions (strip_outer_model_family, strip_tier_suffixes, strip_date_suffix, strip_preview_exp_suffix, strip_claude_thinking_suffix, strip_max_suffix, strip_trailing_iteration, fix_claude_wrong_order, normalize_version_separator). 168 Rust tests pass including 34+ normalization tests.
  • packages/core/src/lib.rs: Applied normalization at all aggregation points.

CLI

  • packages/cli/src/wrapped.ts: Replaced MODEL_DISPLAY_NAMES map and 60-line regex cascade with compact BRAND_PREFIXES array + rule-based formatModelName.
  • packages/cli/src/table.ts: Simplified formatModelName (Rust pre-strips dates/prefixes).

Frontend (new)

  • packages/frontend/src/lib/normalizeModel.ts (new file): TypeScript port of Rust normalizer. Exports normalizeDisplayModelId() and formatModelDisplayName().
  • packages/frontend/src/app/api/users/[username]/route.ts: API aggregation now normalizes + deduplicates model keys.
  • packages/frontend/src/components/BreakdownPanel.tsx: Model names normalized and pretty-printed in breakdown panel.
  • packages/frontend/src/components/profile/index.tsx: Model names pretty-printed in profile usage table and tags.

Testing

  • 168 Rust tests pass (34+ normalization-specific)
  • TypeScript LSP diagnostics clean on all changed files
  • Native module builds successfully
  • tokscale submit works end-to-end against local server

Summary by cubic

Unifies model IDs across core, CLI, and frontend using a rule-based normalizer so reports and UI aggregate under clean, consistent names. Removes hardcoded maps and normalizes variants (dates, prefixes, tiers, thinking, etc.) without affecting pricing.

  • New Features

    • Added normalize_display_model_id in Rust and a TypeScript port used in API routes, BreakdownPanel, and profile views.
    • Normalizes common variants: routing/provider prefixes, -preview/-exp/-latest, date suffixes, tier suffixes, trailing iterations, Claude thinking, and version separators.
    • Fixes Claude naming order (claude-4-sonnet → claude-sonnet-4) and preserves real variants like codex-max.
    • Added formatModelDisplayName for pretty, human-readable names (keeps o1/o3 lowercase).
  • Refactors

    • Removed DISPLAY_ALIASES and MODEL_DISPLAY_NAMES; replaced with rule-based formatting in CLI and frontend.
    • Applied normalization at all aggregation points in Rust reports to deduplicate model keys.
    • Simplified CLI formatModelName with brand prefixes and dot-separated versions.

Written for commit bc9190e. Summary will update on new commits.

Add normalize_display_model_id() to unify model ID variants that refer
to the same underlying model. This ensures models like
claude-opus-4-1-20250805 and claude-opus-4-1 aggregate together in
reports and UI.

Normalization rules applied in order:
- Strip routing prefixes (antigravity-)
- Strip provider prefixes (qwen/, moonshotai/, etc.)
- Strip date suffixes (-YYYYMMDD)
- Strip -preview/-exp/-latest suffixes
- Apply known aliases (wrong naming order, -max tiers, thinking-max)
- Strip -thinking from Claude models
- Normalize Claude version separators (3.5 → 3-5)

Note: codex-max is preserved as a real distinct model variant.
Pricing lookup is NOT affected - this is display/aggregation only.
…AMES with rule-based formatModelName

- Change version separator normalization from Claude-only dot→hyphen to
  universal hyphen→dot (e.g., claude-opus-4-5 → claude-opus-4.5)
- Replace hardcoded MODEL_DISPLAY_NAMES map (40+ entries) and 60-line
  regex cascade with a compact rule-based formatModelName using
  BRAND_PREFIXES
- Update DISPLAY_ALIASES to output dot-notation versions
…etty display

- Implement strip_tier_suffixes() in Rust and TypeScript (strips -low, -high, -fast, -free, -xhigh, -extra-high-fast, etc.)
- Add normalizeModel.ts: TypeScript port of Rust normalizer + formatModelDisplayName()
- Normalize and deduplicate models in API route, BreakdownPanel, and profile page
- Preserve lowercase for o1/o3 series in display names
- 168 Rust tests pass including 34+ normalization tests
@vercel

vercel Bot commented Jan 26, 2026

Copy link
Copy Markdown
Contributor

@hellosunghyun is attempting to deploy a commit to the Inevitable Team on Vercel.

A member of the Team first needs to authorize it.

@cubic-dev-ai cubic-dev-ai Bot 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.

1 issue found across 8 files

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="packages/frontend/src/components/BreakdownPanel.tsx">

<violation number="1" location="packages/frontend/src/components/BreakdownPanel.tsx:244">
P2: Normalizing model IDs without aggregating duplicate normalized IDs can produce multiple rows with the same display name and split costs, while the summary count dedupes them. Consider aggregating modelEntries by normalized modelId before sorting so the list matches the normalized summary count.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

for (const source of sources) {
if (source.models && Object.keys(source.models).length > 0) {
for (const [rawModelId, data] of Object.entries(source.models)) {
const modelId = normalizeDisplayModelId(rawModelId);

@cubic-dev-ai cubic-dev-ai Bot Jan 26, 2026

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.

P2: Normalizing model IDs without aggregating duplicate normalized IDs can produce multiple rows with the same display name and split costs, while the summary count dedupes them. Consider aggregating modelEntries by normalized modelId before sorting so the list matches the normalized summary count.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/frontend/src/components/BreakdownPanel.tsx, line 244:

<comment>Normalizing model IDs without aggregating duplicate normalized IDs can produce multiple rows with the same display name and split costs, while the summary count dedupes them. Consider aggregating modelEntries by normalized modelId before sorting so the list matches the normalized summary count.</comment>

<file context>
@@ -236,32 +237,33 @@ const ModelsList = styled.div`
+   for (const source of sources) {
+     if (source.models && Object.keys(source.models).length > 0) {
+       for (const [rawModelId, data] of Object.entries(source.models)) {
+         const modelId = normalizeDisplayModelId(rawModelId);
+         modelEntries.push({
+           modelId,
</file context>
Fix with Cubic

@hellosunghyun

hellosunghyun commented Jan 26, 2026

Copy link
Copy Markdown
Contributor Author

If you disagree with any of these changes or the overall direction, feel free to reject — no hard feelings at all.

The screenshots above show the before/after comparison on the profile page. The key visual improvement is that ~80 raw model ID variants (with dates, thinking suffixes, provider prefixes, tier flags) collapse into ~40 clean, human-readable entries.

image image

@junhoyeo junhoyeo self-assigned this Jan 27, 2026
@junhoyeo

Copy link
Copy Markdown
Owner

Following the discussion in #117 (comment), I believe we should maintain the internal/detailed model IDs. Adding a frontend-side switch or configuration option to aggregate model names would be an acceptable approach.

@junhoyeo junhoyeo removed their assignment Jan 27, 2026
@cantalupo555

Copy link
Copy Markdown
Contributor

Following the discussion in #117 (comment), I believe we should maintain the internal/detailed model IDs. Adding a frontend-side switch or configuration option to aggregate model names would be an acceptable approach.

@junhoyeo It will undoubtedly be an interesting approach
since we can use the same model in different tools.

image

I will be finalizing the implementation very soon. #117 (comment)

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.

3 participants