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
- Preserve and pass the existing credential as `previous` so its refresh token is carried forward on rotation.
- Retry the initial OAuth login and ensure the request asks for offline access / refresh-token scope (offline_access).
- Check you are not constructing OAuthCredentials manually from just an access_token — go through the exchange flow.
- 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
- Always pass the previous credential to credentialsFromTokenResponse so refresh tokens survive rotation.
- Persist the refresh token immediately on first exchange; OpenAI may not resend it on refresh.
- Request offline access/refresh scope in the OAuth login so a refresh token is issued at all.
- Never build OAuthCredentials by hand from an access token alone.
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
- Stored OpenAI credential has no refresh token.
- OpenAI token ${operation} response missing access_token/expi
- OpenAI token ${operation} response did not include an id_tok
- Failed to extract the ChatGPT account id from the OpenAI acc
- OAuth state mismatch.
AI-assisted analysis of coleam00/Archon@0773b97458 (2026-09-01).
Data as JSON: /api/errors/8c90293206cf6d84.
Report an issue: GitHub.