headroomlabs-ai/headroom · error · RuntimeError

Copilot token exchange returned an empty token.

Error message

Copilot token exchange returned an empty token.

What it means

Raised in CopilotTokenProvider._exchange_token after a successful HTTP exchange with GitHub's Copilot token endpoint: the response payload parsed, but payload['token'] is missing, empty, or whitespace-only after strip. This means the OAuth credential was accepted enough to get a 200 response yet no usable API token came back — most often the Copilot entitlement is absent from the GitHub account.

Source

Thrown at headroom/copilot_auth.py:1186

            if not _should_exchange_oauth_token():
                direct_token = CopilotAPIToken(
                    token=oauth_token,
                    expires_at=time.time() + 3600,
                    api_url=_configured_api_url(),
                )
                self._cached = direct_token
                return direct_token

            exchanged = await self._exchange_token(oauth_token)
            self._cached = exchanged
            return exchanged

    async def _exchange_token(self, oauth_token: str) -> CopilotAPIToken:
        headers = _copilot_token_exchange_headers(oauth_token)
        payload = await asyncio.to_thread(self._exchange_token_sync, headers)
        token = str(payload.get("token") or "").strip()
        if not token:
            raise RuntimeError("Copilot token exchange returned an empty token.")

        expires_at = _parse_expiry(payload.get("expires_at")) or (time.time() + 1800)
        api_url = await asyncio.to_thread(
            _api_url_from_exchange_payload,
            payload,
            oauth_token=oauth_token,
        )
        refresh_in = payload.get("refresh_in")
        sku = payload.get("sku")
        return CopilotAPIToken(
            token=token,
            expires_at=expires_at,
            api_url=api_url,
            refresh_in=int(refresh_in) if isinstance(refresh_in, int | float) else None,
            sku=str(sku) if isinstance(sku, str) and sku.strip() else None,
        )

    @staticmethod

View on GitHub (pinned to 322425c43b)

Solutions

  1. Confirm the GitHub account actually has a Copilot entitlement (individual subscription or an org seat) at github.com/settings/copilot
  2. If a seat was just assigned, wait briefly and retry — provisioning can lag
  3. Re-login to refresh the OAuth token in case it belongs to a different account than the one with Copilot
  4. Inspect the exchange response (log payload keys, never the token) to distinguish 'no entitlement' from shape drift
Defensive patterns

Strategy: try-catch

Try / catch

try:
    api_token = await provider.get_token()
except RuntimeError as e:
    if "empty token" in str(e):
        raise SystemExit(
            "Copilot exchange returned no token — verify the account has a Copilot "
            "subscription/seat at github.com/settings/copilot"
        ) from e
    raise

Prevention

When it happens

Trigger: Exchange GET to _token_exchange_url() returns 200 with JSON lacking a 'token' field — e.g. the account has no Copilot subscription (free/paid) attached, the SKU is not provisioned, or an unexpected payload shape from a changed endpoint.

Common situations: Personal access token or OAuth token from an account without Copilot access; organization Copilot seat not yet assigned; attempting immediately after seat purchase before provisioning; endpoint contract drift returning an error object with HTTP 200.

Related errors


AI-assisted analysis of headroomlabs-ai/headroom@322425c43b (2026-08-15). Data as JSON: /api/errors/9f4623e300445ad0. Report an issue: GitHub.