[BUG] Claude apps gateway spend meter ignores the ~10% Amazon Bedrock CRIS tier difference (in-region vs global inference profiles)
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report
- [x] I am using the latest version of Claude Code
What's Wrong?
The Claude apps gateway's spend meter prices every request from a single per-model USD rate table and has no notion of which Amazon Bedrock cross-region inference (CRIS) tier served the request. Bedrock bills the two CRIS tiers about 10% apart, per AWS's own cross-region inference docs:
| Feature | Geographic CRIS (us. / eu. / apac.) | Global CRIS (global.) |
| --- | --- | --- |
| Cost | Standard pricing | Approximately 10% savings |
So in-region / geographic inference costs roughly 10% more per token than global for the identical model and token counts. The meter charges both the same.
Rate resolution keys off the model ID string only. us.anthropic.claude-sonnet-5 and global.anthropic.claude-sonnet-5 canonicalize to the same catalog entry and receive identical per-Mtok rates. Because one table serves both tiers, at most one tier can be metered correctly — for the other, every period_to_date_spend figure and every cap decision is off by ~10%, systematically and in one direction. It does not average out over a billing period.
The consequential direction is under-metering: if the built-in rates track the cheaper global tier, then a gateway pointed at us./eu./apac. profiles under-counts real Bedrock spend by ~10%. Caps then permit ~10% more spend than configured, which defeats the point of a circuit breaker — an org that sets a $500/month cap to bound a shared credential is actually exposed to ~$550. Conversely, if the rates track standard/geographic pricing, global-profile deployments over-count by ~10% and trip caps early. Either way one tier is wrong, and nothing in the config or logs tells an operator which side they are on or by how much.
I recognise the spend-limits docs already say caps "estimate spend from token counts at USD list price" and are "a circuit breaker, not an invoice", with reconciliation against Bedrock invocation logs as the path to authoritative billing. Filing anyway because the CRIS tier delta is a documented, deterministic, known-magnitude 10% that the gateway has enough information to apply — not inherent estimation noise — and because an operator currently has no supported way to correct for it.
What Should Happen?
Any one of these resolves it:
- Tier-aware rates — adjust the rate by CRIS tier when the resolved upstream model ID carries a
global.versus a geographic (us./eu./apac.) prefix. - Operator-supplied rate overrides — a per-model rate override in
gateway.yaml(e.g. acosts:/rates:key onmodels[]) so operators can encode their actual tier-adjusted or negotiated Bedrock pricing. This also covers private pricing agreements, which no built-in table can know. - Document the skew — if this is working-as-intended, state on the spend-limits page which tier the built-in rates are calibrated to and that the other tier is metered ~10% off, so operators can size caps deliberately instead of finding the drift at reconciliation.
Option 2 is the most general. Option 3 is the cheapest and would have saved the investigation behind this report.
Environment Info
- Claude Code version on the gateway server: 2.1.218 (
linux-x64, run asclaude gateway --config gateway.yaml) - Upstream: Amazon Bedrock,
auth: {}(task-role credentials via the AWS default chain) - Deployment: ECS Fargate behind an internal ALB, RDS for PostgreSQL store,
admin:block enabled with write and read keys - Models configured via cross-region inference profiles
Additional Context
Traced through the 2.1.218 linux-x64 binary. The metering path:
- The per-request cost function takes
(upstreamModelId, usage, configuredModelId), resolves a rate record, multiplies token counts by per-Mtok rates, and returns cents. Its only pricing inputs are the model ID strings and the token counts. - Rate resolution order: exact rates for the upstream model ID → rates for the configured
models[].id→ the unknown-model default tier ($5/$25 per Mtok). Model IDs are canonicalized across Anthropic / Bedrock / Agent Platform / Foundry forms, so theglobal.and geographic Bedrock prefixes collapse onto one catalog entry. - The result increments the
spendtable for all three period buckets. There is no multiplier, region, or CRIS-tier input anywhere on that path.
Possibly useful for a fix: the response usage object the meter parses carries an inference_geo field, and the gateway's usage schema already threads it through. That reports where inference actually ran, which is a more robust discriminator than prefix-matching the requested model ID — a global profile can route anywhere, so the request prefix states the tier but the response states the fact.
Config-side workarounds are closed off: models[] validates as a strict object accepting only id, label, description, and upstream_model; the admin: block exposes only keys, retention windows, and group_limit_mode. Neither admits a rate override, and unknown keys fail boot.