@@ -6,12 +6,14 @@ import { setTimeout as sleep } from "node:timers/promises"
66import { createServer } from "http"
77import { OpenAIWebSocketPool } from "./ws-pool"
88import { escapeHtml } from "@/util/html"
9+ import { ProviderError } from "@/provider/error"
910
1011const CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann"
1112const ISSUER = "https://auth.openai.com"
1213const CODEX_API_ENDPOINT = "https://chatgpt.com/backend-api/codex/responses"
1314const OAUTH_PORT = 1455
1415const OAUTH_POLLING_SAFETY_MARGIN_MS = 3000
16+ const TOKEN_REFRESH_WINDOW_MS = 5 * 60 * 1000
1517const ALLOWED_MODELS = new Set ( [ "gpt-5.5" , "gpt-5.3-codex-spark" , "gpt-5.4" , "gpt-5.4-mini" ] )
1618const DISALLOWED_MODELS = new Set ( [ "gpt-5.5-pro" ] )
1719
@@ -133,6 +135,25 @@ async function refreshAccessToken(refreshToken: string, issuer = ISSUER): Promis
133135 } ) . toString ( ) ,
134136 } )
135137 if ( ! response . ok ) {
138+ const body = await response . text ( )
139+ const code = ( ( ) => {
140+ try {
141+ const parsed = JSON . parse ( body )
142+ return parsed ?. error ?. code ?? parsed ?. code
143+ } catch {
144+ return undefined
145+ }
146+ } ) ( )
147+ if (
148+ response . status === 401 ||
149+ code === "refresh_token_expired" ||
150+ code === "refresh_token_reused" ||
151+ code === "refresh_token_invalidated"
152+ ) {
153+ throw new ProviderError . AuthenticationError (
154+ "Your ChatGPT login could not be refreshed. Run `opencode auth login` to sign in again." ,
155+ )
156+ }
136157 throw new Error ( `Token refresh failed: ${ response . status } ` )
137158 }
138159 return response . json ( )
@@ -401,7 +422,7 @@ export async function CodexAuthPlugin(input: PluginInput, options: CodexAuthPlug
401422 async loader ( getAuth ) {
402423 const auth = await getAuth ( )
403424 const websocketFetch = options . experimentalWebSockets
404- ? OpenAIWebSocketPool . createWebSocketFetch ( { httpFetch : fetch } )
425+ ? OpenAIWebSocketPool . createWebSocketFetch ( { httpFetch : fetch , recoverWithHttp : auth . type === "oauth" } )
405426 : undefined
406427 if ( websocketFetch ) {
407428 websocketFetches . push ( websocketFetch )
@@ -411,7 +432,9 @@ export async function CodexAuthPlugin(input: PluginInput, options: CodexAuthPlug
411432
412433 let refreshPromise :
413434 | Promise < {
435+ refresh : string
414436 access : string
437+ expires : number
415438 accountId : string | undefined
416439 } >
417440 | undefined
@@ -436,24 +459,26 @@ export async function CodexAuthPlugin(input: PluginInput, options: CodexAuthPlug
436459 return websocketFetch ? websocketFetch ( requestInput , init ) : fetch ( requestInput , init )
437460
438461 const authWithAccount = currentAuth as typeof currentAuth & { accountId ?: string }
439-
440- if ( ! currentAuth . access || currentAuth . expires < Date . now ( ) ) {
462+ const refresh = async ( ) => {
441463 if ( ! refreshPromise ) {
442464 refreshPromise = refreshAccessToken ( currentAuth . refresh , issuer )
443465 . then ( async ( tokens ) => {
444466 const accountId = extractAccountId ( tokens ) || authWithAccount . accountId
467+ const expires = Date . now ( ) + ( tokens . expires_in ?? 3600 ) * 1000
445468 await input . client . auth . set ( {
446469 path : { id : "openai" } ,
447470 body : {
448471 type : "oauth" ,
449472 refresh : tokens . refresh_token ,
450473 access : tokens . access_token ,
451- expires : Date . now ( ) + ( tokens . expires_in ?? 3600 ) * 1000 ,
474+ expires,
452475 ...( accountId && { accountId } ) ,
453476 } ,
454477 } )
455478 return {
479+ refresh : tokens . refresh_token ,
456480 access : tokens . access_token ,
481+ expires,
457482 accountId,
458483 }
459484 } )
@@ -463,10 +488,14 @@ export async function CodexAuthPlugin(input: PluginInput, options: CodexAuthPlug
463488 }
464489
465490 const refreshed = await refreshPromise
491+ currentAuth . refresh = refreshed . refresh
466492 currentAuth . access = refreshed . access
493+ currentAuth . expires = refreshed . expires
467494 authWithAccount . accountId = refreshed . accountId
468495 }
469496
497+ if ( ! currentAuth . access || currentAuth . expires < Date . now ( ) + TOKEN_REFRESH_WINDOW_MS ) await refresh ( )
498+
470499 const headers = new Headers ( )
471500 if ( init ?. headers ) {
472501 if ( init . headers instanceof Headers ) {
@@ -482,9 +511,7 @@ export async function CodexAuthPlugin(input: PluginInput, options: CodexAuthPlug
482511 }
483512 }
484513 headers . set ( "authorization" , `Bearer ${ currentAuth . access } ` )
485- if ( authWithAccount . accountId ) {
486- headers . set ( "ChatGPT-Account-Id" , authWithAccount . accountId )
487- }
514+ if ( authWithAccount . accountId ) headers . set ( "ChatGPT-Account-Id" , authWithAccount . accountId )
488515
489516 const parsed =
490517 requestInput instanceof URL
@@ -499,8 +526,24 @@ export async function CodexAuthPlugin(input: PluginInput, options: CodexAuthPlug
499526 ...init ,
500527 headers,
501528 }
502- if ( websocketFetch && parsed . pathname . endsWith ( "/responses" ) ) return websocketFetch ( url , requestInit )
503- return fetch ( url , OpenAIWebSocketPool . withoutInternalHeaders ( requestInit ) )
529+ const request = ( ) =>
530+ websocketFetch && parsed . pathname . endsWith ( "/responses" )
531+ ? websocketFetch ( url , requestInit )
532+ : fetch ( url , OpenAIWebSocketPool . withoutInternalHeaders ( requestInit ) )
533+ const response = await request ( )
534+ if ( response . status !== 401 ) return response
535+ await response . body ?. cancel ( )
536+ const latestAuth = await getAuth ( )
537+ if ( latestAuth . type === "oauth" ) {
538+ currentAuth . refresh = latestAuth . refresh
539+ currentAuth . access = latestAuth . access
540+ currentAuth . expires = latestAuth . expires
541+ authWithAccount . accountId = ( latestAuth as typeof latestAuth & { accountId ?: string } ) . accountId
542+ }
543+ await refresh ( )
544+ headers . set ( "authorization" , `Bearer ${ currentAuth . access } ` )
545+ if ( authWithAccount . accountId ) headers . set ( "ChatGPT-Account-Id" , authWithAccount . accountId )
546+ return request ( )
504547 } ,
505548 }
506549 } ,
0 commit comments