codex-multi-auth 2.1.13-beta.2 → 2.2.1
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 +9 -0
- package/README.md +5 -4
- package/dist/lib/accounts.d.ts.map +1 -1
- package/dist/lib/accounts.js +30 -1
- package/dist/lib/accounts.js.map +1 -1
- package/dist/lib/auth/device-auth.d.ts.map +1 -1
- package/dist/lib/auth/device-auth.js +15 -0
- package/dist/lib/auth/device-auth.js.map +1 -1
- package/dist/lib/auth/org-override.d.ts +19 -0
- package/dist/lib/auth/org-override.d.ts.map +1 -0
- package/dist/lib/auth/org-override.js +23 -0
- package/dist/lib/auth/org-override.js.map +1 -0
- package/dist/lib/auth/server.d.ts.map +1 -1
- package/dist/lib/auth/server.js +12 -7
- package/dist/lib/auth/server.js.map +1 -1
- package/dist/lib/capability-policy.d.ts.map +1 -1
- package/dist/lib/capability-policy.js +5 -0
- package/dist/lib/capability-policy.js.map +1 -1
- package/dist/lib/circuit-breaker.d.ts +6 -0
- package/dist/lib/circuit-breaker.d.ts.map +1 -1
- package/dist/lib/circuit-breaker.js +8 -0
- package/dist/lib/circuit-breaker.js.map +1 -1
- package/dist/lib/codex-cli/state.d.ts.map +1 -1
- package/dist/lib/codex-cli/state.js +28 -7
- package/dist/lib/codex-cli/state.js.map +1 -1
- package/dist/lib/codex-manager/account-manager-commands.d.ts +14 -0
- package/dist/lib/codex-manager/account-manager-commands.d.ts.map +1 -0
- package/dist/lib/codex-manager/account-manager-commands.js +43 -0
- package/dist/lib/codex-manager/account-manager-commands.js.map +1 -0
- package/dist/lib/codex-manager/commands/best.d.ts +1 -1
- package/dist/lib/codex-manager/commands/best.d.ts.map +1 -1
- package/dist/lib/codex-manager/commands/best.js +4 -3
- package/dist/lib/codex-manager/commands/best.js.map +1 -1
- package/dist/lib/codex-manager/commands/budget.d.ts.map +1 -1
- package/dist/lib/codex-manager/commands/budget.js +6 -0
- package/dist/lib/codex-manager/commands/budget.js.map +1 -1
- package/dist/lib/codex-manager/commands/debug-bundle.d.ts +16 -0
- package/dist/lib/codex-manager/commands/debug-bundle.d.ts.map +1 -1
- package/dist/lib/codex-manager/commands/debug-bundle.js +80 -5
- package/dist/lib/codex-manager/commands/debug-bundle.js.map +1 -1
- package/dist/lib/codex-manager/commands/forecast.d.ts +1 -1
- package/dist/lib/codex-manager/commands/forecast.d.ts.map +1 -1
- package/dist/lib/codex-manager/commands/forecast.js +11 -8
- package/dist/lib/codex-manager/commands/forecast.js.map +1 -1
- package/dist/lib/codex-manager/commands/integrations.js +2 -2
- package/dist/lib/codex-manager/commands/integrations.js.map +1 -1
- package/dist/lib/codex-manager/commands/models.js +3 -3
- package/dist/lib/codex-manager/commands/models.js.map +1 -1
- package/dist/lib/codex-manager/commands/report.d.ts.map +1 -1
- package/dist/lib/codex-manager/commands/report.js +11 -9
- package/dist/lib/codex-manager/commands/report.js.map +1 -1
- package/dist/lib/codex-manager/commands/status.d.ts +2 -0
- package/dist/lib/codex-manager/commands/status.d.ts.map +1 -1
- package/dist/lib/codex-manager/commands/status.js +84 -25
- package/dist/lib/codex-manager/commands/status.js.map +1 -1
- package/dist/lib/codex-manager/commands/switch.d.ts.map +1 -1
- package/dist/lib/codex-manager/commands/switch.js +7 -0
- package/dist/lib/codex-manager/commands/switch.js.map +1 -1
- package/dist/lib/codex-manager/commands/workspace.d.ts.map +1 -1
- package/dist/lib/codex-manager/commands/workspace.js +12 -1
- package/dist/lib/codex-manager/commands/workspace.js.map +1 -1
- package/dist/lib/codex-manager/experimental-settings-prompt.d.ts.map +1 -1
- package/dist/lib/codex-manager/experimental-settings-prompt.js +13 -3
- package/dist/lib/codex-manager/experimental-settings-prompt.js.map +1 -1
- package/dist/lib/codex-manager/forecast-report-commands.d.ts.map +1 -1
- package/dist/lib/codex-manager/forecast-report-commands.js +14 -13
- package/dist/lib/codex-manager/forecast-report-commands.js.map +1 -1
- package/dist/lib/codex-manager/help.d.ts.map +1 -1
- package/dist/lib/codex-manager/help.js +7 -5
- package/dist/lib/codex-manager/help.js.map +1 -1
- package/dist/lib/codex-manager/repair-commands.d.ts.map +1 -1
- package/dist/lib/codex-manager/repair-commands.js +18 -8
- package/dist/lib/codex-manager/repair-commands.js.map +1 -1
- package/dist/lib/codex-manager/settings-hub/experimental.d.ts.map +1 -1
- package/dist/lib/codex-manager/settings-hub/experimental.js +8 -0
- package/dist/lib/codex-manager/settings-hub/experimental.js.map +1 -1
- package/dist/lib/codex-manager.d.ts +3 -0
- package/dist/lib/codex-manager.d.ts.map +1 -1
- package/dist/lib/codex-manager.js +121 -93
- package/dist/lib/codex-manager.js.map +1 -1
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +327 -18
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/context-overflow.d.ts +7 -2
- package/dist/lib/context-overflow.d.ts.map +1 -1
- package/dist/lib/context-overflow.js +55 -37
- package/dist/lib/context-overflow.js.map +1 -1
- package/dist/lib/errors.d.ts +16 -0
- package/dist/lib/errors.d.ts.map +1 -1
- package/dist/lib/errors.js +23 -0
- package/dist/lib/errors.js.map +1 -1
- package/dist/lib/forecast.d.ts +1 -0
- package/dist/lib/forecast.d.ts.map +1 -1
- package/dist/lib/forecast.js +25 -2
- package/dist/lib/forecast.js.map +1 -1
- package/dist/lib/integration-generators.d.ts.map +1 -1
- package/dist/lib/integration-generators.js +1 -1
- package/dist/lib/integration-generators.js.map +1 -1
- package/dist/lib/local-bridge.d.ts +8 -0
- package/dist/lib/local-bridge.d.ts.map +1 -1
- package/dist/lib/local-bridge.js +81 -7
- package/dist/lib/local-bridge.js.map +1 -1
- package/dist/lib/local-client-tokens.d.ts.map +1 -1
- package/dist/lib/local-client-tokens.js +117 -64
- package/dist/lib/local-client-tokens.js.map +1 -1
- package/dist/lib/logger.d.ts +9 -1
- package/dist/lib/logger.d.ts.map +1 -1
- package/dist/lib/logger.js +104 -19
- package/dist/lib/logger.js.map +1 -1
- package/dist/lib/model-capability-matrix.d.ts.map +1 -1
- package/dist/lib/model-capability-matrix.js +13 -10
- package/dist/lib/model-capability-matrix.js.map +1 -1
- package/dist/lib/oc-chatgpt-orchestrator.d.ts +7 -1
- package/dist/lib/oc-chatgpt-orchestrator.d.ts.map +1 -1
- package/dist/lib/oc-chatgpt-orchestrator.js +68 -9
- package/dist/lib/oc-chatgpt-orchestrator.js.map +1 -1
- package/dist/lib/policy/runtime-policy.d.ts.map +1 -1
- package/dist/lib/policy/runtime-policy.js +23 -1
- package/dist/lib/policy/runtime-policy.js.map +1 -1
- package/dist/lib/prompts/codex.d.ts +1 -1
- package/dist/lib/prompts/codex.d.ts.map +1 -1
- package/dist/lib/prompts/codex.js +128 -33
- package/dist/lib/prompts/codex.js.map +1 -1
- package/dist/lib/prompts/fetch-utils.d.ts +72 -0
- package/dist/lib/prompts/fetch-utils.d.ts.map +1 -0
- package/dist/lib/prompts/fetch-utils.js +165 -0
- package/dist/lib/prompts/fetch-utils.js.map +1 -0
- package/dist/lib/prompts/host-codex-prompt.d.ts.map +1 -1
- package/dist/lib/prompts/host-codex-prompt.js +24 -10
- package/dist/lib/prompts/host-codex-prompt.js.map +1 -1
- package/dist/lib/quota-cache.d.ts.map +1 -1
- package/dist/lib/quota-cache.js +26 -2
- package/dist/lib/quota-cache.js.map +1 -1
- package/dist/lib/quota-probe.d.ts +21 -0
- package/dist/lib/quota-probe.d.ts.map +1 -1
- package/dist/lib/quota-probe.js +41 -1
- package/dist/lib/quota-probe.js.map +1 -1
- package/dist/lib/quota-readiness.d.ts +3 -1
- package/dist/lib/quota-readiness.d.ts.map +1 -1
- package/dist/lib/quota-readiness.js +16 -3
- package/dist/lib/quota-readiness.js.map +1 -1
- package/dist/lib/recovery/storage.d.ts +7 -0
- package/dist/lib/recovery/storage.d.ts.map +1 -1
- package/dist/lib/recovery/storage.js +178 -10
- package/dist/lib/recovery/storage.js.map +1 -1
- package/dist/lib/refresh-lease.d.ts +1 -1
- package/dist/lib/refresh-lease.d.ts.map +1 -1
- package/dist/lib/refresh-lease.js +25 -3
- package/dist/lib/refresh-lease.js.map +1 -1
- package/dist/lib/refresh-queue.d.ts.map +1 -1
- package/dist/lib/refresh-queue.js +31 -17
- package/dist/lib/refresh-queue.js.map +1 -1
- package/dist/lib/request/fetch-helpers.d.ts +0 -5
- package/dist/lib/request/fetch-helpers.d.ts.map +1 -1
- package/dist/lib/request/fetch-helpers.js +39 -7
- package/dist/lib/request/fetch-helpers.js.map +1 -1
- package/dist/lib/request/helpers/model-map.d.ts +1 -0
- package/dist/lib/request/helpers/model-map.d.ts.map +1 -1
- package/dist/lib/request/helpers/model-map.js +10 -0
- package/dist/lib/request/helpers/model-map.js.map +1 -1
- package/dist/lib/rotation.d.ts +12 -0
- package/dist/lib/rotation.d.ts.map +1 -1
- package/dist/lib/rotation.js +36 -0
- package/dist/lib/rotation.js.map +1 -1
- package/dist/lib/routing-mutex.d.ts +12 -0
- package/dist/lib/routing-mutex.d.ts.map +1 -1
- package/dist/lib/routing-mutex.js +38 -1
- package/dist/lib/routing-mutex.js.map +1 -1
- package/dist/lib/runtime/account-check-types.d.ts +1 -0
- package/dist/lib/runtime/account-check-types.d.ts.map +1 -1
- package/dist/lib/runtime/account-check-types.js +1 -0
- package/dist/lib/runtime/account-check-types.js.map +1 -1
- package/dist/lib/runtime/account-check.d.ts.map +1 -1
- package/dist/lib/runtime/account-check.js +15 -4
- package/dist/lib/runtime/account-check.js.map +1 -1
- package/dist/lib/runtime/quota-probe.d.ts.map +1 -1
- package/dist/lib/runtime/quota-probe.js +2 -5
- package/dist/lib/runtime/quota-probe.js.map +1 -1
- package/dist/lib/runtime/runtime-observability.d.ts.map +1 -1
- package/dist/lib/runtime/runtime-observability.js +24 -2
- package/dist/lib/runtime/runtime-observability.js.map +1 -1
- package/dist/lib/runtime-rotation-proxy.d.ts +24 -1
- package/dist/lib/runtime-rotation-proxy.d.ts.map +1 -1
- package/dist/lib/runtime-rotation-proxy.js +289 -26
- package/dist/lib/runtime-rotation-proxy.js.map +1 -1
- package/dist/lib/storage/account-clear.d.ts.map +1 -1
- package/dist/lib/storage/account-clear.js +4 -2
- package/dist/lib/storage/account-clear.js.map +1 -1
- package/dist/lib/storage/account-persistence.d.ts.map +1 -1
- package/dist/lib/storage/account-persistence.js +11 -1
- package/dist/lib/storage/account-persistence.js.map +1 -1
- package/dist/lib/storage/flagged-storage-io.d.ts.map +1 -1
- package/dist/lib/storage/flagged-storage-io.js +4 -1
- package/dist/lib/storage/flagged-storage-io.js.map +1 -1
- package/dist/lib/storage/import-export.d.ts.map +1 -1
- package/dist/lib/storage/import-export.js +29 -9
- package/dist/lib/storage/import-export.js.map +1 -1
- package/dist/lib/storage/paths.d.ts.map +1 -1
- package/dist/lib/storage/paths.js +93 -0
- package/dist/lib/storage/paths.js.map +1 -1
- package/dist/lib/storage/record-utils.d.ts.map +1 -1
- package/dist/lib/storage/record-utils.js +6 -1
- package/dist/lib/storage/record-utils.js.map +1 -1
- package/dist/lib/storage/storage-parser.d.ts.map +1 -1
- package/dist/lib/storage/storage-parser.js +7 -1
- package/dist/lib/storage/storage-parser.js.map +1 -1
- package/dist/lib/storage/transactions.d.ts +8 -0
- package/dist/lib/storage/transactions.d.ts.map +1 -1
- package/dist/lib/storage/transactions.js +14 -1
- package/dist/lib/storage/transactions.js.map +1 -1
- package/dist/lib/storage.d.ts.map +1 -1
- package/dist/lib/storage.js +59 -18
- package/dist/lib/storage.js.map +1 -1
- package/dist/lib/table-formatter.d.ts.map +1 -1
- package/dist/lib/table-formatter.js +20 -2
- package/dist/lib/table-formatter.js.map +1 -1
- package/dist/lib/types.d.ts +7 -0
- package/dist/lib/types.d.ts.map +1 -1
- package/dist/lib/ui/auth-menu.d.ts.map +1 -1
- package/dist/lib/ui/auth-menu.js +8 -2
- package/dist/lib/ui/auth-menu.js.map +1 -1
- package/dist/lib/ui/display-width.d.ts +27 -0
- package/dist/lib/ui/display-width.d.ts.map +1 -0
- package/dist/lib/ui/display-width.js +194 -0
- package/dist/lib/ui/display-width.js.map +1 -0
- package/dist/lib/ui/select.d.ts +23 -0
- package/dist/lib/ui/select.d.ts.map +1 -1
- package/dist/lib/ui/select.js +64 -16
- package/dist/lib/ui/select.js.map +1 -1
- package/dist/lib/ui/theme.d.ts +13 -0
- package/dist/lib/ui/theme.d.ts.map +1 -1
- package/dist/lib/ui/theme.js +34 -1
- package/dist/lib/ui/theme.js.map +1 -1
- package/node_modules/@codex-ai/sdk/dist/index.d.ts +4 -0
- package/node_modules/@codex-ai/sdk/dist/index.js +2 -0
- package/node_modules/@codex-ai/sdk/package.json +8 -0
- package/package.json +10 -6
- package/scripts/codex-bin-resolver.js +28 -1
- package/scripts/codex-routing.js +3 -0
- package/scripts/codex.js +508 -1
- package/scripts/mcodex.js +367 -0
|
@@ -1,18 +1,58 @@
|
|
|
1
|
-
import { createHash, timingSafeEqual } from "node:crypto";
|
|
2
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
1
|
+
import { createHash, randomUUID, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
3
3
|
import { createServer } from "node:http";
|
|
4
4
|
import { AccountManager, extractAccountId, } from "./accounts.js";
|
|
5
|
+
import { withRoutingMutex } from "./routing-mutex.js";
|
|
5
6
|
import { getStoragePath } from "./storage.js";
|
|
6
|
-
import { getFetchTimeoutMs, getNetworkErrorCooldownMs, getRetryAllAccountsMaxRetries, getServerErrorCooldownMs, getSessionAffinity, getSessionAffinityMaxEntries, getSessionAffinityTtlMs, getStreamStallTimeoutMs, getMinRotationIntervalMs, getTokenInvalidationCooldownMs, getTokenRefreshSkewMs, loadPluginConfig, } from "./config.js";
|
|
7
|
+
import { getFetchTimeoutMs, getNetworkErrorCooldownMs, getRetryAllAccountsMaxRetries, getServerErrorCooldownMs, getSessionAffinity, getSessionAffinityMaxEntries, getSessionAffinityTtlMs, getStreamStallTimeoutMs, getMinRotationIntervalMs, getTokenInvalidationCooldownMs, getTokenRefreshSkewMs, getPidOffsetEnabled, getRoutingMutexMode, loadPluginConfig, } from "./config.js";
|
|
7
8
|
import { CODEX_BASE_URL, HTTP_STATUS, OPENAI_HEADERS, OPENAI_HEADER_VALUES, URL_PATHS, } from "./constants.js";
|
|
8
9
|
import { getModelFamily } from "./prompts/codex.js";
|
|
10
|
+
import { CURRENT_CODEX_MODEL } from "./request/helpers/model-map.js";
|
|
9
11
|
import { queuedRefresh } from "./refresh-queue.js";
|
|
10
12
|
import { mutateRuntimeObservabilitySnapshot, recordRuntimePoolExhaustion, recordRuntimeReload, recordRuntimeReset, } from "./runtime/runtime-observability.js";
|
|
11
13
|
import { createRuntimeUsageRecorder, evaluateRuntimePolicy, loadRuntimePolicyState, } from "./policy/runtime-policy.js";
|
|
12
14
|
import { isWorkspaceDisabledError } from "./request/fetch-helpers.js";
|
|
15
|
+
import { createLogger, maskString, runWithCorrelationId } from "./logger.js";
|
|
13
16
|
import { SessionAffinityStore } from "./session-affinity.js";
|
|
14
17
|
import { isRecord } from "./utils.js";
|
|
15
18
|
const DEFAULT_HOST = "127.0.0.1";
|
|
19
|
+
function isLoopbackHost(host) {
|
|
20
|
+
const normalized = host.trim().toLowerCase();
|
|
21
|
+
return (normalized === "127.0.0.1" ||
|
|
22
|
+
normalized === "localhost" ||
|
|
23
|
+
normalized === "::1" ||
|
|
24
|
+
normalized === "[::1]");
|
|
25
|
+
}
|
|
26
|
+
// IPv6 literals must be presented in two distinct forms and the proxy
|
|
27
|
+
// previously conflated them (runtime-proxy IPv6 bug). Node's
|
|
28
|
+
// net.Server.listen(port, host) requires the RAW literal ("::1"); a bracketed
|
|
29
|
+
// literal ("[::1]") makes the bind fail or behave wrong. Conversely a URL
|
|
30
|
+
// authority requires the BRACKETED literal ("[::1]") so "http://[::1]:port"
|
|
31
|
+
// parses unambiguously — the raw form yields the unparseable "http://::1:port".
|
|
32
|
+
// Normalize each form ONCE at startup so concurrent rotation paths never race
|
|
33
|
+
// on inconsistent host string representations.
|
|
34
|
+
function stripIpv6Brackets(host) {
|
|
35
|
+
const trimmed = host.trim();
|
|
36
|
+
if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
|
|
37
|
+
return trimmed.slice(1, -1);
|
|
38
|
+
}
|
|
39
|
+
return trimmed;
|
|
40
|
+
}
|
|
41
|
+
// Raw literal suitable for server.listen: "[::1]" -> "::1", others unchanged.
|
|
42
|
+
function toBindHost(host) {
|
|
43
|
+
return stripIpv6Brackets(host);
|
|
44
|
+
}
|
|
45
|
+
// URL authority host: IPv6 literals are bracketed ("::1" -> "[::1]") while
|
|
46
|
+
// IPv4 addresses and hostnames (no embedded colon) pass through unchanged.
|
|
47
|
+
function toUrlHost(host) {
|
|
48
|
+
const bare = stripIpv6Brackets(host);
|
|
49
|
+
return bare.includes(":") ? `[${bare}]` : bare;
|
|
50
|
+
}
|
|
51
|
+
// Structured logger for the default-on runtime proxy (errors-logging-01,
|
|
52
|
+
// runtime-proxy-04). Previously the 1900-LOC proxy had zero logger integration;
|
|
53
|
+
// failures surfaced only as a last-write-wins status.lastError string. Logs are
|
|
54
|
+
// level-gated and carry the per-request correlation id set in handleRequest.
|
|
55
|
+
const proxyLog = createLogger("runtime-proxy");
|
|
16
56
|
const DEFAULT_QUOTA_REMAINING_THRESHOLD = 10;
|
|
17
57
|
const DEFAULT_AUTH_FAILURE_COOLDOWN_MS = 30_000;
|
|
18
58
|
const DEFAULT_MAX_RUNTIME_ACCOUNT_ATTEMPTS = 4;
|
|
@@ -105,6 +145,10 @@ function createOutboundHeaders(incoming, account, accessToken, accountId) {
|
|
|
105
145
|
}
|
|
106
146
|
headers.delete("host");
|
|
107
147
|
headers.delete("x-api-key");
|
|
148
|
+
// Never forward inbound client credentials upstream: a Cookie / proxy-auth
|
|
149
|
+
// header would ride along with the managed OAuth Bearer to OpenAI.
|
|
150
|
+
headers.delete("cookie");
|
|
151
|
+
headers.delete("proxy-authorization");
|
|
108
152
|
headers.set("authorization", `Bearer ${accessToken}`);
|
|
109
153
|
headers.set(OPENAI_HEADERS.ACCOUNT_ID, accountId);
|
|
110
154
|
headers.set(OPENAI_HEADERS.BETA, OPENAI_HEADER_VALUES.BETA_RESPONSES);
|
|
@@ -161,6 +205,17 @@ function recordLastRuntimeAccount(status, identity) {
|
|
|
161
205
|
// vitest workers (each pointing at their own temp storage file) cannot
|
|
162
206
|
// corrupt each other's snapshots. See issue #474.
|
|
163
207
|
const STORAGE_META_CACHE = new Map();
|
|
208
|
+
// The mtime+size short-circuit (L3) may only be trusted once the cached
|
|
209
|
+
// mtime is far enough in the past that no *subsequent* write could share the
|
|
210
|
+
// same coarse mtime tick. Filesystems report mtime at wildly different
|
|
211
|
+
// granularities (ext4 ns, FAT 2s, some network/Windows volumes ~1s, and CI
|
|
212
|
+
// containers occasionally coarser), and our writers use atomic rename, so two
|
|
213
|
+
// rapid CLI bumps can land on an identical mtimeMs. Within this settle window
|
|
214
|
+
// we therefore ignore mtime equality and fall back to the read + sha1 path
|
|
215
|
+
// (the real source of truth). Outside it, the file has been quiescent long
|
|
216
|
+
// enough that mtime equality provably means "unchanged", so we skip the read.
|
|
217
|
+
// 2s comfortably exceeds the coarsest mtime granularity we expect in practice.
|
|
218
|
+
const MTIME_SHORTCIRCUIT_SETTLE_MS = 2_000;
|
|
164
219
|
function hashStorageBytes(bytes) {
|
|
165
220
|
return createHash("sha1").update(bytes).digest("hex");
|
|
166
221
|
}
|
|
@@ -171,7 +226,11 @@ function metaFromSnapshot(snapshot) {
|
|
|
171
226
|
};
|
|
172
227
|
}
|
|
173
228
|
/**
|
|
174
|
-
* Cheap, hot-path-safe single read with mtime-cache short-circuit.
|
|
229
|
+
* Cheap, hot-path-safe single read with mtime-cache short-circuit. When the
|
|
230
|
+
* file's `mtimeMs` and `size` match the cached snapshot for this path we
|
|
231
|
+
* return the cached value WITHOUT reading or hashing the file. Only when
|
|
232
|
+
* mtime/size differ (or were never cached) do we `readFileSync` + sha1 and,
|
|
233
|
+
* if the content hash still matches, skip the `JSON.parse`. Transient
|
|
175
234
|
* failures (EBUSY/EPERM/EACCES/EAGAIN, partial-write SyntaxError) fall through
|
|
176
235
|
* to the last cached value for this path; defaults are only returned when the
|
|
177
236
|
* file has never been successfully read.
|
|
@@ -187,11 +246,36 @@ export function readStorageMetaFromDisk(storagePath = getStoragePath()) {
|
|
|
187
246
|
return { pinnedAccountIndex: null, affinityGeneration: 0 };
|
|
188
247
|
}
|
|
189
248
|
try {
|
|
249
|
+
// mtime+size short-circuit (L3): when neither has changed since the last
|
|
250
|
+
// successful read AND the cached mtime has settled (see
|
|
251
|
+
// MTIME_SHORTCIRCUIT_SETTLE_MS) we return the cached snapshot without
|
|
252
|
+
// reading or hashing the file. During the settle window we deliberately
|
|
253
|
+
// fall through to the read + sha1 path below, which stays the source of
|
|
254
|
+
// truth and protects against the coarse-mtime collision described on
|
|
255
|
+
// StorageMetaSnapshot. So this is a pure fast path for the common
|
|
256
|
+
// "file quiescent, proxy polling every request" case.
|
|
257
|
+
const stats = statSync(storagePath);
|
|
258
|
+
const cachedByStat = STORAGE_META_CACHE.get(storagePath);
|
|
259
|
+
if (cachedByStat &&
|
|
260
|
+
cachedByStat.mtimeMs === stats.mtimeMs &&
|
|
261
|
+
cachedByStat.size === stats.size &&
|
|
262
|
+
Date.now() - stats.mtimeMs > MTIME_SHORTCIRCUIT_SETTLE_MS) {
|
|
263
|
+
return metaFromSnapshot(cachedByStat);
|
|
264
|
+
}
|
|
190
265
|
const bytes = readFileSync(storagePath);
|
|
191
266
|
const contentHash = hashStorageBytes(bytes);
|
|
192
267
|
const cached = STORAGE_META_CACHE.get(storagePath);
|
|
193
268
|
if (cached && cached.contentHash === contentHash) {
|
|
194
|
-
|
|
269
|
+
// Content is byte-identical despite the mtime/size change (e.g. an
|
|
270
|
+
// atomic-rename rewrite of the same bytes). Refresh the stat fields so
|
|
271
|
+
// the next request takes the fast path, but skip the JSON.parse.
|
|
272
|
+
const refreshed = {
|
|
273
|
+
...cached,
|
|
274
|
+
mtimeMs: stats.mtimeMs,
|
|
275
|
+
size: stats.size,
|
|
276
|
+
};
|
|
277
|
+
STORAGE_META_CACHE.set(storagePath, refreshed);
|
|
278
|
+
return metaFromSnapshot(refreshed);
|
|
195
279
|
}
|
|
196
280
|
const parsed = JSON.parse(bytes.toString("utf8"));
|
|
197
281
|
const pinnedAccountIndex = typeof parsed.pinnedAccountIndex === "number" &&
|
|
@@ -205,6 +289,8 @@ export function readStorageMetaFromDisk(storagePath = getStoragePath()) {
|
|
|
205
289
|
? parsed.affinityGeneration
|
|
206
290
|
: 0;
|
|
207
291
|
const snapshot = {
|
|
292
|
+
mtimeMs: stats.mtimeMs,
|
|
293
|
+
size: stats.size,
|
|
208
294
|
contentHash,
|
|
209
295
|
pinnedAccountIndex,
|
|
210
296
|
affinityGeneration,
|
|
@@ -264,7 +350,26 @@ async function persistRuntimeActiveAccount(accountManager, account, family, isPi
|
|
|
264
350
|
return;
|
|
265
351
|
}
|
|
266
352
|
try {
|
|
267
|
-
|
|
353
|
+
// accounts-01/08: serialize the cursor mutation through the routing mutex
|
|
354
|
+
// (when routingMutex="enabled") because this commit spans an await
|
|
355
|
+
// (syncCodexCliActiveSelectionForIndex), which is the lost-update window the
|
|
356
|
+
// mutex exists to close. In legacy mode markSwitchedLocked runs inline, so
|
|
357
|
+
// behavior is unchanged by default.
|
|
358
|
+
//
|
|
359
|
+
// L4 fix: in "enabled" mode the SELECTION path already committed the cursor
|
|
360
|
+
// for this account inside the routing mutex (atomic select+commit, see the
|
|
361
|
+
// hot-path caller). Re-running markSwitchedLocked here — after the upstream
|
|
362
|
+
// fetch, in a *separate* critical section — would redundantly re-advance the
|
|
363
|
+
// cursor and could clobber a concurrent request's atomic advance that landed
|
|
364
|
+
// while this request was awaiting upstream. So only do the locked cursor
|
|
365
|
+
// commit in legacy mode, where selection does NOT commit under a lock and
|
|
366
|
+
// this remains the sole commit site (preserving legacy behavior exactly).
|
|
367
|
+
// `saveToDiskDebounced` + CLI sync still run in both modes: they snapshot the
|
|
368
|
+
// current in-memory state / mirror the CLI selection and do not advance the
|
|
369
|
+
// in-memory rotation cursor.
|
|
370
|
+
if (accountManager.getRoutingMutexMode() !== "enabled") {
|
|
371
|
+
await accountManager.markSwitchedLocked(account, "rotation", family);
|
|
372
|
+
}
|
|
268
373
|
accountManager.saveToDiskDebounced();
|
|
269
374
|
await accountManager.syncCodexCliActiveSelectionForIndex(account.index);
|
|
270
375
|
}
|
|
@@ -392,7 +497,11 @@ function buildResponsesRequestContext(req, body) {
|
|
|
392
497
|
method: "POST",
|
|
393
498
|
upstreamPath: URL_PATHS.CODEX_RESPONSES,
|
|
394
499
|
model,
|
|
395
|
-
|
|
500
|
+
// The /codex/responses path is codex-family. A model-less request must
|
|
501
|
+
// bucket into the codex family (rotation/cooldown/budget), so fall back to
|
|
502
|
+
// the current codex model — NOT the general DEFAULT_MODEL (gpt-5.5), whose
|
|
503
|
+
// family is gpt-5.2 and would mis-account a pass-through codex request.
|
|
504
|
+
family: getModelFamily(model ?? CURRENT_CODEX_MODEL),
|
|
396
505
|
stream: parsedBody?.stream === true,
|
|
397
506
|
sessionKey: resolveSessionKey(headers, parsedBody),
|
|
398
507
|
};
|
|
@@ -583,9 +692,57 @@ function parseRetryAfterBodyMs(bodyText, now) {
|
|
|
583
692
|
}
|
|
584
693
|
return null;
|
|
585
694
|
}
|
|
586
|
-
async function readErrorBody(response) {
|
|
695
|
+
async function readErrorBody(response, timeoutMs, maxBytes = 1024 * 1024) {
|
|
696
|
+
// The outbound fetch's abort timer is cleared once headers arrive, so a
|
|
697
|
+
// stalled error body would otherwise hang this handler forever (the success
|
|
698
|
+
// path is per-chunk stall-bounded; the error path was not). Read the body via
|
|
699
|
+
// a reader, bound it by an idle timeout AND a size cap, and cancel the stream
|
|
700
|
+
// on timeout/overflow so the socket is released.
|
|
701
|
+
const body = response.body;
|
|
702
|
+
if (!body || typeof body.getReader !== "function") {
|
|
703
|
+
// Fallback for impls without a streamable body: race text() against a timer.
|
|
704
|
+
try {
|
|
705
|
+
return await withTimeout(response.text(), timeoutMs, () => undefined, "error body stalled");
|
|
706
|
+
}
|
|
707
|
+
catch {
|
|
708
|
+
return "";
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
const reader = body.getReader();
|
|
712
|
+
const chunks = [];
|
|
713
|
+
let total = 0;
|
|
587
714
|
try {
|
|
588
|
-
|
|
715
|
+
for (;;) {
|
|
716
|
+
let idleTimer;
|
|
717
|
+
const idle = new Promise((_resolve, reject) => {
|
|
718
|
+
idleTimer = setTimeout(() => reject(new Error("error body stalled")), Math.max(1, timeoutMs));
|
|
719
|
+
});
|
|
720
|
+
let result;
|
|
721
|
+
try {
|
|
722
|
+
result = await Promise.race([reader.read(), idle]);
|
|
723
|
+
}
|
|
724
|
+
finally {
|
|
725
|
+
if (idleTimer)
|
|
726
|
+
clearTimeout(idleTimer);
|
|
727
|
+
}
|
|
728
|
+
if (result.done)
|
|
729
|
+
break;
|
|
730
|
+
if (result.value) {
|
|
731
|
+
total += result.value.byteLength;
|
|
732
|
+
if (total > maxBytes)
|
|
733
|
+
break; // cap: enough for diagnostics, no OOM
|
|
734
|
+
chunks.push(result.value);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
catch {
|
|
739
|
+
// stalled or errored — fall through with whatever we have
|
|
740
|
+
}
|
|
741
|
+
finally {
|
|
742
|
+
await reader.cancel().catch(() => undefined);
|
|
743
|
+
}
|
|
744
|
+
try {
|
|
745
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
589
746
|
}
|
|
590
747
|
catch {
|
|
591
748
|
return "";
|
|
@@ -684,8 +841,26 @@ function getQuotaNearExhaustionWaitMs(headers, remainingThreshold, now) {
|
|
|
684
841
|
}
|
|
685
842
|
return candidates.length > 0 ? Math.max(...candidates) : 0;
|
|
686
843
|
}
|
|
844
|
+
/**
|
|
845
|
+
* `chooseAccount` is a SYNC selector that internally advances the rotation
|
|
846
|
+
* cursor (the session-affinity-preferred branch and the round-robin fallback
|
|
847
|
+
* both call `accountManager.markSwitched(...)`, and the hybrid selector advances
|
|
848
|
+
* its own cursor). It does NOT acquire the routing mutex itself.
|
|
849
|
+
*
|
|
850
|
+
* Concurrency (L4): when `routingMutex === "enabled"`, the proxy hot path runs
|
|
851
|
+
* this whole call AND the subsequent `markSwitchedLocked` commit inside a single
|
|
852
|
+
* `withRoutingMutex` acquisition, so concurrent requests serialize selection +
|
|
853
|
+
* cursor advance and cannot stampede the same account. `withRoutingMutex` is
|
|
854
|
+
* reentrant (AsyncLocalStorage), so the nested `markSwitchedLocked` — and the
|
|
855
|
+
* later one in `persistRuntimeActiveAccount` — run inline without re-acquiring
|
|
856
|
+
* the non-reentrant FIFO queue (no deadlock). In legacy mode the inline
|
|
857
|
+
* `markSwitched` calls below are used unchanged and no lock is taken, so default
|
|
858
|
+
* behavior and perf are identical. See the hot-path caller in
|
|
859
|
+
* `startRuntimeRotationProxy` and the regression in
|
|
860
|
+
* `test/runtime-rotation-proxy.test.ts`.
|
|
861
|
+
*/
|
|
687
862
|
export function chooseAccount(params) {
|
|
688
|
-
const { accountManager, sessionAffinityStore, sessionKey, family, model, attemptedIndexes, now, policy, pinnedIndex, skipReasons, stickyBoostByAccount, } = params;
|
|
863
|
+
const { accountManager, sessionAffinityStore, sessionKey, family, model, attemptedIndexes, now, policy, pinnedIndex, skipReasons, stickyBoostByAccount, pidOffsetEnabled, } = params;
|
|
689
864
|
// Manual pin (from `codex-multi-auth switch <n>`) overrides every other
|
|
690
865
|
// selection signal. We do NOT call markSwitched here — the proxy must not
|
|
691
866
|
// clobber the pin set by the CLI. See issue #474.
|
|
@@ -725,6 +900,7 @@ export function chooseAccount(params) {
|
|
|
725
900
|
skipReasons?.set(preferred.index, reason);
|
|
726
901
|
}
|
|
727
902
|
else {
|
|
903
|
+
// L4 (deferred): unlocked cursor mutation — see chooseAccount header.
|
|
728
904
|
accountManager.markSwitched(preferred, "rotation", family);
|
|
729
905
|
return preferred;
|
|
730
906
|
}
|
|
@@ -735,6 +911,10 @@ export function chooseAccount(params) {
|
|
|
735
911
|
...(policy?.scoreBoostByAccount ?? {}),
|
|
736
912
|
...(stickyBoostByAccount ?? {}),
|
|
737
913
|
},
|
|
914
|
+
// accounts-05: carry the PID-offset distribution into the default-on proxy
|
|
915
|
+
// path too (index.ts already does). Without it, parallel proxy processes can
|
|
916
|
+
// stampede the same account instead of spreading across the pool.
|
|
917
|
+
pidOffsetEnabled,
|
|
738
918
|
});
|
|
739
919
|
if (selected &&
|
|
740
920
|
!attemptedIndexes.has(selected.index) &&
|
|
@@ -758,6 +938,7 @@ export function chooseAccount(params) {
|
|
|
758
938
|
const live = accountManager.getAccountByIndex(account.index);
|
|
759
939
|
if (!live)
|
|
760
940
|
continue;
|
|
941
|
+
// L4 (deferred): unlocked cursor mutation — see chooseAccount header.
|
|
761
942
|
accountManager.markSwitched(live, "rotation", family);
|
|
762
943
|
return live;
|
|
763
944
|
}
|
|
@@ -888,8 +1069,27 @@ export async function startRuntimeRotationProxy(options) {
|
|
|
888
1069
|
const pluginConfig = loadPluginConfig();
|
|
889
1070
|
let activeAccountManager = options.accountManager ?? (await AccountManager.loadFromDisk());
|
|
890
1071
|
const knownAccountManagers = new Set([activeAccountManager]);
|
|
1072
|
+
// accounts-01/08: apply the configured routing-mutex mode so the proxy's
|
|
1073
|
+
// async select->commit path (persistRuntimeActiveAccount) can serialize cursor
|
|
1074
|
+
// mutations when routingMutex="enabled". Legacy mode keeps the inline fast path.
|
|
1075
|
+
const routingMutexMode = getRoutingMutexMode(pluginConfig);
|
|
1076
|
+
activeAccountManager.setRoutingMutexMode(routingMutexMode);
|
|
891
1077
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
892
1078
|
const host = options.host ?? DEFAULT_HOST;
|
|
1079
|
+
// Defense in depth (runtime-proxy-01): the proxy presents managed OAuth tokens
|
|
1080
|
+
// and must never be reachable off-box. It is loopback-only with NO opt-out —
|
|
1081
|
+
// binding a non-loopback host would expose every managed account to the
|
|
1082
|
+
// network, so it is refused unconditionally.
|
|
1083
|
+
if (!isLoopbackHost(host)) {
|
|
1084
|
+
throw new Error(`Runtime rotation proxy refuses to bind non-loopback host "${host}". ` +
|
|
1085
|
+
"It forwards managed OAuth tokens and is loopback-only.");
|
|
1086
|
+
}
|
|
1087
|
+
// Normalize the validated host into its two representations exactly once so the
|
|
1088
|
+
// listen() bind and the emitted baseUrl can never disagree under concurrent
|
|
1089
|
+
// rotation: bindHost is the raw literal Node's listen() expects ("[::1]"->"::1"),
|
|
1090
|
+
// urlHost is the bracketed form a URL authority requires ("::1"->"[::1]").
|
|
1091
|
+
const bindHost = toBindHost(host);
|
|
1092
|
+
const urlHost = toUrlHost(host);
|
|
893
1093
|
const port = options.port ?? 0;
|
|
894
1094
|
const upstreamBaseUrl = options.upstreamBaseUrl ?? CODEX_BASE_URL;
|
|
895
1095
|
const clientApiKey = typeof options.clientApiKey === "string" &&
|
|
@@ -905,6 +1105,7 @@ export async function startRuntimeRotationProxy(options) {
|
|
|
905
1105
|
const serverErrorCooldownMs = getServerErrorCooldownMs(pluginConfig);
|
|
906
1106
|
const tokenInvalidationCooldownMs = getTokenInvalidationCooldownMs(pluginConfig);
|
|
907
1107
|
const minRotationIntervalMs = getMinRotationIntervalMs(pluginConfig);
|
|
1108
|
+
const pidOffsetEnabled = getPidOffsetEnabled(pluginConfig);
|
|
908
1109
|
let lastGlobalAccountIndex = null;
|
|
909
1110
|
let lastGlobalSwitchAt = 0;
|
|
910
1111
|
const fetchTimeoutMs = options.fetchTimeoutMs ?? getFetchTimeoutMs(pluginConfig);
|
|
@@ -953,6 +1154,7 @@ export async function startRuntimeRotationProxy(options) {
|
|
|
953
1154
|
AccountManager.resetVolatileRuntimeState();
|
|
954
1155
|
recordRuntimeReset("pool-exhausted-no-account");
|
|
955
1156
|
const reloaded = await AccountManager.loadFromDisk();
|
|
1157
|
+
reloaded.setRoutingMutexMode(routingMutexMode);
|
|
956
1158
|
activeAccountManager = reloaded;
|
|
957
1159
|
knownAccountManagers.add(reloaded);
|
|
958
1160
|
lastStaleRuntimeReloadAt = Date.now();
|
|
@@ -969,10 +1171,27 @@ export async function startRuntimeRotationProxy(options) {
|
|
|
969
1171
|
return staleRuntimeReloadPromise;
|
|
970
1172
|
};
|
|
971
1173
|
const handleRequest = async (req, res) => {
|
|
1174
|
+
// Per-request trace id (errors-logging-03): distinct from sessionKey, which
|
|
1175
|
+
// is shared across a thread's requests. Bound to this request's async context
|
|
1176
|
+
// so every proxyLog line and usage row can be correlated to one request.
|
|
1177
|
+
const traceId = randomUUID();
|
|
1178
|
+
return runWithCorrelationId(traceId, () => handleRequestInner(req, res, traceId));
|
|
1179
|
+
};
|
|
1180
|
+
const handleRequestInner = async (req, res, traceId) => {
|
|
972
1181
|
let usageRecorder = null;
|
|
973
1182
|
let accountManager = activeAccountManager;
|
|
974
1183
|
try {
|
|
975
1184
|
const incomingUrl = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
1185
|
+
// Authenticate before discriminating path/method so an unauthenticated
|
|
1186
|
+
// caller cannot enumerate which endpoints exist: an unknown caller always
|
|
1187
|
+
// gets 401, never a 404 that would confirm a path is invalid (vs. just
|
|
1188
|
+
// unauthorized). Authorized callers still fall through to the 404 below
|
|
1189
|
+
// when they hit an unsupported path/method.
|
|
1190
|
+
const incomingHeaders = headersFromIncoming(req);
|
|
1191
|
+
if (!isAuthorizedClient(incomingHeaders, clientApiKey)) {
|
|
1192
|
+
writeUnauthorized(res);
|
|
1193
|
+
return;
|
|
1194
|
+
}
|
|
976
1195
|
const isResponsesRequest = req.method === "POST" && isResponsesPath(incomingUrl.pathname);
|
|
977
1196
|
const isModelsRequest = req.method === "GET" && isModelsPath(incomingUrl.pathname);
|
|
978
1197
|
const isThreadGoalRequest = (req.method === "GET" || req.method === "POST") &&
|
|
@@ -981,11 +1200,6 @@ export async function startRuntimeRotationProxy(options) {
|
|
|
981
1200
|
writeMethodOrPathError(res);
|
|
982
1201
|
return;
|
|
983
1202
|
}
|
|
984
|
-
const incomingHeaders = headersFromIncoming(req);
|
|
985
|
-
if (!isAuthorizedClient(incomingHeaders, clientApiKey)) {
|
|
986
|
-
writeUnauthorized(res);
|
|
987
|
-
return;
|
|
988
|
-
}
|
|
989
1203
|
status.totalRequests += 1;
|
|
990
1204
|
const requestBody = isResponsesRequest || (isThreadGoalRequest && req.method === "POST")
|
|
991
1205
|
? await readRequestBody(req, maxRequestBodyBytes)
|
|
@@ -1028,7 +1242,7 @@ export async function startRuntimeRotationProxy(options) {
|
|
|
1028
1242
|
: "responses",
|
|
1029
1243
|
model: context.model,
|
|
1030
1244
|
projectKey,
|
|
1031
|
-
requestId:
|
|
1245
|
+
requestId: traceId,
|
|
1032
1246
|
startedAt: requestStartedAt,
|
|
1033
1247
|
});
|
|
1034
1248
|
if (policyError) {
|
|
@@ -1091,7 +1305,22 @@ export async function startRuntimeRotationProxy(options) {
|
|
|
1091
1305
|
now() - lastGlobalSwitchAt < minRotationIntervalMs
|
|
1092
1306
|
? { [lastGlobalAccountIndex]: 1000 }
|
|
1093
1307
|
: {};
|
|
1094
|
-
|
|
1308
|
+
// L4 fix (routing mutex): when `routingMutex === "enabled"`, run the
|
|
1309
|
+
// selection AND the cursor commit inside ONE mutex acquisition so two
|
|
1310
|
+
// concurrent requests cannot read the same cursor and stampede before
|
|
1311
|
+
// the locked commit lands. `chooseAccount` is sync and mutates the
|
|
1312
|
+
// cursor internally (session-affinity `markSwitched`, the hybrid
|
|
1313
|
+
// selector's own advance, and the round-robin fallback `markSwitched`);
|
|
1314
|
+
// holding the mutex across the whole call serializes all of those.
|
|
1315
|
+
// We then `markSwitchedLocked` the winner to (a) re-commit the cursor
|
|
1316
|
+
// under the lock across the await boundary and (b) hand
|
|
1317
|
+
// `persistRuntimeActiveAccount` a cursor that is already correct. That
|
|
1318
|
+
// later `markSwitchedLocked` runs INLINE (reentrant) within this held
|
|
1319
|
+
// section, so there is no double-acquire and no deadlock on the
|
|
1320
|
+
// non-reentrant FIFO queue. In legacy mode the inline `markSwitched`
|
|
1321
|
+
// calls inside `chooseAccount` are used unchanged and no lock is taken,
|
|
1322
|
+
// so default behavior and perf are identical to before.
|
|
1323
|
+
const selectAccount = () => chooseAccount({
|
|
1095
1324
|
accountManager,
|
|
1096
1325
|
sessionAffinityStore,
|
|
1097
1326
|
sessionKey: context.sessionKey,
|
|
@@ -1103,7 +1332,22 @@ export async function startRuntimeRotationProxy(options) {
|
|
|
1103
1332
|
pinnedIndex,
|
|
1104
1333
|
skipReasons: accountSkipReasons,
|
|
1105
1334
|
stickyBoostByAccount: rotationStickyBoost,
|
|
1335
|
+
pidOffsetEnabled,
|
|
1106
1336
|
});
|
|
1337
|
+
const selected = routingMutexMode === "enabled"
|
|
1338
|
+
? await withRoutingMutex(routingMutexMode, async () => {
|
|
1339
|
+
const candidate = selectAccount();
|
|
1340
|
+
if (candidate && pinnedIndex === null) {
|
|
1341
|
+
// Re-commit the cursor under the held mutex. Skipped when a
|
|
1342
|
+
// manual pin is active so the proxy never clobbers the pin
|
|
1343
|
+
// (see #474); pinned selections are deterministic and need no
|
|
1344
|
+
// cursor advance. Runs inline via reentrancy — see comment
|
|
1345
|
+
// above.
|
|
1346
|
+
await accountManager.markSwitchedLocked(candidate, "rotation", context.family);
|
|
1347
|
+
}
|
|
1348
|
+
return candidate;
|
|
1349
|
+
})
|
|
1350
|
+
: selectAccount();
|
|
1107
1351
|
if (!selected) {
|
|
1108
1352
|
if (!reloadedAfterNoAccount &&
|
|
1109
1353
|
!isPinned &&
|
|
@@ -1212,7 +1456,7 @@ export async function startRuntimeRotationProxy(options) {
|
|
|
1212
1456
|
continue;
|
|
1213
1457
|
}
|
|
1214
1458
|
if (upstream.status === HTTP_STATUS.TOO_MANY_REQUESTS) {
|
|
1215
|
-
const bodyText = await readErrorBody(upstream);
|
|
1459
|
+
const bodyText = await readErrorBody(upstream, streamStallTimeoutMs);
|
|
1216
1460
|
const retryAfterMs = parseRetryAfterHeaderMs(upstream.headers, now()) ??
|
|
1217
1461
|
parseRetryAfterBodyMs(bodyText, now()) ??
|
|
1218
1462
|
60_000;
|
|
@@ -1229,7 +1473,7 @@ export async function startRuntimeRotationProxy(options) {
|
|
|
1229
1473
|
continue;
|
|
1230
1474
|
}
|
|
1231
1475
|
if (upstream.status === 402 || upstream.status === HTTP_STATUS.FORBIDDEN) {
|
|
1232
|
-
const bodyText = await readErrorBody(upstream);
|
|
1476
|
+
const bodyText = await readErrorBody(upstream, streamStallTimeoutMs);
|
|
1233
1477
|
const errorCode = extractErrorCodeFromBody(bodyText);
|
|
1234
1478
|
if (isWorkspaceDisabledError(upstream.status, errorCode, bodyText)) {
|
|
1235
1479
|
const accountWasEnabled = accountManager.getAccountByIndex(refreshed.account.index)?.enabled !==
|
|
@@ -1312,7 +1556,7 @@ export async function startRuntimeRotationProxy(options) {
|
|
|
1312
1556
|
return;
|
|
1313
1557
|
}
|
|
1314
1558
|
if (upstream.status === HTTP_STATUS.UNAUTHORIZED) {
|
|
1315
|
-
const bodyText = await readErrorBody(upstream);
|
|
1559
|
+
const bodyText = await readErrorBody(upstream, streamStallTimeoutMs);
|
|
1316
1560
|
accountManager.refundToken(refreshed.account, context.family, context.model);
|
|
1317
1561
|
accountManager.recordFailure(refreshed.account, context.family, context.model);
|
|
1318
1562
|
if (isTokenInvalidationError(bodyText)) {
|
|
@@ -1349,7 +1593,7 @@ export async function startRuntimeRotationProxy(options) {
|
|
|
1349
1593
|
continue;
|
|
1350
1594
|
}
|
|
1351
1595
|
if (upstream.status >= 500) {
|
|
1352
|
-
await readErrorBody(upstream);
|
|
1596
|
+
await readErrorBody(upstream, streamStallTimeoutMs);
|
|
1353
1597
|
accountManager.refundToken(refreshed.account, context.family, context.model);
|
|
1354
1598
|
accountManager.recordFailure(refreshed.account, context.family, context.model);
|
|
1355
1599
|
accountManager.markAccountCoolingDown(refreshed.account, serverErrorCooldownMs, "server-error");
|
|
@@ -1462,7 +1706,20 @@ export async function startRuntimeRotationProxy(options) {
|
|
|
1462
1706
|
}
|
|
1463
1707
|
}
|
|
1464
1708
|
catch (error) {
|
|
1465
|
-
|
|
1709
|
+
const rawErrorMessage = error instanceof Error ? error.message : String(error);
|
|
1710
|
+
// errors-logging-08: redact any email/token material that leaked into a
|
|
1711
|
+
// raw upstream or refresh error string before it reaches status consumers
|
|
1712
|
+
// or the structured log. maskString is a no-op for clean diagnostic text.
|
|
1713
|
+
const maskedErrorMessage = maskString(rawErrorMessage);
|
|
1714
|
+
status.lastError = maskedErrorMessage;
|
|
1715
|
+
// errors-logging-01: surface the failure through the structured logger
|
|
1716
|
+
// (redaction-safe) with the request trace id, instead of only stashing a
|
|
1717
|
+
// last-write-wins status string.
|
|
1718
|
+
proxyLog.error("runtime proxy request failed", {
|
|
1719
|
+
traceId,
|
|
1720
|
+
code: isRuntimeProxyHttpError(error) ? error.code : "codex_runtime_rotation_proxy_error",
|
|
1721
|
+
error: maskedErrorMessage,
|
|
1722
|
+
});
|
|
1466
1723
|
if (!res.headersSent) {
|
|
1467
1724
|
if (isRuntimeProxyHttpError(error)) {
|
|
1468
1725
|
await usageRecorder?.record({
|
|
@@ -1519,20 +1776,26 @@ export async function startRuntimeRotationProxy(options) {
|
|
|
1519
1776
|
};
|
|
1520
1777
|
server.once("error", onError);
|
|
1521
1778
|
server.once("listening", onListening);
|
|
1522
|
-
server.listen(port,
|
|
1779
|
+
server.listen(port, bindHost);
|
|
1523
1780
|
});
|
|
1524
1781
|
server.on("error", onPostStartupServerError);
|
|
1525
1782
|
const address = server.address();
|
|
1526
1783
|
const resolvedPort = typeof address === "object" && address ? address.port : port;
|
|
1527
1784
|
return {
|
|
1528
|
-
host,
|
|
1785
|
+
host: bindHost,
|
|
1529
1786
|
port: resolvedPort,
|
|
1530
|
-
baseUrl: `http://${
|
|
1787
|
+
baseUrl: `http://${urlHost}:${resolvedPort}`,
|
|
1531
1788
|
close: async () => {
|
|
1532
1789
|
await closeServer(server, sockets);
|
|
1533
1790
|
await activeAccountManager.flushPendingSave();
|
|
1534
1791
|
},
|
|
1535
|
-
getStatus: () => ({
|
|
1792
|
+
getStatus: () => ({
|
|
1793
|
+
...status,
|
|
1794
|
+
// Redact any email/token material that leaked into a raw upstream or
|
|
1795
|
+
// refresh error string before exposing it to status/report consumers
|
|
1796
|
+
// (errors-logging-08). maskString is a no-op for clean diagnostic text.
|
|
1797
|
+
lastError: status.lastError === null ? null : maskString(status.lastError),
|
|
1798
|
+
}),
|
|
1536
1799
|
};
|
|
1537
1800
|
}
|
|
1538
1801
|
async function closeServer(server, sockets) {
|