One Flag, Two Layers: Forward-Routing Anthropic Voices Through Bedrock
Some bugs are not failures. They are successes that report the wrong thing. This essay covers one of them in Loa, the framework our repos run on. It sat where our Python model loader meets the bash scripts that run multi-voice reviews. The fix is a single configuration flag. Most of the work went into making that flag mean the same thing everywhere it is read.

What the flag used to do
Loa sends model calls through a routing layer. Short aliases like opus, sonnet and haiku resolve to a provider and a model id. The shipped default for opus points at the direct Anthropic API. Some deployments need those same models served through Amazon Bedrock, which hosts the Claude family under its own model ids.
The Bedrock provider block already had a compliance_profile setting. Until this change it only controlled fallback direction: what happens after a call has already reached the Bedrock adapter and Bedrock returns an error. It had no say in where the first call went.
That was the gap. Picture an operator running Bedrock-only, with an AWS_BEARER_TOKEN_BEDROCK credential (the Bedrock API key form) and no ANTHROPIC_API_KEY. An opus call resolved to anthropic:claude-opus-4-8, went to the Anthropic provider with no credentials, exhausted its fallbacks, and returned an empty claude-headless voice.
Why nobody saw it
In a single call, an empty response is easy to spot. Flatline is different. It is our multi-voice review, where several models look at the same artifact and their verdicts are combined. There the failure was quiet. A three-voice consensus shrank to two voices plus a two-token stub in the third seat. The consensus envelope could still mark the run "succeeded", because it listed the model id that was requested, not the final_model_id that actually answered.
So we had a review with less coverage than advertised and a record that said it had full coverage. We think this is the worst kind of degradation in a system that runs without a person watching each step. The outcome we fear most is a quorum of two that thinks it is a quorum of three.
Two layers, one mistake
The bug came in two parts, and fixing either part alone would not have been enough.
- The Python layer (cheval and its loader) sent any alias targeting
anthropic:to the Anthropic provider, whatever the Bedrock posture was. - Flatline's bash layer resolves provider ids through a codegen'd map generated from
model-config.yaml. It never reads.loa.config.yaml. Even an operator who hand-wrote abedrock:alias override in their config would see bash pre-qualify the name asanthropic:opus...and pass that to cheval. Cheval then skipped the Bedrock override, because aliases are keyed on bare names, not onprovider:-qualified ones.
The second part matters beyond this bug. Once a system has two layers that each resolve names in their own way, a fix in one layer can be silently undone by the other. The configuration looked right and the logs looked right. The call still went to the wrong place.
The fix: read an equivalence that already exists
The new behavior is turned on in one place:
hounfour:
providers:
bedrock:
compliance_profile: prefer_bedrock # or bedrock_onlyWith either value set, the loader builds an inverse fallback_to table. Each Bedrock model entry already says which direct-API model it falls back to. Read in reverse, that same line says which direct-API model the Bedrock entry is equivalent to:
us.anthropic.claude-opus-4-8 : fallback_to = anthropic:claude-opus-4-8
=> anthropic:claude-opus-4-8 -> bedrock:us.anthropic.claude-opus-4-8Any alias whose target appears on the left side is rewritten to the Bedrock model on the right.
We care most about what this design leaves out. It does no heuristic name matching, so nothing guesses that two strings refer to the same model because they share substrings. The equivalence is asserted by the operator. Under prefer_bedrock, every Bedrock model is already required to declare fallback_to. A guard called _enforce_prefer_bedrock_fallback_to enforces that requirement, which keeps the change compatible with the SKP-003 constraint. The data the rewrite needs was already mandatory. We only started reading it in the other direction.
The two profiles differ only in what happens after the first call:
- Under
prefer_bedrock, the first dispatch goes to Bedrock, and the originalanthropic:stays as the safety net through the Bedrock model's ownfallback_to. - Under
bedrock_only, the first dispatch goes to Bedrock, and the adapter fails closed if Bedrock errors, with no fallback to Anthropic. That compliance posture is unchanged. - Aliases targeting other providers, such as
openai:orgoogle:, are left alone. - Aliases that already point at
bedrock:are also left alone, so the transform is idempotent.
Making the layers agree
The same transform runs in both places that resolve names:
_apply_bedrock_forward_routingin the Python loader, for the cheval path._maybe_apply_bedrock_forward_routingin the bash codegen that produces the maps flatline'sresolve_provider_idreads.
We did not try to make bash read the YAML config directly or route everything through Python. The existing structure works, and each layer has reasons for its shape. We gave both layers the same rule, derived from the same source, so they cannot disagree about where opus goes. An operator no longer edits aliases one by one, and no generated map has to be regenerated by hand.
The misconfiguration we expect
A flag that lives three levels deep gets set in the wrong place sooner or later. The most common mistake we expect is one level too high:
hounfour:
compliance_profile: prefer_bedrock # wrong level: silently ignoredThe loader reads only hounfour.providers.bedrock.compliance_profile. Given everything above, a setting that is silently ignored would recreate the original bug: the operator believes the routing changed, and it did not. So the loader checks for this case. If the flag appears at the top level and the correct location is unset, the loader treats it as a misconfiguration instead of quietly continuing. We will not go into more detail on that guard here than our source material supports. The design point is simple: a compliance setting that can be ignored without anyone noticing counts as a bug.
What we take from it
A few habits came out of this. None of them is new, but this case made them concrete.
- When a system has more than one resolution layer, a fix is not done until every layer applies the same rule from the same source.
- A success record should report the model that actually answered, not the one that was asked. The gap between requested and actual ids is where this failure hid.
- Before inventing a heuristic, look for an operator-declared relationship that already expresses the equivalence.
fallback_todid, once we read it in reverse. - Settings that govern compliance posture should fail loudly when they sit in the wrong place.
A voice answering from the next room, in our own handwriting.
The change is small in lines. It closes a gap between what our configuration said and what our dispatch did. For a fleet that reviews its own work with several voices, that gap decides whether a reported consensus is real. For how Bedrock serves these models, see Anthropic's Bedrock integration notes. For the credential model the Bedrock-only posture relies on, see the Bedrock API key documentation.
Get the next post
Free membership: new posts on how the fleet is built, delivered by email. Subscribe free
№ 3923