BerriAI/litellm · error · GetAccessTokenError

Token exchange failed

Error message

Token exchange failed: {exc}

What it means

GetAccessTokenError raised in the final step of the ChatGPT device flow: the POST to CHATGPT_OAUTH_TOKEN_URL exchanging the authorization code for tokens returned an HTTP error status (raise_for_status fired). The upstream status code is attached and the httpx error text is embedded, so the exact failure (e.g. 400 invalid_grant, 401 bad client) is visible in the message.

Solutions

  1. Restart the login flow to obtain a fresh authorization code and complete it without pauses
  2. Upgrade litellm so CHATGPT_CLIENT_ID and the redirect URI match the current OAuth app registration
  3. Read the embedded status/text: 400 invalid_grant means stale code, 5xx means retry later
  4. Avoid running two device logins concurrently, which can race the code exchange
Defensive patterns

Strategy: try-catch

Try / catch

from litellm.llms.chatgpt.authenticator import GetAccessTokenError

try:
    tokens = authenticator.login()
except GetAccessTokenError as e:
    if "Token exchange failed" in str(e) and e.status_code in (400, 401):
        print("Authorization code rejected (stale or app mismatch). Restart login; upgrade litellm if it repeats.")
    elif e.status_code >= 500:
        time.sleep(10)
        tokens = authenticator.login()
    else:
        raise

Prevention

When it happens

Trigger: Exchanging the one-time authorization code at the token endpoint after the user approved; the code is expired/reused (400), the client_id/redirect mismatch (400/401), or the token endpoint is erroring (5xx).

Common situations: Delays between approval and exchange letting the short-lived code expire; retrying the flow mid-exchange so the code is consumed twice; litellm version whose client_id or redirect_uri no longer matches OpenAI's registered app.

Related errors


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

Appendix: 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 77b7c6c40c)