feat: normalize model IDs for display aggregation (rule-based) - #131
feat: normalize model IDs for display aggregation (rule-based)#131hellosunghyun wants to merge 3 commits into
Conversation
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
|
@hellosunghyun is attempting to deploy a commit to the Inevitable Team on Vercel. A member of the Team first needs to authorize it. |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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>
|
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
I will be finalizing the implementation very soon. #117 (comment) |



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_ALIASESmap 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
claude-opus-4-1-20250805claude-opus-4.1antigravity-claude-opus-4-5-thinkingclaude-opus-4.5gemini-claude-opus-4.5-thinking-13claude-opus-4.5claude-4-sonnet-thinkingclaude-sonnet-4claude-4.5-opus-high-thinkingclaude-opus-4.5gemini-2.5-pro-preview-05-06gemini-2.5-progemini-2.5-flash-latestgemini-2.5-flashqwen/qwen3-32bqwen3-32bgpt-5-1-codex-max-0gpt-5.1-codex-maxgpt-5.2-codex-fastgpt-5.2-codexgpt-5.2-extra-high-fastgpt-5.2minimax-m2.1-freeminimax-m2.1claude-3-5-sonnet-20241022claude-3.5-sonnetNormalization rules (applied in order)
antigravity-)gemini-claude-opus-4.5→claude-opus-4.5)qwen/,accounts/fireworks/models/, etc.)-YYYYMMDD)-preview/-expsuffixes (including-preview-DD-DD)-latest-low,-high,-fast,-free,-xhigh,-extra-high-fast,-medium-fast, etc.)-max)-thinking/-high-thinkingfrom Claude models-max(exceptcodex-maxwhich is a real model)claude-4-sonnet→claude-sonnet-4, only version ≥ 4)4-5→4.5between single digits)Design decisions
DISPLAY_ALIASESHashMap and the 40+ entryMODEL_DISPLAY_NAMESmap inwrapped.tsare removed. All normalization is rule-based.codex-maxis preserved. It's the only-maxvariant that is a real distinct model.o1/o3stay lowercase. These are official branding.Changes
Rust core
packages/core/src/pricing/aliases.rs: RemovedDISPLAY_ALIASESHashMap. Addednormalize_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: ReplacedMODEL_DISPLAY_NAMESmap and 60-line regex cascade with compactBRAND_PREFIXESarray + rule-basedformatModelName.packages/cli/src/table.ts: SimplifiedformatModelName(Rust pre-strips dates/prefixes).Frontend (new)
packages/frontend/src/lib/normalizeModel.ts(new file): TypeScript port of Rust normalizer. ExportsnormalizeDisplayModelId()andformatModelDisplayName().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
tokscale submitworks end-to-end against local serverSummary 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
Refactors
Written for commit bc9190e. Summary will update on new commits.