fix(openrouter): support default_headers for custom HTTP header injection#36582
Open
Hamza Kyamanywa (untilhamza) wants to merge 5 commits into
Open
fix(openrouter): support default_headers for custom HTTP header injection#36582Hamza Kyamanywa (untilhamza) wants to merge 5 commits into
default_headers for custom HTTP header injection#36582Hamza Kyamanywa (untilhamza) wants to merge 5 commits into
Conversation
…tion
Add a `default_headers` field to `ChatOpenRouter` so callers can inject
arbitrary HTTP headers into every request. The headers are merged into the
underlying httpx client and forwarded by OpenRouter to the upstream provider.
Before this change, attempting to set `default_headers` on `ChatOpenRouter`
silently corrupted the value: `ChatOpenRouter` doesn't inherit from
`ChatOpenAI` and doesn't declare `default_headers` as a recognized field, so
the `build_extra` validator (mode="before") swept it into `model_kwargs` with
the misleading warning::
WARNING! default_headers is not default parameter.
default_headers was transferred to model_kwargs.
Please confirm that default_headers is what you intended.
The header then ended up in the request body instead of as an HTTP header,
never reaching the upstream provider. This blocked any feature that depends
on per-request HTTP header injection — for example, xAI's `x-grok-conv-id`
header for sticky-routing prompt cache hits, which gives a +45.9pp cache
hit rate improvement on direct xAI in our measurements.
This change uses the same httpx-pre-build pattern that already exists in
`_build_client` for the app-attribution headers (`HTTP-Referer`, `X-Title`,
`X-OpenRouter-Categories`), simply teaching it to also merge user-supplied
headers from the new `default_headers` field. User-supplied headers take
precedence over built-in attribution headers if a key collides.
## Test
Adds four unit tests in `TestChatOpenRouterInstantiation`:
- `test_default_headers_passed_to_client`: basic case — header reaches both
sync and async httpx clients
- `test_default_headers_coexist_with_app_attribution`: works alongside
`app_url` / `app_title` without breaking either
- `test_default_headers_override_app_attribution`: user-supplied value wins
on key collision
- `test_default_headers_none_no_custom_headers`: backwards-compat sanity
check (default behavior preserved)
Verified to fail before the fix with the exact "transferred to model_kwargs"
warning, and pass after. Full openrouter package test suite (224 tests) and
`make format` / `make lint` (ruff + mypy) all green.
This comment has been minimized.
This comment has been minimized.
6 tasks
default_headers for custom HTTP header injection
Merging this PR will create unknown performance changes
Comparing |
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.
Fixes #36581
Adds a
default_headers: dict[str, str] | Nonefield toChatOpenRouterand merges its values into the underlying httpx client headers in_build_client. Without this, attempting to setdefault_headerstriggersbuild_extra's misleading "transferred to model_kwargs" warning and the header value ends up in the request body instead of being sent as an HTTP header — blocking any feature that needs per-request header injection (e.g. xAI'sx-grok-conv-idfor sticky-routing prompt cache hits).Fix
Three changes in
langchain_openrouter/chat_models.py:default_headers: dict[str, str] | None = Noneas a recognized field onChatOpenRouter. This stopsbuild_extrafrom sweeping it intomodel_kwargs— the field becomes a real first-class option._build_client, mergeself.default_headersinto the existingextra_headersdict that's already used to inject the app-attribution headers (HTTP-Referer,X-Title,X-OpenRouter-Categories). User-supplied headers are merged last so they take precedence over built-in keys on collision.Tests
Adds four unit tests in
TestChatOpenRouterInstantiation:test_default_headers_passed_to_client— header reaches both sync and async httpx clientstest_default_headers_coexist_with_app_attribution— works alongsideapp_url/app_titlewithout breaking eithertest_default_headers_override_app_attribution— user value wins on key collisiontest_default_headers_none_no_custom_headers— backwards-compat sanity check (default behavior preserved)Verified to fail before the fix with the exact "transferred to model_kwargs" warning, and pass after.
How verified
From
libs/partners/openrouter:make format— cleanmake lint(ruff + mypy) — cleanmake test— 224/224 passing (220 existing + 4 new)