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:
parent
715d8a90b9
commit
a65a406301
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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) == {}
|
||||
Loading…
Reference in New Issue