From 174c16fa265a4bcebecc51d6ba26d9a9eb7072da Mon Sep 17 00:00:00 2001 From: "devin-ai-integration[bot]" <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 10 Aug 2026 11:24:02 -0700 Subject: [PATCH] fix(llm): send OpenRouter app attribution on the request itself (#1045) --- strix/config/models.py | 14 +++++++++----- strix/core/inputs.py | 16 +++++++++++++++- tests/test_inputs.py | 29 +++++++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 6 deletions(-) diff --git a/strix/config/models.py b/strix/config/models.py index e8544975..e632bb06 100644 --- a/strix/config/models.py +++ b/strix/config/models.py @@ -652,27 +652,31 @@ def _install_openrouter_stream_cost_capture() -> None: litellm.OpenrouterConfig = _StrixOpenrouterConfig # type: ignore[misc] -_OPENROUTER_ATTRIBUTION_HEADERS = { +OPENROUTER_ATTRIBUTION_HEADERS = { "HTTP-Referer": "https://strix.ai", "X-Title": "Strix", "X-OpenRouter-Categories": "cli-agent", } +def is_openrouter_model(model_name: str | None) -> bool: + return bool(model_name) and "openrouter/" in (model_name or "").strip().lower() + + def _configure_openrouter_attribution(model_name: str | None) -> None: import litellm current: object = litellm.headers existing: dict[str, str] = current if isinstance(current, dict) else {} - if not model_name or "openrouter/" not in model_name.strip().lower(): - if any(key in existing for key in _OPENROUTER_ATTRIBUTION_HEADERS): + if not is_openrouter_model(model_name): + if any(key in existing for key in OPENROUTER_ATTRIBUTION_HEADERS): remaining = { - k: v for k, v in existing.items() if k not in _OPENROUTER_ATTRIBUTION_HEADERS + k: v for k, v in existing.items() if k not in OPENROUTER_ATTRIBUTION_HEADERS } litellm.headers = remaining or None # type: ignore[assignment] return - litellm.headers = {**existing, **_OPENROUTER_ATTRIBUTION_HEADERS} # type: ignore[assignment] + litellm.headers = {**existing, **OPENROUTER_ATTRIBUTION_HEADERS} # type: ignore[assignment] def _configure_extra_headers(llm: LlmSettings) -> None: diff --git a/strix/core/inputs.py b/strix/core/inputs.py index b1b1a475..a1106ae2 100644 --- a/strix/core/inputs.py +++ b/strix/core/inputs.py @@ -10,10 +10,12 @@ from openai.types.shared import Reasoning from strix.config.models import ( DEFAULT_MODEL_RETRY, + OPENROUTER_ATTRIBUTION_HEADERS, bedrock_route_supports_prompt_caching, is_bedrock_route, is_claude_model, is_known_openai_bare_model, + is_openrouter_model, model_supports_reasoning, request_timeout_extra_args, ) @@ -203,12 +205,13 @@ def make_model_settings( extra_headers: dict[str, str] | None = None, has_tools: bool = True, ) -> ModelSettings: + headers = _request_headers(model_name, extra_headers) model_settings = ModelSettings( parallel_tool_calls=False if has_tools else None, retry=DEFAULT_MODEL_RETRY, include_usage=True, extra_args=request_timeout_extra_args(request_timeout), - extra_headers=dict(extra_headers) if extra_headers else None, + extra_headers=headers, ) if ( reasoning_effort is not None @@ -231,6 +234,17 @@ def make_model_settings( return model_settings +def _request_headers( + model_name: str, extra_headers: dict[str, str] | None +) -> dict[str, str] | None: + headers: dict[str, str] = {} + if is_openrouter_model(model_name): + headers.update(OPENROUTER_ATTRIBUTION_HEADERS) + if extra_headers: + headers.update(extra_headers) + return headers or None + + def _reasoning_settings( effort: ReasoningEffort, extra_args: dict[str, Any] | None, diff --git a/tests/test_inputs.py b/tests/test_inputs.py index 7dea64be..2ff9a603 100644 --- a/tests/test_inputs.py +++ b/tests/test_inputs.py @@ -361,3 +361,32 @@ def test_make_model_settings_timeout_survives_reasoning_resolve() -> None: assert settings.extra_args is not None assert settings.extra_args["timeout"] == 120.0 + + +def test_openrouter_attribution_rides_on_the_request_headers() -> None: + # litellm.headers is ignored once a request carries any header of its own, + # so the attribution must be part of the per-request headers. + headers = make_model_settings( + None, model_name="openrouter/anthropic/claude-sonnet-4-5" + ).extra_headers + assert headers == { + "HTTP-Referer": "https://strix.ai", + "X-Title": "Strix", + "X-OpenRouter-Categories": "cli-agent", + } + + +def test_openrouter_attribution_absent_for_other_providers() -> None: + assert make_model_settings(None, model_name="anthropic/claude-sonnet-4-5").extra_headers is None + + +def test_user_headers_override_openrouter_attribution() -> None: + headers = make_model_settings( + None, + model_name="openrouter/anthropic/claude-sonnet-4-5", + extra_headers={"X-Title": "Custom", "X-Tenant": "acme"}, + ).extra_headers + assert headers is not None + assert headers["X-Title"] == "Custom" + assert headers["X-Tenant"] == "acme" + assert headers["HTTP-Referer"] == "https://strix.ai"