Conversation
This was referenced Sep 15, 2026
The same model billed through a different door is a different invoice, and
the tools that ran it record a different id per door: Claude Code with
CLAUDE_CODE_USE_BEDROCK=1 writes `anthropic.claude-haiku-4-5-20251001-v1:0`,
Hermes on its bedrock provider writes `anthropic.claude-fable-5-1` /
`openai.gpt-5.6-luna`, where a direct session writes `claude-fable-5-1`.
Reports key model rows by display name (parser.ts modelBreakdown,
menubar-json topModels, usage-aggregator), so those ids either surfaced raw
or, had they been aliased to the friendly name, would have merged with the
direct row and hidden which account the spend landed on.
`getModelRoute(id)` recognises the Bedrock shapes -- bare foundation-model
ids, `us.`/`eu.`/`global.` inference-profile prefixes, the `-vN:M` version
(and Bedrock's `-1:0` spelling on its OpenAI ids), `:300k` context tags,
`arn:aws:bedrock:...` wrappers and LiteLLM's `bedrock/` prefix -- and returns
`{ id, label, baseModel }`. `getShortModelName` renders a routed id as
"<base short name> (Bedrock)", so every Bedrock spelling of one model shares
a row, the direct row is unchanged, and pricing still runs on the full id (a
cross-region profile keeps its own rate). Vendor segments come from a fixed
table so dotted version numbers (`gpt-4.1-mini`, `glm-4.7`) are never
mistaken for a route; a segment that does not name its brand
(`deepseek.r1`) is re-joined with the vendor so it still finds its short name.
`looksLikeLocalModel` now consults the route first: `-v1:0` is a version, not
an Ollama tag, so an unpriced Bedrock model reaches Unpriced instead of being
treated as free local inference.
Display-only. Model rows are keyed at aggregation time from raw ids that the
session and daily caches already hold, so no cache version moves. A user
alias on the full Bedrock id still wins over the route.
Bedrock is the first route; OpenRouter, Vertex and Azure spellings belong in
the same hook once a provider is shown to record them (getagentseal#1450).
Closes getagentseal#1449
ulises-c
force-pushed
the
feat/model-route-bedrock
branch
from
September 15, 2026 22:09
fbe3049 to
4c5591a
Compare
ulises-c
marked this pull request as ready for review
September 15, 2026 22:32
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
Closes #1449 — the pilot of a three-step series toward answering "how much metered API spend am I incurring on top of my subscriptions?": this PR (Bedrock, from the model id) → #1450 (OpenRouter and routes read from the provider's own column, each route carrying metered / subscription, plus the list of routes to add next) → #1451 (
--route/--billingfilters, payload fields, a "By billing" grouping beside "By tool").Haiku 4.5 (Bedrock),GPT-5.6 Luna (Bedrock)— instead of surfacing as a raw id or, if aliased, merging into the direct-API row. Same tokens, different invoice; the row now says which.getModelRoute(id)insrc/models.tsrecognises the Bedrock id shapes tools actually write: bareanthropic.claude-…-v1:0,us./eu./global.inference-profile prefixes, Bedrock's OpenAI ids (openai.gpt-5.6-luna,-1:0version spelling),:300kcontext tags,arn:aws:bedrock:…and LiteLLMbedrock/wrappers.getShortModelNameappends the route label. Every Bedrock spelling of one model folds to one row; direct ids are byte-for-byte unchanged; pricing still runs on the full id, so cross-region profiles keep their uplifted rate.looksLikeLocalModelno longer reads Bedrock's-v1:0as an Ollama tag, so an unpriced Bedrock model reaches Unpriced instead of being treated as free local inference.Why a separate row: the route decides whether the spend is metered
The motivating case: Claude Code and Codex on fixed-fee subscriptions (Claude Max, ChatGPT), plus API usage on top — Claude through Bedrock, models through OpenRouter. The subscription is a sunk monthly cost; the API usage is the variable part, and it is the number worth watching. Today CodeBurn cannot separate them:
codeburn plantreats a whole harness as under the plan,proxy-pathmarks a whole project, and Hermes'cost_status = includedis honoured per session (hermes.ts:483) but is not a dimension anywhere. A Bedrock call is always metered, so keeping its row apart is the first cut a user can actually read the variable spend off.On the mechanism, versus #1241 (#1239): that PR folded OpenCode's
minimax/MiniMax-M3andMiniMaxAI/MiniMax-M3into one row with the split kept asrawModels— right there, since both were the same metered door at the same price. Bedrock vs direct is a different billing relationship, and LiteLLM carries it as distinct SKUs (aus.inference profile prices ~10% above the bare id), so it falls under #1053's rule ("distinct SKUs that only share a label stay two rows"). The display-suffix mechanism is #1031's (grok-4.5-build→Grok 4.5 (build)): ids and pricing untouched. Because #1241 routes Trend throughgetShortModelName, desktop andcodeburn modelsagree on the Bedrock rows without further work.The suffix is the only slot the row key offers today (
modelBreakdownhas been keyed ongetShortModelNamesince v0.1.0). Making the route — and the metered / subscription mode it implies — a real, filterable field is #1450 / #1451, following the store-at-parse / apply-at-read pattern #890 lays out.Display-only: model rows are keyed at aggregation time from raw ids the caches already hold, so no cache version moves. Confined to
src/models.ts; the Semgrep hot-path rule (src/providers/,src/parser.ts) does not apply and the change has no bracket-assigns regardless (ran it locally: 0 findings).Testing
npm testpasses —main(3a1dac2): 4110 passed / 5 skipped; branch: 4126 passed / 5 skipped, +16 (15 intests/model-routes.test.ts, 1 intests/menubar-json.test.tsfor the topModels contract), zero new failuresnpm run buildsucceeds; alsonpx tsc --noEmit,npm run test:locks(36 passed) andnpm run verify:upgrade(PASSED, 1 skipped) — the same stepstests.yml/upgrade-path.ymlrunReal data, this machine, both providers that record Bedrock ids today:
Claude Code — one session run with
CLAUDE_CODE_USE_BEDROCK=1 ANTHROPIC_MODEL=us.anthropic.claude-haiku-4-5-20251001-v1:0(transcript recordsanthropic.claude-haiku-4-5-20251001-v1:0), alongside direct-API sessions of the same model.codeburn models --provider claude:Hermes Agent — sessions on
--provider bedrock(state.dbmodel = openai.gpt-5.6-luna,billing_provider = bedrock) next to direct Anthropic sessions.codeburn models --provider hermes:Before this change the Bedrock rows read
anthropic.claude-haiku-4-5-20251001-v1:0/openai.gpt-5.6-lunaverbatim.Not covered: Vertex
@dateids (still stripped and merged with direct — documented indocs/providers/claude.md), and routes whose model id does not change (OpenRouter), which need the provider's own route column rather than id shape (#1450). Filtering / GUI: #1451.Affiliation: none. I use Bedrock through an employer AWS account and wanted to see the split.