Skip to content

fix(aio): use bedrock inference-profile region for cost matching - #74489

Closed
posthog[bot] wants to merge 1 commit into
masterfrom
posthog-code/bedrock-inference-profile-region-costs
Closed

fix(aio): use bedrock inference-profile region for cost matching#74489
posthog[bot] wants to merge 1 commit into
masterfrom
posthog-code/bedrock-inference-profile-region-costs

Conversation

@posthog

@posthog posthog Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Problem

Closes #74366

AWS Bedrock cross-region inference profiles put the region the request ran in at the front of the model ID, so events arrive as bedrock/us.anthropic.claude-sonnet-5. findCostFromModel resolved that to a catalog row first and then handed only $ai_provider (bedrock) to the provider resolver, so the region was already gone. With no exact amazon-bedrock key, the resolver fell into its partial-match loop and did Object.keys(providerCosts).find(key => key.includes('amazon-bedrock')) - it returned whichever amazon-bedrock-* key happened to sit first in llm-costs.json, not the one for the region the request actually ran in.

Two consequences:

  • us.anthropic.claude-sonnet-5 picked amazon-bedrock-claude-on-aws instead of amazon-bedrock-us-east-1, a flat 9.09% understatement across input, output, cache read, and cache write (and the 1-hour cache write bucket, which is derived from the selected input price).
  • It is a generic "first partial match wins" bug, not only a Global-vs-US one. anthropic/claude-sonnet-4.6 only has amazon-bedrock-eu-west-1 and amazon-bedrock-global rows, so a us. request was being billed at EU rates.

The failure is silent: no errors, no warnings, just quietly wrong numbers in the metric AI observability exists to get right.

Changes

extractInferenceProfileRegion pulls the region token off the raw model string and resolveModelCostForProvider takes it as a preferred-region hint. Handles us., eu., global., and apac., mapping onto keys shaped like amazon-bedrock-us-east-1, amazon-bedrock-us, amazon-bedrock-eu-west-1, and amazon-bedrock-global.

Details worth calling out for review:

  • The prefix sits after a provider/ segment, so we read the last slash-delimited segment first. That also covers inference-profile ARNs.
  • The hint is tried ahead of both the un-regioned provider key and the partial-match fallback. An eu. request on a model with both amazon-bedrock and amazon-bedrock-eu-west-1 rows now gets the EU row.
  • Region prefixes only match on a key boundary, so us never matches something like amazon-bedrock-usw.
  • Each token falls back to global before giving up. That is what fixes the sonnet-4.6 case above: with no US row, the un-regioned list price is a closer stand-in than another region's premium, and it makes the pick deterministic instead of dependent on catalog key order.
  • Candidates are sorted before picking. llm-costs.json key order is regenerated by a scheduled job and must not decide which region we bill at.
  • An explicit regional $ai_provider stays authoritative, and models with no region prefix keep today's behavior.

Note

This changes attributed cost for Bedrock inference-profile events going forward. Already-ingested events are not backfilled.

How did you test this code?

Automated only - I (Claude) did not run the app or ingest live events.

New cases, and the regression each catches:

  • extractInferenceProfileRegion is parameterized over the prefix shapes. The load-bearing rows are bedrock/us.… (a naive startsWith('us.') misses it, which is the trap here) and the negative rows claude-opus-4.5 / openai/gpt-4.1-mini, which would start reading a version segment as a region if the split logic regressed.
  • resolveModelCostForProvider region cases: region beats another provider-specific key (the 9.09% bug), region beats the un-regioned key, global fallback instead of a different region (the EU-rates bug), no match on a shared prefix, and unprefixed models unchanged. These are distinct branches rather than data variations, so they are separate cases.
  • One findCostFromModel case as a wiring guard: the region has to actually be threaded from the raw model string into the resolver. Every resolver test would still pass if that thread were dropped.

Per the issue, tests assert the correct region is chosen rather than pinning today's specific key ordering.

Test Suites: 2 passed, 2 total
Tests:       141 passed, 141 total

The wider src/ingestion/pipelines/ai suite passes too (958 tests), except pre-existing local-environment failures unrelated to this change: the S3 blob-store integration tests need object storage running, and consumer.test.ts / pipeline.test.ts need the node-rdkafka native binding. tsc --noEmit and eslint are clean over ai/costs.

I also ran the resolver against the real catalog with and without the hint, to bound the blast radius. Only Bedrock rows move, and they move the way you would want:

