Skip to content

fix(pricing): resolve price collisions by provider rank, not map order - #12

Merged
hallelx2 merged 1 commit into
mainfrom
halleluyaholudele/hal-563-price-provider-precedence
Aug 2, 2026
Merged

fix(pricing): resolve price collisions by provider rank, not map order#12
hallelx2 merged 1 commit into
mainfrom
halleluyaholudele/hal-563-price-provider-precedence

Conversation

@hallelx2

@hallelx2 hallelx2 commented Aug 2, 2026

Copy link
Copy Markdown
Owner

The bug

Upstream price feeds list a model once per place you can buy it. LiteLLM carries 64 GLM entries, and six collapse onto the canonical ID glm-4.6:

upstream ID in / out
zai/glm-4.6 0.600 / 2.200 Zhipu's own API — what vectorless is billed
together_ai/zai-org/GLM-4.6 0.600 / 2.200 reseller
novita/zai-org/glm-4.6 0.550 / 2.200 reseller
vercel_ai_gateway/zai/glm-4.6 0.450 / 1.800 gateway
openrouter/z-ai/glm-4.6 0.400 / 1.750 gateway
cerebras/zai-glm-4.6 2.250 / 2.750 host

Canonical() throws the namespace away, so vet() kept whichever entry the map visited last — a 5.6x spread decided by iteration order.

Five runs against the same feed gave claude-sonnet-4-5-20250929 at 3.30, 3.30, 3.60, 3.60, 3.00. And glm-4.6 resolved to a reseller rate on every observed run and to Zhipu's real 0.60/2.20 on none, understating spend 18-25% per call. Turning on UseRemote made the number worse than leaving it off.

The fix

Collisions resolve by how authoritative the source is — vendor's own API > its global cloud > regional > GovCloud > resellers — with a lexicographic tie-break whose only job is to remove the dependence on map order.

Two further defects surfaced while testing:

  • Canonical() stripped the date before the version suffix, so -v1 blocked the date regex and claude-sonnet-4-5-20250929-v1:0 keyed separately from claude-sonnet-4-5-20250929. The dated bucket then held only regional and GovCloud entries, quietly billing their 10-20% premium.
  • The drift check ran after the collapse and failed the entire snapshot on one bad row. LiteLLM ships wandb/zai-org/GLM-4.5 at 55000/200000; had it won its collision, all 1,070 other models would have gone with it. Rows are now vetted individually, dropped, and reported via OnError; only an implausible fraction condemns the snapshot.

Also: a winning entry that omits cache rates no longer erases the embedded ones. zai/glm-4.6 carries no cache-write rate, and the family-multiplier fallback priced cached reads at 0.30 against the 0.11 z.ai charges.

Verification

Against the live LiteLLM feed, five consecutive fresh-cache runs:

glm-4.6      in=0.6000 out=2.2000 cacheR=0.1100
glm-4.5      in=0.6000 out=2.2000 cacheR=0.1100
glm-4.5-air  in=0.2000 out=1.1000 cacheR=0.0300
dropped 2 entries with implausible rates: replicate/google/gemini-2.5-flash, wandb/zai-org/GLM-4.5

Identical every run. Before this change the same probe gave 0.55/2.20, then 0.45/1.80, then 0.45/1.80.

New tests use the real upstream IDs and rates — the previous fixtures had one entry per canonical key, so no collision was ever constructed and the bug was invisible to every existing test. TestCollisionIsDeterministic installs the same feed 60 times to exercise many map orderings.

Note

vet() is unexported; its signature changed to return dropped IDs. No public API change.

Closes HAL-563

Summary by Sourcery

Resolve remote pricing collisions by provider precedence instead of map iteration order, and harden vetting and canonicalization to produce stable, accurate rates from noisy upstream feeds.

Bug Fixes:

  • Ensure canonical model IDs strip version and date suffixes in the correct order so Bedrock variants collapse to the intended key and avoid regional/GovCloud premiums for standard lookups.
  • Prevent a single implausible upstream price entry from invalidating the entire remote pricing snapshot by vetting and dropping rows individually while only rejecting snapshots with widespread corruption.
  • Fix cache rate handling so a winning remote entry that omits cache prices no longer overwrites published cache rates from the embedded defaults.
  • Make collision resolution for models sold through multiple providers deterministic and aligned with vendor authority (first-party API over cloud, regional, GovCloud, and resellers) instead of depending on map order.

