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.
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +4 -2
- package/config/schema/config.schema.json +8 -0
- package/dist/lib/api-route-store.d.ts +1 -0
- package/dist/lib/api-route-store.js +2 -1
- package/dist/lib/auth/account-access.js +2 -1
- package/dist/lib/auth/auth.js +3 -2
- package/dist/lib/auth/device-auth.js +2 -1
- package/dist/lib/auth/http.d.ts +1 -0
- package/dist/lib/auth/http.js +1 -0
- package/dist/lib/codex-manager/account-manager-commands.js +1 -0
- package/dist/lib/codex-manager/commands/locale.d.ts +12 -0
- package/dist/lib/codex-manager/commands/locale.js +25 -0
- package/dist/lib/codex-manager/commands/status.d.ts +4 -0
- package/dist/lib/codex-manager/commands/status.js +476 -5
- package/dist/lib/codex-manager/help.js +3 -1
- package/dist/lib/codex-manager.js +98 -29
- package/dist/lib/config.js +3 -0
- package/dist/lib/locale.d.ts +7 -0
- package/dist/lib/locale.js +35 -0
- package/dist/lib/quota-probe.js +2 -1
- package/dist/lib/request/proxy-fetch.d.ts +5 -0
- package/dist/lib/request/proxy-fetch.js +37 -0
- package/dist/lib/request/url-rewriting.js +17 -0
- package/dist/lib/runtime/account-model-catalog.d.ts +2 -0
- package/dist/lib/runtime/account-model-catalog.js +7 -2
- package/dist/lib/runtime/app-bind.d.ts +4 -1
- package/dist/lib/runtime/app-bind.js +63 -3
- package/dist/lib/runtime/responses-websocket.d.ts +30 -0
- package/dist/lib/runtime/responses-websocket.js +243 -29
- package/dist/lib/runtime/rotation-account-selection.d.ts +5 -4
- package/dist/lib/runtime/rotation-server-types.d.ts +4 -1
- package/dist/lib/runtime-rotation-proxy.js +30 -9
- package/dist/lib/schemas.d.ts +5 -0
- package/dist/lib/schemas.js +1 -0
- package/dist/lib/web-dashboard-html.d.ts +1 -0
- package/dist/lib/web-dashboard-html.js +66 -0
- package/dist/lib/web-dashboard.d.ts +36 -0
- package/dist/lib/web-dashboard.js +322 -0
- package/package.json +3 -1
- package/scripts/codex-app-router.js +45 -1
- package/scripts/codex-routing.js +1 -0
- 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
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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 =
|
|
519
|
-
? ["auth", ...
|
|
520
|
-
: [...
|
|
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
|
-
|
|
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;
|
package/dist/lib/config.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/lib/quota-probe.js
CHANGED
|
@@ -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
|
|
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 =
|
|
91
|
-
return { id: key, kind: "oauth", priority: index, enabled: true, models: usable ? entry
|
|
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
|
-
|
|
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;
|