Resolution diff across the real catalog, per region token
us  anthropic/claude-haiku-4.5     amazon-bedrock-eu-west-1     → amazon-bedrock-global       -9.09%
us  anthropic/claude-opus-4.7      amazon-bedrock-eu-west-1     → amazon-bedrock-us           -9.09%
us  anthropic/claude-sonnet-4.6    amazon-bedrock-eu-west-1     → amazon-bedrock-global       -9.09%
us  anthropic/claude-sonnet-5      amazon-bedrock-claude-on-aws → amazon-bedrock-us-east-1    +10.00%
us: 4 models changed

eu  amazon/nova-2-lite-v1          amazon-bedrock               → amazon-bedrock-eu-west-1     0.00%
eu  amazon/nova-lite-v1            amazon-bedrock               → amazon-bedrock-eu-west-1     0.00%
eu  amazon/nova-micro-v1           amazon-bedrock               → amazon-bedrock-eu-west-1     0.00%
eu  amazon/nova-pro-v1             amazon-bedrock               → amazon-bedrock-eu-west-1     0.00%
eu  anthropic/claude-opus-4.5      amazon-bedrock               → amazon-bedrock-eu-west-1   +10.00%
eu  anthropic/claude-opus-4.8      amazon-bedrock               → amazon-bedrock-eu-west-1   +10.00%
eu  anthropic/claude-sonnet-4      amazon-bedrock               → amazon-bedrock-eu-west-1     0.00%
eu  anthropic/claude-sonnet-4.5    amazon-bedrock               → amazon-bedrock-eu-west-1   +10.00%
eu  anthropic/claude-sonnet-5      amazon-bedrock-claude-on-aws → amazon-bedrock-global        0.00%
eu  openai/gpt-oss-120b            amazon-bedrock               → amazon-bedrock-eu-west-1     0.00%
eu  openai/gpt-oss-20b             amazon-bedrock               → amazon-bedrock-eu-west-1     0.00%
eu: 11 models changed

global: 3 models changed (all onto amazon-bedrock-global)
apac:   3 models changed (all onto amazon-bedrock-global, no APAC rows in the catalog yet)

Automatic notifications

  • Publish to changelog?
  • Alert Sales and Marketing teams?

Docs update

No user-facing docs cover the regional pricing-row selection, so nothing to update.

🤖 Agent context

Autonomy: Fully autonomous

Written by Claude (Claude Code) from a PostHog inbox report. Skills invoked: /writing-tests.

Two judgment calls a reviewer may want to push back on. First, the hint is tried before the exact un-regioned provider key, not only before the partial-match fallback. The narrower reading of the issue would leave eu.amazon.nova-lite-v1 on the generic amazon-bedrock row even though an amazon-bedrock-eu-west-1 row exists; if a request names its region and we have that region's price, using it seemed clearly more correct. Second, the global fallback per region token. Without it, us.anthropic.claude-sonnet-4-6 (the highest-volume prefixed model) stays on EU rates and the pick stays hostage to catalog key ordering. I considered leaving unmatched regions on the old partial match and rejected it for exactly that reason.

I did not touch the unprefixed-model path, which still takes the first partial match. It is the same class of nondeterminism, but changing it is a broader behavior change than this fix needs.


Created with PostHog Desktop from this inbox report.

Bedrock cross-region inference profiles prefix the model ID with the region
the request ran in (`bedrock/us.anthropic.claude-sonnet-5`). The cost matcher
resolved the model to a catalog row first and then handed only `$ai_provider`
(`bedrock`) to the provider resolver, so the region was gone by then. With no
exact `amazon-bedrock` key the resolver fell into its partial-match loop and
returned whichever `amazon-bedrock-*` key happened to come first in
`llm-costs.json`, regardless of region.

Extract the region token from the raw model string and thread it through as a
preferred-region hint, tried ahead of both the un-regioned provider key and the
partial-match fallback. Candidate keys are sorted so the pick no longer depends
on key ordering in the generated catalog. Models without a region prefix keep
their current behavior.

Generated-By: PostHog Code
Task-Id: 9370f695-5848-462e-97e9-7268c09d6f4c
@trunk-io

trunk-io Bot commented Jul 29, 2026

Copy link
Copy Markdown

Merging to master in this repository is managed by Trunk.

  • To merge this pull request, check the box to the left or comment /trunk merge below.

After your PR is submitted to the merge queue, this comment will be automatically updated with its status. If the PR fails, failure details will also be posted here

@Radu-Raicea
Radu-Raicea marked this pull request as ready for review August 3, 2026 19:07
@pr-assigner-resolver-posthog
pr-assigner-resolver-posthog Bot requested review from a team August 3, 2026 19:07
@richardsolomou

Copy link
Copy Markdown
Member

@Radu-Raicea #74380 already merged the same fix and closed #74366, so #74489 is superseded and can be closed

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.

US Bedrock inference-profile events use Global token rates

1 participant