feat(openai): inject app attribution headers for OpenRouter and Vercel AI Gateway

This commit is contained in:
zhayujie
2026-05-20 11:43:17 +08:00
parent 4a1f62b185
commit 16b7271826
2 changed files with 41 additions and 3 deletions

View File

@@ -422,8 +422,9 @@ class _AzureChatHTTPClient(OpenAIHTTPClient):
)
self._api_version = api_version
def _build_headers(self, api_key, extra_headers):
# Azure uses api-key header, not Bearer token.
def _build_headers(self, api_key, extra_headers, url=None):
# Azure uses api-key header, not Bearer token. No attribution
# headers — Azure deployments are the customer's own tenant.
key = api_key if api_key is not None else self.api_key
headers = {"Content-Type": "application/json"}
if key:

View File

@@ -24,6 +24,7 @@ Design goals:
import json
from typing import Any, Dict, Generator, Optional
from urllib.parse import urlparse
import requests
@@ -34,6 +35,37 @@ DEFAULT_API_BASE = "https://api.openai.com/v1"
DEFAULT_TIMEOUT = 600 # seconds; matches old openai SDK default
_APP_TITLE = "CowAgent"
_APP_REFERER = "https://github.com/zhayujie/CowAgent"
# Per-gateway app attribution headers, only sent when the request host
# matches a documented gateway. Sending these to user-configured custom
# proxies would leak app identity, so we dispatch by host suffix.
_ATTRIBUTION_HEADERS_BY_HOST: Dict[str, Dict[str, str]] = {
"openrouter.ai": {
"HTTP-Referer": _APP_REFERER,
"X-Title": _APP_TITLE,
},
"ai-gateway.vercel.sh": {
"HTTP-Referer": _APP_REFERER,
"X-Title": _APP_TITLE,
},
}
def _resolve_attribution_headers(url: str) -> Dict[str, str]:
try:
host = (urlparse(url).hostname or "").lower()
except Exception:
return {}
if not host:
return {}
for suffix, headers in _ATTRIBUTION_HEADERS_BY_HOST.items():
if host == suffix or host.endswith("." + suffix):
return dict(headers)
return {}
class OpenAIHTTPError(Exception):
"""Raised for non-2xx responses. Carries status code + parsed body."""
@@ -159,11 +191,16 @@ class OpenAIHTTPClient:
self,
api_key: Optional[str],
extra_headers: Optional[Dict[str, str]],
url: Optional[str] = None,
) -> Dict[str, str]:
key = api_key if api_key is not None else self.api_key
headers = {"Content-Type": "application/json"}
if key:
headers["Authorization"] = f"Bearer {key}"
if url:
attribution = _resolve_attribution_headers(url)
if attribution:
headers.update(attribution)
if self.extra_headers:
headers.update(self.extra_headers)
if extra_headers:
@@ -185,7 +222,7 @@ class OpenAIHTTPClient:
):
base = (api_base or self.api_base).rstrip("/") if api_base else self.api_base
url = f"{base}{path}" if path.startswith("/") else f"{base}/{path}"
headers = self._build_headers(api_key, extra_headers)
headers = self._build_headers(api_key, extra_headers, url=url)
req_timeout = timeout if timeout is not None else self.timeout
proxies = (
{"http": proxy, "https": proxy} if proxy else self.proxies