headroom/docs
Tejas Chopra c07da992dd
Per-request backend selection for routing extensions (#2809)
## The gap

Headroom picks its egress backend **once**, at startup:
`create_proxy_backend` returns a single `Backend` (or `None` for the
direct Anthropic path) and every request goes through it. That is the
right shape for *"run this whole proxy against Bedrock instead of
Anthropic"* and the wrong shape for *"this request is cheaper on a
different provider than the last one."*

`ModelRouter` already lets an extension change `body["model"]` per
request — but only within the protocol the request arrived in, because a
model id alone cannot move a request to another provider.

So an extension can currently **decide** something Headroom has no way
to **carry out**. This adds the missing half.

## The seam

An extension publishes a decision on the request state:

```python
request.state.headroom_route = SimpleNamespace(
    model="moonshot/kimi-k2",   # required
    provider="moonshot",        # optional; inferred from the model id if absent
    reason="cheaper at this prefix length",
)
```

Headroom resolves a `LiteLLMBackend` for that provider — which is where
translation already lives — and serves **that one request** from it.
Nothing in core names any particular extension; the field is duck-typed,
so an extension does not import Headroom to talk to Headroom.

## Absent means unchanged

This is the property the tests are built around, and the reason this
should be safe to merge.

With nothing published, every path is what it was before. Advice that is
**absent, malformed, names an unknown provider, names a native provider,
or fails to build** all resolve to `self.anthropic_backend` — including
when that is `None`, which is the direct-API path and must survive. A
routing preference can never take traffic down.

## Coverage

| path | |
|---|---|
| `/v1/messages` | non-streaming + streaming |
| `/v1/chat/completions` | non-streaming + streaming |
| Responses API | untouched — does not use the backend abstraction |

Streaming is the one that matters. The resolver rewrites
`body["model"]`, so had `_stream_response_bedrock` kept reading
`self.anthropic_backend`, every streamed routed request would have sent
a foreign model id to Anthropic. Both streaming helpers now take an
optional `backend`, defaulting to the configured one.

## Details worth review

- **Validate the provider name before building.** `LiteLLMBackend`
accepts *any* provider string — the registry falls through to a generic
pass-through config — so a typo silently builds a backend that only
fails later, at request time, with an error pointing nowhere near the
typo. `_known_provider()` checks against `litellm.provider_list` first.
- **Cache per provider, and cache the failures too**, or a broken
provider name costs a construction attempt on every request. (Bedrock
construction calls out to AWS to enumerate inference profiles — it is
not free.)
- **`backend_owns_translation` now asks the per-request backend.** It
decides whether Headroom or the backend owns the `max_tokens` /
`max_completion_tokens` spelling; asking `self.anthropic_backend` would
answer "Headroom does" for a request about to be served by a translating
backend that does.
- **`_route_resolver` lives in `route_advice.py`, not on a handler
mixin.** Two mixins need it, and reaching across sibling mixins only
works by accident of how `HeadroomProxy` composes them.

## Tests

`tests/test_route_advice.py` — 20 tests, most of them asserting the
absent-means-unchanged property from a different angle.

Local runs: 20/20 on the new file; **1102 passed, 1 failed** on `-k
"openai or chat_completions or ccr"`, and **414 passed, 0 failed** on
`-k "stream or bedrock or route_advice"`. The single failure is
`test_realignment_live_multi_turn::test_ccr_marker_round_trip_live`,
which fails identically on this branch's merge-base — verified by
checking out `59314cff~1` and re-running it.

Note for anyone reproducing: `pytest-asyncio` is a declared dev
dependency but was missing from my venv, which made every `async def
test_` in the repo fail. Worth checking before diagnosing a large
failure count.

## Docs

`docs/content/docs/pipeline-extensions.mdx` gains a section on the
contract, next to the existing `x-headroom-base-url` one.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 17:01:32 -07:00
..
app docs: improve discoverability for AI agents and search crawlers 2026-05-13 17:36:06 -07:00
components docs: sync Vercel docs with current code and add in-depth proxy config (#2475) 2026-07-21 16:26:56 -07:00
content/docs Per-request backend selection for routing extensions (#2809) 2026-08-05 17:01:32 -07:00
lib docs: sync Vercel docs with current code and add in-depth proxy config (#2475) 2026-07-21 16:26:56 -07:00
overrides fix: repair release and docs pipelines 2026-04-16 12:53:51 -05:00
screenshots Merge pull request #147 from JerrettDavis/feat/anthropic-usage-insights 2026-04-12 10:54:22 -07:00
.gitignore new docs UI + ts doc coverage 2026-04-12 13:15:58 +06:00
bun.lock fix(deps): remediate dependency CVEs and publish SBOM (#1509) 2026-06-27 15:28:12 -07:00
claude-code-bedrock-headroom.md fix(bedrock): route ARNs via converse, named AWS profiles, and au. re… (#1456) 2026-07-02 22:51:05 -05:00
next.config.mjs new docs UI + ts doc coverage 2026-04-12 13:15:58 +06:00
observability.md fix: remove rtk and lean-ctx CLI context tools (#2677) 2026-07-30 22:59:41 -07:00
package-lock.json deps: bump next from 16.2.10 to 16.3.0 in /docs (#2750) 2026-08-04 21:50:56 -05:00
package.json deps: bump next from 16.2.10 to 16.3.0 in /docs (#2750) 2026-08-04 21:50:56 -05:00
platform-feature-matrix.json fix: harden persistent install startup (#1851) 2026-07-10 00:40:34 -04:00
platform-stabilization.md fix: harden persistent install startup (#1851) 2026-07-10 00:40:34 -04:00
postcss.config.mjs new docs UI + ts doc coverage 2026-04-12 13:15:58 +06:00
proxy.ts new docs UI + ts doc coverage 2026-04-12 13:15:58 +06:00
README.md new docs UI + ts doc coverage 2026-04-12 13:15:58 +06:00
source.config.ts docs(ci): add CI/CD flow diagrams (#1062) 2026-06-16 23:05:15 -07:00
tsconfig.json new docs UI + ts doc coverage 2026-04-12 13:15:58 +06:00
vercel.json fix: add Vercel deploy config and workflow for docs site (#1739) 2026-07-14 13:25:18 -04:00

docs

This is a Next.js application generated with Create Fumadocs.

Run development server:

npm run dev
# or
pnpm dev
# or
yarn dev

Open http://localhost:3000 with your browser to see the result.

Explore

In the project, you can see:

  • lib/source.ts: Code for content source adapter, loader() provides the interface to access your content.
  • lib/layout.shared.tsx: Shared options for layouts, optional but preferred to keep.
Route Description
app/(home) The route group for your landing page and other pages.
app/docs The documentation layout and pages.
app/api/search/route.ts The Route Handler for search.

Fumadocs MDX

A source.config.ts config file has been included, you can customise different options like frontmatter schema.

Read the Introduction for further details.

Learn More

To learn more about Next.js and Fumadocs, take a look at the following resources: