@@ -45,6 +45,8 @@ import {
4545 getTokenRefreshSkewMs ,
4646 getSessionRecovery ,
4747 getAutoResume ,
48+ getToastDurationMs ,
49+ getPerProjectAccounts ,
4850 loadPluginConfig ,
4951} from "./lib/config.js" ;
5052import {
@@ -70,7 +72,7 @@ import {
7072 sanitizeEmail ,
7173 shouldUpdateAccountIdFromToken ,
7274} from "./lib/accounts.js" ;
73- import { getStoragePath , loadAccounts , saveAccounts , type AccountStorageV3 } from "./lib/storage.js" ;
75+ import { getStoragePath , loadAccounts , saveAccounts , setStoragePath , type AccountStorageV3 } from "./lib/storage.js" ;
7476import {
7577 createCodexHeaders ,
7678 extractRequestUrl ,
@@ -86,6 +88,7 @@ import {
8688 RATE_LIMIT_SHORT_RETRY_THRESHOLD_MS ,
8789 resetRateLimitBackoff ,
8890} from "./lib/request/rate-limit-backoff.js" ;
91+ import { addJitter } from "./lib/rotation.js" ;
8992import { getModelFamily , MODEL_FAMILIES , type ModelFamily } from "./lib/prompts/codex.js" ;
9093import type { AccountIdSource , OAuthAuthDetails , TokenResult , UserConfig } from "./lib/types.js" ;
9194import {
@@ -385,12 +388,15 @@ export const OpenAIOAuthPlugin: Plugin = async ({ client }: PluginInput) => {
385388 const showToast = async (
386389 message : string ,
387390 variant : "info" | "success" | "warning" | "error" = "success" ,
391+ options ?: { title ?: string ; duration ?: number } ,
388392 ) : Promise < void > => {
389393 try {
390394 await client . tui . showToast ( {
391395 body : {
392396 message,
393397 variant,
398+ ...( options ?. title && { title : options . title } ) ,
399+ ...( options ?. duration && { duration : options . duration } ) ,
394400 } ,
395401 } ) ;
396402 } catch {
@@ -600,6 +606,12 @@ export const OpenAIOAuthPlugin: Plugin = async ({ client }: PluginInput) => {
600606 const retryAllAccountsRateLimited = getRetryAllAccountsRateLimited ( pluginConfig ) ;
601607 const retryAllAccountsMaxWaitMs = getRetryAllAccountsMaxWaitMs ( pluginConfig ) ;
602608 const retryAllAccountsMaxRetries = getRetryAllAccountsMaxRetries ( pluginConfig ) ;
609+ const toastDurationMs = getToastDurationMs ( pluginConfig ) ;
610+ const perProjectAccounts = getPerProjectAccounts ( pluginConfig ) ;
611+
612+ if ( perProjectAccounts ) {
613+ setStoragePath ( process . cwd ( ) ) ;
614+ }
603615
604616 const sessionRecoveryEnabled = getSessionRecovery ( pluginConfig ) ;
605617 const autoResumeEnabled = getAutoResume ( pluginConfig ) ;
@@ -792,13 +804,14 @@ while (attempted.size < Math.max(1, accountCount)) {
792804 const { response : errorResponse , rateLimit, errorBody } =
793805 await handleErrorResponse ( response ) ;
794806
795- if ( recoveryHook && errorBody && isRecoverableError ( errorBody ) ) {
796- const errorType = detectErrorType ( errorBody ) ;
797- const toastContent = getRecoveryToastContent ( errorType ) ;
798- await showToast (
799- `${ toastContent . title } : ${ toastContent . message } ` ,
800- "warning" ,
801- ) ;
807+ if ( recoveryHook && errorBody && isRecoverableError ( errorBody ) ) {
808+ const errorType = detectErrorType ( errorBody ) ;
809+ const toastContent = getRecoveryToastContent ( errorType ) ;
810+ await showToast (
811+ `${ toastContent . title } : ${ toastContent . message } ` ,
812+ "warning" ,
813+ { duration : toastDurationMs } ,
814+ ) ;
802815 logDebug ( `[${ PLUGIN_NAME } ] Recoverable error detected: ${ errorType } ` ) ;
803816 }
804817
@@ -817,15 +830,16 @@ while (attempted.size < Math.max(1, accountCount)) {
817830 rateLimitToastDebounceMs ,
818831 )
819832 ) {
820- await showToast (
821- `Rate limited. Retrying in ${ waitLabel } (attempt ${ attempt } )...` ,
822- "warning" ,
823- ) ;
833+ await showToast (
834+ `Rate limited. Retrying in ${ waitLabel } (attempt ${ attempt } )...` ,
835+ "warning" ,
836+ { duration : toastDurationMs } ,
837+ ) ;
824838 accountManager . markToastShown ( account . index ) ;
825- }
839+ }
826840
827- await sleep ( delayMs ) ;
828- continue ;
841+ await sleep ( addJitter ( delayMs , 0.2 ) ) ;
842+ continue ;
829843 }
830844
831845 accountManager . markRateLimited (
@@ -848,10 +862,11 @@ while (attempted.size < Math.max(1, accountCount)) {
848862 rateLimitToastDebounceMs ,
849863 )
850864 ) {
851- await showToast (
852- `Rate limited. Switching accounts (retry in ${ waitLabel } ).` ,
853- "warning" ,
854- ) ;
865+ await showToast (
866+ `Rate limited. Switching accounts (retry in ${ waitLabel } ).` ,
867+ "warning" ,
868+ { duration : toastDurationMs } ,
869+ ) ;
855870 accountManager . markToastShown ( account . index ) ;
856871 }
857872 break ;
@@ -876,14 +891,15 @@ while (attempted.size < Math.max(1, accountCount)) {
876891 waitMs <= retryAllAccountsMaxWaitMs ) &&
877892 allRateLimitedRetries < retryAllAccountsMaxRetries
878893 ) {
879- const waitLabel = formatWaitTime ( waitMs ) ;
880- await showToast (
881- `All ${ count } account(s) are rate-limited. Waiting ${ waitLabel } ...` ,
882- "warning" ,
883- ) ;
884- allRateLimitedRetries ++ ;
885- await sleep ( waitMs ) ;
886- continue ;
894+ const waitLabel = formatWaitTime ( waitMs ) ;
895+ await showToast (
896+ `All ${ count } account(s) are rate-limited. Waiting ${ waitLabel } ...` ,
897+ "warning" ,
898+ { duration : toastDurationMs } ,
899+ ) ;
900+ allRateLimitedRetries ++ ;
901+ await sleep ( addJitter ( waitMs , 0.2 ) ) ;
902+ continue ;
887903 }
888904
889905 const waitLabel = waitMs > 0 ? formatWaitTime ( waitMs ) : "a bit" ;
@@ -1336,52 +1352,130 @@ while (attempted.size < Math.max(1, accountCount)) {
13361352 return lines . join ( "\n" ) ;
13371353 } ,
13381354 } ) ,
1339- "openai-accounts-health" : tool ( {
1340- description : "Check health of all OpenAI accounts by validating refresh tokens." ,
1341- args : { } ,
1342- async execute ( ) {
1343- const storage = await loadAccounts ( ) ;
1344- if ( ! storage || storage . accounts . length === 0 ) {
1345- return "No OpenAI accounts configured. Run: opencode auth login" ;
1346- }
1355+ "openai-accounts-health" : tool ( {
1356+ description : "Check health of all OpenAI accounts by validating refresh tokens." ,
1357+ args : { } ,
1358+ async execute ( ) {
1359+ const storage = await loadAccounts ( ) ;
1360+ if ( ! storage || storage . accounts . length === 0 ) {
1361+ return "No OpenAI accounts configured. Run: opencode auth login" ;
1362+ }
13471363
1348- const results : string [ ] = [
1349- `Health Check (${ storage . accounts . length } accounts):` ,
1350- "" ,
1351- ] ;
1364+ const results : string [ ] = [
1365+ `Health Check (${ storage . accounts . length } accounts):` ,
1366+ "" ,
1367+ ] ;
1368+
1369+ let healthyCount = 0 ;
1370+ let unhealthyCount = 0 ;
1371+
1372+ for ( let i = 0 ; i < storage . accounts . length ; i ++ ) {
1373+ const account = storage . accounts [ i ] ;
1374+ if ( ! account ) continue ;
1375+
1376+ const label = formatAccountLabel ( account , i ) ;
1377+ try {
1378+ const refreshResult = await queuedRefresh ( account . refreshToken ) ;
1379+ if ( refreshResult . type === "success" ) {
1380+ results . push ( ` ✓ ${ label } : Healthy` ) ;
1381+ healthyCount ++ ;
1382+ } else {
1383+ results . push ( ` ✗ ${ label } : Token refresh failed` ) ;
1384+ unhealthyCount ++ ;
1385+ }
1386+ } catch ( error ) {
1387+ const errorMsg = error instanceof Error ? error . message : String ( error ) ;
1388+ results . push ( ` ✗ ${ label } : Error - ${ errorMsg . slice ( 0 , 50 ) } ` ) ;
1389+ unhealthyCount ++ ;
1390+ }
1391+ }
13521392
1353- let healthyCount = 0 ;
1354- let unhealthyCount = 0 ;
1355-
1356- for ( let i = 0 ; i < storage . accounts . length ; i ++ ) {
1357- const account = storage . accounts [ i ] ;
1358- if ( ! account ) continue ;
1359-
1360- const label = formatAccountLabel ( account , i ) ;
1361- try {
1362- const refreshResult = await queuedRefresh ( account . refreshToken ) ;
1363- if ( refreshResult . type === "success" ) {
1364- results . push ( ` ✓ ${ label } : Healthy` ) ;
1365- healthyCount ++ ;
1366- } else {
1367- results . push ( ` ✗ ${ label } : Token refresh failed` ) ;
1368- unhealthyCount ++ ;
1369- }
1370- } catch ( error ) {
1371- const errorMsg = error instanceof Error ? error . message : String ( error ) ;
1372- results . push ( ` ✗ ${ label } : Error - ${ errorMsg . slice ( 0 , 50 ) } ` ) ;
1373- unhealthyCount ++ ;
1374- }
1375- }
1393+ results . push ( "" ) ;
1394+ results . push ( `Summary: ${ healthyCount } healthy, ${ unhealthyCount } unhealthy` ) ;
1395+
1396+ return results . join ( "\n" ) ;
1397+ } ,
1398+ } ) ,
1399+ "openai-accounts-remove" : tool ( {
1400+ description : "Remove an OpenAI account by index (1-based). Use openai-accounts to list accounts first." ,
1401+ args : {
1402+ index : tool . schema . number ( ) . describe (
1403+ "Account number to remove (1-based, e.g., 1 for first account)" ,
1404+ ) ,
1405+ } ,
1406+ async execute ( { index } ) {
1407+ const storage = await loadAccounts ( ) ;
1408+ if ( ! storage || storage . accounts . length === 0 ) {
1409+ return "No OpenAI accounts configured. Nothing to remove." ;
1410+ }
13761411
1377- results . push ( "" ) ;
1378- results . push ( `Summary: ${ healthyCount } healthy, ${ unhealthyCount } unhealthy` ) ;
1412+ const targetIndex = Math . floor ( ( index ?? 0 ) - 1 ) ;
1413+ if (
1414+ ! Number . isFinite ( targetIndex ) ||
1415+ targetIndex < 0 ||
1416+ targetIndex >= storage . accounts . length
1417+ ) {
1418+ return `Invalid account number: ${ index } \n\nValid range: 1-${ storage . accounts . length } \n\nUse openai-accounts to list all accounts.` ;
1419+ }
13791420
1380- return results . join ( "\n" ) ;
1381- } ,
1382- } ) ,
1421+ const account = storage . accounts [ targetIndex ] ;
1422+ if ( ! account ) {
1423+ return `Account ${ index } not found.` ;
1424+ }
13831425
1384- } ,
1426+ const label = formatAccountLabel ( account , targetIndex ) ;
1427+
1428+ storage . accounts . splice ( targetIndex , 1 ) ;
1429+
1430+ if ( storage . accounts . length === 0 ) {
1431+ storage . activeIndex = 0 ;
1432+ storage . activeIndexByFamily = { } ;
1433+ } else {
1434+ if ( storage . activeIndex >= storage . accounts . length ) {
1435+ storage . activeIndex = 0 ;
1436+ } else if ( storage . activeIndex > targetIndex ) {
1437+ storage . activeIndex -= 1 ;
1438+ }
1439+
1440+ if ( storage . activeIndexByFamily ) {
1441+ for ( const family of MODEL_FAMILIES ) {
1442+ const idx = storage . activeIndexByFamily [ family ] ;
1443+ if ( typeof idx === "number" ) {
1444+ if ( idx >= storage . accounts . length ) {
1445+ storage . activeIndexByFamily [ family ] = 0 ;
1446+ } else if ( idx > targetIndex ) {
1447+ storage . activeIndexByFamily [ family ] = idx - 1 ;
1448+ }
1449+ }
1450+ }
1451+ }
1452+ }
1453+
1454+ await saveAccounts ( storage ) ;
1455+
1456+ if ( cachedAccountManager ) {
1457+ const managedAccounts = cachedAccountManager . getAccountsSnapshot ( ) ;
1458+ const managedAccount = managedAccounts . find (
1459+ ( acc ) => acc . refreshToken === account . refreshToken
1460+ ) ;
1461+ if ( managedAccount ) {
1462+ cachedAccountManager . removeAccount ( managedAccount ) ;
1463+ await cachedAccountManager . saveToDisk ( ) ;
1464+ }
1465+ }
1466+
1467+ const remaining = storage . accounts . length ;
1468+ return [
1469+ `Removed: ${ label } ` ,
1470+ "" ,
1471+ remaining > 0
1472+ ? `Remaining accounts: ${ remaining } `
1473+ : "No accounts remaining. Run: opencode auth login" ,
1474+ ] . join ( "\n" ) ;
1475+ } ,
1476+ } ) ,
1477+
1478+ } ,
13851479 } ;
13861480} ;
13871481
0 commit comments