codex-multi-auth 2.22.0 → 2.23.0

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.
Files changed (43) hide show
  1. package/.codex-plugin/plugin.json +1 -1
  2. package/README.md +4 -2
  3. package/config/schema/config.schema.json +8 -0
  4. package/dist/lib/api-route-store.d.ts +1 -0
  5. package/dist/lib/api-route-store.js +2 -1
  6. package/dist/lib/auth/account-access.js +2 -1
  7. package/dist/lib/auth/auth.js +3 -2
  8. package/dist/lib/auth/device-auth.js +2 -1
  9. package/dist/lib/auth/http.d.ts +1 -0
  10. package/dist/lib/auth/http.js +1 -0
  11. package/dist/lib/codex-manager/account-manager-commands.js +1 -0
  12. package/dist/lib/codex-manager/commands/locale.d.ts +12 -0
  13. package/dist/lib/codex-manager/commands/locale.js +25 -0
  14. package/dist/lib/codex-manager/commands/status.d.ts +4 -0
  15. package/dist/lib/codex-manager/commands/status.js +476 -5
  16. package/dist/lib/codex-manager/help.js +3 -1
  17. package/dist/lib/codex-manager.js +98 -29
  18. package/dist/lib/config.js +3 -0
  19. package/dist/lib/locale.d.ts +7 -0
  20. package/dist/lib/locale.js +35 -0
  21. package/dist/lib/quota-probe.js +2 -1
  22. package/dist/lib/request/proxy-fetch.d.ts +5 -0
  23. package/dist/lib/request/proxy-fetch.js +37 -0
  24. package/dist/lib/request/url-rewriting.js +17 -0
  25. package/dist/lib/runtime/account-model-catalog.d.ts +2 -0
  26. package/dist/lib/runtime/account-model-catalog.js +7 -2
  27. package/dist/lib/runtime/app-bind.d.ts +4 -1
  28. package/dist/lib/runtime/app-bind.js +63 -3
  29. package/dist/lib/runtime/responses-websocket.d.ts +30 -0
  30. package/dist/lib/runtime/responses-websocket.js +243 -29
  31. package/dist/lib/runtime/rotation-account-selection.d.ts +5 -4
  32. package/dist/lib/runtime/rotation-server-types.d.ts +4 -1
  33. package/dist/lib/runtime-rotation-proxy.js +30 -9
  34. package/dist/lib/schemas.d.ts +5 -0
  35. package/dist/lib/schemas.js +1 -0
  36. package/dist/lib/web-dashboard-html.d.ts +1 -0
  37. package/dist/lib/web-dashboard-html.js +66 -0
  38. package/dist/lib/web-dashboard.d.ts +36 -0
  39. package/dist/lib/web-dashboard.js +322 -0
  40. package/package.json +3 -1
  41. package/scripts/codex-app-router.js +45 -1
  42. package/scripts/codex-routing.js +1 -0
  43. package/scripts/codex.js +150 -13
@@ -39,10 +39,13 @@ import { runInitConfigCommand } from "./codex-manager/commands/init-config.js";
39
39
  import { runReportCommand } from "./codex-manager/commands/report.js";
40
40
  import { runRotationCommand } from "./codex-manager/commands/rotation.js";
41
41
  import { runFeaturesCommand, runStatusCommand, } from "./codex-manager/commands/status.js";
42
+ import { runWebCommand } from "./web-dashboard.js";
42
43
  import { loadPersistedRuntimeObservabilitySnapshot } from "./runtime/runtime-observability.js";
43
44
  import { runSwitchCommand } from "./codex-manager/commands/switch.js";
44
45
  import { runHistoryCommand } from "./codex-manager/commands/history.js";
46
+ import { runLocaleCommand } from "./codex-manager/commands/locale.js";
45
47
  import { runUnpinCommand } from "./codex-manager/commands/unpin.js";
48
+ import { resolveOutputLocale } from "./locale.js";
46
49
  import { runWorkspaceCommand } from "./codex-manager/commands/workspace.js";
47
50
  import { runUsageCommand } from "./codex-manager/commands/usage.js";
48
51
  import { refreshQuotaCacheForMenu } from "./codex-manager/login-menu-data.js";
@@ -140,6 +143,7 @@ const IMPLEMENTED_FEATURES = [
140
143
  { id: 52, name: "Why-selected and monitor diagnostics" },
141
144
  { id: 53, name: "Config explain, init-config templates, and debug bundle" },
142
145
  { id: 54, name: "mcodex convenience launcher (monitor/tmux/forward)" },
146
+ { id: 55, name: "Local real-time web pool dashboard" },
143
147
  ];
