coleam00/Archon · error

OpenAI token ${operation} response missing refresh_token.

Error message

OpenAI token ${operation} response missing refresh_token.

What it means

credentialsFromTokenResponse requires a refresh token: it takes json.refresh_token if present, otherwise falls back to previous.refresh. If neither the new response nor the prior credential supplies a refresh token, it throws. Without a refresh token the stored credential can never be renewed and would dead-end at expiry.

Source

Thrown at packages/core/src/credentials/openai-oauth.ts:237

 * Map a token response onto the stored credential blob. Fails loud on a
 * missing `id_token` at exchange time (the whole point of owning this flow);
 * on refresh, a response that omits `id_token`/`refresh_token` PRESERVES the
 * previous values instead of degrading the blob.
 */
function credentialsFromTokenResponse(
  json: OpenAiTokenResponse,
  operation: 'exchange' | 'refresh',
  previous?: OAuthCredentials
): OpenAiOAuthCredentials {
  const access = typeof json.access_token === 'string' ? json.access_token : '';
  const expiresIn = typeof json.expires_in === 'number' ? json.expires_in : NaN;
  if (!access || !Number.isFinite(expiresIn)) {
    throw new Error(`OpenAI token ${operation} response missing access_token/expires_in.`);
  }
  const prevRefresh = typeof previous?.refresh === 'string' ? previous.refresh : '';
  const refresh = typeof json.refresh_token === 'string' ? json.refresh_token : prevRefresh;
  if (!refresh) {
    throw new Error(`OpenAI token ${operation} response missing refresh_token.`);
  }
  const prevIdToken = typeof previous?.id_token === 'string' ? previous.id_token : '';
  const idToken = typeof json.id_token === 'string' && json.id_token ? json.id_token : prevIdToken;
  if (!idToken) {
    // Fail loud: an id_token-less credential reproduces the exact #1924
    // breakage ("invalid ID token format" in the Codex CLI) — never store one.
    throw new Error(
      `OpenAI token ${operation} response did not include an id_token (required by the Codex CLI).`
    );
  }
  const prevAccountId = typeof previous?.accountId === 'string' ? previous.accountId : '';
  const accountId = accountIdFromAccessToken(access) ?? prevAccountId;
  if (!accountId) {
    throw new Error('Failed to extract the ChatGPT account id from the OpenAI access token.');
  }
  return {
    // Preserve any extra fields a future token response taught us to keep.
    ...(previous ?? {}),

View on GitHub (pinned to 0773b97458)

Solutions

  1. Preserve and pass the existing credential as `previous` so its refresh token is carried forward on rotation.
  2. Retry the initial OAuth login and ensure the request asks for offline access / refresh-token scope (offline_access).
  3. Check you are not constructing OAuthCredentials manually from just an access_token — go through the exchange flow.
  4. Capture the full token response; some providers send refresh_token only on first exchange, so never discard it.

Example fix

// before
const creds = await refreshOpenAiOAuthCredentials(current) // throws later if response lacks refresh_token
// after: keep prior credential so fallback refresh survives
const json = await postTokenRequest(...);
const creds = credentialsFromTokenResponse(json, 'refresh', currentCredential); // pass previous!
Defensive patterns

Strategy: try-catch

Validate before calling

if (typeof json.refresh_token !== 'string' && !(previous && typeof previous.refresh === 'string')) {
  throw new Error('Neither response nor previous credential carries a refresh_token; restart OAuth login');
}

Type guard

function hasRefreshToken(c: { refresh?: unknown }): c is { refresh: string } {
  return typeof c.refresh === 'string' && c.refresh.length > 0;
}

Try / catch

try {
  return await refreshOpenAiOAuthCredentials(creds);
} catch (e) {
  if (e.message.includes('missing refresh_token')) {
    return await runFullOpenAiLogin(); // only recovery: fresh login
  }
  throw e;
}

Prevention

When it happens

Trigger: An exchange or refresh response omits refresh_token AND no previous credential (or a previous credential with a non-string refresh) was supplied to credentialsFromTokenResponse.

Common situations: OpenAI rotates refresh tokens and the refresh response legitimately omits refresh_token while the caller dropped the previous credential; a misconfigured client (no offline_access scope) never receives refresh tokens; code paths that construct credentials from a bare access token.

Related errors


AI-assisted analysis of coleam00/Archon@0773b97458 (2026-09-01). Data as JSON: /api/errors/8c90293206cf6d84. Report an issue: GitHub.