Enhancements:

  • Introduce provider ranking logic that classifies upstream IDs by source type (first-party, vendor cloud, regional, GovCloud, reseller) and uses a deterministic tie-breaker to choose winners among colliding prices.
  • Add reporting of dropped remote pricing entries with implausible rates via the remote error callback to surface upstream feed issues without failing healthy snapshots.

Tests:

  • Add collision-focused pricing tests built from real LiteLLM feed data to verify correct precedence for GLM and Claude Sonnet clusters, deterministic collision resolution across many map orderings, and resilience to poisoned entries.
  • Extend canonicalization tests to cover Bedrock IDs that combine date and version suffixes, ensuring they map to the same canonical key as undated IDs.

Summary by CodeRabbit

  • Bug Fixes
    • Improved normalization of Bedrock model IDs with version and date suffixes.
    • Resolved pricing conflicts consistently across providers, regions, and gateways.
    • Preserved cache, write, and reasoning rates when valid pricing data is refreshed.
    • Filtered implausible pricing updates while retaining usable rates and reporting dropped entries.
    • Added safeguards to reject snapshots containing excessive invalid pricing data.
  • Tests
    • Expanded coverage for model normalization, pricing precedence, regional routing, and invalid-rate handling.

Upstream feeds list a model once per place you can buy it. LiteLLM
carries 64 GLM entries, six of which collapse onto the canonical ID
glm-4.6 with a 5.6x spread: zai 0.60, novita 0.55, vercel 0.45,
openrouter 0.40, cerebras 2.25. Canonical() discards the namespace, so
vet() kept whichever entry the map happened to visit last.

That made every cost llmgate reported unreproducible. Five runs against
the same feed gave claude-sonnet-4-5-20250929 at 3.30, 3.30, 3.60, 3.60
and 3.00. Worse for us, glm-4.6 — which vectorless calls on Zhipu's own
API — resolved to a reseller rate on every observed run and to Zhipu's
actual 0.60/2.20 on none of them, understating spend by 18-25% per call.
Enabling UseRemote made the number worse than leaving it off.

Collisions now resolve by how authoritative the source is: the vendor's
own API, then its global cloud endpoint, then regional, then GovCloud,
then resellers. Equal rank falls back to the lexicographically smaller
ID, which carries no meaning beyond making the result independent of
iteration order. Whoever calls a model's own API — the common case —
now gets the rate they are billed.

Two further defects surfaced while testing this:

Canonical() stripped the date suffix before the version suffix, so
"-v1" blocked the date regex and claude-sonnet-4-5-20250929-v1:0 keyed
separately from claude-sonnet-4-5-20250929. The dated bucket then held
only regional and GovCloud entries, quietly billing their 10-20%
premium. Version now comes off first.

The drift check ran after the collapse and failed the whole snapshot on
one bad row. LiteLLM ships wandb/zai-org/GLM-4.5 at 55000/200000, an
upstream units error that lands on glm-4.5; had it won its collision,
every other model in the feed would have been discarded with it. Rows
are now vetted individually and dropped, with a report through OnError,
and only an implausible fraction of the feed condemns the snapshot.

Finally, a winning entry that omits cache rates no longer erases the
embedded ones — zai/glm-4.6 carries no cache-write rate, and falling
through to the family multiplier priced cached reads at 0.30 against
the 0.11 z.ai charges.

Verified against the live LiteLLM feed: five consecutive fresh-cache
runs now resolve glm-4.6, glm-4.5 and glm-4.5-air to Zhipu's own rates,
identically, with both poisoned rows dropped and 1070 models retained.

@sourcery-ai sourcery-ai 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.

Sorry @hallelx2, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The pricing pipeline adds GLM classification, improves Bedrock model-ID normalization, ranks canonical pricing collisions deterministically, filters implausible remote rates, reports dropped IDs, and restores missing cache-related rates.

Changes

Pricing pipeline

