Skip to content

fix(auth): treat a blank api_key as unset so the OPENROUTER_API_KEY fallback still applies - #595

Open
rajarshidattapy wants to merge 1 commit into
OpenRouterTeam:mainfrom
rajarshidattapy:fix/blank-api-key-env-fallback
Open

fix(auth): treat a blank api_key as unset so the OPENROUTER_API_KEY fallback still applies#595
rajarshidattapy wants to merge 1 commit into
OpenRouterTeam:mainfrom
rajarshidattapy:fix/blank-api-key-env-fallback

Conversation

@rajarshidattapy

Copy link
Copy Markdown

Fixes #591.

Problem

get_security_from_env only falls back to OPENROUTER_API_KEY when no security was supplied
at all, so OpenRouter(api_key="") short-circuits it — which is exactly what the documented
api_key=os.getenv("OPENROUTER_API_KEY", "") pattern produces when the variable is unset.

The failure mode is worse than #591 assumed. Verified against a stub server, no request is sent
at all; it dies inside httpx before leaving the process:

api_key='', env=ENVKEY  ->  <no request sent>  [LocalProtocolError: Illegal header value b'Bearer ']
api_key='', env unset   ->  <no request sent>  [LocalProtocolError: Illegal header value b'Bearer ']

A 401 at least points at credentials. Illegal header value doesn't mention them, and there is
no request in any log to correlate against.

Change

src/openrouter/_hooks/registration.py | +34 -2   blank api_key normalized to None
tests/test_api_key_resolution.py      | new, 5 tests

#591 suggests fixing this at sdk.py:190-196, but that file is generated and actively churns —
every regen adds namespaces — so it cannot be .genignored the way logger.py could in #590.

_hooks/registration.py is the right home. It is the only file under src/ without a
DO NOT EDIT header, its own comment says it is generated once and then free to modify, and
sdk.py:237 calls hooks.sdk_init(config) and assigns the result — so a hook can normalize
config.security at construction, before anything reads it. Nothing is frozen and there is
nothing to re-fix after a regeneration.

Full resolution matrix after the change:

OpenRouter()                   env unset   ->  no Authorization header
OpenRouter()                   env=ENVKEY  ->  Bearer ENVKEY
OpenRouter(api_key='EXPLICIT') env=ENVKEY  ->  Bearer EXPLICIT
OpenRouter(api_key='')         env=ENVKEY  ->  Bearer ENVKEY     (was LocalProtocolError)
OpenRouter(api_key='')         env unset   ->  no Authorization  (was LocalProtocolError)

A callable api_key is deliberately left untouched — it is resolved per-request, and inspecting
it at init would defeat that. A callable returning "" still hits the old error; that is noted
in the hook's docstring and is not the documented pattern.

Verification

  • 5 new tests pass, driving a stub server that records the Authorization header it receives
  • they discriminate: reverting the hook fails exactly the two that matter —
    test_blank_api_key_falls_back_to_the_env_var and
    test_no_credentials_anywhere_sends_no_authorization_header
  • mypy 769 files clean, pyright 0 errors, pylint 10.00/10

Fix 1 (docs) is not included

The ~119 api_key=os.getenv("OPENROUTER_API_KEY", "") snippets live in generated
docs/sdks/** files. .genignore only covers docs/overview.mdx, and listing 119 generated
files would freeze them all. I looked for a gen.yaml knob — usageSnippets exposes only
optionalPropertyRendering and sdkInitStyle, and security renders as a required constructor
argument, so neither applies. That half needs Speakeasy to change how usage snippets render
security.

This hook makes the pattern harmless in the meantime, which is why it is worth landing
independently of what happens to the docs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Docs pattern os.getenv("OPENROUTER_API_KEY", "") defeats the built-in env fallback

1 participant