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 the

View on GitHub (pinned to a221052e0d)

Solutions

  1. Log the raw body's keys to confirm the shape drifted
  2. Update the OpenRouterExchangeResponse parsing to the current API shape
  3. If a proxy rewrites bodies, bypass it for the token URL
  4. 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

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


AI-assisted analysis of tinyhumansai/openhuman@a221052e0d (2026-08-16). Data as JSON: /api/errors/1530842394a69bce. Report an issue: GitHub.