Layer / File(s) Summary
Model ID canonicalization
pricing/canonical.go, pricing/canonical_test.go
GLM and AutoGLM IDs use the Zhipu family classification. Bedrock version suffixes are removed before date suffixes.
Canonical collision ranking
pricing/provider.go, pricing/provider_internal_test.go
Pricing sources receive deterministic authority rankings. Equal-ranked entries use cache-data preference and lexical tie-breaking.
Remote pricing validation
pricing/remote.go, pricing/collision_test.go
Remote refresh filters implausible rates, reports dropped IDs, resolves collisions with source ranking, restores missing cache rates, and validates these cases with regression tests.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant refresh
  participant vet
  participant better
  participant OnError
  refresh->>vet: validate and canonicalize remote prices
  vet->>better: resolve canonical model collisions
  vet->>OnError: report dropped model IDs
  vet-->>refresh: install cleaned pricing snapshot
Loading

Possibly related PRs

  • hallelx2/llmgate#9: Introduced the related model-ID normalization and remote pricing pipeline.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: deterministic pricing collision resolution by provider rank instead of map iteration order.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch halleluyaholudele/hal-563-price-provider-precedence

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@sourcery-ai

sourcery-ai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Reviewer's Guide

This PR fixes non-deterministic and often incorrect remote pricing resolution by ranking upstream providers per canonical model, tightening canonicalization of Bedrock/Anthropic IDs, vetting and reporting implausible rates per-row before collapse, and ensuring embedded cache/extra rates survive silent feeds, backed by new collision-focused tests.

Sequence diagram for UseRemote vet errors and dropped rate reporting

sequenceDiagram
    actor Caller
    participant UseRemote
    participant Source as src
    participant vet
    participant OnError as cfg.OnError
    participant Store as remote.Store

    Caller->>UseRemote: UseRemote(ctx, cfg)
    loop periodic refresh
        UseRemote->>src: Fetch prices
        src-->>UseRemote: raw map[string]Price
        UseRemote->>vet: vet(raw)
        vet-->>UseRemote: cleaned, dropped, err

        alt err != nil
            UseRemote->>OnError: OnError(src.Name(), err)
        else err == nil
            opt len(dropped) > 0 and cfg.OnError != nil
                UseRemote->>UseRemote: sortedSample(dropped, 5)
                UseRemote->>OnError: OnError(src.Name(), fmt.Errorf(dropped summary))
            end
            UseRemote->>Store: remote.Store(&snapshot{prices: cleaned, asOf: time.Now(), source: src.Name()})
        end
    end
Loading

File-Level Changes

Change Details Files
Remote price vetting now filters implausible entries before collapsing to canonical keys, returns dropped IDs, and reports them via UseRemote.OnError instead of failing the entire snapshot unless a large fraction is bad.
  • Change vet() signature to return (cleaned, dropped, error) and update UseRemote to handle and report dropped IDs using a deterministic sorted error sample.
  • Introduce a pre-collapse pass that skips non-positive prices, invalid Canonical IDs, and entries whose rates drift beyond maxDrift from embedded defaults into a usable map.
  • If all usable entries are dropped or more than 10% are implausible, treat the snapshot as corrupt with a detailed error; otherwise proceed.
  • After collapse, keep and backfill cache read/write and reasoning rates from defaultPrices when the winning remote entry omits them, so silent feeds do not erase embedded rates.
pricing/remote.go
Canonical model ID normalization is corrected to strip version suffixes before date suffixes, and GLM models are assigned a dedicated family to support provider ranking without altering cache multipliers.
  • Reorder Canonical() suffix processing so versionSuffix is removed before dateSuffix, fixing Bedrock IDs like claude-sonnet-4-5-20250929-v1:0 that previously failed to canonicalize to the undated ID.
  • Add a new familyZhipu enum and map GLM/autoglm IDs to this family in familyOf(), while intentionally omitting it from familyCacheMultipliers to keep the conservative cache defaults.
  • Extend canonical tests to cover date+version Bedrock IDs across plain, regional, and GovCloud prefixes.
