codex-multi-auth 2.11.0 → 2.13.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 +7 -1
- package/dist/lib/accounts.d.ts +75 -0
- package/dist/lib/accounts.d.ts.map +1 -1
- package/dist/lib/accounts.js +213 -42
- package/dist/lib/capability-policy.d.ts.map +1 -1
- package/dist/lib/capability-policy.js +2 -1
- package/dist/lib/codex-manager/account-manager-commands.d.ts.map +1 -1
- package/dist/lib/codex-manager/account-manager-commands.js +1 -0
- package/dist/lib/codex-manager/commands/limits.d.ts +21 -0
- package/dist/lib/codex-manager/commands/limits.d.ts.map +1 -0
- package/dist/lib/codex-manager/commands/limits.js +158 -0
- package/dist/lib/codex-manager/help.d.ts.map +1 -1
- package/dist/lib/codex-manager/help.js +1 -0
- package/dist/lib/codex-manager/persist-selected-account.d.ts.map +1 -1
- package/dist/lib/codex-manager/persist-selected-account.js +14 -0
- package/dist/lib/codex-manager.d.ts.map +1 -1
- package/dist/lib/codex-manager.js +12 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +2 -1
- package/dist/lib/constants.d.ts +9 -0
- package/dist/lib/constants.d.ts.map +1 -1
- package/dist/lib/constants.js +29 -0
- package/dist/lib/entitlement-cache.d.ts.map +1 -1
- package/dist/lib/entitlement-cache.js +2 -1
- package/dist/lib/forecast.d.ts.map +1 -1
- package/dist/lib/forecast.js +1 -14
- package/dist/lib/policy/runtime-policy.d.ts +9 -1
- package/dist/lib/policy/runtime-policy.d.ts.map +1 -1
- package/dist/lib/policy/runtime-policy.js +1 -0
- package/dist/lib/preemptive-quota-scheduler.d.ts +12 -0
- package/dist/lib/preemptive-quota-scheduler.d.ts.map +1 -1
- package/dist/lib/preemptive-quota-scheduler.js +21 -0
- package/dist/lib/redaction.d.ts +17 -0
- package/dist/lib/redaction.d.ts.map +1 -0
- package/dist/lib/redaction.js +28 -0
- package/dist/lib/request/error-classification.js +2 -2
- package/dist/lib/runtime/rotation-storage-meta.d.ts.map +1 -1
- package/dist/lib/runtime/rotation-storage-meta.js +4 -1
- package/dist/lib/runtime-rotation-proxy.d.ts +13 -0
- package/dist/lib/runtime-rotation-proxy.d.ts.map +1 -1
- package/dist/lib/runtime-rotation-proxy.js +47 -7
- package/dist/lib/storage.d.ts +20 -2
- package/dist/lib/storage.d.ts.map +1 -1
- package/dist/lib/storage.js +69 -25
- package/dist/lib/usage/ledger.d.ts +1 -1
- package/dist/lib/usage/ledger.d.ts.map +1 -1
- package/dist/lib/usage/ledger.js +9 -0
- package/dist/lib/usage/pricing.d.ts +16 -1
- package/dist/lib/usage/pricing.d.ts.map +1 -1
- package/dist/lib/usage/pricing.js +46 -8
- package/dist/lib/usage/redaction.d.ts +1 -1
- package/dist/lib/usage/redaction.d.ts.map +1 -1
- package/dist/lib/usage/redaction.js +13 -0
- package/dist/lib/usage/types.d.ts +33 -0
- package/dist/lib/usage/types.d.ts.map +1 -1
- package/dist/lib/usage/types.js +28 -1
- package/dist/lib/usage/usage-extraction.d.ts +2 -1
- package/dist/lib/usage/usage-extraction.d.ts.map +1 -1
- package/dist/lib/usage/usage-extraction.js +44 -4
- package/package.json +1 -1
- package/scripts/codex-routing.js +1 -0
- package/scripts/codex.js +157 -2
|
@@ -66,18 +66,58 @@ export function extractUsageTokenCounts(usage) {
|
|
|
66
66
|
totalTokens: providedTotal,
|
|
67
67
|
};
|
|
68
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* Read the billed service tier off a Responses payload.
|
|
71
|
+
*
|
|
72
|
+
* `service_tier` sits on the response object, not inside `usage`, and a stream
|
|
73
|
+
* event wraps that object one level down. An unrecognised value becomes
|
|
74
|
+
* `unknown` rather than being dropped, because dropping it would price the row
|
|
75
|
+
* at standard rates and under-count a more expensive tier.
|
|
76
|
+
*/
|
|
77
|
+
function extractServiceTier(payload) {
|
|
78
|
+
const raw = payload.service_tier ??
|
|
79
|
+
(isRecord(payload.response) ? payload.response.service_tier : undefined);
|
|
80
|
+
if (typeof raw !== "string")
|
|
81
|
+
return undefined;
|
|
82
|
+
const normalized = raw.trim().toLowerCase();
|
|
83
|
+
if (normalized.length === 0)
|
|
84
|
+
return undefined;
|
|
85
|
+
// `default` is what the API calls the standard tier on the wire.
|
|
86
|
+
if (normalized === "default" || normalized === "standard")
|
|
87
|
+
return "standard";
|
|
88
|
+
// The upstream catalog names this tier `priority` with the display name
|
|
89
|
+
// "Fast", and lists `fast` in `additional_speed_tiers`, so a response can
|
|
90
|
+
// report either spelling. Without this it becomes `unknown`, which prices
|
|
91
|
+
// as unpriced and fails a cost budget closed on a tier we DO have a rate
|
|
92
|
+
// for.
|
|
93
|
+
if (normalized === "fast")
|
|
94
|
+
return "priority";
|
|
95
|
+
if (normalized === "priority" ||
|
|
96
|
+
normalized === "flex" ||
|
|
97
|
+
normalized === "batch" ||
|
|
98
|
+
normalized === "scale") {
|
|
99
|
+
return normalized;
|
|
100
|
+
}
|
|
101
|
+
return "unknown";
|
|
102
|
+
}
|
|
69
103
|
/**
|
|
70
104
|
* Pull the `usage` object out of a Responses payload, whether it arrived as a
|
|
71
|
-
* bare response object (non-streaming) or wrapped in a stream event envelope
|
|
105
|
+
* bare response object (non-streaming) or wrapped in a stream event envelope,
|
|
106
|
+
* and attach the tier it was billed at.
|
|
72
107
|
*/
|
|
73
108
|
export function extractResponsesUsage(payload) {
|
|
74
109
|
if (!isRecord(payload))
|
|
75
110
|
return null;
|
|
111
|
+
const serviceTier = extractServiceTier(payload);
|
|
76
112
|
const direct = extractUsageTokenCounts(payload.usage);
|
|
77
|
-
if (direct)
|
|
78
|
-
return direct;
|
|
113
|
+
if (direct) {
|
|
114
|
+
return serviceTier ? { ...direct, serviceTier } : direct;
|
|
115
|
+
}
|
|
79
116
|
if (isRecord(payload.response)) {
|
|
80
|
-
|
|
117
|
+
const nested = extractUsageTokenCounts(payload.response.usage);
|
|
118
|
+
if (!nested)
|
|
119
|
+
return null;
|
|
120
|
+
return serviceTier ? { ...nested, serviceTier } : nested;
|
|
81
121
|
}
|
|
82
122
|
return null;
|
|
83
123
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codex-multi-auth",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.13.0",
|
|
4
4
|
"description": "Codex CLI multi-account OAuth manager with account switching, health checks, runtime rotation, diagnostics, and recovery tools for @openai/codex",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
package/scripts/codex-routing.js
CHANGED
package/scripts/codex.js
CHANGED
|
@@ -78,6 +78,8 @@ const APP_RUNTIME_HELPER_USE_CANONICAL_HOME_ENV =
|
|
|
78
78
|
"CODEX_MULTI_AUTH_APP_ROTATION_USE_CANONICAL_HOME";
|
|
79
79
|
const APP_RUNTIME_HELPER_INSTALL_APP_SERVER_SHIM_ENV =
|
|
80
80
|
"CODEX_MULTI_AUTH_APP_ROTATION_INSTALL_APP_SERVER_SHIM";
|
|
81
|
+
const RUNTIME_ROTATION_PROXY_UPSTREAM_BASE_URL_ENV =
|
|
82
|
+
"CODEX_MULTI_AUTH_RUNTIME_PROXY_UPSTREAM_BASE_URL";
|
|
81
83
|
const APP_SERVER_CONFIG_ARGS_ENV =
|
|
82
84
|
"CODEX_MULTI_AUTH_APP_SERVER_CONFIG_ARGS_JSON";
|
|
83
85
|
const APP_RUNTIME_HELPER_STATUS_FILE =
|
|
@@ -168,6 +170,95 @@ let warnedShadowHomeSqliteLinkFailure = false;
|
|
|
168
170
|
const warnedShadowHomeLinkOnlyDirectoryFailures = new Set();
|
|
169
171
|
const warnedShadowHomeSqliteSidecarPlaceholderFailures = new Set();
|
|
170
172
|
|
|
173
|
+
/**
|
|
174
|
+
* True only for a NUMERIC loopback literal, as `new URL().hostname` reports it.
|
|
175
|
+
*
|
|
176
|
+
* A name is deliberately not enough. `localhost` is resolved by the OS at
|
|
177
|
+
* connect time, so an /etc/hosts or Windows hosts entry can point it at a
|
|
178
|
+
* routable address, and the upstream request carries the managed OAuth bearer
|
|
179
|
+
* token and the request body. WHATWG URL always reports an IPv6 host in its
|
|
180
|
+
* bracketed form, so `::1` is only ever seen here as `[::1]`.
|
|
181
|
+
*/
|
|
182
|
+
function isLoopbackUrlHostname(hostname) {
|
|
183
|
+
if (hostname === "[::1]") {
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
const ipv4 = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(hostname);
|
|
187
|
+
if (!ipv4) {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
const octets = ipv4.slice(1).map((part) => Number(part));
|
|
191
|
+
if (octets.some((octet) => !Number.isInteger(octet) || octet > 255)) {
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
// The whole 127.0.0.0/8 block is loopback, not just 127.0.0.1.
|
|
195
|
+
return octets[0] === 127;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Whether the raw URL text carries an explicit `:port`.
|
|
200
|
+
*
|
|
201
|
+
* `new URL()` erases a port that matches the scheme default, so
|
|
202
|
+
* `http://127.0.0.1:80/x` reports `parsed.port === ""` and a `!parsed.port`
|
|
203
|
+
* check would reject a perfectly valid, explicitly-ported loopback URL.
|
|
204
|
+
*/
|
|
205
|
+
function hasExplicitUrlPort(raw) {
|
|
206
|
+
const withoutScheme = raw.replace(/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//, "");
|
|
207
|
+
const authority = withoutScheme.split(/[/?#]/, 1)[0] ?? "";
|
|
208
|
+
// Strip userinfo first, or a password containing ':' reads as a port.
|
|
209
|
+
const hostAndPort = authority.slice(authority.lastIndexOf("@") + 1);
|
|
210
|
+
if (hostAndPort.startsWith("[")) {
|
|
211
|
+
const close = hostAndPort.indexOf("]");
|
|
212
|
+
return close !== -1 && /^:\d+$/.test(hostAndPort.slice(close + 1));
|
|
213
|
+
}
|
|
214
|
+
const colon = hostAndPort.indexOf(":");
|
|
215
|
+
return colon !== -1 && /^:\d+$/.test(hostAndPort.slice(colon));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function resolveRuntimeRotationProxyUpstreamBaseUrl(env = process.env) {
|
|
219
|
+
const raw = (env[RUNTIME_ROTATION_PROXY_UPSTREAM_BASE_URL_ENV] ?? "").trim();
|
|
220
|
+
if (!raw) {
|
|
221
|
+
return undefined;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
let parsed;
|
|
225
|
+
try {
|
|
226
|
+
parsed = new URL(raw);
|
|
227
|
+
} catch {
|
|
228
|
+
throw new Error(
|
|
229
|
+
`${RUNTIME_ROTATION_PROXY_UPSTREAM_BASE_URL_ENV} must be an absolute loopback HTTP URL.`,
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (
|
|
234
|
+
parsed.protocol !== "http:" ||
|
|
235
|
+
!isLoopbackUrlHostname(parsed.hostname.toLowerCase()) ||
|
|
236
|
+
!(parsed.port || hasExplicitUrlPort(raw)) ||
|
|
237
|
+
parsed.username ||
|
|
238
|
+
parsed.password ||
|
|
239
|
+
parsed.search ||
|
|
240
|
+
parsed.hash
|
|
241
|
+
) {
|
|
242
|
+
throw new Error(
|
|
243
|
+
`${RUNTIME_ROTATION_PROXY_UPSTREAM_BASE_URL_ENV} must use HTTP with a numeric loopback host (127.0.0.0/8 or [::1]; a name such as "localhost" is not accepted because it can resolve off-host), an explicit port, and no credentials, query, or fragment.`,
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return parsed.toString().replace(/\/$/, "");
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* @param clientApiKey Shared secret the wrapper and proxy authenticate with.
|
|
252
|
+
* @param upstreamBaseUrl Already-resolved upstream, or undefined for the
|
|
253
|
+
* default backend. Callers pass the value they resolved rather than the env, so
|
|
254
|
+
* one launch never parses (and never re-reports) the same variable twice.
|
|
255
|
+
*/
|
|
256
|
+
function createRuntimeRotationProxyOptions(clientApiKey, upstreamBaseUrl) {
|
|
257
|
+
return upstreamBaseUrl
|
|
258
|
+
? { clientApiKey, upstreamBaseUrl }
|
|
259
|
+
: { clientApiKey };
|
|
260
|
+
}
|
|
261
|
+
|
|
171
262
|
async function loadRuntimeConstants() {
|
|
172
263
|
const fallback = {
|
|
173
264
|
RUNTIME_ROTATION_PROXY_PROVIDER_ID: `${APP_SERVER_ACCOUNT_DISPLAY_NAME}-runtime-proxy`,
|
|
@@ -4711,7 +4802,12 @@ async function runRuntimeRotationAppHelper(identityToken = "") {
|
|
|
4711
4802
|
throw new Error("runtime rotation config helpers are unavailable");
|
|
4712
4803
|
}
|
|
4713
4804
|
const clientApiKey = createRuntimeRotationProxyClientApiKey();
|
|
4714
|
-
proxyServer = await proxyModule.startRuntimeRotationProxy(
|
|
4805
|
+
proxyServer = await proxyModule.startRuntimeRotationProxy(
|
|
4806
|
+
createRuntimeRotationProxyOptions(
|
|
4807
|
+
clientApiKey,
|
|
4808
|
+
resolveRuntimeRotationProxyUpstreamBaseUrl(),
|
|
4809
|
+
),
|
|
4810
|
+
);
|
|
4715
4811
|
const useCanonicalHome =
|
|
4716
4812
|
(process.env[APP_RUNTIME_HELPER_USE_CANONICAL_HOME_ENV] ?? "").trim() ===
|
|
4717
4813
|
"1";
|
|
@@ -5112,13 +5208,52 @@ async function createRuntimeRotationProxyContextIfEnabled(
|
|
|
5112
5208
|
baseContext,
|
|
5113
5209
|
rawArgs,
|
|
5114
5210
|
) {
|
|
5211
|
+
// Resolve the configured upstream BEFORE the enabled gate. Every path that
|
|
5212
|
+
// leaves this function with `baseContext` forwards Responses traffic, and its
|
|
5213
|
+
// managed OAuth bearer token, straight to the real backend. An explicit
|
|
5214
|
+
// upstream is a routing requirement rather than a rotation preference, so
|
|
5215
|
+
// `CODEX_MULTI_AUTH_RUNTIME_ROTATION_PROXY=0`, `CODEX_MULTI_AUTH_BYPASS=1`, a
|
|
5216
|
+
// missing `dist/lib/config.js` or a disabled setting must all fail closed
|
|
5217
|
+
// here instead of silently reaching chatgpt.com.
|
|
5218
|
+
let configuredUpstreamBaseUrl;
|
|
5219
|
+
try {
|
|
5220
|
+
configuredUpstreamBaseUrl =
|
|
5221
|
+
resolveRuntimeRotationProxyUpstreamBaseUrl(baseContext.env);
|
|
5222
|
+
} catch (error) {
|
|
5223
|
+
baseContext.cleanup?.();
|
|
5224
|
+
return {
|
|
5225
|
+
startupError: `codex-multi-auth runtime rotation upstream is invalid: ${
|
|
5226
|
+
error instanceof Error ? error.message : String(error)
|
|
5227
|
+
}`,
|
|
5228
|
+
};
|
|
5229
|
+
}
|
|
5230
|
+
// The one exception is a subcommand that never reaches the backend at all
|
|
5231
|
+
// (`--version`, `login`, ...): there is no traffic to misroute, so it passes
|
|
5232
|
+
// through with the upstream simply unused.
|
|
5233
|
+
const requireConfiguredUpstream =
|
|
5234
|
+
configuredUpstreamBaseUrl !== undefined &&
|
|
5235
|
+
shouldUseRuntimeRoutingForForwardedArgs(rawArgs);
|
|
5236
|
+
|
|
5115
5237
|
const enabled = await isRuntimeRotationProxyEnabled(rawArgs, baseContext.env);
|
|
5116
5238
|
if (!enabled) {
|
|
5239
|
+
if (requireConfiguredUpstream) {
|
|
5240
|
+
baseContext.cleanup?.();
|
|
5241
|
+
return {
|
|
5242
|
+
startupError: `codex-multi-auth runtime rotation is disabled, so ${RUNTIME_ROTATION_PROXY_UPSTREAM_BASE_URL_ENV} cannot be honored and requests would go to the direct backend instead. Enable runtime rotation or unset that variable.`,
|
|
5243
|
+
};
|
|
5244
|
+
}
|
|
5117
5245
|
return baseContext;
|
|
5118
5246
|
}
|
|
5119
5247
|
|
|
5120
5248
|
const configTomlModule = await loadRuntimeConfigTomlModule();
|
|
5121
5249
|
if (!configTomlModule) {
|
|
5250
|
+
if (requireConfiguredUpstream) {
|
|
5251
|
+
baseContext.cleanup?.();
|
|
5252
|
+
return {
|
|
5253
|
+
startupError:
|
|
5254
|
+
"codex-multi-auth runtime rotation config helpers are unavailable; configured upstream routing cannot continue.",
|
|
5255
|
+
};
|
|
5256
|
+
}
|
|
5122
5257
|
console.error(
|
|
5123
5258
|
"codex-multi-auth runtime rotation config helpers are unavailable; continuing without runtime rotation.",
|
|
5124
5259
|
);
|
|
@@ -5188,6 +5323,13 @@ async function createRuntimeRotationProxyContextIfEnabled(
|
|
|
5188
5323
|
|
|
5189
5324
|
const proxyModule = await loadRuntimeRotationProxyModule();
|
|
5190
5325
|
if (!proxyModule) {
|
|
5326
|
+
if (requireConfiguredUpstream) {
|
|
5327
|
+
baseContext.cleanup?.();
|
|
5328
|
+
return {
|
|
5329
|
+
startupError:
|
|
5330
|
+
"codex-multi-auth runtime rotation proxy is unavailable; configured upstream routing cannot continue.",
|
|
5331
|
+
};
|
|
5332
|
+
}
|
|
5191
5333
|
console.error(
|
|
5192
5334
|
"codex-multi-auth runtime rotation proxy is unavailable; continuing without runtime rotation.",
|
|
5193
5335
|
);
|
|
@@ -5198,7 +5340,12 @@ async function createRuntimeRotationProxyContextIfEnabled(
|
|
|
5198
5340
|
let shadowContext;
|
|
5199
5341
|
try {
|
|
5200
5342
|
const clientApiKey = createRuntimeRotationProxyClientApiKey();
|
|
5201
|
-
proxyServer = await proxyModule.startRuntimeRotationProxy(
|
|
5343
|
+
proxyServer = await proxyModule.startRuntimeRotationProxy(
|
|
5344
|
+
createRuntimeRotationProxyOptions(
|
|
5345
|
+
clientApiKey,
|
|
5346
|
+
configuredUpstreamBaseUrl,
|
|
5347
|
+
),
|
|
5348
|
+
);
|
|
5202
5349
|
shadowContext = createRuntimeRotationProxyCodexHome(
|
|
5203
5350
|
baseContext.env,
|
|
5204
5351
|
proxyServer.baseUrl,
|
|
@@ -5211,6 +5358,14 @@ async function createRuntimeRotationProxyContextIfEnabled(
|
|
|
5211
5358
|
} catch {
|
|
5212
5359
|
// Best-effort cleanup only.
|
|
5213
5360
|
}
|
|
5361
|
+
if (requireConfiguredUpstream) {
|
|
5362
|
+
baseContext.cleanup?.();
|
|
5363
|
+
return {
|
|
5364
|
+
startupError: `codex-multi-auth runtime rotation proxy failed to start with configured upstream: ${
|
|
5365
|
+
error instanceof Error ? error.message : String(error)
|
|
5366
|
+
}`,
|
|
5367
|
+
};
|
|
5368
|
+
}
|
|
5214
5369
|
console.error(
|
|
5215
5370
|
`codex-multi-auth runtime rotation proxy failed to start; continuing without runtime rotation: ${error instanceof Error ? error.message : String(error)}`,
|
|
5216
5371
|
);
|