Skip to content

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

Open
LukasParke wants to merge 4 commits into
mainfrom
fix/blank-api-key-env-fallback
Open

LukasParke wants to merge 4 commits into
mainfrom
fix/blank-api-key-env-fallback

Conversation

@LukasParke

@LukasParke LukasParke commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #591

Problem

OpenRouter(api_key=...) wraps any non-None value in Security(api_key=...) (sdk.py), and get_security_from_env (utils/security.py) returns early whenever security is set. So the documented pattern api_key=os.getenv("OPENROUTER_API_KEY", "") passes "" when the variable is unset, the env fallback is skipped, and the client builds an empty Bearer header (httpx rejects it with LocalProtocolError: Illegal header value b'Bearer ').

Change

  • src/openrouter/_hooks/registration.py: register an sdk_init hook that sets config.security = None when a non-callable api_key is empty or whitespace. Env fallback then works as it does for OpenRouter(). If no key is available at all, no Authorization header is sent. Callable api_keys are resolved per request and are left alone. This file is user-owned: it is generated once, isn't tracked in gen.lock, and has no DO NOT EDIT header. A regen won't overwrite the fix, and the generated sdk.py / security.py stay untouched.
  • docs/overview.mdx (in .genignore, hand-maintained): os.getenv("OPENROUTER_API_KEY", "") -> os.getenv("OPENROUTER_API_KEY").
  • tests/test_api_key_resolution.py: 4 small tests using httpx.MockTransport that record the Authorization header. The 3 blank-key tests fail without the hook and pass with it.

This uses the same approach as the closed #595, with a smaller hook and a simpler test (no thread/HTTP server).

Not included: generated snippets

The other ~225 occurrences in README.md, README-PYPI.md, USAGE.md and docs/sdks/** come from Speakeasy. I didn't edit them by hand because the next regen would overwrite those edits. gen.yaml's usageSnippets only exposes optionalPropertyRendering and sdkInitStyle, and neither changes how security env vars render. Fixing the snippets (e.g. emitting os.getenv("OPENROUTER_API_KEY") or omitting api_key) needs an upstream Speakeasy template change. Until then, this hook means the generated pattern works correctly.

Verification

uv run --group dev pytest (9 passed), mypy src, pyright src (0 errors), and pylint src --rcfile pylintrc (10.00/10) all pass.

🤖 Generated with Claude Code


Devin Review

…ack applies

The documented `api_key=os.getenv("OPENROUTER_API_KEY", "")` pattern yields
Security(api_key="") when the env var is unset, which short-circuits
get_security_from_env and produces an empty `Bearer ` header. Add an
sdk_init hook (user-owned, survives regeneration) that normalizes a blank
api_key to None, and fix the hand-maintained docs/overview.mdx snippet.

Fixes #591

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@mintlify

mintlify Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated
openrouter-production 🟢 Ready View Preview Oct 21, 2026, 5:52 PM

💡 Tip: Enable Automations to automatically generate PRs for you.

@mintlify

mintlify Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated
openrouter-staging 🟢 Ready View Preview Oct 21, 2026, 5:52 PM

devin-ai-integration[bot]

This comment was marked as resolved.

The sdk_init hook clears a blank api_key so the OPENROUTER_API_KEY
fallback applies, but the generated env fallback reads the variable per
request without stripping it, so a whitespace-only env value (or a
callable api_key returning blank) still produced a "Bearer   " header.
Register the same hook as a BeforeRequest hook that removes an
Authorization header whose bearer token is blank, keeping
"blank credential = absent" end to end without touching generated code.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
devin-ai-integration[bot]

This comment was marked as resolved.

…blank

A callable api_key is resolved per request, after get_security_from_env
has already skipped the environment, so a blank result left the request
unauthenticated even when OPENROUTER_API_KEY was set. Replace a blank
bearer token with the environment key when one is available, and drop
the header only when it is not.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
devin-ai-integration[bot]

This comment was marked as resolved.

…lback

The generated security code leaves an already-prefixed bearer token as
is; match that when substituting the environment key so a value of
"Bearer <token>" is not sent as "Bearer Bearer <token>".

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

This branch was successfully deployed

1 active (outdated) deployment
staging - docs — 9108f1ba Deployed Oct 21, 2026 by mintlify[bot]
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