pricing/canonical.go
pricing/canonical_test.go
Provider ranking logic is introduced to deterministically choose the authoritative upstream entry per canonical model based on vendor vs cloud vs regional vs GovCloud vs reseller, with tie-breaking that is independent of map iteration and sensitive to cache richness.
  • Add provider.go with sourceRank enum and rankOf() that classifies IDs as first-party, vendor cloud, regional, GovCloud, or reseller using namespaces, region markers, gov markers, and model families.
  • Define firstPartyNamespaces per family, vendorCloudNamespaces, govMarkers, regionMarkers, and dottedRegionPrefixes to drive ranking for Anthropic, OpenAI, Google, and Zhipu GLM models.
  • Implement better() to choose between candidates for the same canonical key by rank first, then lexicographic ID, while preferring entries that carry cache rates at equal rank.
  • Use winner maps and better() in vet()’s collapse phase so canonical keys adopt the same deterministic winner regardless of Go map iteration order.
pricing/provider.go
pricing/remote.go
New tests exercise collision resolution, provider ranking, drift behavior, and cache rate preservation using real LiteLLM-style feeds, including the previously invisible collision bug and the poisoned GLM entry.
  • Add collision_test.go (pricing_test) that sets up remote snapshots via withFeed(), then verifies GLM collisions choose Zhipu first-party, gateway-qualified IDs still resolve to first-party rates, Sonnet 4.5 beats regional/GovCloud premiums, collisions are deterministic across 60 runs, poisoned entries do not condemn the snapshot, and cache rates survive silent feeds.
  • Introduce provider_internal_test.go that pins rankOf() behavior against real upstream IDs for multiple vendors, checks better() forms a strict total order on equal-rank IDs, and ensures richer cache-bearing entries are preferred at equal rank.
pricing/collision_test.go
pricing/provider_internal_test.go

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

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

Actionable comments posted: 3

🧹 Nitpick comments (2)
pricing/collision_test.go (1)

156-158: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Building the poisoned feed by slicing glmFeed is fragile.

