opencode-multi-account-core 0.2.82 → 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 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 wire behavior stays in the provider packages. Server Mode behavior stays in
27
- `@kyoli-gam/core`, `@kyoli-gam/gateway`, and `@kyoli-gam/cli`.
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
- retryResponse = await runtime.fetch(input, init);
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 (retryResponse.status < 500) return retryResponse;
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
- const dispatchResponseStatus = async (account, accountUuid, runtime, response, allow401Retry, from401RefreshRetry) => {
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
- response = recovered;
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 retryResponse = await retryRuntime.fetch(input, init);
1579
- return dispatchResponseStatus(account, accountUuid, retryRuntime, retryResponse, false, true);
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 response;
1672
+ let result;
1635
1673
  try {
1636
1674
  runtime = await runtimeFactory.getRuntime(accountUuid);
1637
- response = await runtime.fetch(input, init);
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, response, true, false);
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,