opencode-multi-account-core 0.2.83 → 0.2.84
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/dist/index.d.ts +41 -1
- package/dist/index.js +331 -12
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,6 +16,7 @@ The npm package name remains `opencode-multi-account-core` for compatibility.
|
|
|
16
16
|
| `AccountStore` | File-locked account JSON storage |
|
|
17
17
|
| `AccountManager` | Account cache, selection, state mutation |
|
|
18
18
|
| `Executor` | Retry loop and account rotation |
|
|
19
|
+
| `TurnSupervisor` | Pre-output commit gate shared by Plugin and Server Mode |
|
|
19
20
|
| `Claims` | Cross-process account claims |
|
|
20
21
|
| `ProactiveRefreshQueue` | Background token refresh |
|
|
21
22
|
| `NativePluginLifecycle` | OpenCode loader/runtime/refresh wiring |
|
|
@@ -23,8 +24,8 @@ The npm package name remains `opencode-multi-account-core` for compatibility.
|
|
|
23
24
|
| `NativePluginLoader` | Shared `getAuth -> lifecycle.load -> hooks` flow |
|
|
24
25
|
| `NativePluginBootstrapAuth` | Stored-account to OpenCode `auth.json` sync helper |
|
|
25
26
|
|
|
26
|
-
Provider
|
|
27
|
-
|
|
27
|
+
Provider endpoints stay in the provider packages. The Codex Responses startup classifier
|
|
28
|
+
lives here so Plugin and Server Mode make the same pre-output retry decision.
|
|
28
29
|
|
|
29
30
|
## Safety
|
|
30
31
|
|
package/dist/index.d.ts
CHANGED
|
@@ -321,6 +321,35 @@ declare let ACCOUNTS_FILENAME: string;
|
|
|
321
321
|
*/
|
|
322
322
|
declare function setAccountsFilename(filename: string): void;
|
|
323
323
|
|
|
324
|
+
type TurnFailureClass = "rate_limit" | "quota" | "auth" | "permanent" | "transient" | "neutral";
|
|
325
|
+
type TurnFailurePhase = "connect" | "headers" | "startup" | "mid_stream" | "terminal";
|
|
326
|
+
interface TurnFailureSignal {
|
|
327
|
+
class: TurnFailureClass;
|
|
328
|
+
phase: TurnFailurePhase;
|
|
329
|
+
code?: string;
|
|
330
|
+
message?: string;
|
|
331
|
+
httpStatus?: number;
|
|
332
|
+
metadata?: Record<string, unknown>;
|
|
333
|
+
retryAfterSeconds?: number;
|
|
334
|
+
resetAt?: string;
|
|
335
|
+
retryScope?: "same_account" | "next_account" | "none";
|
|
336
|
+
}
|
|
337
|
+
interface SupervisedTurnResponse {
|
|
338
|
+
response: Response;
|
|
339
|
+
failure?: TurnFailureSignal;
|
|
340
|
+
downstreamVisible?: boolean;
|
|
341
|
+
}
|
|
342
|
+
type TurnResponseSupervisor = (response: Response) => Promise<SupervisedTurnResponse>;
|
|
343
|
+
interface SseStartupSupervisorOptions {
|
|
344
|
+
maxBufferedBytes: number;
|
|
345
|
+
classifyFailure(frame: string): TurnFailureSignal | undefined;
|
|
346
|
+
isCommitFrame(frame: string): boolean;
|
|
347
|
+
createBufferLimitFailure(bufferedBytes: number): TurnFailureSignal;
|
|
348
|
+
createFailureResponse(failure: TurnFailureSignal): Response;
|
|
349
|
+
}
|
|
350
|
+
declare function superviseSseResponseStartup(response: Response, options: SseStartupSupervisorOptions): Promise<SupervisedTurnResponse>;
|
|
351
|
+
declare function drainSseFrames(buffer: string, onFrame: (frame: string) => void): string;
|
|
352
|
+
|
|
324
353
|
interface ExecutorAccountManager {
|
|
325
354
|
getAccountCount(): number;
|
|
326
355
|
refresh(): Promise<void>;
|
|
@@ -343,6 +372,7 @@ interface ExecutorDependencies {
|
|
|
343
372
|
sleep: (ms: number) => Promise<void>;
|
|
344
373
|
showToast: (client: PluginClient, message: string, variant: "info" | "warning" | "success" | "error") => Promise<void>;
|
|
345
374
|
getAccountLabel: (account: ManagedAccount) => string;
|
|
375
|
+
responseSupervisor?: TurnResponseSupervisor;
|
|
346
376
|
}
|
|
347
377
|
declare function createExecutorForProvider(providerName: string, dependencies: ExecutorDependencies): {
|
|
348
378
|
executeWithAccountRotation: (manager: ExecutorAccountManager, runtimeFactory: ExecutorRuntimeFactory, client: PluginClient, input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
|
@@ -506,6 +536,16 @@ declare const anthropicOAuthAdapter: OAuthAdapter;
|
|
|
506
536
|
|
|
507
537
|
declare const openAIOAuthAdapter: OAuthAdapter;
|
|
508
538
|
|
|
539
|
+
declare const CODEX_UNKNOWN_RATE_LIMIT_BACKOFF_MS: number;
|
|
540
|
+
declare const CODEX_STARTUP_PROBE_MAX_BYTES: number;
|
|
541
|
+
declare function superviseCodexResponseStartup(response: Response): Promise<SupervisedTurnResponse>;
|
|
542
|
+
declare function isCodexStartupOutputFrame(frame: string): boolean;
|
|
543
|
+
declare function isCodexStartupOutputEvent(value: unknown, event?: string): boolean;
|
|
544
|
+
declare function classifyCodexSseStartupFailure(frame: string): TurnFailureSignal | undefined;
|
|
545
|
+
declare function classifyCodexJsonEventFailure(value: unknown, phase: TurnFailurePhase): TurnFailureSignal | undefined;
|
|
546
|
+
declare function classifyCodexFailure(code: string | undefined, message: string): TurnFailureClass;
|
|
547
|
+
declare function parseCodexRetryAfterSeconds(message: string): number | undefined;
|
|
548
|
+
|
|
509
549
|
declare const PoolConfigSchema: v.ObjectSchema<{
|
|
510
550
|
readonly name: v.StringSchema<undefined>;
|
|
511
551
|
readonly baseProvider: v.StringSchema<undefined>;
|
|
@@ -738,4 +778,4 @@ declare const __openCodeNativeBootstrapAuthTestUtils: {
|
|
|
738
778
|
|
|
739
779
|
declare function zeroCostProviderModels(provider: unknown): Promise<Record<string, any>>;
|
|
740
780
|
|
|
741
|
-
export { ACCOUNTS_FILENAME, ANSI, type AccountManagerClass, type AccountManagerDependencies, type AccountManagerInstance, type AccountMetadataPatch, type AccountSelectionStrategy, AccountSelectionStrategySchema, type AccountStorage, AccountStorageSchema, AccountStore, type BuildFailoverPlanOptions, type CascadeState, CascadeStateManager, type ChainConfig, ChainConfigSchema, type ChainEntryConfig, ChainEntryConfigSchema, type ClaimsManager, type ClaimsMap, type ConfigLoader, type CoreConfig, type CredentialRefreshPatch, CredentialRefreshPatchSchema, type DiskCredentials, type ExecutorAccountManager, type ExecutorDependencies, type ExecutorRuntimeFactory, type FailoverCandidate, type FailoverPlan, type FailoverSkip, type KeyAction, type ManagedAccount, type MenuItem, type NativePluginLifecycle, type NativePluginLifecycleOptions, type NativePluginLoaderResult, type NativePluginManagedAccount, type NativePluginManagerClass, type NativePluginManagerLike, type NativePluginRefreshQueueLike, type NativePluginRuntimeFactoryLike, type NativePluginStoreLike, type OAuthAdapter, type OAuthAdapterPlanLabels, type OAuthAdapterTransformConfig, type OAuthCredentials, OAuthCredentialsSchema, type OpenCodeNativeAuthLoaderOptions, type OpenCodeNativeAuthMethod, type OpenCodeNativeAuthMethodsOptions, type OpenCodeNativeBootstrapAccount, type OpenCodeNativeBootstrapAuthOptions, type OpenCodeNativeBootstrapStore, type OpenCodeNativeLoaderContext, type PluginClient, type PluginConfig, PluginConfigSchema, type PoolChainConfig, PoolChainConfigSchema, type PoolConfig, PoolConfigSchema, PoolManager, type ProactiveRefreshDependencies, type ProactiveRefreshQueueClass, type ProactiveRefreshQueueInstance, type ProfileData, type QuotaResetPaceOptions, type QuotaRoutingWindow, type RateLimitAccountManager, type RateLimitDependencies, type RuntimeFactoryLike, type SelectOptions, type StoredAccount, StoredAccountSchema, TokenRefreshError, type TokenRefreshResult, type UsageLimitEntry, UsageLimitEntrySchema, type UsageLimits, UsageLimitsSchema, __openCodeNativeBootstrapAuthTestUtils, anthropicOAuthAdapter, confirm, createAccountManagerForProvider, createClaimsManager, createConfigLoader, createExecutorForProvider, createMinimalClient, createOpenCodeNativeAuthLoader, createOpenCodeNativeAuthMethods, createOpenCodeNativePluginLifecycle, createProactiveRefreshQueueForProvider, createRateLimitHandlers, debugLog, deduplicateAccounts, formatWaitTime, getAccountLabel, getClearedOAuthBody, getConfig, getConfigDir, getErrorCode, initCoreConfig, isClaimedByOther, isTTY, isTokenRefreshError, loadAccounts, loadConfig, loadPoolChainConfig, migrateFromAuthJson, openAIOAuthAdapter, parseKey, readClaims, readStorageFromDisk, releaseClaim, resetConfigCache, savePoolChainConfig, scoreQuotaResetPace, select, setAccountsFilename, setConfigGetter, showToast, sleep, syncOpenCodeNativeBootstrapAuth, updateConfigField, withDirectoryLock, writeClaim, zeroCostProviderModels };
|
|
781
|
+
export { ACCOUNTS_FILENAME, ANSI, type AccountManagerClass, type AccountManagerDependencies, type AccountManagerInstance, type AccountMetadataPatch, type AccountSelectionStrategy, AccountSelectionStrategySchema, type AccountStorage, AccountStorageSchema, AccountStore, type BuildFailoverPlanOptions, CODEX_STARTUP_PROBE_MAX_BYTES, CODEX_UNKNOWN_RATE_LIMIT_BACKOFF_MS, type CascadeState, CascadeStateManager, type ChainConfig, ChainConfigSchema, type ChainEntryConfig, ChainEntryConfigSchema, type ClaimsManager, type ClaimsMap, type ConfigLoader, type CoreConfig, type CredentialRefreshPatch, CredentialRefreshPatchSchema, type DiskCredentials, type ExecutorAccountManager, type ExecutorDependencies, type ExecutorRuntimeFactory, type FailoverCandidate, type FailoverPlan, type FailoverSkip, type KeyAction, type ManagedAccount, type MenuItem, type NativePluginLifecycle, type NativePluginLifecycleOptions, type NativePluginLoaderResult, type NativePluginManagedAccount, type NativePluginManagerClass, type NativePluginManagerLike, type NativePluginRefreshQueueLike, type NativePluginRuntimeFactoryLike, type NativePluginStoreLike, type OAuthAdapter, type OAuthAdapterPlanLabels, type OAuthAdapterTransformConfig, type OAuthCredentials, OAuthCredentialsSchema, type OpenCodeNativeAuthLoaderOptions, type OpenCodeNativeAuthMethod, type OpenCodeNativeAuthMethodsOptions, type OpenCodeNativeBootstrapAccount, type OpenCodeNativeBootstrapAuthOptions, type OpenCodeNativeBootstrapStore, type OpenCodeNativeLoaderContext, type PluginClient, type PluginConfig, PluginConfigSchema, type PoolChainConfig, PoolChainConfigSchema, type PoolConfig, PoolConfigSchema, PoolManager, type ProactiveRefreshDependencies, type ProactiveRefreshQueueClass, type ProactiveRefreshQueueInstance, type ProfileData, type QuotaResetPaceOptions, type QuotaRoutingWindow, type RateLimitAccountManager, type RateLimitDependencies, type RuntimeFactoryLike, type SelectOptions, type SseStartupSupervisorOptions, type StoredAccount, StoredAccountSchema, type SupervisedTurnResponse, TokenRefreshError, type TokenRefreshResult, type TurnFailureClass, type TurnFailurePhase, type TurnFailureSignal, type TurnResponseSupervisor, type UsageLimitEntry, UsageLimitEntrySchema, type UsageLimits, UsageLimitsSchema, __openCodeNativeBootstrapAuthTestUtils, anthropicOAuthAdapter, classifyCodexFailure, classifyCodexJsonEventFailure, classifyCodexSseStartupFailure, confirm, createAccountManagerForProvider, createClaimsManager, createConfigLoader, createExecutorForProvider, createMinimalClient, createOpenCodeNativeAuthLoader, createOpenCodeNativeAuthMethods, createOpenCodeNativePluginLifecycle, createProactiveRefreshQueueForProvider, createRateLimitHandlers, debugLog, deduplicateAccounts, drainSseFrames, formatWaitTime, getAccountLabel, getClearedOAuthBody, getConfig, getConfigDir, getErrorCode, initCoreConfig, isClaimedByOther, isCodexStartupOutputEvent, isCodexStartupOutputFrame, isTTY, isTokenRefreshError, loadAccounts, loadConfig, loadPoolChainConfig, migrateFromAuthJson, openAIOAuthAdapter, parseCodexRetryAfterSeconds, parseKey, readClaims, readStorageFromDisk, releaseClaim, resetConfigCache, savePoolChainConfig, scoreQuotaResetPace, select, setAccountsFilename, setConfigGetter, showToast, sleep, superviseCodexResponseStartup, superviseSseResponseStartup, syncOpenCodeNativeBootstrapAuth, updateConfigField, withDirectoryLock, writeClaim, zeroCostProviderModels };
|
package/dist/index.js
CHANGED
|
@@ -1530,26 +1530,43 @@ function extractStickyKey(input, init) {
|
|
|
1530
1530
|
const requestHeader = input instanceof Request ? input.headers.get("x-claude-code-session-id") ?? void 0 : void 0;
|
|
1531
1531
|
return readStickyHeaderFromInit(init?.headers) ?? requestHeader;
|
|
1532
1532
|
}
|
|
1533
|
+
async function createFetchArgumentsFactory(input, init) {
|
|
1534
|
+
if (!(input instanceof Request)) {
|
|
1535
|
+
if (!(init?.body instanceof ReadableStream)) return () => [input, init];
|
|
1536
|
+
const body2 = new Uint8Array(await new Response(init.body).arrayBuffer());
|
|
1537
|
+
return () => [input, {
|
|
1538
|
+
...init,
|
|
1539
|
+
body: body2.slice()
|
|
1540
|
+
}];
|
|
1541
|
+
}
|
|
1542
|
+
const request = new Request(input, init);
|
|
1543
|
+
const body = request.body ? new Uint8Array(await request.arrayBuffer()) : void 0;
|
|
1544
|
+
return () => [new Request(request, { body: body?.slice() })];
|
|
1545
|
+
}
|
|
1533
1546
|
function createExecutorForProvider(providerName, dependencies) {
|
|
1534
1547
|
const {
|
|
1535
1548
|
handleRateLimitResponse,
|
|
1536
1549
|
formatWaitTime: formatWaitTime2,
|
|
1537
1550
|
sleep: sleep4,
|
|
1538
1551
|
showToast: showToast2,
|
|
1539
|
-
getAccountLabel: getAccountLabel2
|
|
1552
|
+
getAccountLabel: getAccountLabel2,
|
|
1553
|
+
responseSupervisor
|
|
1540
1554
|
} = dependencies;
|
|
1541
1555
|
async function executeWithAccountRotation(manager, runtimeFactory, client, input, init) {
|
|
1542
1556
|
const maxRetries = Math.max(MIN_MAX_RETRIES, manager.getAccountCount() * RETRIES_PER_ACCOUNT);
|
|
1543
1557
|
let previousAccountUuid;
|
|
1558
|
+
let lastStartupFailure;
|
|
1559
|
+
const startupFailureAccounts = /* @__PURE__ */ new Set();
|
|
1544
1560
|
const stickyKey = extractStickyKey(input, init);
|
|
1561
|
+
const createFetchArguments = responseSupervisor ? await createFetchArgumentsFactory(input, init) : () => [input, init];
|
|
1545
1562
|
async function retryServerErrors(account, runtime) {
|
|
1563
|
+
let lastResult;
|
|
1546
1564
|
for (let attempt = 0; attempt < MAX_SERVER_RETRIES_PER_ATTEMPT; attempt++) {
|
|
1547
1565
|
const backoff = Math.min(SERVER_RETRY_BASE_MS * 2 ** attempt, SERVER_RETRY_MAX_MS);
|
|
1548
1566
|
const jitteredBackoff = backoff * (0.5 + Math.random() * 0.5);
|
|
1549
1567
|
await sleep4(jitteredBackoff);
|
|
1550
|
-
let retryResponse;
|
|
1551
1568
|
try {
|
|
1552
|
-
|
|
1569
|
+
lastResult = await fetchAndSupervise(runtime);
|
|
1553
1570
|
} catch (error) {
|
|
1554
1571
|
if (isAbortError(error)) throw error;
|
|
1555
1572
|
if (await handleRuntimeFetchFailure(manager, runtimeFactory, client, account, error)) {
|
|
@@ -1558,25 +1575,39 @@ function createExecutorForProvider(providerName, dependencies) {
|
|
|
1558
1575
|
void showToast2(client, `${getAccountLabel2(account)} network error \u2014 switching`, "warning");
|
|
1559
1576
|
return null;
|
|
1560
1577
|
}
|
|
1561
|
-
if (
|
|
1578
|
+
if (lastResult.response.status < 500) return lastResult;
|
|
1562
1579
|
}
|
|
1563
|
-
return null;
|
|
1580
|
+
return lastResult?.failure?.retryScope === "same_account" ? lastResult : null;
|
|
1564
1581
|
}
|
|
1565
|
-
|
|
1582
|
+
async function fetchAndSupervise(runtime) {
|
|
1583
|
+
const [attemptInput, attemptInit] = createFetchArguments();
|
|
1584
|
+
const response = await runtime.fetch(attemptInput, attemptInit);
|
|
1585
|
+
return responseSupervisor ? responseSupervisor(response) : { response };
|
|
1586
|
+
}
|
|
1587
|
+
const dispatchResponseStatus = async (account, accountUuid, runtime, initialResult, allow401Retry, from401RefreshRetry) => {
|
|
1588
|
+
let result = initialResult;
|
|
1589
|
+
let response = result.response;
|
|
1590
|
+
if (result.failure && (result.downstreamVisible || result.failure.retryScope === "none")) {
|
|
1591
|
+
return { type: "handled", response };
|
|
1592
|
+
}
|
|
1566
1593
|
if (response.status >= 500) {
|
|
1567
1594
|
const recovered = await retryServerErrors(account, runtime);
|
|
1568
1595
|
if (recovered === null) {
|
|
1569
1596
|
return { type: "retryOuter" };
|
|
1570
1597
|
}
|
|
1571
|
-
|
|
1598
|
+
result = recovered;
|
|
1599
|
+
response = result.response;
|
|
1600
|
+
if (result.failure?.retryScope === "same_account" && response.status >= 500) {
|
|
1601
|
+
return { type: "handled", response };
|
|
1602
|
+
}
|
|
1572
1603
|
}
|
|
1573
1604
|
if (response.status === 401) {
|
|
1574
1605
|
if (allow401Retry) {
|
|
1575
1606
|
runtimeFactory.invalidate(accountUuid);
|
|
1576
1607
|
try {
|
|
1577
1608
|
const retryRuntime = await runtimeFactory.getRuntime(accountUuid);
|
|
1578
|
-
const
|
|
1579
|
-
return dispatchResponseStatus(account, accountUuid, retryRuntime,
|
|
1609
|
+
const retryResult = await fetchAndSupervise(retryRuntime);
|
|
1610
|
+
return dispatchResponseStatus(account, accountUuid, retryRuntime, retryResult, false, true);
|
|
1580
1611
|
} catch (error) {
|
|
1581
1612
|
if (isAbortError(error)) throw error;
|
|
1582
1613
|
await handleRuntimeFetchFailure(manager, runtimeFactory, client, account, error);
|
|
@@ -1617,6 +1648,13 @@ function createExecutorForProvider(providerName, dependencies) {
|
|
|
1617
1648
|
}
|
|
1618
1649
|
if (response.status === 429) {
|
|
1619
1650
|
await handleRateLimitResponse(manager, client, account, response);
|
|
1651
|
+
if (result.failure?.retryScope === "next_account") {
|
|
1652
|
+
startupFailureAccounts.add(accountUuid);
|
|
1653
|
+
lastStartupFailure = response;
|
|
1654
|
+
if (startupFailureAccounts.size >= manager.getAccountCount()) {
|
|
1655
|
+
return { type: "handled", response };
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1620
1658
|
return { type: "handled" };
|
|
1621
1659
|
}
|
|
1622
1660
|
return { type: "success", response };
|
|
@@ -1631,10 +1669,10 @@ function createExecutorForProvider(providerName, dependencies) {
|
|
|
1631
1669
|
}
|
|
1632
1670
|
previousAccountUuid = accountUuid;
|
|
1633
1671
|
let runtime;
|
|
1634
|
-
let
|
|
1672
|
+
let result;
|
|
1635
1673
|
try {
|
|
1636
1674
|
runtime = await runtimeFactory.getRuntime(accountUuid);
|
|
1637
|
-
|
|
1675
|
+
result = await fetchAndSupervise(runtime);
|
|
1638
1676
|
} catch (error) {
|
|
1639
1677
|
if (isAbortError(error)) throw error;
|
|
1640
1678
|
if (await handleRuntimeFetchFailure(manager, runtimeFactory, client, account, error)) {
|
|
@@ -1643,7 +1681,7 @@ function createExecutorForProvider(providerName, dependencies) {
|
|
|
1643
1681
|
void showToast2(client, `${getAccountLabel2(account)} network error \u2014 switching`, "warning");
|
|
1644
1682
|
continue;
|
|
1645
1683
|
}
|
|
1646
|
-
const transition = await dispatchResponseStatus(account, accountUuid, runtime,
|
|
1684
|
+
const transition = await dispatchResponseStatus(account, accountUuid, runtime, result, true, false);
|
|
1647
1685
|
if (transition.type === "retryOuter" || transition.type === "handled") {
|
|
1648
1686
|
if (transition.type === "handled" && transition.response) {
|
|
1649
1687
|
return transition.response;
|
|
@@ -1653,6 +1691,7 @@ function createExecutorForProvider(providerName, dependencies) {
|
|
|
1653
1691
|
await manager.markSuccess(accountUuid);
|
|
1654
1692
|
return transition.response;
|
|
1655
1693
|
}
|
|
1694
|
+
if (lastStartupFailure) return lastStartupFailure;
|
|
1656
1695
|
throw new Error(
|
|
1657
1696
|
`Exhausted ${maxRetries} retries across all accounts. All attempts failed due to auth errors, rate limits, or token issues.`
|
|
1658
1697
|
);
|
|
@@ -2283,6 +2322,275 @@ var openAIOAuthAdapter = {
|
|
|
2283
2322
|
supported: true
|
|
2284
2323
|
};
|
|
2285
2324
|
|
|
2325
|
+
// src/turn-supervisor.ts
|
|
2326
|
+
async function superviseSseResponseStartup(response, options) {
|
|
2327
|
+
const contentType = response.headers.get("content-type")?.toLowerCase();
|
|
2328
|
+
if (!response.ok || !response.body || !contentType?.includes("text/event-stream")) {
|
|
2329
|
+
return { response };
|
|
2330
|
+
}
|
|
2331
|
+
const reader = response.body.getReader();
|
|
2332
|
+
const chunks = [];
|
|
2333
|
+
const decoder = new TextDecoder();
|
|
2334
|
+
let bufferedBytes = 0;
|
|
2335
|
+
let pendingText = "";
|
|
2336
|
+
let failure;
|
|
2337
|
+
let downstreamVisible = false;
|
|
2338
|
+
let done = false;
|
|
2339
|
+
while (!failure && !downstreamVisible) {
|
|
2340
|
+
const next = await reader.read();
|
|
2341
|
+
if (next.done) {
|
|
2342
|
+
done = true;
|
|
2343
|
+
break;
|
|
2344
|
+
}
|
|
2345
|
+
chunks.push(next.value);
|
|
2346
|
+
bufferedBytes += next.value.byteLength;
|
|
2347
|
+
pendingText += decoder.decode(next.value, { stream: true });
|
|
2348
|
+
pendingText = drainSseFrames(pendingText, inspectFrame);
|
|
2349
|
+
if (!failure && !downstreamVisible && bufferedBytes >= options.maxBufferedBytes) {
|
|
2350
|
+
failure = options.createBufferLimitFailure(bufferedBytes);
|
|
2351
|
+
}
|
|
2352
|
+
}
|
|
2353
|
+
if (done && !failure && !downstreamVisible) {
|
|
2354
|
+
pendingText += decoder.decode();
|
|
2355
|
+
if (pendingText.trim()) inspectFrame(pendingText);
|
|
2356
|
+
}
|
|
2357
|
+
if (failure) {
|
|
2358
|
+
await reader.cancel().catch(() => void 0);
|
|
2359
|
+
return {
|
|
2360
|
+
failure,
|
|
2361
|
+
downstreamVisible: false,
|
|
2362
|
+
response: options.createFailureResponse(failure)
|
|
2363
|
+
};
|
|
2364
|
+
}
|
|
2365
|
+
return {
|
|
2366
|
+
downstreamVisible,
|
|
2367
|
+
response: new Response(replayResponseBody(chunks, reader), {
|
|
2368
|
+
status: response.status,
|
|
2369
|
+
statusText: response.statusText,
|
|
2370
|
+
headers: filterStreamingResponseHeaders(response.headers)
|
|
2371
|
+
})
|
|
2372
|
+
};
|
|
2373
|
+
function inspectFrame(frame) {
|
|
2374
|
+
if (failure || downstreamVisible) return;
|
|
2375
|
+
failure = options.classifyFailure(frame);
|
|
2376
|
+
if (!failure && options.isCommitFrame(frame)) downstreamVisible = true;
|
|
2377
|
+
}
|
|
2378
|
+
}
|
|
2379
|
+
function drainSseFrames(buffer, onFrame) {
|
|
2380
|
+
let remainder = buffer;
|
|
2381
|
+
while (true) {
|
|
2382
|
+
const separator = /(?:\r\n|\r|\n)(?:\r\n|\r|\n)/.exec(remainder);
|
|
2383
|
+
if (!separator || separator.index === void 0) return remainder;
|
|
2384
|
+
const frame = remainder.slice(0, separator.index);
|
|
2385
|
+
remainder = remainder.slice(separator.index + separator[0].length);
|
|
2386
|
+
if (frame.trim()) onFrame(frame);
|
|
2387
|
+
}
|
|
2388
|
+
}
|
|
2389
|
+
function replayResponseBody(chunks, reader) {
|
|
2390
|
+
let index = 0;
|
|
2391
|
+
return new ReadableStream({
|
|
2392
|
+
async pull(controller) {
|
|
2393
|
+
if (index < chunks.length) {
|
|
2394
|
+
controller.enqueue(chunks[index++]);
|
|
2395
|
+
return;
|
|
2396
|
+
}
|
|
2397
|
+
const next = await reader.read();
|
|
2398
|
+
if (next.done) {
|
|
2399
|
+
controller.close();
|
|
2400
|
+
return;
|
|
2401
|
+
}
|
|
2402
|
+
controller.enqueue(next.value);
|
|
2403
|
+
},
|
|
2404
|
+
cancel(reason) {
|
|
2405
|
+
return reader.cancel(reason);
|
|
2406
|
+
}
|
|
2407
|
+
});
|
|
2408
|
+
}
|
|
2409
|
+
function filterStreamingResponseHeaders(headers) {
|
|
2410
|
+
const filtered = new Headers(headers);
|
|
2411
|
+
filtered.delete("content-encoding");
|
|
2412
|
+
filtered.delete("content-length");
|
|
2413
|
+
filtered.delete("transfer-encoding");
|
|
2414
|
+
filtered.delete("connection");
|
|
2415
|
+
return filtered;
|
|
2416
|
+
}
|
|
2417
|
+
|
|
2418
|
+
// src/adapters/codex-responses.ts
|
|
2419
|
+
var CODEX_UNKNOWN_RATE_LIMIT_BACKOFF_MS = 5 * 60 * 1e3;
|
|
2420
|
+
var CODEX_STARTUP_PROBE_MAX_BYTES = 64 * 1024;
|
|
2421
|
+
function superviseCodexResponseStartup(response) {
|
|
2422
|
+
return superviseSseResponseStartup(response, {
|
|
2423
|
+
maxBufferedBytes: CODEX_STARTUP_PROBE_MAX_BYTES,
|
|
2424
|
+
classifyFailure: classifyCodexSseStartupFailure,
|
|
2425
|
+
isCommitFrame: isCodexStartupOutputFrame,
|
|
2426
|
+
createBufferLimitFailure: createCodexBufferLimitFailure,
|
|
2427
|
+
createFailureResponse: codexFailureResponse
|
|
2428
|
+
});
|
|
2429
|
+
}
|
|
2430
|
+
function createCodexBufferLimitFailure(bufferedBytes) {
|
|
2431
|
+
return {
|
|
2432
|
+
class: "transient",
|
|
2433
|
+
phase: "startup",
|
|
2434
|
+
code: "startup_buffer_limit_exceeded",
|
|
2435
|
+
message: `Codex upstream produced ${bufferedBytes} bytes without output; retrying before exposing the stream.`,
|
|
2436
|
+
httpStatus: 502,
|
|
2437
|
+
retryScope: "same_account"
|
|
2438
|
+
};
|
|
2439
|
+
}
|
|
2440
|
+
function isCodexStartupOutputFrame(frame) {
|
|
2441
|
+
const payload = readSseJsonRecord(frame);
|
|
2442
|
+
if (!payload) return false;
|
|
2443
|
+
return isCodexStartupOutputEvent(payload, readSseEvent(frame));
|
|
2444
|
+
}
|
|
2445
|
+
function isCodexStartupOutputEvent(value, event) {
|
|
2446
|
+
const payload = readRecord(value);
|
|
2447
|
+
if (!payload) return false;
|
|
2448
|
+
const type = readString(payload.type) ?? event;
|
|
2449
|
+
if (type === "response.output_text.delta" || type === "response.refusal.delta" || type === "response.function_call_arguments.delta" || type === "response.function_call_arguments.done" || type === "response.output_item.done" || type === "response.completed" || type === "response.incomplete") {
|
|
2450
|
+
return true;
|
|
2451
|
+
}
|
|
2452
|
+
if (type === "response.output_item.added") {
|
|
2453
|
+
const item = readRecord(payload.item);
|
|
2454
|
+
const itemType = readString(item?.type);
|
|
2455
|
+
return itemType === "message" || itemType === "function_call";
|
|
2456
|
+
}
|
|
2457
|
+
return false;
|
|
2458
|
+
}
|
|
2459
|
+
function classifyCodexSseStartupFailure(frame) {
|
|
2460
|
+
const event = readSseEvent(frame);
|
|
2461
|
+
const payload = readSseJsonRecord(frame);
|
|
2462
|
+
if (!payload) return void 0;
|
|
2463
|
+
const payloadType = readString(payload.type);
|
|
2464
|
+
if (event !== "response.failed" && event !== "error" && payloadType !== "response.failed" && payloadType !== "error") {
|
|
2465
|
+
return void 0;
|
|
2466
|
+
}
|
|
2467
|
+
return classifyCodexFailurePayload(payload, "startup");
|
|
2468
|
+
}
|
|
2469
|
+
function classifyCodexJsonEventFailure(value, phase) {
|
|
2470
|
+
const payload = readRecord(value);
|
|
2471
|
+
if (!payload) return void 0;
|
|
2472
|
+
const payloadType = readString(payload.type);
|
|
2473
|
+
if (payloadType !== "response.failed" && payloadType !== "error") return void 0;
|
|
2474
|
+
return classifyCodexFailurePayload(payload, phase);
|
|
2475
|
+
}
|
|
2476
|
+
function classifyCodexFailure(code, message) {
|
|
2477
|
+
const normalizedCode = code?.toLowerCase();
|
|
2478
|
+
if (normalizedCode === "rate_limit_exceeded" || normalizedCode === "usage_limit_reached") return "rate_limit";
|
|
2479
|
+
if (normalizedCode === "insufficient_quota" || normalizedCode === "usage_not_included" || normalizedCode === "quota_exceeded") return "quota";
|
|
2480
|
+
if (normalizedCode === "invalid_api_key" || normalizedCode === "invalid_iam_token") return "auth";
|
|
2481
|
+
if (normalizedCode === "server_is_overloaded" || normalizedCode === "slow_down" || normalizedCode === "model_at_capacity") return "transient";
|
|
2482
|
+
const normalized = `${code ?? ""} ${message}`.toLowerCase();
|
|
2483
|
+
if (normalized.includes("rate_limit") || normalized.includes("usage_limit") || normalized.includes("usage limit") || normalized.includes("codex/settings/usage") || normalized.includes("upgrade to pro") || normalized.includes("purchase more credits")) return "rate_limit";
|
|
2484
|
+
if (normalized.includes("quota exceeded") || normalized.includes("insufficient quota") || normalized.includes("usage not included")) return "quota";
|
|
2485
|
+
return "transient";
|
|
2486
|
+
}
|
|
2487
|
+
function parseCodexRetryAfterSeconds(message) {
|
|
2488
|
+
const match = message.match(/try again in\s*(\d+(?:\.\d+)?)\s*(ms|s|seconds?)/i);
|
|
2489
|
+
if (!match) return void 0;
|
|
2490
|
+
const value = Number.parseFloat(match[1]);
|
|
2491
|
+
if (!Number.isFinite(value) || value <= 0) return void 0;
|
|
2492
|
+
return match[2].toLowerCase() === "ms" ? value / 1e3 : value;
|
|
2493
|
+
}
|
|
2494
|
+
function classifyCodexFailurePayload(payload, phase) {
|
|
2495
|
+
const response = readRecord(payload.response);
|
|
2496
|
+
const error = readRecord(response?.error) ?? readRecord(payload.error);
|
|
2497
|
+
const code = readString(error?.code) ?? readString(error?.type) ?? readString(payload.code);
|
|
2498
|
+
const message = readString(error?.message) ?? readString(payload.message) ?? (code ? `Codex upstream failed: ${code}` : "Codex upstream failed before producing output.");
|
|
2499
|
+
const failureClass = classifyCodexFailure(code, message);
|
|
2500
|
+
const retryAfterSeconds = parseCodexRetryAfterSeconds(message);
|
|
2501
|
+
const resetAt = parseCodexResetMetadata(error) ?? parseCodexResetMetadata(payload) ?? defaultCodexResetAt(failureClass, code, message);
|
|
2502
|
+
return {
|
|
2503
|
+
class: failureClass,
|
|
2504
|
+
phase,
|
|
2505
|
+
code: code ?? (failureClass === "rate_limit" ? "rate_limit_exceeded" : "upstream_response_failed"),
|
|
2506
|
+
message,
|
|
2507
|
+
httpStatus: readNumber(payload.status) ?? httpStatusFromCodexFailure(failureClass),
|
|
2508
|
+
retryAfterSeconds: retryAfterSeconds ?? secondsUntilIso(resetAt),
|
|
2509
|
+
resetAt,
|
|
2510
|
+
retryScope: failureClass === "rate_limit" || failureClass === "quota" || failureClass === "auth" ? "next_account" : failureClass === "transient" ? "same_account" : "none"
|
|
2511
|
+
};
|
|
2512
|
+
}
|
|
2513
|
+
function codexFailureResponse(failure) {
|
|
2514
|
+
const headers = new Headers({ "content-type": "application/json; charset=utf-8" });
|
|
2515
|
+
if (failure.retryAfterSeconds) headers.set("retry-after", String(failure.retryAfterSeconds));
|
|
2516
|
+
if (failure.resetAt) headers.set("x-kyoli-account-reset-at", failure.resetAt);
|
|
2517
|
+
return new Response(JSON.stringify({
|
|
2518
|
+
error: {
|
|
2519
|
+
type: failure.code ?? "upstream_response_failed",
|
|
2520
|
+
message: failure.message ?? "Codex upstream failed before producing output.",
|
|
2521
|
+
upstream_status: "response.failed"
|
|
2522
|
+
}
|
|
2523
|
+
}), {
|
|
2524
|
+
status: failure.httpStatus ?? 502,
|
|
2525
|
+
headers
|
|
2526
|
+
});
|
|
2527
|
+
}
|
|
2528
|
+
function httpStatusFromCodexFailure(failureClass) {
|
|
2529
|
+
if (failureClass === "rate_limit" || failureClass === "quota") return 429;
|
|
2530
|
+
if (failureClass === "auth") return 401;
|
|
2531
|
+
if (failureClass === "permanent") return 403;
|
|
2532
|
+
return 502;
|
|
2533
|
+
}
|
|
2534
|
+
function parseCodexResetMetadata(value) {
|
|
2535
|
+
if (!value) return void 0;
|
|
2536
|
+
const resetsAt = readNumeric(value.resets_at);
|
|
2537
|
+
if (resetsAt !== void 0 && resetsAt > 0) {
|
|
2538
|
+
const ms = resetsAt > 1e12 ? resetsAt : resetsAt * 1e3;
|
|
2539
|
+
const date = new Date(ms);
|
|
2540
|
+
return Number.isNaN(date.getTime()) ? void 0 : date.toISOString();
|
|
2541
|
+
}
|
|
2542
|
+
const resetsInSeconds = readNumeric(value.resets_in_seconds);
|
|
2543
|
+
if (resetsInSeconds !== void 0 && resetsInSeconds > 0) {
|
|
2544
|
+
return new Date(Date.now() + resetsInSeconds * 1e3).toISOString();
|
|
2545
|
+
}
|
|
2546
|
+
return void 0;
|
|
2547
|
+
}
|
|
2548
|
+
function defaultCodexResetAt(failureClass, code, message) {
|
|
2549
|
+
if (failureClass !== "rate_limit" && failureClass !== "quota") return void 0;
|
|
2550
|
+
if (isCodexUsageLimitFailure(code, message)) return void 0;
|
|
2551
|
+
return new Date(Date.now() + CODEX_UNKNOWN_RATE_LIMIT_BACKOFF_MS).toISOString();
|
|
2552
|
+
}
|
|
2553
|
+
function isCodexUsageLimitFailure(code, message) {
|
|
2554
|
+
const normalized = `${code ?? ""} ${message}`.toLowerCase();
|
|
2555
|
+
return normalized.includes("usage_limit_reached") || normalized.includes("usage limit") || normalized.includes("codex/settings/usage") || normalized.includes("upgrade to plus") || normalized.includes("upgrade to pro") || normalized.includes("purchase more credits");
|
|
2556
|
+
}
|
|
2557
|
+
function readSseEvent(frame) {
|
|
2558
|
+
return frame.split(/\r\n|\r|\n/).find((line) => line.startsWith("event:"))?.slice("event:".length).trim();
|
|
2559
|
+
}
|
|
2560
|
+
function readSseData(frame) {
|
|
2561
|
+
const lines = frame.split(/\r\n|\r|\n/).filter((line) => line.startsWith("data:")).map((line) => line.slice("data:".length).trimStart());
|
|
2562
|
+
return lines.length > 0 ? lines.join("\n") : void 0;
|
|
2563
|
+
}
|
|
2564
|
+
function readSseJsonRecord(frame) {
|
|
2565
|
+
const data = readSseData(frame);
|
|
2566
|
+
if (!data || data === "[DONE]") return void 0;
|
|
2567
|
+
try {
|
|
2568
|
+
return readRecord(JSON.parse(data));
|
|
2569
|
+
} catch {
|
|
2570
|
+
return void 0;
|
|
2571
|
+
}
|
|
2572
|
+
}
|
|
2573
|
+
function readRecord(value) {
|
|
2574
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
2575
|
+
}
|
|
2576
|
+
function readString(value) {
|
|
2577
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
2578
|
+
}
|
|
2579
|
+
function readNumber(value) {
|
|
2580
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
2581
|
+
}
|
|
2582
|
+
function readNumeric(value) {
|
|
2583
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
2584
|
+
if (typeof value !== "string") return void 0;
|
|
2585
|
+
const parsed = Number(value.trim());
|
|
2586
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
2587
|
+
}
|
|
2588
|
+
function secondsUntilIso(iso) {
|
|
2589
|
+
if (!iso) return void 0;
|
|
2590
|
+
const ms = new Date(iso).getTime() - Date.now();
|
|
2591
|
+
return Number.isFinite(ms) && ms > 0 ? Math.ceil(ms / 1e3) : void 0;
|
|
2592
|
+
}
|
|
2593
|
+
|
|
2286
2594
|
// src/pool-types.ts
|
|
2287
2595
|
import * as v5 from "valibot";
|
|
2288
2596
|
var PoolConfigSchema = v5.object({
|
|
@@ -2955,6 +3263,8 @@ export {
|
|
|
2955
3263
|
AccountSelectionStrategySchema,
|
|
2956
3264
|
AccountStorageSchema,
|
|
2957
3265
|
AccountStore,
|
|
3266
|
+
CODEX_STARTUP_PROBE_MAX_BYTES,
|
|
3267
|
+
CODEX_UNKNOWN_RATE_LIMIT_BACKOFF_MS,
|
|
2958
3268
|
CascadeStateManager,
|
|
2959
3269
|
ChainConfigSchema,
|
|
2960
3270
|
ChainEntryConfigSchema,
|
|
@@ -2970,6 +3280,9 @@ export {
|
|
|
2970
3280
|
UsageLimitsSchema,
|
|
2971
3281
|
__openCodeNativeBootstrapAuthTestUtils,
|
|
2972
3282
|
anthropicOAuthAdapter,
|
|
3283
|
+
classifyCodexFailure,
|
|
3284
|
+
classifyCodexJsonEventFailure,
|
|
3285
|
+
classifyCodexSseStartupFailure,
|
|
2973
3286
|
confirm,
|
|
2974
3287
|
createAccountManagerForProvider,
|
|
2975
3288
|
createClaimsManager,
|
|
@@ -2983,6 +3296,7 @@ export {
|
|
|
2983
3296
|
createRateLimitHandlers,
|
|
2984
3297
|
debugLog,
|
|
2985
3298
|
deduplicateAccounts,
|
|
3299
|
+
drainSseFrames,
|
|
2986
3300
|
formatWaitTime,
|
|
2987
3301
|
getAccountLabel,
|
|
2988
3302
|
getClearedOAuthBody,
|
|
@@ -2991,6 +3305,8 @@ export {
|
|
|
2991
3305
|
getErrorCode,
|
|
2992
3306
|
initCoreConfig,
|
|
2993
3307
|
isClaimedByOther,
|
|
3308
|
+
isCodexStartupOutputEvent,
|
|
3309
|
+
isCodexStartupOutputFrame,
|
|
2994
3310
|
isTTY,
|
|
2995
3311
|
isTokenRefreshError,
|
|
2996
3312
|
loadAccounts,
|
|
@@ -2998,6 +3314,7 @@ export {
|
|
|
2998
3314
|
loadPoolChainConfig,
|
|
2999
3315
|
migrateFromAuthJson,
|
|
3000
3316
|
openAIOAuthAdapter,
|
|
3317
|
+
parseCodexRetryAfterSeconds,
|
|
3001
3318
|
parseKey,
|
|
3002
3319
|
readClaims,
|
|
3003
3320
|
readStorageFromDisk,
|
|
@@ -3010,6 +3327,8 @@ export {
|
|
|
3010
3327
|
setConfigGetter,
|
|
3011
3328
|
showToast,
|
|
3012
3329
|
sleep,
|
|
3330
|
+
superviseCodexResponseStartup,
|
|
3331
|
+
superviseSseResponseStartup,
|
|
3013
3332
|
syncOpenCodeNativeBootstrapAuth,
|
|
3014
3333
|
updateConfigField,
|
|
3015
3334
|
withDirectoryLock,
|