144
148
  async function runForecast(args) {
145
149
  return runForecastCommand(args, {
@@ -267,31 +271,50 @@ export function buildSelectAccountTraced() {
267
271
  /**
268
272
  * Shared handler for `list` and `status` (aliases of the same view).
269
273
  */
270
- const runListOrStatusCommand = (rest) => runStatusCommand({
271
- loadApiRoutes,
272
- loadResetCreditState,
273
- loadInferenceRequestTimes,
274
- loadModelInventory,
275
- setStoragePath,
276
- getStoragePath,
277
- loadAccounts,
278
- inspectStorageHealth,
279
- resolveActiveIndex,
280
- formatRateLimitEntry,
281
- loadRuntimeObservabilitySnapshot: loadPersistedRuntimeObservabilitySnapshot,
282
- loadAppBindStatus: async () => getAppBindStatus()
283
- .then((status) => status.router ? {
284
- ...status.router,
285
- // Keep historical diagnostics, but never revive a crashed router's
286
- // persisted "running" marker or its account-selection signal.
287
- state: status.running ? status.router.state : "stopped",
288
- nativeOpenai: status.running && status.state?.nativeOpenai === true,
289
- } : null)
290
- .catch(() => null),
291
- loadAppHelperStatus: readAppRuntimeHelperAccountSignal,
292
- loadQuotaCache,
293
- json: rest.includes("--json") || rest.includes("-j"),
294
- });
274
+ function buildStatusCommandDeps(rest, logInfo) {
275
+ return {
276
+ loadApiRoutes,
277
+ loadResetCreditState,
278
+ loadInferenceRequestTimes,
279
+ loadModelInventory,
280
+ setStoragePath,
281
+ getStoragePath,
282
+ loadAccounts,
283
+ inspectStorageHealth,
284
+ resolveActiveIndex,
285
+ formatRateLimitEntry,
286
+ loadRuntimeObservabilitySnapshot: loadPersistedRuntimeObservabilitySnapshot,
287
+ loadAppBindStatus: async () => getAppBindStatus()
288
+ .then((status) => status.router ? {
289
+ ...status.router,
290
+ // Keep historical diagnostics, but never revive a crashed router's
291
+ // persisted "running" marker or its account-selection signal.
292
+ state: status.running ? status.router.state : "stopped",
293
+ nativeOpenai: status.running && status.state?.nativeOpenai === true,
294
+ } : null)
295
+ .catch(() => null),
296
+ loadAppHelperStatus: readAppRuntimeHelperAccountSignal,
297
+ loadQuotaCache,
298
+ json: rest.includes("--json") || rest.includes("-j"),
299
+ compact: !rest.includes("--details") && !rest.includes("--verbose") && !rest.includes("--json") && !rest.includes("-j"),
300
+ locale: resolveOutputLocale(loadPluginConfig().locale),
301
+ ...(logInfo ? { logInfo } : {}),
302
+ };
303
+ }
304
+ const runListOrStatusCommand = (rest) => runStatusCommand(buildStatusCommandDeps(rest));
305
+ async function loadStatusJsonSnapshot() {
306
+ let serialized = "";
307
+ const exitCode = await runStatusCommand(buildStatusCommandDeps(["--json"], (message) => {
308
+ serialized = message;
309
+ }));
310
+ if (exitCode !== 0)
311
+ throw new Error("Unable to load status snapshot.");
312
+ const parsed = JSON.parse(serialized);
313
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
314
+ throw new Error("Status snapshot is not an object.");
315
+ }
316
+ return parsed;
317
+ }
295
318
  /**
296
319
  * Command registry for the `auth` dispatcher (audit roadmap §4.1.1 phase 2).
297
320
  *
@@ -307,6 +330,10 @@ const CLI_COMMAND_HANDLERS = new Map([
307
330
  ["login", (rest) => runAuthLogin(rest, { runForecast, createRepairCommandDeps })],
308
331
  ["list", runListOrStatusCommand],
309
332
  ["status", runListOrStatusCommand],
333
+ ["web", (rest) => runWebCommand(rest, {
334
+ getStatus: loadStatusJsonSnapshot,
335
+ locale: resolveOutputLocale(loadPluginConfig().locale),
336
+ })],
310
337
  [
311
338
  "limits",
312
339
  (rest) => runLimitsCommand(rest, {
@@ -481,6 +508,12 @@ const CLI_COMMAND_HANDLERS = new Map([
481
508
  if (subcommand === "template") {
482
509
  return runInitConfigCommand(configArgs);
483
510
  }
511
+ if (subcommand === "locale") {
512
+ return runLocaleCommand(configArgs, {
513
+ loadConfig: () => loadPluginConfig(),
514
+ saveConfig: savePluginConfig,
515
+ });
516
+ }
484
517
  console.error(`Unknown config command: ${subcommand ?? "(missing)"}`);
485
518
  return 1;
486
519
  },
@@ -506,6 +539,32 @@ const CLI_COMMAND_HANDLERS = new Map([
506
539
  ],
507
540
  ]);
508
541
  export async function runCodexMultiAuthCli(rawArgs) {
542
+ const localeArgs = [...rawArgs];
543
+ const previousLocaleOverride = process.env.CODEX_MULTI_AUTH_LOCALE;
544
+ let localeOverrideChanged = false;
545
+ const restoreLocaleOverride = () => {
546
+ if (!localeOverrideChanged)
547
+ return;
548
+ if (previousLocaleOverride === undefined)
549
+ delete process.env.CODEX_MULTI_AUTH_LOCALE;
550
+ else
551
+ process.env.CODEX_MULTI_AUTH_LOCALE = previousLocaleOverride;
552
+ };
553
+ for (let i = 0; i < localeArgs.length; i += 1) {
554
+ const arg = localeArgs[i];
555
+ if (arg === "--locale" && localeArgs[i + 1]) {
556
+ process.env.CODEX_MULTI_AUTH_LOCALE = localeArgs[i + 1];
557
+ localeOverrideChanged = true;
558
+ localeArgs.splice(i, 2);
559
+ i -= 1;
560
+ }
561
+ else if (typeof arg === "string" && arg.startsWith("--locale=")) {
562
+ process.env.CODEX_MULTI_AUTH_LOCALE = arg.slice("--locale=".length);
563
+ localeOverrideChanged = true;
564
+ localeArgs.splice(i, 1);
565
+ i -= 1;
566
+ }
567
+ }
509
568
  // Lazy install setup (audit roadmap §4.5.4): app detection, Codex app bind,
510
569
  // and launcher routing moved out of npm postinstall to the first CLI run.
511
570
  // ensureFirstRunSetup never throws; the catch is belt-and-braces so no
@@ -515,31 +574,41 @@ export async function runCodexMultiAuthCli(rawArgs) {
515
574
  }).catch(() => undefined);
516
575
  const startupDisplaySettings = await loadDashboardDisplaySettings();
517
576
  applyUiThemeFromDashboardSettings(startupDisplaySettings);
518
- const args = rawArgs[0] && rawArgs[0] !== "auth" && ACCOUNT_MANAGER_COMMANDS.has(rawArgs[0])
519
- ? ["auth", ...rawArgs]
520
- : [...rawArgs];
577
+ const args = localeArgs[0] && localeArgs[0] !== "auth" && ACCOUNT_MANAGER_COMMANDS.has(localeArgs[0])
578
+ ? ["auth", ...localeArgs]
579
+ : [...localeArgs];
521
580
  if (args.length === 0) {
581
+ restoreLocaleOverride();
522
582
  printUsage();
523
583
  return 0;
524
584
  }
525
585
  if (args[0] === "--help" || args[0] === "-h") {
586
+ restoreLocaleOverride();
526
587
  printUsage();
527
588
  return 0;
528
589
  }
529
590
  const [root, sub, ...rest] = args;
530
591
  if (root !== "auth") {
592
+ restoreLocaleOverride();
531
593
  printUsage();
532
594
  return 1;
533
595
  }
534
596
  const command = sub ?? "login";
535
597
  if (command === "--help" || command === "-h") {
598
+ restoreLocaleOverride();
536
599
  printUsage();
537
600
  return 0;
538
601
  }
539
602
  const handler = CLI_COMMAND_HANDLERS.get(command);
540
603
  if (handler) {
541
- return handler(rest);
604
+ try {
605
+ return await handler(rest);
606
+ }
607
+ finally {
608
+ restoreLocaleOverride();
609
+ }
542
610
  }
611
+ restoreLocaleOverride();
543
612
  console.error(`Unknown command: ${command}`);
544
613
  printUsage();
545
614
  return 1;
@@ -1,6 +1,7 @@
1
1
  import { existsSync, promises as fs, readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { parseBooleanEnv } from "./env-parsing.js";
4
+ import { normalizeOutputLocaleSetting } from "./locale.js";
4
5
  import { stripModelEffortSuffix } from "./constants.js";
5
6
  import { withRetry, withRetrySync } from "./fs-retry.js";
6
7
  import { logWarn } from "./logger.js";
@@ -113,6 +114,7 @@ function resolvePluginConfigPath() {
113
114
  * CODEX_MODE is enabled by default for better Codex CLI parity
114
115
  */
115
116
  export const DEFAULT_PLUGIN_CONFIG = {
117
+ locale: "auto",
116
118
  codexMode: true,
117
119
  codexRuntimeRotationProxy: true,
118
120
  codexTuiV2: true,
@@ -1275,6 +1277,7 @@ function normalizeConfigExplainValue(value) {
1275
1277
  return value;
1276
1278
  }
1277
1279
  const CONFIG_EXPLAIN_ENTRIES = [
1280
+ { key: "locale", envNames: ["CODEX_MULTI_AUTH_LOCALE"], getValue: config => normalizeOutputLocaleSetting(process.env.CODEX_MULTI_AUTH_LOCALE?.trim() || config.locale || "auto") ?? "auto" },
1278
1281
  { key: "codexMode", envNames: ["CODEX_MODE"], getValue: getCodexMode },
1279
1282
  {
1280
1283
  key: "codexRuntimeRotationProxy",
@@ -0,0 +1,7 @@
1
+ export type OutputLocale = "en" | "zh";
2
+ export type OutputLocaleSetting = OutputLocale | "auto";
3
+ /** Resolve an explicit locale, environment override, or system language. */
4
+ export declare function resolveOutputLocale(requested?: string | null, env?: NodeJS.ProcessEnv): OutputLocale;
5
+ export declare function normalizeOutputLocaleSetting(value: string): OutputLocaleSetting | null;
6
+ export declare function localized(locale: OutputLocale, en: string, zh: string): string;
7
+ export declare function localeName(locale: OutputLocale): string;
@@ -0,0 +1,35 @@
1
+ const SUPPORTED_LOCALES = new Set(["auto", "en", "zh"]);
2
+ /** Resolve an explicit locale, environment override, or system language. */
3
+ export function resolveOutputLocale(requested, env = process.env) {
4
+ const configured = requested?.trim().toLowerCase();
5
+ const value = (env.CODEX_MULTI_AUTH_LOCALE?.trim() || configured || "auto")
6
+ .trim()
7
+ .toLowerCase();
8
+ if (value === "en" || value.startsWith("en-"))
9
+ return "en";
10
+ if (value === "zh" || value.startsWith("zh-"))
11
+ return "zh";
12
+ if (value !== "" && value !== "auto" && SUPPORTED_LOCALES.has(value)) {
13
+ return value === "zh" ? "zh" : "en";
14
+ }
15
+ const system = [env.LC_ALL, env.LC_MESSAGES, env.LANG]
16
+ .find((candidate) => typeof candidate === "string" && candidate.trim())
17
+ ?.toLowerCase() ?? "";
18
+ return system.startsWith("zh") ? "zh" : "en";
19
+ }
20
+ export function normalizeOutputLocaleSetting(value) {
21
+ const normalized = value.trim().toLowerCase();
22
+ if (normalized === "auto")
23
+ return "auto";
24
+ if (normalized === "en" || normalized.startsWith("en-"))
25
+ return "en";
26
+ if (normalized === "zh" || normalized.startsWith("zh-"))
27
+ return "zh";
28
+ return null;
29
+ }
30
+ export function localized(locale, en, zh) {
31
+ return locale === "zh" ? zh : en;
32
+ }
33
+ export function localeName(locale) {
34
+ return locale === "zh" ? "中文" : "English";
35
+ }
@@ -1,4 +1,5 @@
1
1
  import { CODEX_BASE_URL, MAX_RATE_LIMIT_DELAY_MS } from "./constants.js";
2
+ import { fetchWithProxy } from "./request/proxy-fetch.js";
2
3
  import { createCodexHeaders, getUnsupportedCodexModelInfo } from "./request/fetch-helpers.js";
3
4
  import { QUOTA_PROBE_MODEL_CHAIN, resolveProbeReasoningEffort, } from "./request/helpers/model-map.js";
4
5
  import { CodexUnavailableError, isCodexUnavailableError } from "./errors.js";
@@ -367,7 +368,7 @@ export async function fetchCodexQuotaSnapshot(options) {
367
368
  snapshot.diagnosticProbeRequests += 1;
368
369
  snapshot.runtimeMetrics.diagnosticProbeRequests += 1;
369
370
  });
370
- response = await fetch(`${CODEX_BASE_URL}/codex/responses`, {
371
+ response = await fetchWithProxy(`${CODEX_BASE_URL}/codex/responses`, {
371
372
  method: "POST",
372
373
  headers,
373
374
  body: JSON.stringify(probeBody),
@@ -0,0 +1,5 @@
1
+ type FetchInput = Parameters<typeof globalThis.fetch>[0];
2
+ type FetchInit = Parameters<typeof globalThis.fetch>[1];
3
+ export declare function setProxyFetchImpl(impl: typeof globalThis.fetch | null): void;
4
+ export declare function fetchWithProxy(input: FetchInput, init?: FetchInit): ReturnType<typeof globalThis.fetch>;
5
+ export {};
@@ -0,0 +1,37 @@
1
+ import { fetch as undiciFetch } from "undici";
2
+ import { applyProxyCompatibleInit, extractRequestUrl, } from "./url-rewriting.js";
3
+ // Transport selection contract, in precedence order:
4
+ // 1. An implementation registered via setProxyFetchImpl (embedding contract;
5
+ // works regardless of when the host installs it).
6
+ // 2. A global fetch replaced after this module loaded, or a test double.
7
+ // 3. A global fetch installed before module load — honored when it is
8
+ // distinguishable from the platform fetch (Node's built-in is named
9
+ // "fetch"); hosts needing guaranteed interception use setProxyFetchImpl.
10
+ // 4. The package's Undici fetch, but only when the request carries a proxy
11
+ // dispatcher: Node 22's built-in fetch rejects the Undici 8 handler
12
+ // contract, so a proxied call needs the matching implementation.
13
+ // 5. The ambient global fetch for ordinary direct requests.
14
+ const initialGlobalFetch = globalThis.fetch;
15
+ let overrideFetch = null;
16
+ export function setProxyFetchImpl(impl) {
17
+ overrideFetch = impl;
18
+ }
19
+ function isTestDouble(fetchImpl) {
20
+ const candidate = fetchImpl;
21
+ return candidate._isMockFunction === true || candidate.mock !== undefined;
22
+ }
23
+ function isPlatformFetch(fetchImpl) {
24
+ return fetchImpl.name === "fetch" && !isTestDouble(fetchImpl);
25
+ }
26
+ export function fetchWithProxy(input, init) {
27
+ const url = extractRequestUrl(input);
28
+ const proxiedInit = applyProxyCompatibleInit(url, init);
29
+ const currentGlobalFetch = globalThis.fetch;
30
+ const baseFetch = overrideFetch ??
31
+ (currentGlobalFetch !== initialGlobalFetch || !isPlatformFetch(currentGlobalFetch)
32
+ ? currentGlobalFetch
33
+ : proxiedInit.dispatcher
34
+ ? undiciFetch
35
+ : currentGlobalFetch);
36
+ return baseFetch(input, proxiedInit);
37
+ }
@@ -98,11 +98,28 @@ function shouldBypassProxyForUrl(url, noProxyValue) {
98
98
  }
99
99
  return false;
100
100
  }
101
+ /**
102
+ * Loopback destinations never traverse a configured proxy: routing them through
103
+ * HTTP_PROXY would leak managed bearer tokens to the proxy and break the
104
+ * loopback-only runtime proxy contract. This bypass applies even when NO_PROXY
105
+ * is unset, matching curl/git proxy conventions.
106
+ */
107
+ function isLoopbackRequestHost(url) {
108
+ const hostname = url.hostname.replace(/^\[|\]$/g, "").toLowerCase();
109
+ return (hostname === "localhost" ||
110
+ hostname.endsWith(".localhost") ||
111
+ hostname === "::1" ||
112
+ hostname.startsWith("127.") ||
113
+ hostname.startsWith("::ffff:127."));
114
+ }
101
115
  export function resolveProxyUrlForRequest(url, env = process.env) {
102
116
  const parsed = new URL(url);
103
117
  if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
104
118
  return undefined;
105
119
  }
120
+ if (isLoopbackRequestHost(parsed)) {
121
+ return undefined;
122
+ }
106
123
  const httpProxy = resolveProxyEnvValue(env, "http_proxy", "HTTP_PROXY");
107
124
  const httpsProxy = resolveProxyEnvValue(env, "https_proxy", "HTTPS_PROXY");
108
125
  if (!httpProxy && !httpsProxy) {
@@ -23,6 +23,8 @@ export declare class AccountModelCatalog {
23
23
  private readonly pending;
24
24
  constructor(fetchCatalog: (accountKey: string) => Promise<unknown>, now?: () => number, ttlMs?: number);
25
25
  private read;
26
+ /** Includes successful empty catalogs; stale or failed discovery remains unknown. */
27
+ hasUsablePickerSnapshot(key: string): boolean;
26
28
  /** Picker-only stale-while-refresh view; routing still awaits fresh discovery. */
27
29
  cachedList(accountKeys: string[]): CatalogModel[];
28
30
  invalidate(): void;
@@ -83,12 +83,17 @@ export class AccountModelCatalog {
83
83
  this.pending.delete(key);
84
84
  }
85
85
  }
86
+ /** Includes successful empty catalogs; stale or failed discovery remains unknown. */
87
+ hasUsablePickerSnapshot(key) {
88
+ const entry = this.cache.get(key);
89
+ return Boolean(entry && !entry.error && entry.checkedAt <= this.now() && this.now() - entry.checkedAt < 15 * 60_000);
90
+ }
86
91
  /** Picker-only stale-while-refresh view; routing still awaits fresh discovery. */
87
92
  cachedList(accountKeys) {
88
93
  return buildVisibleModelUnion(accountKeys.map((key, index) => {
89
94
  const entry = this.cache.get(key);
90
- const usable = entry && !entry.error && entry.checkedAt <= this.now() && this.now() - entry.checkedAt < 15 * 60_000;
91
- return { id: key, kind: "oauth", priority: index, enabled: true, models: usable ? entry.models ?? [] : [], visibleModels: null };
95
+ const usable = this.hasUsablePickerSnapshot(key);
96
+ return { id: key, kind: "oauth", priority: index, enabled: true, models: usable ? entry?.models ?? [] : [], visibleModels: null };
92
97
  }));
93
98
  }
94
99
  invalidate() { this.cache.clear(); }
@@ -1,4 +1,4 @@
1
- import type { WebSocketDisconnectDiagnostic } from "./responses-websocket.js";
1
+ import type { WebSocketDisconnectDiagnostic, WebSocketUpgradeRejectionDiagnostic } from "./responses-websocket.js";
2
2
  export interface AppBindPaths {
3
3
  codexHome: string;
4
4
  configPath: string;
@@ -39,6 +39,9 @@ export interface AppBindState {
39
39
  }
40
40
  export interface AppBindRouterStatus {
41
41
  lastWebsocketDisconnect?: WebSocketDisconnectDiagnostic | null;
42
+ websocketActiveConnections?: number | null;
43
+ websocketCapacityReclaims?: number | null;
44
+ lastWebsocketUpgradeRejection?: WebSocketUpgradeRejectionDiagnostic | null;
42
45
  schedulingStrategy?: "hybrid" | "sequential";
43
46
  state: string | null;
44
47
  pid: number | null;
@@ -232,6 +232,29 @@ function readWebsocketDisconnect(value) {
232
232
  if (!isRecord(value) || typeof value.at !== "number" || !Number.isFinite(value.at) || value.at < 0)
233
233
  return null;
234
234
  const { cause, stage, closeCode, routeKind, connectionAgeMs, eventCount } = value;
235
+ const transport = {};
236
+ for (const key of ["requestBytes", "inputItemCount"]) {
237
+ const field = value[key];
238
+ if (field !== undefined) {
239
+ if (typeof field !== "number" || !Number.isSafeInteger(field) || field < 0)
240
+ return null;
241
+ transport[key] = field;
242
+ }
243
+ }
244
+ for (const key of ["isPrewarm", "hasPreviousResponse", "reusedConnection", "sendCompleted", "compressionNegotiated", "tcpEndReceived", "tcpCloseHadError"]) {
245
+ const field = value[key];
246
+ if (field !== undefined) {
247
+ if (typeof field !== "boolean")
248
+ return null;
249
+ transport[key] = field;
250
+ }
251
+ }
252
+ const errorCode = value.transportErrorCode;
253
+ if (errorCode !== undefined) {
254
+ if (errorCode !== "ECONNRESET" && errorCode !== "EPIPE" && errorCode !== "ETIMEDOUT" && errorCode !== "OTHER")
255
+ return null;
256
+ transport.transportErrorCode = errorCode;
257
+ }
235
258
  if (routeKind !== undefined && routeKind !== "oauth" && routeKind !== "api" && routeKind !== "zdr")
236
259
  return null;
237
260
  for (const count of [connectionAgeMs, eventCount])
@@ -246,7 +269,24 @@ function readWebsocketDisconnect(value) {
246
269
  return { at: value.at, cause, stage, ...(typeof closeCode === "number" ? { closeCode } : {}),
247
270
  ...(routeKind === "oauth" || routeKind === "api" || routeKind === "zdr" ? { routeKind } : {}),
248
271
  ...(typeof connectionAgeMs === "number" ? { connectionAgeMs } : {}),
249
- ...(typeof eventCount === "number" ? { eventCount } : {}) };
272
+ ...(typeof eventCount === "number" ? { eventCount } : {}), ...transport };
273
+ }
274
+ function readWebsocketCounter(value, maximum = Number.MAX_SAFE_INTEGER) {
275
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 && value <= maximum
276
+ ? value : null;
277
+ }
278
+ function readWebsocketUpgradeRejection(value) {
279
+ if (!isRecord(value))
280
+ return null;
281
+ const at = readWebsocketCounter(value.at, 8.64e15);
282
+ const activeConnections = readWebsocketCounter(value.activeConnections, 64);
283
+ const { reason, status } = value;
284
+ if (at === null || activeConnections === null)
285
+ return null;
286
+ if (!((reason === "origin" && status === 403) ||
287
+ ((reason === "capacity" || reason === "repeated_disconnect") && status === 426)))
288
+ return null;
289
+ return { at, reason, status, activeConnections };
250
290
  }
251
291
  async function readRouterStatus(path) {
252
292
  const record = await readJsonFile(path);
@@ -254,6 +294,9 @@ async function readRouterStatus(path) {
254
294
  return null;
255
295
  return {
256
296
  lastWebsocketDisconnect: readWebsocketDisconnect(record.lastWebsocketDisconnect),
297
+ websocketActiveConnections: readWebsocketCounter(record.websocketActiveConnections, 64),
298
+ websocketCapacityReclaims: readWebsocketCounter(record.websocketCapacityReclaims),
299
+ lastWebsocketUpgradeRejection: readWebsocketUpgradeRejection(record.lastWebsocketUpgradeRejection),
257
300
  schedulingStrategy: record.schedulingStrategy === "hybrid" || record.schedulingStrategy === "sequential"
258
301
  ? record.schedulingStrategy : undefined,
259
302
  state: readString(record, "state"),
@@ -1407,7 +1450,22 @@ export function formatWebsocketDisconnect(value) {
1407
1450
  const diagnostic = readWebsocketDisconnect(value);
1408
1451
  if (!diagnostic || diagnostic.at > 8.64e15)
1409
1452
  return null;
1410
- return `Last WebSocket disconnect: ${new Date(diagnostic.at).toISOString()}; cause=${diagnostic.cause}; stage=${diagnostic.stage}${diagnostic.closeCode !== undefined ? `; closeCode=${diagnostic.closeCode}` : ""}${diagnostic.routeKind !== undefined ? `; route=${diagnostic.routeKind}` : ""}${diagnostic.connectionAgeMs !== undefined ? `; connectionAgeMs=${diagnostic.connectionAgeMs}` : ""}${diagnostic.eventCount !== undefined ? `; events=${diagnostic.eventCount}` : ""}`;
1453
+ const transport = ["requestBytes", "inputItemCount", "isPrewarm", "hasPreviousResponse", "reusedConnection", "sendCompleted", "compressionNegotiated", "transportErrorCode", "tcpEndReceived", "tcpCloseHadError"]
1454
+ .filter(key => diagnostic[key] !== undefined).map(key => `; ${key}=${diagnostic[key]}`).join("");
1455
+ return `Last WebSocket disconnect: ${new Date(diagnostic.at).toISOString()}; cause=${diagnostic.cause}; stage=${diagnostic.stage}${diagnostic.closeCode !== undefined ? `; closeCode=${diagnostic.closeCode}` : ""}${diagnostic.routeKind !== undefined ? `; route=${diagnostic.routeKind}` : ""}${diagnostic.connectionAgeMs !== undefined ? `; connectionAgeMs=${diagnostic.connectionAgeMs}` : ""}${diagnostic.eventCount !== undefined ? `; events=${diagnostic.eventCount}` : ""}${transport}`;
1456
+ }
1457
+ function formatWebsocketAdmission(status) {
1458
+ const active = readWebsocketCounter(status?.websocketActiveConnections, 64);
1459
+ const reclaimed = readWebsocketCounter(status?.websocketCapacityReclaims);
1460
+ const counters = [
1461
+ ...(active !== null ? [`active=${active}`] : []),
1462
+ ...(reclaimed !== null ? [`idleReclaimed=${reclaimed}`] : []),
1463
+ ];
1464
+ const rejection = readWebsocketUpgradeRejection(status?.lastWebsocketUpgradeRejection);
1465
+ return [
1466
+ ...(counters.length ? [`WebSocket connections: ${counters.join("; ")}`] : []),
1467
+ ...(rejection ? [`Last WebSocket upgrade rejection: ${new Date(rejection.at).toISOString()}; reason=${rejection.reason}; status=${rejection.status}; active=${rejection.activeConnections}`] : []),
1468
+ ];
1411
1469
  }
1412
1470
  export function formatAppBindStatus(status) {
1413
1471
  if (status.unmanagedBind && !status.state) {
@@ -1434,11 +1492,13 @@ export function formatAppBindStatus(status) {
1434
1492
  parts.push(`lastAccount=Account ${status.router.lastAccountIndex + 1}`);
1435
1493
  }
1436
1494
  const diagnostic = formatWebsocketDisconnect(status.router?.lastWebsocketDisconnect);
1495
+ const admission = formatWebsocketAdmission(status.router);
1437
1496
  if (status.state.nativeOpenai)
1438
- return [`Codex app bind: ${parts.join(", ")}, native OpenAI, live account catalogs`, ...(diagnostic ? [diagnostic] : [])].join("\n");
1497
+ return [`Codex app bind: ${parts.join(", ")}, native OpenAI, live account catalogs`, ...(diagnostic ? [diagnostic] : []), ...admission].join("\n");
1439
1498
  return [
1440
1499
  `Codex app bind: ${parts.join(", ")}`,
1441
1500
  ...(diagnostic ? [diagnostic] : []),
1501
+ ...admission,
1442
1502
  [
1443
1503
  "Note: Codex Desktop may hide history while the app bind selects the",
1444
1504
  "codex-multi-auth-runtime-proxy provider; use `codex-multi-auth rotation",
@@ -9,12 +9,33 @@ export interface WebSocketDisconnectDiagnostic {
9
9
  routeKind?: "oauth" | "api" | "zdr";
10
10
  connectionAgeMs?: number;
11
11
  eventCount?: number;
12
+ requestBytes?: number;
13
+ inputItemCount?: number;
14
+ isPrewarm?: boolean;
15
+ hasPreviousResponse?: boolean;
16
+ reusedConnection?: boolean;
17
+ /** Local send callback success, not evidence of upstream receipt. */
18
+ sendCompleted?: boolean;
19
+ compressionNegotiated?: boolean;
20
+ transportErrorCode?: "ECONNRESET" | "EPIPE" | "ETIMEDOUT" | "OTHER";
21
+ tcpEndReceived?: boolean;
22
+ tcpCloseHadError?: boolean;
23
+ }
24
+ /** Local admission evidence only; no headers, credentials, or conversation IDs. */
25
+ export interface WebSocketUpgradeRejectionDiagnostic {
26
+ at: number;
27
+ reason: "origin" | "capacity" | "repeated_disconnect";
28
+ status: number;
29
+ activeConnections: number;
12
30
  }
13
31
  interface WebSocketStats {
14
32
  connections: number;
33
+ activeConnections: number;
34
+ capacityReclaims: number;
15
35
  upstreamConnections: number;
16
36
  upstreamRequests: number;
17
37
  lastDisconnect?: WebSocketDisconnectDiagnostic;
38
+ lastUpgradeRejection?: WebSocketUpgradeRejectionDiagnostic;
18
39
  }
19
40
  /** Adapts only the transport; authenticated HTTP routing remains the policy authority. */
20
41
  export declare class ResponsesWebSocketGateway {
@@ -24,6 +45,9 @@ export declare class ResponsesWebSocketGateway {
24
45
  private readonly contexts;
25
46
  private readonly scope;
26
47
  private readonly sessions;
48
+ /** Credential-hashed, bounded API handshake backoff survives local reconnects. */
49
+ private readonly apiHandshakeBackoff;
50
+ private readonly websocketFailures;
27
51
  private readonly wss;
28
52
  private readonly maxBytes;
29
53
  /** Loopback hops must never go through an environment HTTP proxy (NODE_USE_ENV_PROXY). */
@@ -35,6 +59,12 @@ export declare class ResponsesWebSocketGateway {
35
59
  routeModel?: (model: string, headers: Headers) => string;
36
60
  });
37
61
  readonly fetch: typeof fetch;
62
+ private rememberDisconnect;
63
+ private needsHttpFallback;
64
+ /** One writer for the upgrade-rejection diagnostic so the two reject paths cannot drift. */
65
+ private recordUpgradeRejection;
66
+ /** Release idle setup sockets only; active work and generated history stay pinned. */
67
+ private reclaimIdleSession;
38
68
  handle(req: IncomingMessage, handler: () => Promise<void>): void;
39
69
  attach(server: Server, baseUrl: string): void;
40
70
  private serve;