Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ from openrouter import OpenRouter
import os

with OpenRouter(
api_key=os.getenv("OPENROUTER_API_KEY", ""),
api_key=os.getenv("OPENROUTER_API_KEY"),
) as open_router:
res = open_router.chat.send(
messages=[
Expand Down
50 changes: 48 additions & 2 deletions src/openrouter/_hooks/registration.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,59 @@
from .types import Hooks
import os

import httpx

from openrouter import components
from openrouter.sdkconfiguration import SDKConfiguration

from .types import BeforeRequestContext, BeforeRequestHook, Hooks, SDKInitHook


# This file is only ever generated once on the first generation and then is free to be modified.
# Any hooks you wish to add should be registered in the init_hooks function. Feel free to define them
# in this file or in separate files in the hooks folder.


class BlankAPIKeyIsUnsetHook(SDKInitHook, BeforeRequestHook):
"""Treat a blank `api_key` (e.g. from `os.getenv("OPENROUTER_API_KEY", "")`)
as not supplied, so the `OPENROUTER_API_KEY` env-var fallback still applies
instead of sending an empty `Bearer ` token.
Comment thread
LukasParke marked this conversation as resolved.

The env var is read per request and a callable `api_key` is resolved per
request, so either can still yield a blank token after init; a blank
bearer token is replaced with `OPENROUTER_API_KEY` when that is set, and
otherwise the `Authorization` header is dropped.
"""

def sdk_init(self, config: SDKConfiguration) -> SDKConfiguration:
security = config.security
if isinstance(security, components.Security) and not (
security.api_key or ""
).strip():
config.security = None
return config

def before_request(
self, hook_ctx: BeforeRequestContext, request: httpx.Request
) -> httpx.Request:
auth = request.headers.get("authorization")
if auth is not None:
scheme, _, token = auth.strip().partition(" ")
if not auth.strip() or (scheme.lower() == "bearer" and not token.strip()):
env_key = (os.getenv("OPENROUTER_API_KEY") or "").strip()
env_scheme, _, env_token = env_key.partition(" ")
if env_scheme.lower() == "bearer":
env_key = env_token.strip()
if env_key:
request.headers["authorization"] = f"Bearer {env_key}"
Comment thread
LukasParke marked this conversation as resolved.
else:
del request.headers["authorization"]
return request


Comment thread
LukasParke marked this conversation as resolved.
def init_hooks(hooks: Hooks):
# pylint: disable=unused-argument
"""Add hooks by calling hooks.register{sdk_init/before_request/after_success/after_error}Hook
with an instance of a hook that implements that specific Hook interface
Hooks are registered per SDK instance, and are valid for the lifetime of the SDK instance"""
blank_api_key_hook = BlankAPIKeyIsUnsetHook()
hooks.register_sdk_init_hook(blank_api_key_hook)
hooks.register_before_request_hook(blank_api_key_hook)
50 changes: 50 additions & 0 deletions tests/test_api_key_resolution.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import httpx
import pytest

from openrouter import OpenRouter


def _authorization_header(**kwargs):
seen = []

def handler(request: httpx.Request) -> httpx.Response:
seen.append(request.headers.get("authorization"))
return httpx.Response(200, json={"data": {"count": 0}})

client = httpx.Client(transport=httpx.MockTransport(handler))
try:
OpenRouter(client=client, **kwargs).models.count()
except Exception: # pylint: disable=broad-except
pass # response shape is irrelevant; only the request header matters
assert seen, "no request was sent"
return seen[0]


@pytest.mark.parametrize("api_key", ["", " "])
def test_blank_api_key_falls_back_to_env_var(monkeypatch, api_key):
monkeypatch.setenv("OPENROUTER_API_KEY", "from-env")
assert _authorization_header(api_key=api_key) == "Bearer from-env"


def test_blank_api_key_without_env_var_sends_no_authorization(monkeypatch):
monkeypatch.delenv("OPENROUTER_API_KEY", raising=False)
assert _authorization_header(api_key="") is None


@pytest.mark.parametrize("env_value", ["", " "])
def test_blank_api_key_and_blank_env_var_sends_no_authorization(
monkeypatch, env_value
):
monkeypatch.setenv("OPENROUTER_API_KEY", env_value)
assert _authorization_header(api_key="") is None


@pytest.mark.parametrize("env_value", ["from-env", "Bearer from-env"])
def test_blank_callable_api_key_falls_back_to_env_var(monkeypatch, env_value):
monkeypatch.setenv("OPENROUTER_API_KEY", env_value)
assert _authorization_header(api_key=lambda: "") == "Bearer from-env"


def test_explicit_api_key_wins_over_env_var(monkeypatch):
monkeypatch.setenv("OPENROUTER_API_KEY", "from-env")
assert _authorization_header(api_key="explicit") == "Bearer explicit"
Loading