BerriAI/litellm · error · GetAccessTokenError

Token exchange failed: {exc}

Error message

Token exchange failed: {exc}

What it means

Raised as `GetAccessTokenError` when the final OAuth token-exchange request (`POST` to `CHATGPT_OAUTH_TOKEN_URL` with the authorization code) returns an HTTP error status. This happens after successful device approval, while converting the authorization code into access/refresh/id tokens.

Source

Thrown at litellm/llms/chatgpt/authenticator.py:267

        try:
            client: Final = _get_httpx_client()
            redirect_uri: Final = f"{CHATGPT_AUTH_BASE}/deviceauth/callback"
            body: Final = (
                "grant_type=authorization_code"
                f"&code={code_data['authorization_code']}"
                f"&redirect_uri={redirect_uri}"
                f"&client_id={CHATGPT_CLIENT_ID}"
                f"&code_verifier={code_data['code_verifier']}"
            )
            resp: Final = client.post(
                CHATGPT_OAUTH_TOKEN_URL,
                headers={"Content-Type": "application/x-www-form-urlencoded"},
                content=body,
            )
            resp.raise_for_status()
            data: Final = resp.json()
        except httpx.HTTPStatusError as exc:
            raise GetAccessTokenError(
                message=f"Token exchange failed: {exc}",
                status_code=exc.response.status_code,
            )
        except Exception as exc:
            raise GetAccessTokenError(
                message=f"Token exchange failed: {exc}",
                status_code=400,
            )

        if not all(key in data for key in ("access_token", "refresh_token", "id_token")):
            raise GetAccessTokenError(
                message=f"Token exchange response missing fields: {data}",
                status_code=400,
            )
        return {
            "access_token": data["access_token"],
            "refresh_token": data["refresh_token"],
            "id_token": data["id_token"],

View on GitHub (pinned to 6c2dcb801b)

Solutions

  1. Retry the full device login from the start — a failed exchange invalidates the code.
  2. Upgrade litellm so the PKCE and redirect-uri construction matches the current OpenAI contract.
  3. Check system clock skew (NTP) — expired-code 400s are often time-related.
  4. Consult the propagated HTTP status and OpenAI status page for server-side causes.
Defensive patterns

Strategy: retry

Try / catch

try:
    tokens = authenticator._exchange_code_for_tokens(code_data)
except GetAccessTokenError as e:
    if e.status_code >= 500:
        retry_login_flow()  # server-side: retry whole flow
    else:
        retry_login_flow()  # 4xx: code is spent/invalid; fresh flow is the only option

Prevention

When it happens

Trigger: The token exchange POST returns 400 (invalid/expired authorization code, PKCE `code_verifier` mismatch), 401, or 5xx. The upstream HTTP status is propagated into the exception's status_code.

Common situations: Clock skew or delays between approval and exchange expiring the code; a modified/old litellm whose `code_verifier` handling or `redirect_uri` no longer matches OpenAI's expectations; auth server incidents.

Related errors


AI-assisted analysis of BerriAI/litellm@6c2dcb801b (2026-08-15). Data as JSON: /api/errors/135d140d1ef1fc5b. Report an issue: GitHub.