diff --git a/src/llm/registry.py b/src/llm/registry.py index d89f088d..6dd5eecc 100644 --- a/src/llm/registry.py +++ b/src/llm/registry.py @@ -32,6 +32,27 @@ from .history_adapters import ( ) from .types import ProviderClient +# Client-level ``default_headers`` applied to OpenAI-compatible clients, keyed by +# base-URL prefix. Currently only OpenRouter, which uses them for app attribution +# (https://openrouter.ai/docs/app-attribution); add a prefix here to tag another +# provider. Other OpenAI-compatible backends ignore unrecognized headers. +_DEFAULT_HEADERS_BY_BASE_URL: dict[str, dict[str, str]] = { + "https://openrouter.ai": { + "HTTP-Referer": "https://honcho.dev", + "X-Openrouter-Title": "Honcho", + }, +} + + +def _default_headers_for(base_url: str | None) -> dict[str, str]: + """Default headers for ``base_url`` (prefix match); these merge under any + per-request ``extra_headers`` passthrough, which wins on key collision.""" + if base_url: + for prefix, headers in _DEFAULT_HEADERS_BY_BASE_URL.items(): + if base_url.startswith(prefix): + return headers + return {} + @lru_cache(maxsize=1) def get_anthropic_client() -> AsyncAnthropic: @@ -49,6 +70,7 @@ def get_openai_client() -> AsyncOpenAI: return AsyncOpenAI( api_key=settings.LLM.OPENAI_API_KEY, base_url=settings.LLM.OPENAI_BASE_URL, + default_headers=_default_headers_for(settings.LLM.OPENAI_BASE_URL), ) @@ -70,7 +92,11 @@ def get_openai_override_client( base_url: str | None, api_key: str | None ) -> AsyncOpenAI: """OpenAI client for a specific (base_url, api_key) pair. Cached by key.""" - return AsyncOpenAI(api_key=api_key, base_url=base_url) + return AsyncOpenAI( + api_key=api_key, + base_url=base_url, + default_headers=_default_headers_for(base_url), + ) @lru_cache(maxsize=128) @@ -106,6 +132,7 @@ if settings.LLM.OPENAI_API_KEY: CLIENTS["openai"] = AsyncOpenAI( api_key=settings.LLM.OPENAI_API_KEY, base_url=settings.LLM.OPENAI_BASE_URL, + default_headers=_default_headers_for(settings.LLM.OPENAI_BASE_URL), ) if settings.LLM.GEMINI_API_KEY: diff --git a/tests/llm/test_registry.py b/tests/llm/test_registry.py new file mode 100644 index 00000000..71f3302e --- /dev/null +++ b/tests/llm/test_registry.py @@ -0,0 +1,22 @@ +"""Tests for src.llm.registry helpers.""" + +from __future__ import annotations + +from src.llm.registry import _default_headers_for # pyright: ignore[reportPrivateUsage] + + +def test_default_headers_for_openrouter_base_url() -> None: + """OpenRouter base URLs get the app-attribution headers.""" + headers = _default_headers_for("https://openrouter.ai/api/v1") + assert headers["HTTP-Referer"] == "https://honcho.dev" + assert headers["X-Openrouter-Title"] == "Honcho" + + +def test_default_headers_for_non_openrouter_base_url() -> None: + """Other OpenAI-compatible providers get no extra headers.""" + assert _default_headers_for("https://api.openai.com/v1") == {} + + +def test_default_headers_for_none_base_url() -> None: + """A missing base URL (default OpenAI) gets no extra headers.""" + assert _default_headers_for(None) == {}