fix(pricing): map opus-4-7 ids to claude-opus-4-7 instead of legacy claude-opus-4 - #580
Merged
Merged
Conversation
β¦de-opus-4
`normalize_model_name` only had explicit branches for opus 4.5 and 4.6
and fell through to a catch-all `else if contains_delimited_fragment("4")`
that returned `claude-opus-4`. For ids like `aws.claude-opus-4-7` the
catch-all swallowed 4.7, after which `exact_match_openrouter` reached
`anthropic/claude-opus-4` via the `model_part` index β i.e. the legacy
opus 4 entry priced at $15/$75 per M instead of the 4.7 entry at $5/$25.
Real-world impact: a session with 8.4M input / 873K output / 41.3M
cache-read / 12.1M cache-write tokens billed at ~$480 instead of ~$160,
roughly a 3x overcharge.
Add an explicit 4.7 branch in front of 4.6 so the opus normalizer
short-circuits to `claude-opus-4-7` before reaching the catch-all.
Future minor versions (4.8+) will still need their own branches; the
catch-all is left in place to preserve the existing behavior tests
(`test_normalize_opus_4_60_does_not_map_to_4_6` etc.) rely on.
Add three regression tests:
- `test_normalize_opus_4_7_prefers_4_7_over_4` β short-form `opus-4-7`
- `test_normalize_opus_4_7_dot_prefers_4_7_over_4` β dot-form `opus-4.7`
- `test_aws_opus_4_7_does_not_degrade_to_opus_4` β the actual reported
case, asserting the resolved key and the computed cost falls in the
4.7 price band (~$160) rather than the legacy opus-4 band (~$480).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Contributor
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
junhoyeo
approved these changes
May 24, 2026
Owner
|
Thanks! |
Owner
|
@xczllgit this has been merged to v3.0.0: https://github.com/junhoyeo/tokscale/releases/tag/v3.0.0 thanks for the contribution! |
3 tasks
junhoyeo
added a commit
that referenced
this pull request
Jun 10, 2026
β¦ng (#634) * fix(pricing): parse modern Claude version from id instead of hardcoding `normalize_model_name` matched each opus/sonnet/haiku minor version with a hardcoded branch and a catch-all `claude-opus-4`. Every new release (4.5 β 4.6 β 4.7 β 4.8) needed a new branch; a missing one let ids like `aws.claude-opus-4-8` fall through to the catch-all, which OpenRouter then resolved to legacy `anthropic/claude-opus-4` ($15/$75 per M instead of $5/$25) β a ~3x overcharge. This repeated for 4.7 (#580) and again for 4.8. The modern Claude line (major >= 4) follows the regular `claude-{family}-{major}[-{minor}]` scheme, so parse the version straight from the id and build the canonical key dynamically. New minor releases (4.9, 5.0, β¦) now resolve to their own pricing key with no code change β pricing values still come entirely from the upstream datasets. Boundary contract from the old matcher is preserved: `opus-4-60` β `claude-opus-4` (two-digit minor degrades to major), `opus-14-6` β None (two-digit major is not the modern line), undelimited `opus4`/`opus-4x` β None. The irregular legacy 3.x naming keeps its explicit branches. Tests: - test_normalize_future_minor_versions_resolve_without_hardcoding β 4.9/5.0 and cross-family (sonnet/haiku) parse without a hardcoded entry - test_normalize_modern_claude_boundaries β locks the boundary contract - 4.8 regression tests mirroring the 4.7 set (short/dot/aws forms + cost band) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * style: wrap haiku condition to satisfy cargo fmt Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(pricing): enforce never-degrade across all Claude families and majors Generalize the opus-only version handling in PricingLookup so every Claude family (opus, sonnet, haiku, fable) gets the same guarantees: - normalize_claude_opus_4_minor -> normalize_claude_family_minor: family/major/minor parsed from the id in both orders (sonnet-4.7, claude-4-6-sonnet), so reversed-order sonnet/haiku ids resolve to their canonical key instead of cross-family fuzzy fallbacks. - normalize_claude_family_bare_major: bare modern majors (claude-opus-5, claude-sonnet-5, fable-5) normalize to a canonical key that resolves only via an exact dataset hit. - contains_delimited_modern_major_minor: the 4.x major-minor None-guard in normalize_model_name now covers majors 4-9, so 4-60 / 5-0 / dated forms stay unresolved instead of degrading. - resolves_unsafe_claude_version (was resolves_different_claude_opus_4_ minor): vetoes cross-family resolutions (bedrock sonnet ids billed as opus), cross-version resolutions for any family (sonnet-4-7 -> sonnet-4.6, haiku-4-6 -> haiku-4.5 / 3.5-haiku), and any modern-Claude resolution for ids whose version could not be parsed. Bare major 4 stays unpinned to preserve existing claude-opus-4 fuzzy behavior. Whether a version is "known" remains dataset-driven, exactly as it was for opus: the canonical key either exact-matches the pricing dataset or the id resolves unpriced. No hardcoded minor lists were added. Reworks PR #634 on top of main's mechanisms instead of merging its parallel parser. Constraint: unknown minors/majors must resolve unpriced, never to a cheaper or different price Constraint: all of main's existing pricing tests must pass unchanged Rejected: PR #634's original generic parser | degrades two-digit minors (opus-4-60 -> claude-opus-4) and drops reversed-order coverage Rejected: extending the hardcoded 4.5/4.6/4.7 allowlist in has_unrecognized_claude_four_minor | recreates the new-minor-release maintenance trap PR #634 set out to remove Rejected: pinning bare major 4 ids (claude-opus-4) to exact-only | changes long-standing dated/regional 4.x resolution covered by existing tests Confidence: high Scope-risk: moderate Directive: requested_claude_version deliberately unpins bare major 4; do not "complete" it to all majors without auditing dated-id (claude-opus-4-20250514) and regional-key resolution Not-tested: provider-hinted lookups (lookup_with_provider) against cross-family adversarial datasets; models.dev reseller keys that legitimately embed reversed-order ids (cortecs/claude-4-6-sonnet) are covered only via real-data probing --------- Co-authored-by: xiaochaozheng <xiaochaozheng@meituan.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: Junho Yeo <i@junho.io>
pinion05
added a commit
to pinion05/tokscale
that referenced
this pull request
Jun 23, 2026
β¦ng (junhoyeo#634) * fix(pricing): parse modern Claude version from id instead of hardcoding `normalize_model_name` matched each opus/sonnet/haiku minor version with a hardcoded branch and a catch-all `claude-opus-4`. Every new release (4.5 β 4.6 β 4.7 β 4.8) needed a new branch; a missing one let ids like `aws.claude-opus-4-8` fall through to the catch-all, which OpenRouter then resolved to legacy `anthropic/claude-opus-4` ($15/$75 per M instead of $5/$25) β a ~3x overcharge. This repeated for 4.7 (junhoyeo#580) and again for 4.8. The modern Claude line (major >= 4) follows the regular `claude-{family}-{major}[-{minor}]` scheme, so parse the version straight from the id and build the canonical key dynamically. New minor releases (4.9, 5.0, β¦) now resolve to their own pricing key with no code change β pricing values still come entirely from the upstream datasets. Boundary contract from the old matcher is preserved: `opus-4-60` β `claude-opus-4` (two-digit minor degrades to major), `opus-14-6` β None (two-digit major is not the modern line), undelimited `opus4`/`opus-4x` β None. The irregular legacy 3.x naming keeps its explicit branches. Tests: - test_normalize_future_minor_versions_resolve_without_hardcoding β 4.9/5.0 and cross-family (sonnet/haiku) parse without a hardcoded entry - test_normalize_modern_claude_boundaries β locks the boundary contract - 4.8 regression tests mirroring the 4.7 set (short/dot/aws forms + cost band) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * style: wrap haiku condition to satisfy cargo fmt Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(pricing): enforce never-degrade across all Claude families and majors Generalize the opus-only version handling in PricingLookup so every Claude family (opus, sonnet, haiku, fable) gets the same guarantees: - normalize_claude_opus_4_minor -> normalize_claude_family_minor: family/major/minor parsed from the id in both orders (sonnet-4.7, claude-4-6-sonnet), so reversed-order sonnet/haiku ids resolve to their canonical key instead of cross-family fuzzy fallbacks. - normalize_claude_family_bare_major: bare modern majors (claude-opus-5, claude-sonnet-5, fable-5) normalize to a canonical key that resolves only via an exact dataset hit. - contains_delimited_modern_major_minor: the 4.x major-minor None-guard in normalize_model_name now covers majors 4-9, so 4-60 / 5-0 / dated forms stay unresolved instead of degrading. - resolves_unsafe_claude_version (was resolves_different_claude_opus_4_ minor): vetoes cross-family resolutions (bedrock sonnet ids billed as opus), cross-version resolutions for any family (sonnet-4-7 -> sonnet-4.6, haiku-4-6 -> haiku-4.5 / 3.5-haiku), and any modern-Claude resolution for ids whose version could not be parsed. Bare major 4 stays unpinned to preserve existing claude-opus-4 fuzzy behavior. Whether a version is "known" remains dataset-driven, exactly as it was for opus: the canonical key either exact-matches the pricing dataset or the id resolves unpriced. No hardcoded minor lists were added. Reworks PR junhoyeo#634 on top of main's mechanisms instead of merging its parallel parser. Constraint: unknown minors/majors must resolve unpriced, never to a cheaper or different price Constraint: all of main's existing pricing tests must pass unchanged Rejected: PR junhoyeo#634's original generic parser | degrades two-digit minors (opus-4-60 -> claude-opus-4) and drops reversed-order coverage Rejected: extending the hardcoded 4.5/4.6/4.7 allowlist in has_unrecognized_claude_four_minor | recreates the new-minor-release maintenance trap PR junhoyeo#634 set out to remove Rejected: pinning bare major 4 ids (claude-opus-4) to exact-only | changes long-standing dated/regional 4.x resolution covered by existing tests Confidence: high Scope-risk: moderate Directive: requested_claude_version deliberately unpins bare major 4; do not "complete" it to all majors without auditing dated-id (claude-opus-4-20250514) and regional-key resolution Not-tested: provider-hinted lookups (lookup_with_provider) against cross-family adversarial datasets; models.dev reseller keys that legitimately embed reversed-order ids (cortecs/claude-4-6-sonnet) are covered only via real-data probing --------- Co-authored-by: xiaochaozheng <xiaochaozheng@meituan.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: Junho Yeo <i@junho.io>
t1000040
pushed a commit
to tmobi-internal/tokscale
that referenced
this pull request
Jun 30, 2026
β¦ng (junhoyeo#634) * fix(pricing): parse modern Claude version from id instead of hardcoding `normalize_model_name` matched each opus/sonnet/haiku minor version with a hardcoded branch and a catch-all `claude-opus-4`. Every new release (4.5 β 4.6 β 4.7 β 4.8) needed a new branch; a missing one let ids like `aws.claude-opus-4-8` fall through to the catch-all, which OpenRouter then resolved to legacy `anthropic/claude-opus-4` ($15/$75 per M instead of $5/$25) β a ~3x overcharge. This repeated for 4.7 (junhoyeo#580) and again for 4.8. The modern Claude line (major >= 4) follows the regular `claude-{family}-{major}[-{minor}]` scheme, so parse the version straight from the id and build the canonical key dynamically. New minor releases (4.9, 5.0, β¦) now resolve to their own pricing key with no code change β pricing values still come entirely from the upstream datasets. Boundary contract from the old matcher is preserved: `opus-4-60` β `claude-opus-4` (two-digit minor degrades to major), `opus-14-6` β None (two-digit major is not the modern line), undelimited `opus4`/`opus-4x` β None. The irregular legacy 3.x naming keeps its explicit branches. Tests: - test_normalize_future_minor_versions_resolve_without_hardcoding β 4.9/5.0 and cross-family (sonnet/haiku) parse without a hardcoded entry - test_normalize_modern_claude_boundaries β locks the boundary contract - 4.8 regression tests mirroring the 4.7 set (short/dot/aws forms + cost band) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * style: wrap haiku condition to satisfy cargo fmt Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(pricing): enforce never-degrade across all Claude families and majors Generalize the opus-only version handling in PricingLookup so every Claude family (opus, sonnet, haiku, fable) gets the same guarantees: - normalize_claude_opus_4_minor -> normalize_claude_family_minor: family/major/minor parsed from the id in both orders (sonnet-4.7, claude-4-6-sonnet), so reversed-order sonnet/haiku ids resolve to their canonical key instead of cross-family fuzzy fallbacks. - normalize_claude_family_bare_major: bare modern majors (claude-opus-5, claude-sonnet-5, fable-5) normalize to a canonical key that resolves only via an exact dataset hit. - contains_delimited_modern_major_minor: the 4.x major-minor None-guard in normalize_model_name now covers majors 4-9, so 4-60 / 5-0 / dated forms stay unresolved instead of degrading. - resolves_unsafe_claude_version (was resolves_different_claude_opus_4_ minor): vetoes cross-family resolutions (bedrock sonnet ids billed as opus), cross-version resolutions for any family (sonnet-4-7 -> sonnet-4.6, haiku-4-6 -> haiku-4.5 / 3.5-haiku), and any modern-Claude resolution for ids whose version could not be parsed. Bare major 4 stays unpinned to preserve existing claude-opus-4 fuzzy behavior. Whether a version is "known" remains dataset-driven, exactly as it was for opus: the canonical key either exact-matches the pricing dataset or the id resolves unpriced. No hardcoded minor lists were added. Reworks PR junhoyeo#634 on top of main's mechanisms instead of merging its parallel parser. Constraint: unknown minors/majors must resolve unpriced, never to a cheaper or different price Constraint: all of main's existing pricing tests must pass unchanged Rejected: PR junhoyeo#634's original generic parser | degrades two-digit minors (opus-4-60 -> claude-opus-4) and drops reversed-order coverage Rejected: extending the hardcoded 4.5/4.6/4.7 allowlist in has_unrecognized_claude_four_minor | recreates the new-minor-release maintenance trap PR junhoyeo#634 set out to remove Rejected: pinning bare major 4 ids (claude-opus-4) to exact-only | changes long-standing dated/regional 4.x resolution covered by existing tests Confidence: high Scope-risk: moderate Directive: requested_claude_version deliberately unpins bare major 4; do not "complete" it to all majors without auditing dated-id (claude-opus-4-20250514) and regional-key resolution Not-tested: provider-hinted lookups (lookup_with_provider) against cross-family adversarial datasets; models.dev reseller keys that legitimately embed reversed-order ids (cortecs/claude-4-6-sonnet) are covered only via real-data probing --------- Co-authored-by: xiaochaozheng <xiaochaozheng@meituan.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: Junho Yeo <i@junho.io>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
normalize_model_nameonly had explicit branches for opus 4.5 and 4.6; anything else with "opus" + "4" fell through to a catch-all that returnedclaude-opus-4.aws.claude-opus-4-7this causedexact_match_openrouterto reachanthropic/claude-opus-4via themodel_partindex β the legacy opus 4 entry priced at $15/$75 per M instead of opus 4.7's $5/$25.claude-opus-4-7. Catch-all is left in place to preserve the existing behavior tests rely on (e.g.test_normalize_opus_4_60_does_not_map_to_4_6).Why this matters
A real session with 8.4M input / 873K output / 41.3M cache-read / 12.1M cache-write tokens on
aws.claude-opus-4-7was being billed at ~$480 instead of the correct ~$160 β a roughly 3x overcharge β because the resolver picked the legacy opus 4 price table.How I traced it
The lookup chain for
aws.claude-opus-4-7:aws.is not inPROVIDER_PREFIXES, sostrip_known_provider_prefixskips it.normalize_version_separatorproducesaws.claude-opus-4.7β still no match.normalize_model_namefalls through toclaude-opus-4(the bug).exact_match_openrouter("claude-opus-4")reachesanthropic/claude-opus-4viaopenrouter_model_part, returning the legacy $15/$75 entry.After the fix, step 4 short-circuits to
claude-opus-4-7, which exists in upstream LiteLLM data and prices correctly.Relationship to #578
Independent. #578 normalizes
anthropic/claude-{major}-{minor}-{family}(family-suffix order, e.g.anthropic/claude-4-6-sonnet) and adds 4.5/4.6 aliases. It does not touchnormalize_model_name, has no 4.7 entries, andaws.-prefixed ids never reach its new code path. The two fixes don't overlap and won't conflict at merge.Test plan
cargo test -p tokscale-core --lib pricing::lookupβ 129 passed, 0 failed (3 new tests included)cargo test -p tokscale-coreβ 674 + 10 + 3 + 0 passed across all suites, 0 failedtest_normalize_opus_4_7_prefers_4_7_over_4β short-formopus-4-7test_normalize_opus_4_7_dot_prefers_4_7_over_4β dot-formopus-4.7test_aws_opus_4_7_does_not_degrade_to_opus_4β the actual reported case, asserts both the resolved key (claude-opus-4-7) and that computed cost lands in the 4.7 band ($160) rather than legacy opus-4 band ($480)π€ Generated with Claude Code
Summary by cubic
Fix pricing normalization for Opus 4.7 so IDs resolve to
claude-opus-4-7instead of the legacyclaude-opus-4. This corrects cost calculations foraws.claude-opus-4-7and prevents ~3x overcharges.normalize_model_name(before 4.6) to routeopus-4-7andopus-4.7toclaude-opus-4-7.aws.claude-opus-4-7from falling back toanthropic/claude-opus-4; add regression tests to verify the resolved key and 4.7 pricing.Written for commit 6f05b84. Summary will update on new commits. Review in cubic