Skip to content

fix(openrouter): support default_headers for custom HTTP header injection#36582

Open
Hamza Kyamanywa (untilhamza) wants to merge 5 commits into
langchain-ai:masterfrom
untilhamza:fix/openrouter-default-headers
Open

fix(openrouter): support default_headers for custom HTTP header injection#36582
Hamza Kyamanywa (untilhamza) wants to merge 5 commits into
langchain-ai:masterfrom
untilhamza:fix/openrouter-default-headers

Conversation

@untilhamza
Copy link
Copy Markdown
Contributor

Fixes #36581

Adds a default_headers: dict[str, str] | None field to ChatOpenRouter and merges its values into the underlying httpx client headers in _build_client. Without this, attempting to set default_headers triggers build_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's x-grok-conv-id for sticky-routing prompt cache hits).

Fix

Three changes in langchain_openrouter/chat_models.py:

  1. Declare default_headers: dict[str, str] | None = None as a recognized field on ChatOpenRouter. This stops build_extra from sweeping it into model_kwargs — the field becomes a real first-class option.
  2. In _build_client, merge self.default_headers into the existing extra_headers dict 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.
  3. Docstring documenting the new field, the precedence rule, and the use case (xAI sticky routing as the motivating example).

Tests

Adds four unit tests in TestChatOpenRouterInstantiation:

  • test_default_headers_passed_to_client — 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 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.

How verified

From libs/partners/openrouter:

  • make format — clean
  • make lint (ruff + mypy) — clean
  • make test224/224 passing (220 existing + 4 new)

…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.
@github-actions github-actions Bot added fix For PRs that implement a fix integration PR made that is related to a provider partner package integration openrouter `langchain-openrouter` package issues & PRs size: S 50-199 LOC labels Apr 7, 2026
@github-actions

This comment has been minimized.

@github-actions github-actions Bot closed this Apr 7, 2026
@github-actions github-actions Bot reopened this Apr 7, 2026
@mdrxy Mason Daugherty (mdrxy) changed the title fix(openrouter): support default_headers for custom HTTP header injection fix(openrouter): support default_headers for custom HTTP header injection Apr 7, 2026
@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented Apr 7, 2026

Merging this PR will create unknown performance changes

⚠️ No benchmarks were detected in both the base of the PR and the PR.
Please ensure that your benchmarks are correctly instrumented with CodSpeed.

Check out the benchmarks creation guide


Comparing untilhamza:fix/openrouter-default-headers (39faa81) with master (8182d63)

Open in CodSpeed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

external fix For PRs that implement a fix integration PR made that is related to a provider partner package integration openrouter `langchain-openrouter` package issues & PRs size: S 50-199 LOC

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ChatOpenRouter silently swallows default_headers

1 participant