unslothai/unsloth · error · CodexAuthError

ChatGPT did not return a refresh token.

Error message

ChatGPT did not return a refresh token.

What it means

Validation in _validate_token_payload for the refresh token: after falling back (body['refresh_token'] or previous_refresh_token), the result must be a non-empty string. ChatGPT tokens are rotated on every refresh, so the endpoint must hand back a usable refresh token; if it omits one and no previous token was supplied to reuse, the credential cannot be persisted for future refreshes and the flow fails.

Source

Thrown at studio/backend/core/inference/openai_codex_auth.py:216

        raise CodexAuthError("ChatGPT returned an invalid access token.") from exc
    account_id = payload.get("https://api.openai.com/auth", {}).get("chatgpt_account_id")
    if not isinstance(account_id, str) or not account_id or len(account_id) > 512:
        account_id = payload.get("https://api.openai.com/auth.chatgpt_account_id")
    if not isinstance(account_id, str) or not account_id or len(account_id) > 512:
        raise CodexAuthError("The ChatGPT account identifier was missing.")
    return account_id


def _validate_token_payload(body: Any, previous_refresh_token: str = "") -> dict[str, Any]:
    if not isinstance(body, dict):
        raise CodexAuthError("ChatGPT returned an invalid token response.")
    access_token = body.get("access_token")
    refresh_token = body.get("refresh_token") or previous_refresh_token
    expires_in = body.get("expires_in", 3600)
    if not isinstance(access_token, str) or not access_token:
        raise CodexAuthError("ChatGPT returned an invalid token response.")
    if not isinstance(refresh_token, str) or not refresh_token:
        raise CodexAuthError("ChatGPT did not return a refresh token.")
    try:
        expires_in = max(60, min(int(expires_in), 30 * 24 * 3600))
    except (TypeError, ValueError) as exc:
        raise CodexAuthError("ChatGPT returned an invalid token lifetime.") from exc
    return {
        "access_token": access_token,
        "refresh_token": refresh_token,
        "expires_at": int(time.time()) + expires_in,
        "account_id": extract_chatgpt_account_id(access_token),
    }


def save_oauth_bundle(provider_id: str, bundle: dict[str, Any]) -> None:
    credential_secrets.upsert_secret(
        credential_secrets.OPENAI_CODEX_OAUTH_KIND,
        provider_id,
        json.dumps(bundle, separators = (",", ":")),
    )

View on GitHub (pinned to 203007d190)

Solutions

  1. Restart the OAuth authorization flow (full reconnect) to obtain a fresh refresh token instead of refreshing a half-broken credential.
  2. Check the authorization request included the offline-access scope/parameters ChatGPT OAuth requires for refresh tokens.
  3. Persist the latest refresh_token after every exchange so the previous_refresh_token fallback is available on rotation hiccups.
Defensive patterns

Strategy: fallback

Validate before calling

refresh_token = body.get('refresh_token') or previous_refresh_token
if not isinstance(refresh_token, str) or not refresh_token:
    raise ValueError('no refresh token available; full re-authorization required')

Try / catch

try:
    bundle = _validate_token_payload(body, previous_refresh_token=stored_refresh)
except CodexAuthError as e:
    if 'refresh token' in str(e):
        bundle = await full_reauthorization(provider_id)  # fresh code exchange
    else:
        raise

Prevention

When it happens

Trigger: A refresh_token grant response that omits refresh_token while _validate_token_payload was called without previous_refresh_token; or the field present but empty/non-string.

Common situations: Upstream policy change stopping refresh-token rotation; a first-time code-exchange response missing the refresh token (scope/PKCE misconfiguration); mocks that only return access_token.

Related errors


AI-assisted analysis of unslothai/unsloth@203007d190 (2026-08-15). Data as JSON: /api/errors/acb7753540468465. Report an issue: GitHub.