fix(llm): send OpenRouter app attribution on the request itself (#1045)

This commit is contained in:
devin-ai-integration[bot] 2026-08-10 11:24:02 -07:00 committed by GitHub
parent 94a2586aaa
commit 174c16fa26
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 53 additions and 6 deletions

View file

@ -652,27 +652,31 @@ def _install_openrouter_stream_cost_capture() -> None:
litellm.OpenrouterConfig = _StrixOpenrouterConfig # type: ignore[misc] litellm.OpenrouterConfig = _StrixOpenrouterConfig # type: ignore[misc]
_OPENROUTER_ATTRIBUTION_HEADERS = { OPENROUTER_ATTRIBUTION_HEADERS = {
"HTTP-Referer": "https://strix.ai", "HTTP-Referer": "https://strix.ai",
"X-Title": "Strix", "X-Title": "Strix",
"X-OpenRouter-Categories": "cli-agent", "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: def _configure_openrouter_attribution(model_name: str | None) -> None:
import litellm import litellm
current: object = litellm.headers current: object = litellm.headers
existing: dict[str, str] = current if isinstance(current, dict) else {} existing: dict[str, str] = current if isinstance(current, dict) else {}
if not model_name or "openrouter/" not in model_name.strip().lower(): if not is_openrouter_model(model_name):
if any(key in existing for key in _OPENROUTER_ATTRIBUTION_HEADERS): if any(key in existing for key in OPENROUTER_ATTRIBUTION_HEADERS):
remaining = { 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] litellm.headers = remaining or None # type: ignore[assignment]
return 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: def _configure_extra_headers(llm: LlmSettings) -> None:

View file

@ -10,10 +10,12 @@ from openai.types.shared import Reasoning
from strix.config.models import ( from strix.config.models import (
DEFAULT_MODEL_RETRY, DEFAULT_MODEL_RETRY,
OPENROUTER_ATTRIBUTION_HEADERS,
bedrock_route_supports_prompt_caching, bedrock_route_supports_prompt_caching,
is_bedrock_route, is_bedrock_route,
is_claude_model, is_claude_model,
is_known_openai_bare_model, is_known_openai_bare_model,
is_openrouter_model,
model_supports_reasoning, model_supports_reasoning,
request_timeout_extra_args, request_timeout_extra_args,
) )
@ -203,12 +205,13 @@ def make_model_settings(
extra_headers: dict[str, str] | None = None, extra_headers: dict[str, str] | None = None,
has_tools: bool = True, has_tools: bool = True,
) -> ModelSettings: ) -> ModelSettings:
headers = _request_headers(model_name, extra_headers)
model_settings = ModelSettings( model_settings = ModelSettings(
parallel_tool_calls=False if has_tools else None, parallel_tool_calls=False if has_tools else None,
retry=DEFAULT_MODEL_RETRY, retry=DEFAULT_MODEL_RETRY,
include_usage=True, include_usage=True,
extra_args=request_timeout_extra_args(request_timeout), extra_args=request_timeout_extra_args(request_timeout),
extra_headers=dict(extra_headers) if extra_headers else None, extra_headers=headers,
) )
if ( if (
reasoning_effort is not None reasoning_effort is not None
@ -231,6 +234,17 @@ def make_model_settings(
return 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( def _reasoning_settings(
effort: ReasoningEffort, effort: ReasoningEffort,
extra_args: dict[str, Any] | None, extra_args: dict[str, Any] | None,

View file

@ -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 is not None
assert settings.extra_args["timeout"] == 120.0 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"