tinyhumansai/openhuman · error · Error
OpenRouter key exchange succeeded but no API key was returne
Error message
OpenRouter key exchange succeeded but no API key was returned.
What it means
OpenRouter answered the key exchange with 2xx but the JSON body lacks a string `key` field. This is a response-contract mismatch — the parser no longer matches what the API returns (or a proxy rewrote the body); retrying the same exchange rarely helps.
Source
Thrown at app/src/utils/openrouterOAuth.ts:94
let body: OpenRouterExchangeResponse | null = null;
try {
body = (await response.json()) as OpenRouterExchangeResponse;
} catch {
body = null;
}
if (!response.ok) {
const detail =
typeof body?.error === 'string'
? body.error
: body?.error && typeof body.error === 'object'
? body.error.message
: null;
throw new Error(detail || `OpenRouter key exchange failed (${response.status}).`);
}
if (!body?.key || typeof body.key !== 'string') {
throw new Error('OpenRouter key exchange succeeded but no API key was returned.');
}
return body.key;
}
function toOpenRouterCallbackUrl(redirectUri: string): string {
let parsed: URL;
try {
parsed = new URL(redirectUri);
} catch {
throw new Error('OpenRouter OAuth listener returned an invalid redirect URL.');
}
// Preserve the port the loopback listener actually bound to (carried in
// redirectUri): when the requested port is busy, the Tauri command falls back
// to an OS-assigned ephemeral port, so hardcoding OPENROUTER_LOOPBACK_PORT here
// sent OpenRouter a callback_url pointing at the wrong port. The PKCE
// callback_url is per-request, so the dynamic port is valid (this matches theView on GitHub (pinned to a221052e0d)
Solutions
- Log the raw body's keys to confirm the shape drifted
- Update the OpenRouterExchangeResponse parsing to the current API shape
- If a proxy rewrites bodies, bypass it for the token URL
- Pin expectations with a recorded-fixture contract test so drift is caught before users
Example fix
// before
return body.key;
// after — fail loudly with the observed shape
if (!body?.key || typeof body.key !== 'string') {
throw new Error(`OpenRouter response shape changed; keys seen: ${Object.keys(body ?? {}).join(',')}`);
}
return body.key; Defensive patterns
Strategy: try-catch
Type guard
function isKeyResponse(b: unknown): b is { key: string } {
return (
!!b &&
typeof b === 'object' &&
typeof (b as { key?: unknown }).key === 'string' &&
(b as { key: string }).key.length > 0
);
} Try / catch
Catch, log the observed body keys, and surface an explicit 'OpenRouter response format changed' error — do not store an empty key or silently succeed; retrying the same exchange will not fix a contract mismatch.
Prevention
- Pin the token-endpoint response shape with a recorded-fixture contract test
- Fail loudly on shape drift instead of defaulting to an empty key
- Keep corporate proxies off the token endpoint so bodies are not rewritten
When it happens
Trigger: The token endpoint succeeds but returns {} , a non-string key, or a wrapped envelope; API version drift after an upstream OpenRouter change; a rewriting proxy mangling the JSON body.
Common situations: OpenRouter ships a response-shape change; corporate proxy strips or wraps JSON; a test mock returning the wrong fixture.
Related errors
- OpenRouter OAuth returned an invalid callback URL.
- OpenRouter OAuth callback state did not match the request.
- OpenRouter OAuth did not return an authorization code.
- OpenRouter key exchange failed (${response.status}).
- OpenRouter OAuth listener returned an invalid redirect URL.
AI-assisted analysis of tinyhumansai/openhuman@a221052e0d (2026-08-16).
Data as JSON: /api/errors/1530842394a69bce.
Report an issue: GitHub.