feat: send OpenRouter app-attribution headers on OpenAI-compatible clients (#805)

* feat: send OpenRouter app-attribution headers on OpenAI-compatible clients

Sets HTTP-Referer and X-Title on every AsyncOpenAI client constructed in
src/llm/registry.py (default, override-cached, and module-level CLIENTS) and
in the embedding client, so OpenRouter attributes Honcho's requests to the
"Honcho" app in its dashboard/analytics. Other OpenAI-compatible providers
ignore unrecognized headers, so this is safe to send unconditionally.

* fix: scope OpenRouter attribution headers to OpenRouter base URL only

Address review feedback on #805:
- Only inject attribution headers when the configured base_url starts
  with https://openrouter.ai (via new _openrouter_headers() helper)
- Rename X-Title to X-Openrouter-Title per OpenRouter docs recommendation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor: drive default headers from base-URL map, drop embedding path

Replace the OpenRouter-specific _openrouter_headers helper with a generic
_DEFAULT_HEADERS_BY_BASE_URL prefix map + _default_headers_for lookup, so
OpenRouter always receives its attribution headers and another provider can be
added with a single map entry. Revert the embedding-client change (OpenRouter
has no embeddings endpoint, so that gate was dead code) and add a unit test for
the lookup helper.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Ken Weiner 2026-06-23 13:11:36 -07:00 committed by GitHub
parent 715d8a90b9
commit a65a406301
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 50 additions and 1 deletions

View File

@ -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:

View File

@ -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) == {}