Skip to content

Commit 9a795d8

Browse files
committed
Merge branch 'pr-136-head' into release-integration-20260320
# Conflicts: # index.ts # test/index-retry.test.ts
2 parents 2fd3944 + 05369db commit 9a795d8

8 files changed

Lines changed: 1323 additions & 34 deletions

File tree

index.ts

Lines changed: 171 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ import {
117117
parseRateLimitReason,
118118
lookupCodexCliTokensByEmail,
119119
isCodexCliSyncEnabled,
120+
type Workspace,
120121
} from "./lib/accounts.js";
121122
import {
122123
getStoragePath,
@@ -149,6 +150,7 @@ import {
149150
rewriteUrlForCodex,
150151
shouldRefreshToken,
151152
transformRequestForCodex,
153+
isWorkspaceDisabledError,
152154
} from "./lib/request/fetch-helpers.js";
153155
import { applyFastSessionDefaults } from "./lib/request/request-transformer.js";
154156
import {
@@ -377,6 +379,7 @@ export const OpenAIOAuthPlugin: Plugin = async ({ client }: PluginInput) => {
377379
accountIdOverride?: string;
378380
accountIdSource?: AccountIdSource;
379381
accountLabel?: string;
382+
workspaces?: Workspace[];
380383
};
381384

382385
const resolveAccountSelection = (
@@ -399,6 +402,14 @@ export const OpenAIOAuthPlugin: Plugin = async ({ client }: PluginInput) => {
399402
return tokens;
400403
}
401404

405+
// Convert candidates to workspaces
406+
const workspaces: Workspace[] = candidates.map((c) => ({
407+
id: c.accountId,
408+
name: c.label,
409+
enabled: true,
410+
isDefault: c.isDefault,
411+
}));
412+
402413
if (candidates.length === 1) {
403414
const [candidate] = candidates;
404415
if (candidate) {
@@ -407,6 +418,7 @@ export const OpenAIOAuthPlugin: Plugin = async ({ client }: PluginInput) => {
407418
accountIdOverride: candidate.accountId,
408419
accountIdSource: candidate.source,
409420
accountLabel: candidate.label,
421+
workspaces,
410422
};
411423
}
412424
}
@@ -421,6 +433,7 @@ export const OpenAIOAuthPlugin: Plugin = async ({ client }: PluginInput) => {
421433
accountIdOverride: choice.accountId,
422434
accountIdSource: choice.source ?? "token",
423435
accountLabel: choice.label,
436+
workspaces,
424437
};
425438
};
426439

@@ -546,6 +559,23 @@ export const OpenAIOAuthPlugin: Plugin = async ({ client }: PluginInput) => {
546559
});
547560

548561
if (existingIndex === undefined) {
562+
const initialWorkspaceIndex =
563+
result.workspaces && result.workspaces.length > 0
564+
? (() => {
565+
if (accountId) {
566+
const matchingWorkspaceIndex = result.workspaces.findIndex(
567+
(workspace) => workspace.id === accountId,
568+
);
569+
if (matchingWorkspaceIndex >= 0) {
570+
return matchingWorkspaceIndex;
571+
}
572+
}
573+
const firstEnabledWorkspaceIndex = result.workspaces.findIndex(
574+
(workspace) => workspace.enabled !== false,
575+
);
576+
return firstEnabledWorkspaceIndex >= 0 ? firstEnabledWorkspaceIndex : 0;
577+
})()
578+
: undefined;
549579
accounts.push({
550580
accountId,
551581
accountIdSource,
@@ -556,6 +586,8 @@ export const OpenAIOAuthPlugin: Plugin = async ({ client }: PluginInput) => {
556586
expiresAt: result.expires,
557587
addedAt: now,
558588
lastUsed: now,
589+
workspaces: result.workspaces,
590+
currentWorkspaceIndex: initialWorkspaceIndex,
559591
});
560592
continue;
561593
}
@@ -568,6 +600,48 @@ export const OpenAIOAuthPlugin: Plugin = async ({ client }: PluginInput) => {
568600
const nextAccountIdSource =
569601
accountId ? accountIdSource ?? existing.accountIdSource : existing.accountIdSource;
570602
const nextAccountLabel = accountLabel ?? existing.accountLabel;
603+
// Preserve tracked workspace state when auth refreshes do not return workspace metadata.
604+
const mergedWorkspaces = result.workspaces
605+
? result.workspaces.map((newWs) => {
606+
const existingWs = existing.workspaces?.find((w) => w.id === newWs.id);
607+
return existingWs
608+
? {
609+
...newWs,
610+
enabled: existingWs.enabled,
611+
disabledAt: existingWs.disabledAt,
612+
}
613+
: newWs;
614+
})
615+
: existing.workspaces;
616+
const currentWorkspaceId =
617+
existing.workspaces?.[
618+
typeof existing.currentWorkspaceIndex === "number"
619+
? existing.currentWorkspaceIndex
620+
: 0
621+
]?.id;
622+
const nextCurrentWorkspaceIndex =
623+
mergedWorkspaces && mergedWorkspaces.length > 0
624+
? (() => {
625+
if (currentWorkspaceId) {
626+
const matchingWorkspaceIndex = mergedWorkspaces.findIndex(
627+
(workspace) => workspace.id === currentWorkspaceId,
628+
);
629+
if (matchingWorkspaceIndex >= 0) {
630+
return matchingWorkspaceIndex;
631+
}
632+
}
633+
const defaultWorkspaceIndex = mergedWorkspaces.findIndex(
634+
(workspace) => workspace.isDefault === true,
635+
);
636+
if (defaultWorkspaceIndex >= 0) {
637+
return defaultWorkspaceIndex;
638+
}
639+
const firstEnabledWorkspaceIndex = mergedWorkspaces.findIndex(
640+
(workspace) => workspace.enabled !== false,
641+
);
642+
return firstEnabledWorkspaceIndex >= 0 ? firstEnabledWorkspaceIndex : 0;
643+
})()
644+
: existing.currentWorkspaceIndex;
571645
accounts[existingIndex] = {
572646
...existing,
573647
accountId: nextAccountId,
@@ -578,6 +652,8 @@ export const OpenAIOAuthPlugin: Plugin = async ({ client }: PluginInput) => {
578652
accessToken: result.access,
579653
expiresAt: result.expires,
580654
lastUsed: now,
655+
workspaces: mergedWorkspaces,
656+
currentWorkspaceIndex: nextCurrentWorkspaceIndex,
581657
};
582658
}
583659

@@ -1404,7 +1480,7 @@ export const OpenAIOAuthPlugin: Plugin = async ({ client }: PluginInput) => {
14041480
);
14051481
}
14061482

1407-
while (attempted.size < Math.max(1, accountCount)) {
1483+
accountAttemptLoop: while (attempted.size < Math.max(1, accountCount)) {
14081484
let account = null;
14091485
if (
14101486
!usedPreferredSessionAccount &&
@@ -1497,8 +1573,11 @@ while (attempted.size < Math.max(1, accountCount)) {
14971573
continue;
14981574
}
14991575

1500-
const storedAccountId = account.accountId;
1501-
const storedAccountIdSource = account.accountIdSource;
1576+
const currentWorkspace = accountManager.getCurrentWorkspace(account);
1577+
const storedAccountId = currentWorkspace?.id ?? account.accountId;
1578+
const storedAccountIdSource = currentWorkspace
1579+
? "manual"
1580+
: account.accountIdSource;
15021581
const storedEmail = account.email;
15031582
const hadAccountId = !!storedAccountId;
15041583
const runtimeIdentity = resolveRuntimeRequestIdentity({
@@ -1899,19 +1978,99 @@ while (attempted.size < Math.max(1, accountCount)) {
18991978
blockedModel,
19001979
);
19011980
}
1902-
if (errorResponse.status === 403 && !unsupportedModelInfo.isUnsupported) {
1903-
entitlementCache.markBlocked(
1904-
entitlementAccountKey,
1905-
model ?? modelFamily,
1906-
"plan-entitlement",
1981+
const workspaceErrorCode =
1982+
(errorBody as { error?: { code?: string } } | undefined)?.error?.code ?? "";
1983+
const workspaceErrorMessage =
1984+
(errorBody as { error?: { message?: string } } | undefined)?.error?.message ?? "";
1985+
const isDisabledWorkspaceError =
1986+
isWorkspaceDisabledError(
1987+
errorResponse.status,
1988+
workspaceErrorCode,
1989+
workspaceErrorMessage,
1990+
);
1991+
1992+
// Handle workspace disabled/expired errors by rotating to the next workspace
1993+
// within the same account before falling back to another account.
1994+
if (isDisabledWorkspaceError) {
1995+
runtimeMetrics.failedRequests++;
1996+
runtimeMetrics.lastError = `Workspace disabled for account ${account.index + 1}`;
1997+
1998+
if (!account.workspaces || account.workspaces.length === 0) {
1999+
logWarn(
2000+
`Workspace disabled/expired for account ${account.index + 1} without tracked workspaces. Leaving account enabled.`,
2001+
{ errorCode: workspaceErrorCode },
19072002
);
1908-
capabilityPolicyStore.recordFailure(
1909-
entitlementAccountKey,
1910-
capabilityModelKey,
2003+
if (hasRemainingAccounts) {
2004+
continue accountAttemptLoop;
2005+
}
2006+
return errorResponse;
2007+
} else {
2008+
const currentWorkspace = accountManager.getCurrentWorkspace(account);
2009+
const workspaceName = currentWorkspace?.name ?? currentWorkspace?.id ?? "unknown";
2010+
2011+
logWarn(
2012+
`Workspace disabled/expired for account ${account.index + 1} - workspace: ${workspaceName}. Rotating to next workspace.`,
2013+
{ errorCode: workspaceErrorCode },
2014+
);
2015+
2016+
const disabledWorkspace = currentWorkspace
2017+
? accountManager.disableCurrentWorkspace(account, currentWorkspace.id)
2018+
: false;
2019+
let nextWorkspace = disabledWorkspace
2020+
? accountManager.rotateToNextWorkspace(account)
2021+
: accountManager.getCurrentWorkspace(account);
2022+
if (!disabledWorkspace && (!nextWorkspace || nextWorkspace.enabled === false)) {
2023+
nextWorkspace = accountManager.rotateToNextWorkspace(account);
2024+
}
2025+
2026+
if (nextWorkspace) {
2027+
accountManager.saveToDiskDebounced();
2028+
2029+
const newWorkspaceName = nextWorkspace.name ?? nextWorkspace.id;
2030+
await showToast(
2031+
`Workspace ${workspaceName} disabled. Switched to ${newWorkspaceName}.`,
2032+
"warning",
2033+
{ duration: toastDurationMs },
2034+
);
2035+
2036+
logInfo(`Rotated to workspace ${newWorkspaceName} for account ${account.index + 1}`);
2037+
2038+
// Allow the same account to be selected again with fresh request state.
2039+
attempted.delete(account.index);
2040+
continue accountAttemptLoop;
2041+
}
2042+
2043+
logWarn(`All workspaces disabled for account ${account.index + 1}. Disabling account.`);
2044+
2045+
accountManager.setAccountEnabled(account.index, false);
2046+
accountManager.saveToDiskDebounced();
2047+
2048+
await showToast(
2049+
`All workspaces disabled for account ${account.index + 1}. Switching to another account.`,
2050+
"warning",
2051+
{ duration: toastDurationMs },
19112052
);
2053+
2054+
// Forget session affinity and continue the outer loop so another
2055+
// enabled account can service the request.
2056+
sessionAffinityStore?.forgetSession(sessionAffinityKey);
2057+
continue accountAttemptLoop;
19122058
}
2059+
}
2060+
2061+
if (errorResponse.status === 403 && !unsupportedModelInfo.isUnsupported && !isDisabledWorkspaceError) {
2062+
entitlementCache.markBlocked(
2063+
entitlementAccountKey,
2064+
model ?? modelFamily,
2065+
"plan-entitlement",
2066+
);
2067+
capabilityPolicyStore.recordFailure(
2068+
entitlementAccountKey,
2069+
capabilityModelKey,
2070+
);
2071+
}
19132072

1914-
if (recoveryHook && errorBody && isRecoverableError(errorBody)) {
2073+
if (recoveryHook && errorBody && isRecoverableError(errorBody)) {
19152074
const errorType = detectErrorType(errorBody);
19162075
const toastContent = getRecoveryToastContent(errorType);
19172076
await showToast(

0 commit comments

Comments
 (0)