glmFeed[1:] depends on glmFeed starting with { and on the template supplying the opening brace and the trailing comma. Any reformatting of glmFeed breaks the test with a JSON parse error rather than a clear failure.

Define the poisoned row as a separate constant and merge the two objects, or store the fixture as a map[string]any and marshal it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pricing/collision_test.go` around lines 156 - 158, Replace the fragile
glmFeed[1:] concatenation in the collision test with a separately defined
poisoned row and explicitly merge it with the existing feed object before
serialization. Keep the resulting fixture valid JSON while removing assumptions
about glmFeed’s opening brace and formatting.
pricing/remote.go (1)

216-231: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Rows with non-positive rates are dropped without any report.

The loop skips rows where InputPerMTok <= 0 or OutputPerMTok <= 0, and where Canonical(id) == "". These rows never reach dropped, so OnError never mentions them. A schema change that zeroes every rate produces only the generic "none usable" error.

Canonical(id) is also computed twice per row, and a third time in the collapse loop. Compute it once.

♻️ Proposed refactor
 	for id, p := range raw {
-		if p.InputPerMTok <= 0 || p.OutputPerMTok <= 0 {
-			continue
-		}
-		if Canonical(id) == "" {
+		key := Canonical(id)
+		if key == "" {
 			continue
 		}
-		if known, ok := defaultPrices[Canonical(id)]; ok {
+		if p.InputPerMTok <= 0 || p.OutputPerMTok <= 0 {
+			dropped = append(dropped, id)
+			continue
+		}
+		if known, ok := defaultPrices[key]; ok {

If free-tier rows legitimately carry zero rates, keep the silent skip and count them separately instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pricing/remote.go` around lines 216 - 231, Update the raw pricing loop around
Canonical and the rate validation so every discarded row is reported through the
existing dropped/OnError flow, while preserving silent handling for legitimate
free-tier zero-rate rows by counting them separately if applicable. Compute
Canonical(id) once per row, reuse it for validation, defaultPrices lookup, and
the later collapse logic, and ensure the resulting reporting still produces the
generic none-usable error only when appropriate.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pricing/collision_test.go`:
- Around line 72-75: Update the collision assertions around the glm-4.6 check
and the corresponding assertions near lines 145-147 and 189-195 to compare
per-million rates using a small floating-point tolerance instead of exact
equality. Preserve the existing expected values and error messages while
applying the same tolerance-based check consistently to both input and output
rates.

In `@pricing/provider.go`:
- Around line 105-140: Update rankOf to evaluate the model segment (last) before
applying regional ranking, classifying region-pinned Bedrock IDs such as
bedrock/us... as vendor-cloud entries. Restrict regionMarkers matching to the
appropriate namespace portion so incidental eu or us-east text in a model
namespace cannot promote a reseller to regional rank, while preserving
first-party and reseller classification.

In `@pricing/remote.go`:
- Around line 237-244: Update the corruption check in vet to apply an absolute
minimum dropped-row threshold before evaluating the 10% ratio, so a single bad
entry in feeds smaller than ten does not reject the snapshot. Extend
TestPoisonedEntryDoesNotCondemnSnapshot to assert that any reported error
identifies the poisoned entry, ensuring the test exercises per-row drop handling
rather than full-snapshot rejection.

---

Nitpick comments:
In `@pricing/collision_test.go`:
- Around line 156-158: Replace the fragile glmFeed[1:] concatenation in the
collision test with a separately defined poisoned row and explicitly merge it
with the existing feed object before serialization. Keep the resulting fixture
valid JSON while removing assumptions about glmFeed’s opening brace and
formatting.

In `@pricing/remote.go`:
- Around line 216-231: Update the raw pricing loop around Canonical and the rate
validation so every discarded row is reported through the existing
dropped/OnError flow, while preserving silent handling for legitimate free-tier
zero-rate rows by counting them separately if applicable. Compute Canonical(id)
once per row, reuse it for validation, defaultPrices lookup, and the later
collapse logic, and ensure the resulting reporting still produces the generic
none-usable error only when appropriate.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 931b22de-fac2-49ee-9040-f6466b82cda9

📥 Commits

Reviewing files that changed from the base of the PR and between fa53b64 and f08630f.

📒 Files selected for processing (6)
  • pricing/canonical.go
  • pricing/canonical_test.go
  • pricing/collision_test.go
  • pricing/provider.go
  • pricing/provider_internal_test.go
  • pricing/remote.go

Comment thread pricing/collision_test.go
Comment on lines +72 to +75
if p.InputPerMTok != 0.60 || p.OutputPerMTok != 2.20 {
t.Errorf("glm-4.6 = %.4f/%.4f, want Zhipu's own 0.6000/2.2000 — a reseller rate won the collision",
p.InputPerMTok, p.OutputPerMTok)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect how per-token costs are converted to per-million rates.
rg -n -C4 -e '1e6|1_000_000|1e-6|PerMTok\s*[:=]' pricing/sources.go

Repository: hallelx2/llmgate

Length of output: 1641


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '--- sources relevant sections ---\n'
sed -n '1,320p' pricing/sources.go

printf '\n--- collision_test relevant sections ---\n'
sed -n '1,230p' pricing/collision_test.go

printf '\n--- perMTok usages ---\n'
rg -n 'perMTok\(' pricing

Repository: hallelx2/llmgate

Length of output: 245


🏁 Script executed:

#!/bin/bash
set -euo pipefail
cat > /tmp/probe_float.js <<'JS'
const inputs = [
  ['glm-4.6 input', 6.0e-07],
  ['glm-4.6 output', 2.20e-06],
  ['Zhipu-like input', 6.000000000000001e-07],
];
function perMTok(perToken) { return perToken * 1_000_000; }
for (const [name, value] of inputs) {
  const result = perMTok(value);
  console.log(JSON.stringify({ name, input: value, output: result, strictEq: result === 0.60, decimal: result.toFixed(16) }));
}
for (const target of [0.5999999999999999, 0.6000000000000001]) {
  console.log(JSON.stringify({ label: target, decimal: target.toFixed(16), equal060: target === 0.60 }));
}
JS
node /tmp/probe_float.js

Repository: hallelx2/llmgate

Length of output: 644


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- sources relevant sections ---'
sed -n '1,320p' pricing/sources.go

printf '%s\n' ''
printf '%s\n' '--- collision_test relevant sections ---'
sed -n '1,230p' pricing/collision_test.go

printf '%s\n' ''
printf '%s\n' '--- perMTok usages ---'
rg -n 'perMTok\(' pricing

Repository: hallelx2/llmgate

Length of output: 15672


Compare per-million rates with a small tolerance.

perMTok() multiplies per-token rates by 1_000_000, and decimal-to-float parsing can make values like 0.60e-06 resolve as 0.5999999999999999 or 0.60e-06 plus parsing noise in some feeds. Avoid exact float64 equality in the collision assertions that compare expected per-million rates.

Also applies to lines 145-147 and 189-195.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pricing/collision_test.go` around lines 72 - 75, Update the collision
assertions around the glm-4.6 check and the corresponding assertions near lines
145-147 and 189-195 to compare per-million rates using a small floating-point
tolerance instead of exact equality. Preserve the existing expected values and
error messages while applying the same tolerance-based check consistently to
both input and output rates.

Comment thread pricing/provider.go
Comment on lines +105 to +140
namespace, last := splitNamespace(s)

if namespace == "" {
// No namespace: judge by the dotted prefixes instead.
switch {
case strings.HasPrefix(last, "global."):
return rankVendorCloud
case hasAnyPrefix(last, dottedRegionPrefixes):
return rankRegional
case hasAnyPrefix(last, vendorPrefixes):
// "anthropic.claude-sonnet-4-5-v1:0" — a Bedrock ID that
// happens to carry no region.
return rankVendorCloud
default:
// A bare "claude-sonnet-4-5" or "gpt-4o": the vendor's own
// catalogue entry, and the best evidence there is.
return rankFirstParty
}
}

// A namespaced ID. The first segment names who sells it.
head := namespace
if before, _, found := strings.Cut(namespace, "/"); found {
head = before
}

if containsAny(namespace, regionMarkers) {
return rankRegional
}
if fam := familyOf(s); slices.Contains(firstPartyNamespaces[fam], head) {
return rankFirstParty
}
if slices.Contains(vendorCloudNamespaces, head) {
return rankVendorCloud
}
return rankReseller

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Look for namespaced IDs that also carry a dotted region or global prefix in fixtures and feeds.
rg -n --glob '*.go' --glob '*.json' -e '"[a-z_]+/(global|us|eu|apac|au|jp|ca|sa)\.' .
# Show all rank-related tests to confirm the covered ID shapes.
rg -n 'rankOf\(' -C2 .

Repository: hallelx2/llmgate

Length of output: 1246


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== pricing/provider.go outline =="
ast-grep outline pricing/provider.go || true

echo "== pricing/provider.go relevant lines =="
cat -n pricing/provider.go | sed -n '80,210p'

echo "== pricing/provider_internal_test.go relevant lines =="
cat -n pricing/provider_internal_test.go | sed -n '1,140p'

echo "== feed/fixture namespaced dotted IDs =="
rg -n --glob '*.go' --glob '*.json' --glob '*.yml' --glob '*.yaml' -e '[[:alnum:]-_]+/(global\.)?[[:alnum:]-_]+\.[[:alnum:]-_:\.]+|[[:alnum:]-_]+/(us|eu|apac|au|jp|ca|sa)\.[[:alnum:]-_\.:]+' .

Repository: hallelx2/llmgate

Length of output: 15375


🌐 Web query:

LiteLLM model_prices_and_context_window.json Bedrock cross region inference dotted region namespaces examples

💡 Result:

LiteLLM supports Amazon Bedrock cross-region inference by allowing you to use region-prefixed model IDs in your configuration [1][2]. Configuration and Usage To use cross-region inference, you specify the model ID with its region prefix (e.g., us, eu, apac) within the model string [1][3][4]. Example configuration in config.yaml: model_list: - model_name: bedrock-claude-haiku litellm_params: model: bedrock/us.anthropic.claude-3-haiku-20240307-v1:0 aws_region_name: us-east-1 Model Pricing and Context Window Mapping LiteLLM maps these models in model_prices_and_context_window.json [5][6]. Because cross-region inference profiles have specific identifiers, LiteLLM requires explicit entries in this JSON file to accurately track costs and context windows [5][7]. Key points regarding the pricing map: 1. Mapping Requirements: LiteLLM resolves costs by looking for keys that match the full prefixed model name (e.g., bedrock/us.anthropic.claude-3-5-haiku-20240307-v1:0) [5][7]. 2. Resolution Logic: If a specific prefixed entry is missing, LiteLLM may fail to calculate costs correctly or report $0 pricing [8][6][7]. While some modern models have these regional "twins" pre-mapped, users may occasionally encounter issues if a new regional profile is not yet added to the repository [8][6]. 3. Current Status: Most widely used Claude models (e.g., 3-5, 3-7, 4-x series) already include entries for standard regional prefixes like us. and eu. [6]. Troubleshooting Tips If you encounter a "model isn't mapped yet" error or incorrect cost reporting: - Verify the exact model string: Ensure it follows the expected pattern, such as bedrock/{region}.{provider}.{model-id} [5][3]. - Check for Route Overrides: You can explicitly specify the routing path if needed by using bedrock/converse/{model} or bedrock/invoke/{model} [1][2]. - Application Profiles: For advanced use cases involving AWS Application Inference Profiles (which use ARNs), the ARN should generally be provided in the model_id parameter within your litellm_params [9].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail
python3 - <<'PY'
import urllib.request, json

url = "https://github.com/ghraw/BerriAI/litellm/main/model_prices_and_context_window.json"
data = json.load(urllib.request.urlopen(url, timeout=20))
keys = [k for k in data if isinstance(k, str) and "/" in k.lower()]
dotted = [k for k in keys if "/" in k and k.rsplit("/", 1)[1].lower().startswith(("global.", "us.", "eu.", "apac.", "au.", "jp.", "ca.", "sa."))]
print("namespaced_keys", len(keys))
print("namespaced_dotted", len(dotted))
for k in sorted(dotted)[:100]:
    print(k)
PY

Repository: hallelx2/llmgate

Length of output: 2440


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== inspect canonical.go for mapping behavior =="
cat -n pricing/canonical.go | sed -n '1,110p'

echo "== inspect remote.go parsing/ranking behavior =="
cat -n pricing/remote.go | sed -n '220,290p'

echo "== deterministic rankOf implementation probe (without running repo code) =="
python3 - <<'PY'
import re

# Mirrors the relevant rankOf logic from pricing/provider.go.
vendor_cloud_namespaces = {"bedrock", "vertex_ai", "azure", "azure_ai"}
first_party_namespaces = {
    "zai": ["zai", "z-ai"],
    "anthropic": ["anthropic"],
    "openai": ["openai"],
    "gemini": ["gemini"],
    "vertex": [],
}
family_re = re.compile(r'^([a-z_\-]+)[./]', re.I)
region_markers = ["us-east", "us-west", "ap-southeast", "ap-east", "ap-northeast", "me-central", "eu", "us-gov", "usgov", "china"]
dotted_region_prefixes = ["us.", "eu.", "apac.", "au.", "jp.", "ca.", "sa."]

def rank_of(id):
    s = id.lower().strip()
    if s == "":
        return "reseller"
    for g in region_markers:
        if g in s:
            return "gov"
    i = s.rfind("/")
    if i < 0:
        namespace, last = "", s
    else:
        namespace, last = s[:i], s[i+1:]
    if not namespace:
        if last.startswith("global."):
            return "vendor_cloud"
        if any(last.startswith(p) for p in dotted_region_prefixes):
            return "regional"
        return "vendor_cloud"
    head = namespace.split("/", 1)[0]
    if any(sub in namespace for sub in region_markers):
        return "regional"
    fam_match = family_re.search(s)
    if fam_match and head in first_party_namespaces.get(fam_match.group(1), []):
        return "first_party"
    if head in vendor_cloud_namespaces:
        return "vendor_cloud"
    return "reseller"

cases = [
    "bedrock/us-east-1/z-ai.glm-4.6",
    "bedrock/us.anthropic.claude-sonnet-4-5-v1:0",
    "bedrock/global.anthropic.claude-sonnet-4-5-v1:0",
    "reseller/us-east-hosting/glm-4.6",
    "some-new-gateway/us.anthropic.claude-sonnet-4-5-v1:0",
]
for c in cases:
    print(c, "=>", rank_of(c))
PY

Repository: hallelx2/llmgate

Length of output: 7997


Check the model segment before assigning regional rank.

rankOf ranks namespaced IDs by the namespace, so Bedrock region-pinned IDs like bedrock/us.anthropic.claude-sonnet-4-5-v1:0 are treated as vendor-cloud entries. For model collisions, that can let a cheaper global/non-region-ID win unless the better lexicographic source happens to have a cache rate or lower ID. Also restrict regionMarkers checks so a model namespace containing eu or us-east does not make that reseller rank regionally.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pricing/provider.go` around lines 105 - 140, Update rankOf to evaluate the
model segment (last) before applying regional ranking, classifying region-pinned
Bedrock IDs such as bedrock/us... as vendor-cloud entries. Restrict
regionMarkers matching to the appropriate namespace portion so incidental eu or
us-east text in a model namespace cannot promote a reseller to regional rank,
while preserving first-party and reseller classification.

Comment thread pricing/remote.go
Comment on lines +237 to 244
// A handful of bad rows is upstream noise. A large fraction of them is
// a units error or a schema change, and adopting it would misreport
// spend by orders of magnitude — so that still condemns the snapshot.
if len(dropped) > len(raw)/10 {
return nil, dropped, fmt.Errorf(
"%d of %d entries have implausible rates — treating snapshot as corrupt",
len(dropped), len(raw))
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

The corruption threshold rejects small feeds after a single dropped row.

len(raw)/10 uses integer division. If len(raw) < 10, the threshold is 0, so one dropped row condemns the whole snapshot. That contradicts the stated intent that a handful of bad rows is upstream noise.

This also weakens TestPoisonedEntryDoesNotCondemnSnapshot in pricing/collision_test.go. That feed has 9 entries and 1 poisoned row, so vet returns an error and UseRemote installs no snapshot. Lookup then falls back to the embedded table, which already prices glm-4.6 at 0.60, and errs is non-empty because OnError receives the vet error. The test passes without exercising the new per-row drop path.

Add an absolute floor before the ratio applies.

🐛 Proposed fix
-	if len(dropped) > len(raw)/10 {
+	// A ratio alone rejects small feeds on a single bad row, so require a
+	// minimum absolute count as well.
+	const minDroppedForCorrupt = 4
+	if len(dropped) >= minDroppedForCorrupt && len(dropped)*10 > len(raw) {
 		return nil, dropped, fmt.Errorf(
 			"%d of %d entries have implausible rates — treating snapshot as corrupt",
 			len(dropped), len(raw))
 	}

After the fix, extend the regression test to assert that the reported error names the dropped ID, so a full-snapshot rejection cannot satisfy it.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// A handful of bad rows is upstream noise. A large fraction of them is
// a units error or a schema change, and adopting it would misreport
// spend by orders of magnitude — so that still condemns the snapshot.
if len(dropped) > len(raw)/10 {
return nil, dropped, fmt.Errorf(
"%d of %d entries have implausible rates — treating snapshot as corrupt",
len(dropped), len(raw))
}
// A handful of bad rows is upstream noise. A large fraction of them is
// a units error or a schema change, and adopting it would misreport
// spend by orders of magnitude — so that still condemns the snapshot.
// A ratio alone rejects small feeds on a single bad row, so require a
// minimum absolute count as well.
const minDroppedForCorrupt = 4
if len(dropped) >= minDroppedForCorrupt && len(dropped)*10 > len(raw) {
return nil, dropped, fmt.Errorf(
"%d of %d entries have implausible rates — treating snapshot as corrupt",
len(dropped), len(raw))
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pricing/remote.go` around lines 237 - 244, Update the corruption check in vet
to apply an absolute minimum dropped-row threshold before evaluating the 10%
ratio, so a single bad entry in feeds smaller than ten does not reject the
snapshot. Extend TestPoisonedEntryDoesNotCondemnSnapshot to assert that any
reported error identifies the poisoned entry, ensuring the test exercises
per-row drop handling rather than full-snapshot rejection.

@hallelx2
hallelx2 merged commit d6c907d into main Aug 2, 2026
7 checks passed
@hallelx2
hallelx2 deleted the halleluyaholudele/hal-563-price-provider-precedence branch August 2, 2026 22:25
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.

1 participant