codex-multi-auth 2.8.7 → 2.9.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 +1 -1
- package/README.md +14 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +34 -0
- package/dist/lib/account-policy.d.ts +3 -1
- package/dist/lib/account-policy.d.ts.map +1 -1
- package/dist/lib/account-policy.js +14 -1
- package/dist/lib/budget-guard.d.ts +1 -0
- package/dist/lib/budget-guard.d.ts.map +1 -1
- package/dist/lib/budget-guard.js +13 -3
- package/dist/lib/codex-manager/account-pool-write.d.ts +8 -0
- package/dist/lib/codex-manager/account-pool-write.d.ts.map +1 -1
- package/dist/lib/codex-manager/account-pool-write.js +1 -1
- package/dist/lib/codex-manager/help.d.ts +2 -0
- package/dist/lib/codex-manager/help.d.ts.map +1 -1
- package/dist/lib/codex-manager/help.js +38 -1
- package/dist/lib/codex-manager/login-flow.d.ts +3 -0
- package/dist/lib/codex-manager/login-flow.d.ts.map +1 -1
- package/dist/lib/codex-manager/login-flow.js +80 -6
- package/dist/lib/codex-manager/login-menu-actions.d.ts.map +1 -1
- package/dist/lib/codex-manager/login-menu-actions.js +41 -5
- package/dist/lib/codex-manager/login-oauth.d.ts +38 -2
- package/dist/lib/codex-manager/login-oauth.d.ts.map +1 -1
- package/dist/lib/codex-manager/login-oauth.js +205 -23
- package/dist/lib/policy/runtime-policy.d.ts +7 -0
- package/dist/lib/policy/runtime-policy.d.ts.map +1 -1
- package/dist/lib/policy/runtime-policy.js +1 -0
- package/dist/lib/request/fetch-helpers.d.ts +7 -0
- package/dist/lib/request/fetch-helpers.d.ts.map +1 -1
- package/dist/lib/request/fetch-helpers.js +1 -1
- package/dist/lib/request/response-handler.d.ts +3 -1
- package/dist/lib/request/response-handler.d.ts.map +1 -1
- package/dist/lib/request/response-handler.js +26 -5
- package/dist/lib/request/stream-failover-runtime.d.ts +8 -1
- package/dist/lib/request/stream-failover-runtime.d.ts.map +1 -1
- package/dist/lib/request/stream-failover-runtime.js +9 -1
- package/dist/lib/request/stream-failover.d.ts.map +1 -1
- package/dist/lib/request/stream-failover.js +41 -0
- package/dist/lib/runtime/account-check.d.ts.map +1 -1
- package/dist/lib/runtime/account-check.js +8 -1
- package/dist/lib/runtime-rotation-proxy.d.ts.map +1 -1
- package/dist/lib/runtime-rotation-proxy.js +46 -3
- package/dist/lib/storage.d.ts +19 -0
- package/dist/lib/storage.d.ts.map +1 -1
- package/dist/lib/storage.js +32 -0
- package/dist/lib/usage/index.d.ts +2 -0
- package/dist/lib/usage/index.d.ts.map +1 -1
- package/dist/lib/usage/index.js +2 -0
- package/dist/lib/usage/ledger.d.ts.map +1 -1
- package/dist/lib/usage/ledger.js +7 -0
- package/dist/lib/usage/pricing.d.ts +15 -0
- package/dist/lib/usage/pricing.d.ts.map +1 -1
- package/dist/lib/usage/pricing.js +24 -0
- package/dist/lib/usage/stream-usage-deferral.d.ts +39 -0
- package/dist/lib/usage/stream-usage-deferral.d.ts.map +1 -0
- package/dist/lib/usage/stream-usage-deferral.js +54 -0
- package/dist/lib/usage/types.d.ts +6 -0
- package/dist/lib/usage/types.d.ts.map +1 -1
- package/dist/lib/usage/usage-extraction.d.ts +42 -0
- package/dist/lib/usage/usage-extraction.d.ts.map +1 -0
- package/dist/lib/usage/usage-extraction.js +178 -0
- package/package.json +1 -1
|
@@ -8,6 +8,7 @@ import { getModelFamily } from "./prompts/codex.js";
|
|
|
8
8
|
import { CURRENT_CODEX_MODEL } from "./request/helpers/model-map.js";
|
|
9
9
|
import { mutateRuntimeObservabilitySnapshot, recordRuntimeAccountRecovery, recordRuntimePoolExhaustion, } from "./runtime/runtime-observability.js";
|
|
10
10
|
import { createRuntimeUsageRecorder, evaluateRuntimePolicy, loadRuntimePolicyState, } from "./policy/runtime-policy.js";
|
|
11
|
+
import { createUsageStreamScanner } from "./usage/usage-extraction.js";
|
|
11
12
|
import { isWorkspaceDisabledError } from "./request/fetch-helpers.js";
|
|
12
13
|
import { PreemptiveQuotaScheduler, readQuotaSchedulerSnapshot, } from "./preemptive-quota-scheduler.js";
|
|
13
14
|
import { createLogger, maskString, runWithCorrelationId } from "./logger.js";
|
|
@@ -936,11 +937,35 @@ async function handleRequestInner(state, req, res, traceId) {
|
|
|
936
937
|
upstream = await withTimeout(state.fetchImpl(upstreamUrl, upstreamRequestInit), state.fetchTimeoutMs, () => fetchAbortController.abort(), `upstream fetch timed out after ${state.fetchTimeoutMs}ms`);
|
|
937
938
|
}
|
|
938
939
|
catch (error) {
|
|
939
|
-
|
|
940
|
+
// errors-logging-08: a custom fetchImpl, a proxy agent, or an undici
|
|
941
|
+
// cause chain can embed the request URL or credential material in the
|
|
942
|
+
// raw message, so mask before it reaches any state.status consumer.
|
|
943
|
+
const transportError = maskString(error instanceof Error ? error.message : String(error));
|
|
944
|
+
state.status.lastError = transportError;
|
|
945
|
+
// errors-logging-01: give the failure a structured, trace-correlated
|
|
946
|
+
// line instead of only a last-write-wins status string. Nothing else
|
|
947
|
+
// on this path reaches proxyLog, because the request does not throw.
|
|
948
|
+
proxyLog.error("upstream transport failure", {
|
|
949
|
+
traceId,
|
|
950
|
+
code: "codex_runtime_rotation_transport_error",
|
|
951
|
+
error: transportError,
|
|
952
|
+
});
|
|
940
953
|
accountManager.refundToken(refreshed.account, context.family, context.model);
|
|
941
|
-
|
|
954
|
+
// A pre-header transport exception is a property of the network path,
|
|
955
|
+
// not of this account's credentials or quota, so it must NOT feed the
|
|
956
|
+
// account's circuit breaker / health tracker: that is what would
|
|
957
|
+
// otherwise retire a healthy account for an outage it did not cause.
|
|
958
|
+
// See #677.
|
|
959
|
+
//
|
|
960
|
+
// The short timed cooldown IS still applied. It is self-healing, it
|
|
961
|
+
// keeps an account whose upstream hangs from stalling 1/N of every
|
|
962
|
+
// later request for the full fetch timeout, and it is what gives the
|
|
963
|
+
// exhaustion 503 a non-zero `retry_after_ms` to back the client off
|
|
964
|
+
// with (`getMinWaitTimeForFamily` returns 0 while any account is
|
|
965
|
+
// still selectable).
|
|
942
966
|
accountManager.markAccountCoolingDown(refreshed.account, state.networkErrorCooldownMs, "network-error");
|
|
943
967
|
accountManager.saveToDiskDebounced();
|
|
968
|
+
accountSkipReasons.set(refreshed.account.index, "network-error");
|
|
944
969
|
exhaustionReason = "network-error";
|
|
945
970
|
transientAttempts += 1;
|
|
946
971
|
transientExhaustionReason = "network-error";
|
|
@@ -1152,17 +1177,35 @@ async function handleRequestInner(state, req, res, traceId) {
|
|
|
1152
1177
|
state.lastGlobalSwitchAt = state.now();
|
|
1153
1178
|
}
|
|
1154
1179
|
await persistRuntimeActiveAccount(accountManager, refreshed.account, context.family, isPinned && refreshed.account.index === pinnedIndex, state.schedulingStrategy);
|
|
1180
|
+
// Recover the upstream token counts as the body streams past. Without
|
|
1181
|
+
// them every ledger row lands with all-zero tokens and a zero cost, so
|
|
1182
|
+
// evaluateBudgetGuard compares `0 >= limit` for maxTokens/maxCostUsd
|
|
1183
|
+
// and those caps never fire — `budget set --cost 50` would allow
|
|
1184
|
+
// unlimited spend, with only --requests actually enforced.
|
|
1185
|
+
const usageScanner = createUsageStreamScanner({
|
|
1186
|
+
contentType: upstream.headers.get("content-type"),
|
|
1187
|
+
});
|
|
1155
1188
|
const forwarded = await forwardStreamingResponse(upstream, res, state.status, () => {
|
|
1189
|
+
// Deliberately NOT the pre-header transport policy above,
|
|
1190
|
+
// which skips recordFailure (#677). By this point the
|
|
1191
|
+
// upstream accepted the request and began responding, so a
|
|
1192
|
+
// broken stream is a signal about THIS account's session
|
|
1193
|
+
// (upstream tearing it down) rather than about the shared
|
|
1194
|
+
// network path, and it should still credit the breaker.
|
|
1156
1195
|
accountManager.recordFailure(refreshed.account, context.family, context.model);
|
|
1157
1196
|
accountManager.markAccountCoolingDown(refreshed.account, state.networkErrorCooldownMs, "network-error");
|
|
1158
1197
|
state.sessionAffinityStore?.forgetSession(context.sessionKey);
|
|
1159
1198
|
accountManager.saveToDiskDebounced();
|
|
1160
|
-
}, state.streamStallTimeoutMs);
|
|
1199
|
+
}, state.streamStallTimeoutMs, usageScanner.push);
|
|
1200
|
+
// A stream that broke mid-flight still bills for whatever the upstream
|
|
1201
|
+
// reported before the break, so record the counts on both outcomes.
|
|
1202
|
+
const usageTokens = usageScanner.result();
|
|
1161
1203
|
await usageRecorder.record({
|
|
1162
1204
|
outcome: forwarded ? "success" : "failure",
|
|
1163
1205
|
statusCode: upstream.status,
|
|
1164
1206
|
errorCode: forwarded ? null : "stream_forward_failed",
|
|
1165
1207
|
account: refreshed.account,
|
|
1208
|
+
...(usageTokens ?? {}),
|
|
1166
1209
|
});
|
|
1167
1210
|
return;
|
|
1168
1211
|
}
|
package/dist/lib/storage.d.ts
CHANGED
|
@@ -83,6 +83,25 @@ export declare function normalizeAccountStorage(data: unknown): AccountStorageV3
|
|
|
83
83
|
* helper only narrows the lost-update window. See issue #474.
|
|
84
84
|
*/
|
|
85
85
|
export declare function readAffinityGenerationFromDisk(path: string): number;
|
|
86
|
+
/**
|
|
87
|
+
* Advance the session-affinity generation so a running runtime proxy drops its
|
|
88
|
+
* sticky session→account bindings on the next request.
|
|
89
|
+
*
|
|
90
|
+
* Session affinity remembers an account by ITS INDEX, so any mutation that
|
|
91
|
+
* reshuffles the pool (removing an account, restoring a backup) silently
|
|
92
|
+
* re-points every live session at a different account until the generation
|
|
93
|
+
* changes. `switch`/`best`/`unpin` already bump it; the reshuffling paths are
|
|
94
|
+
* the ones that need it most, because they move every index at once.
|
|
95
|
+
*
|
|
96
|
+
* Re-reads the on-disk generation first so concurrent CLI processes do not lose
|
|
97
|
+
* increments via lost-update on the load+mutate pair. `Math.max` keeps the
|
|
98
|
+
* counter monotonic: an extra bump only costs one additional affinity
|
|
99
|
+
* invalidation, while a missed bump lets the proxy cling to the wrong account.
|
|
100
|
+
* See issue #474.
|
|
101
|
+
*/
|
|
102
|
+
export declare function bumpStorageAffinityGeneration(storage: {
|
|
103
|
+
affinityGeneration?: number;
|
|
104
|
+
}, storagePath?: string | null): number;
|
|
86
105
|
/**
|
|
87
106
|
* Synchronously reads both the top-level `pinnedAccountIndex` and
|
|
88
107
|
* `affinityGeneration` fields from the accounts storage file. Used by
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../../lib/storage.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../../lib/storage.ts"],"names":[],"mappings":"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"}
|
package/dist/lib/storage.js
CHANGED
|
@@ -951,6 +951,38 @@ export function readAffinityGenerationFromDisk(path) {
|
|
|
951
951
|
return 0;
|
|
952
952
|
}
|
|
953
953
|
}
|
|
954
|
+
/**
|
|
955
|
+
* Advance the session-affinity generation so a running runtime proxy drops its
|
|
956
|
+
* sticky session→account bindings on the next request.
|
|
957
|
+
*
|
|
958
|
+
* Session affinity remembers an account by ITS INDEX, so any mutation that
|
|
959
|
+
* reshuffles the pool (removing an account, restoring a backup) silently
|
|
960
|
+
* re-points every live session at a different account until the generation
|
|
961
|
+
* changes. `switch`/`best`/`unpin` already bump it; the reshuffling paths are
|
|
962
|
+
* the ones that need it most, because they move every index at once.
|
|
963
|
+
*
|
|
964
|
+
* Re-reads the on-disk generation first so concurrent CLI processes do not lose
|
|
965
|
+
* increments via lost-update on the load+mutate pair. `Math.max` keeps the
|
|
966
|
+
* counter monotonic: an extra bump only costs one additional affinity
|
|
967
|
+
* invalidation, while a missed bump lets the proxy cling to the wrong account.
|
|
968
|
+
* See issue #474.
|
|
969
|
+
*/
|
|
970
|
+
export function bumpStorageAffinityGeneration(storage, storagePath) {
|
|
971
|
+
let diskGeneration = 0;
|
|
972
|
+
try {
|
|
973
|
+
const path = storagePath ?? getStoragePath();
|
|
974
|
+
if (path)
|
|
975
|
+
diskGeneration = readAffinityGenerationFromDisk(path);
|
|
976
|
+
}
|
|
977
|
+
catch {
|
|
978
|
+
// No resolvable storage path (never configured). The in-memory counter
|
|
979
|
+
// is still bumped below; only the lost-update guard is unavailable.
|
|
980
|
+
}
|
|
981
|
+
const inMemoryGeneration = storage.affinityGeneration ?? 0;
|
|
982
|
+
const next = Math.max(inMemoryGeneration, diskGeneration) + 1;
|
|
983
|
+
storage.affinityGeneration = next;
|
|
984
|
+
return next;
|
|
985
|
+
}
|
|
954
986
|
/**
|
|
955
987
|
* Synchronously reads both the top-level `pinnedAccountIndex` and
|
|
956
988
|
* `affinityGeneration` fields from the accounts storage file. Used by
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../lib/usage/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,aAAa,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../lib/usage/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,aAAa,CAAC;AAC5B,cAAc,uBAAuB,CAAC;AACtC,cAAc,4BAA4B,CAAC"}
|
package/dist/lib/usage/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ledger.d.ts","sourceRoot":"","sources":["../../../lib/usage/ledger.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EACX,sBAAsB,EAEtB,gBAAgB,EAChB,gBAAgB,EAEhB,cAAc,EAEd,YAAY,EAGZ,iBAAiB,EAEjB,MAAM,YAAY,CAAC;AAqOpB,wBAAgB,mBAAmB,IAAI,gBAAgB,CAMtD;AAED,wBAAsB,oBAAoB,CACzC,KAAK,EAAE,sBAAsB,GAC3B,OAAO,CAAC,cAAc,CAAC,CASzB;AA+JD,wBAAsB,mBAAmB,CACxC,KAAK,GAAE,gBAAqB,GAC1B,OAAO,CAAC,cAAc,EAAE,CAAC,CAc3B;
|
|
1
|
+
{"version":3,"file":"ledger.d.ts","sourceRoot":"","sources":["../../../lib/usage/ledger.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EACX,sBAAsB,EAEtB,gBAAgB,EAChB,gBAAgB,EAEhB,cAAc,EAEd,YAAY,EAGZ,iBAAiB,EAEjB,MAAM,YAAY,CAAC;AAqOpB,wBAAgB,mBAAmB,IAAI,gBAAgB,CAMtD;AAED,wBAAsB,oBAAoB,CACzC,KAAK,EAAE,sBAAsB,GAC3B,OAAO,CAAC,cAAc,CAAC,CASzB;AA+JD,wBAAsB,mBAAmB,CACxC,KAAK,GAAE,gBAAqB,GAC1B,OAAO,CAAC,cAAc,EAAE,CAAC,CAc3B;AAuDD,wBAAgB,kBAAkB,CACjC,IAAI,EAAE,cAAc,EAAE,EACtB,KAAK,GAAE,iBAAsB,GAC3B,YAAY,CAwBd;AAED,wBAAsB,oBAAoB,CACzC,KAAK,GAAE,iBAAsB,GAC3B,OAAO,CAAC,YAAY,CAAC,CAEvB;AAED,wBAAsB,iBAAiB,CAAC,OAAO,GAAE;IAChD,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,iBAAiB,CAAC,EAAE,MAAM,CAAC;CACtB,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CA2B9B;AAED,wBAAgB,6BAA6B,IAAI,IAAI,CAEpD"}
|
package/dist/lib/usage/ledger.js
CHANGED
|
@@ -405,6 +405,7 @@ function createBucket(key) {
|
|
|
405
405
|
reasoningTokens: 0,
|
|
406
406
|
totalTokens: 0,
|
|
407
407
|
costUsd: 0,
|
|
408
|
+
unpricedRequests: 0,
|
|
408
409
|
};
|
|
409
410
|
}
|
|
410
411
|
function getBucketKey(row, by) {
|
|
@@ -437,6 +438,12 @@ function addRowToBucket(bucket, row) {
|
|
|
437
438
|
bucket.reasoningTokens += row.tokens.reasoningTokens;
|
|
438
439
|
bucket.totalTokens += row.tokens.totalTokens;
|
|
439
440
|
bucket.costUsd = Number((bucket.costUsd + (row.costUsd ?? 0)).toFixed(8));
|
|
441
|
+
// Only rows that actually consumed tokens count as unpriced spend. A blocked
|
|
442
|
+
// or cancelled request carries no tokens and no cost, and must not make a
|
|
443
|
+
// cost budget unevaluable.
|
|
444
|
+
if (row.costUsd === null && row.tokens.totalTokens > 0) {
|
|
445
|
+
bucket.unpricedRequests += 1;
|
|
446
|
+
}
|
|
440
447
|
}
|
|
441
448
|
export function summarizeUsageRows(rows, query = {}) {
|
|
442
449
|
const by = query.by ?? "model";
|
|
@@ -5,6 +5,21 @@ export interface UsageModelPricing {
|
|
|
5
5
|
cachedInputUsdPerMillion?: number;
|
|
6
6
|
reasoningUsdPerMillion?: number;
|
|
7
7
|
}
|
|
8
|
+
/**
|
|
9
|
+
* Models the router can normalize to that have no published rate here yet.
|
|
10
|
+
*
|
|
11
|
+
* Their cost is deliberately reported as unknown (`null`) rather than guessed:
|
|
12
|
+
* a wrong dollar figure is worse than no figure, and it would silently move a
|
|
13
|
+
* `maxCostUsd` budget's trip point. Nothing may treat an unknown cost as zero —
|
|
14
|
+
* `evaluateBudgetGuard` refuses a cost budget while unknown-cost usage is in
|
|
15
|
+
* the window instead of counting it as free (that under-count made cost caps
|
|
16
|
+
* unenforceable for exactly these models, every `pro` tier among them).
|
|
17
|
+
*
|
|
18
|
+
* Add a real rate to MODEL_PRICING and delete the entry here once published;
|
|
19
|
+
* `test/usage-pricing-coverage.test.ts` fails if a NEW routable model appears
|
|
20
|
+
* in neither list.
|
|
21
|
+
*/
|
|
22
|
+
export declare const UNPRICED_ROUTABLE_MODELS: readonly ["gpt-5.1", "gpt-5.2-pro", "gpt-5.4-mini", "gpt-5.4-nano", "gpt-5.4-pro", "gpt-5.5-pro", "gpt-5-mini", "gpt-5-nano"];
|
|
8
23
|
export declare function getUsageModelPricing(model: string | null | undefined): UsageModelPricing | null;
|
|
9
24
|
export declare function estimateUsageCostUsd(model: string | null | undefined, tokens: UsageTokenCounts): number | null;
|
|
10
25
|
export declare function listUsageModelPricing(): Record<string, UsageModelPricing>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pricing.d.ts","sourceRoot":"","sources":["../../../lib/usage/pricing.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEnD,MAAM,WAAW,iBAAiB;IACjC,kBAAkB,EAAE,MAAM,CAAC;IAC3B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,wBAAwB,CAAC,EAAE,MAAM,CAAC;IAClC,sBAAsB,CAAC,EAAE,MAAM,CAAC;CAChC;
|
|
1
|
+
{"version":3,"file":"pricing.d.ts","sourceRoot":"","sources":["../../../lib/usage/pricing.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEnD,MAAM,WAAW,iBAAiB;IACjC,kBAAkB,EAAE,MAAM,CAAC;IAC3B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,wBAAwB,CAAC,EAAE,MAAM,CAAC;IAClC,sBAAsB,CAAC,EAAE,MAAM,CAAC;CAChC;AA2DD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,wBAAwB,+HAS3B,CAAC;AAOX,wBAAgB,oBAAoB,CACnC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAC9B,iBAAiB,GAAG,IAAI,CAM1B;AAED,wBAAgB,oBAAoB,CACnC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAChC,MAAM,EAAE,gBAAgB,GACtB,MAAM,GAAG,IAAI,CAqBf;AAED,wBAAgB,qBAAqB,IAAI,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAEzE"}
|
|
@@ -54,6 +54,30 @@ const MODEL_PRICING = {
|
|
|
54
54
|
reasoningUsdPerMillion: 6,
|
|
55
55
|
},
|
|
56
56
|
};
|
|
57
|
+
/**
|
|
58
|
+
* Models the router can normalize to that have no published rate here yet.
|
|
59
|
+
*
|
|
60
|
+
* Their cost is deliberately reported as unknown (`null`) rather than guessed:
|
|
61
|
+
* a wrong dollar figure is worse than no figure, and it would silently move a
|
|
62
|
+
* `maxCostUsd` budget's trip point. Nothing may treat an unknown cost as zero —
|
|
63
|
+
* `evaluateBudgetGuard` refuses a cost budget while unknown-cost usage is in
|
|
64
|
+
* the window instead of counting it as free (that under-count made cost caps
|
|
65
|
+
* unenforceable for exactly these models, every `pro` tier among them).
|
|
66
|
+
*
|
|
67
|
+
* Add a real rate to MODEL_PRICING and delete the entry here once published;
|
|
68
|
+
* `test/usage-pricing-coverage.test.ts` fails if a NEW routable model appears
|
|
69
|
+
* in neither list.
|
|
70
|
+
*/
|
|
71
|
+
export const UNPRICED_ROUTABLE_MODELS = [
|
|
72
|
+
"gpt-5.1",
|
|
73
|
+
"gpt-5.2-pro",
|
|
74
|
+
"gpt-5.4-mini",
|
|
75
|
+
"gpt-5.4-nano",
|
|
76
|
+
"gpt-5.4-pro",
|
|
77
|
+
"gpt-5.5-pro",
|
|
78
|
+
"gpt-5-mini",
|
|
79
|
+
"gpt-5-nano",
|
|
80
|
+
];
|
|
57
81
|
function normalizeModelName(model) {
|
|
58
82
|
const trimmed = model?.trim().toLowerCase();
|
|
59
83
|
return trimmed && trimmed.length > 0 ? trimmed : null;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { UsageTokenCounts } from "./types.js";
|
|
2
|
+
export interface StreamUsageDeferral<TRow> {
|
|
3
|
+
/** Latest counts reported by upstream, or null if none have arrived. */
|
|
4
|
+
captured: () => UsageTokenCounts | null;
|
|
5
|
+
/** Feed upstream-reported counts. Writes a deferred row immediately. */
|
|
6
|
+
onUsage: (usage: UsageTokenCounts) => void;
|
|
7
|
+
/**
|
|
8
|
+
* Hand over a row to be written once upstream reports its counts, or after
|
|
9
|
+
* `fallbackMs` without them. Only the first pending row is held.
|
|
10
|
+
*/
|
|
11
|
+
defer: (completion: TRow) => void;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Holds a usage-ledger row open until a streaming response reports its tokens.
|
|
15
|
+
*
|
|
16
|
+
* A streaming request's row would otherwise be written when the request handler
|
|
17
|
+
* returns its Response — before the client has read a byte, and therefore
|
|
18
|
+
* before the upstream `usage` event exists. The ledger is append-only, so that
|
|
19
|
+
* row could never be corrected: it would stand at zero tokens and zero cost
|
|
20
|
+
* forever, which is what silently disables the `maxTokens` and `maxCostUsd`
|
|
21
|
+
* budget caps (`evaluateBudgetGuard` compares `0 >= limit`).
|
|
22
|
+
*
|
|
23
|
+
* The fallback timer is not optional bookkeeping. A client that disconnects
|
|
24
|
+
* mid-stream, or an upstream that never emits a terminal usage event, must
|
|
25
|
+
* still cost one row, because `maxRequests` counts rows — losing it would
|
|
26
|
+
* weaken a cap that currently works.
|
|
27
|
+
*
|
|
28
|
+
* Generic over the row type so this stays inside `lib/usage` rather than
|
|
29
|
+
* importing the policy layer that imports it back.
|
|
30
|
+
*/
|
|
31
|
+
export declare function createStreamUsageDeferral<TRow extends object>(options: {
|
|
32
|
+
record: (completion: TRow & Partial<UsageTokenCounts>) => void;
|
|
33
|
+
fallbackMs: number;
|
|
34
|
+
/** Injectable for tests; defaults to an unref'd setTimeout. */
|
|
35
|
+
schedule?: (run: () => void, ms: number) => {
|
|
36
|
+
cancel: () => void;
|
|
37
|
+
};
|
|
38
|
+
}): StreamUsageDeferral<TRow>;
|
|
39
|
+
//# sourceMappingURL=stream-usage-deferral.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stream-usage-deferral.d.ts","sourceRoot":"","sources":["../../../lib/usage/stream-usage-deferral.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEnD,MAAM,WAAW,mBAAmB,CAAC,IAAI;IACxC,wEAAwE;IACxE,QAAQ,EAAE,MAAM,gBAAgB,GAAG,IAAI,CAAC;IACxC,wEAAwE;IACxE,OAAO,EAAE,CAAC,KAAK,EAAE,gBAAgB,KAAK,IAAI,CAAC;IAC3C;;;OAGG;IACH,KAAK,EAAE,CAAC,UAAU,EAAE,IAAI,KAAK,IAAI,CAAC;CAClC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,yBAAyB,CAAC,IAAI,SAAS,MAAM,EAAE,OAAO,EAAE;IACvE,MAAM,EAAE,CAAC,UAAU,EAAE,IAAI,GAAG,OAAO,CAAC,gBAAgB,CAAC,KAAK,IAAI,CAAC;IAC/D,UAAU,EAAE,MAAM,CAAC;IACnB,+DAA+D;IAC/D,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,IAAI,EAAE,EAAE,EAAE,MAAM,KAAK;QAAE,MAAM,EAAE,MAAM,IAAI,CAAA;KAAE,CAAC;CACnE,GAAG,mBAAmB,CAAC,IAAI,CAAC,CAqC5B"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Holds a usage-ledger row open until a streaming response reports its tokens.
|
|
3
|
+
*
|
|
4
|
+
* A streaming request's row would otherwise be written when the request handler
|
|
5
|
+
* returns its Response — before the client has read a byte, and therefore
|
|
6
|
+
* before the upstream `usage` event exists. The ledger is append-only, so that
|
|
7
|
+
* row could never be corrected: it would stand at zero tokens and zero cost
|
|
8
|
+
* forever, which is what silently disables the `maxTokens` and `maxCostUsd`
|
|
9
|
+
* budget caps (`evaluateBudgetGuard` compares `0 >= limit`).
|
|
10
|
+
*
|
|
11
|
+
* The fallback timer is not optional bookkeeping. A client that disconnects
|
|
12
|
+
* mid-stream, or an upstream that never emits a terminal usage event, must
|
|
13
|
+
* still cost one row, because `maxRequests` counts rows — losing it would
|
|
14
|
+
* weaken a cap that currently works.
|
|
15
|
+
*
|
|
16
|
+
* Generic over the row type so this stays inside `lib/usage` rather than
|
|
17
|
+
* importing the policy layer that imports it back.
|
|
18
|
+
*/
|
|
19
|
+
export function createStreamUsageDeferral(options) {
|
|
20
|
+
const schedule = options.schedule ??
|
|
21
|
+
((run, ms) => {
|
|
22
|
+
const timer = setTimeout(run, ms);
|
|
23
|
+
// Never hold the process open for a row that is only a fallback.
|
|
24
|
+
timer.unref?.();
|
|
25
|
+
return { cancel: () => clearTimeout(timer) };
|
|
26
|
+
});
|
|
27
|
+
let captured = null;
|
|
28
|
+
let pending = null;
|
|
29
|
+
let timer = null;
|
|
30
|
+
const flush = (usage) => {
|
|
31
|
+
const completion = pending;
|
|
32
|
+
pending = null;
|
|
33
|
+
timer?.cancel();
|
|
34
|
+
timer = null;
|
|
35
|
+
if (!completion)
|
|
36
|
+
return;
|
|
37
|
+
options.record(usage ? { ...completion, ...usage } : completion);
|
|
38
|
+
};
|
|
39
|
+
return {
|
|
40
|
+
captured: () => captured,
|
|
41
|
+
onUsage: (usage) => {
|
|
42
|
+
captured = usage;
|
|
43
|
+
flush(usage);
|
|
44
|
+
},
|
|
45
|
+
defer: (completion) => {
|
|
46
|
+
// A second defer would orphan the first row's timer, so keep the row
|
|
47
|
+
// already in flight and let it settle on its own terms.
|
|
48
|
+
if (pending)
|
|
49
|
+
return;
|
|
50
|
+
pending = completion;
|
|
51
|
+
timer = schedule(() => flush(null), options.fallbackMs);
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
@@ -74,6 +74,12 @@ export interface UsageSummaryBucket {
|
|
|
74
74
|
reasoningTokens: number;
|
|
75
75
|
totalTokens: number;
|
|
76
76
|
costUsd: number;
|
|
77
|
+
/**
|
|
78
|
+
* Requests that consumed tokens but could not be priced, so `costUsd` is an
|
|
79
|
+
* UNDER-count by an unknown amount. A cost budget cannot be evaluated while
|
|
80
|
+
* this is above zero; see `evaluateBudgetGuard`.
|
|
81
|
+
*/
|
|
82
|
+
unpricedRequests: number;
|
|
77
83
|
}
|
|
78
84
|
export interface UsageSummary {
|
|
79
85
|
since: number | null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../lib/usage/types.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../lib/usage/types.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { UsageTokenCounts } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Read an OpenAI Responses `usage` object into the ledger's token shape.
|
|
4
|
+
*
|
|
5
|
+
* Two containment rules drive the mapping, and getting either backwards
|
|
6
|
+
* mis-prices every row:
|
|
7
|
+
*
|
|
8
|
+
* - `input_tokens_details.cached_tokens` is a SUBSET of `input_tokens`, and
|
|
9
|
+
* `estimateUsageCostUsd` already subtracts it (`billableInputTokens`), so
|
|
10
|
+
* both are passed through raw.
|
|
11
|
+
* - `output_tokens_details.reasoning_tokens` is likewise a SUBSET of
|
|
12
|
+
* `output_tokens`, but `estimateUsageCostUsd` prices `outputTokens` and
|
|
13
|
+
* `reasoningTokens` as DISJOINT buckets and sums them. Passing the raw
|
|
14
|
+
* `output_tokens` alongside the reasoning count would bill reasoning twice,
|
|
15
|
+
* so the reasoning share is subtracted out here.
|
|
16
|
+
*/
|
|
17
|
+
export declare function extractUsageTokenCounts(usage: unknown): UsageTokenCounts | null;
|
|
18
|
+
/**
|
|
19
|
+
* Pull the `usage` object out of a Responses payload, whether it arrived as a
|
|
20
|
+
* bare response object (non-streaming) or wrapped in a stream event envelope.
|
|
21
|
+
*/
|
|
22
|
+
export declare function extractResponsesUsage(payload: unknown): UsageTokenCounts | null;
|
|
23
|
+
export interface UsageStreamScanner {
|
|
24
|
+
/** Feed one forwarded chunk. Never throws. */
|
|
25
|
+
push: (chunk: Uint8Array) => void;
|
|
26
|
+
/** Flush any trailing partial data and return the final counts, if any. */
|
|
27
|
+
result: () => UsageTokenCounts | null;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Observe a forwarded response body and recover its token counts.
|
|
31
|
+
*
|
|
32
|
+
* SSE bodies are scanned line by line so only the current partial line is
|
|
33
|
+
* retained; a full response can be gigabytes of deltas and must never be
|
|
34
|
+
* buffered. Non-SSE JSON bodies have no incremental structure to exploit, so
|
|
35
|
+
* they are accumulated up to `MAX_BUFFERED_BYTES` and parsed at the end —
|
|
36
|
+
* beyond that cap the scanner gives up on usage rather than grow without
|
|
37
|
+
* bound.
|
|
38
|
+
*/
|
|
39
|
+
export declare function createUsageStreamScanner(options: {
|
|
40
|
+
contentType?: string | null;
|
|
41
|
+
}): UsageStreamScanner;
|
|
42
|
+
//# sourceMappingURL=usage-extraction.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"usage-extraction.d.ts","sourceRoot":"","sources":["../../../lib/usage/usage-extraction.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AA+BnD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,uBAAuB,CACtC,KAAK,EAAE,OAAO,GACZ,gBAAgB,GAAG,IAAI,CA+BzB;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,gBAAgB,GAAG,IAAI,CAQ/E;AAED,MAAM,WAAW,kBAAkB;IAClC,8CAA8C;IAC9C,IAAI,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;IAClC,2EAA2E;IAC3E,MAAM,EAAE,MAAM,gBAAgB,GAAG,IAAI,CAAC;CACtC;AAED;;;;;;;;;GASG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE;IACjD,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B,GAAG,kBAAkB,CA6ErB"}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extracting token counts from upstream Responses API traffic.
|
|
3
|
+
*
|
|
4
|
+
* Without this the usage ledger records every request with all-zero tokens and
|
|
5
|
+
* a zero cost, which silently disables the `maxTokens` / `maxCostUsd` budget
|
|
6
|
+
* caps: `evaluateBudgetGuard` compares `0 >= limit`, which never fires, so
|
|
7
|
+
* `codex-multi-auth budget set --cost 50` allows unlimited spend and only
|
|
8
|
+
* `--requests` actually enforces anything.
|
|
9
|
+
*/
|
|
10
|
+
/** Cap on retained bytes so a hostile or unterminated body cannot grow forever. */
|
|
11
|
+
const MAX_BUFFERED_BYTES = 1_048_576;
|
|
12
|
+
/** Terminal Responses stream events that carry the final `usage` object. */
|
|
13
|
+
const TERMINAL_EVENT_TYPES = new Set([
|
|
14
|
+
"response.completed",
|
|
15
|
+
"response.incomplete",
|
|
16
|
+
"response.failed",
|
|
17
|
+
"response.done",
|
|
18
|
+
]);
|
|
19
|
+
function isRecord(value) {
|
|
20
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
21
|
+
}
|
|
22
|
+
function nonNegativeInteger(value) {
|
|
23
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
24
|
+
return 0;
|
|
25
|
+
return Math.max(0, Math.trunc(value));
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Read an OpenAI Responses `usage` object into the ledger's token shape.
|
|
29
|
+
*
|
|
30
|
+
* Two containment rules drive the mapping, and getting either backwards
|
|
31
|
+
* mis-prices every row:
|
|
32
|
+
*
|
|
33
|
+
* - `input_tokens_details.cached_tokens` is a SUBSET of `input_tokens`, and
|
|
34
|
+
* `estimateUsageCostUsd` already subtracts it (`billableInputTokens`), so
|
|
35
|
+
* both are passed through raw.
|
|
36
|
+
* - `output_tokens_details.reasoning_tokens` is likewise a SUBSET of
|
|
37
|
+
* `output_tokens`, but `estimateUsageCostUsd` prices `outputTokens` and
|
|
38
|
+
* `reasoningTokens` as DISJOINT buckets and sums them. Passing the raw
|
|
39
|
+
* `output_tokens` alongside the reasoning count would bill reasoning twice,
|
|
40
|
+
* so the reasoning share is subtracted out here.
|
|
41
|
+
*/
|
|
42
|
+
export function extractUsageTokenCounts(usage) {
|
|
43
|
+
if (!isRecord(usage))
|
|
44
|
+
return null;
|
|
45
|
+
const hasAnyCount = typeof usage.input_tokens === "number" ||
|
|
46
|
+
typeof usage.output_tokens === "number" ||
|
|
47
|
+
typeof usage.total_tokens === "number";
|
|
48
|
+
if (!hasAnyCount)
|
|
49
|
+
return null;
|
|
50
|
+
const inputTokens = nonNegativeInteger(usage.input_tokens);
|
|
51
|
+
const rawOutputTokens = nonNegativeInteger(usage.output_tokens);
|
|
52
|
+
const cachedInputTokens = isRecord(usage.input_tokens_details)
|
|
53
|
+
? Math.min(inputTokens, nonNegativeInteger(usage.input_tokens_details.cached_tokens))
|
|
54
|
+
: 0;
|
|
55
|
+
const reasoningTokens = isRecord(usage.output_tokens_details)
|
|
56
|
+
? Math.min(rawOutputTokens, nonNegativeInteger(usage.output_tokens_details.reasoning_tokens))
|
|
57
|
+
: 0;
|
|
58
|
+
const providedTotal = typeof usage.total_tokens === "number" && Number.isFinite(usage.total_tokens)
|
|
59
|
+
? Math.max(0, Math.trunc(usage.total_tokens))
|
|
60
|
+
: inputTokens + rawOutputTokens;
|
|
61
|
+
return {
|
|
62
|
+
inputTokens,
|
|
63
|
+
outputTokens: rawOutputTokens - reasoningTokens,
|
|
64
|
+
cachedInputTokens,
|
|
65
|
+
reasoningTokens,
|
|
66
|
+
totalTokens: providedTotal,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* 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.
|
|
72
|
+
*/
|
|
73
|
+
export function extractResponsesUsage(payload) {
|
|
74
|
+
if (!isRecord(payload))
|
|
75
|
+
return null;
|
|
76
|
+
const direct = extractUsageTokenCounts(payload.usage);
|
|
77
|
+
if (direct)
|
|
78
|
+
return direct;
|
|
79
|
+
if (isRecord(payload.response)) {
|
|
80
|
+
return extractUsageTokenCounts(payload.response.usage);
|
|
81
|
+
}
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Observe a forwarded response body and recover its token counts.
|
|
86
|
+
*
|
|
87
|
+
* SSE bodies are scanned line by line so only the current partial line is
|
|
88
|
+
* retained; a full response can be gigabytes of deltas and must never be
|
|
89
|
+
* buffered. Non-SSE JSON bodies have no incremental structure to exploit, so
|
|
90
|
+
* they are accumulated up to `MAX_BUFFERED_BYTES` and parsed at the end —
|
|
91
|
+
* beyond that cap the scanner gives up on usage rather than grow without
|
|
92
|
+
* bound.
|
|
93
|
+
*/
|
|
94
|
+
export function createUsageStreamScanner(options) {
|
|
95
|
+
const sse = (options.contentType ?? "").toLowerCase().includes("text/event-stream");
|
|
96
|
+
const decoder = new TextDecoder("utf-8");
|
|
97
|
+
let pending = "";
|
|
98
|
+
let bufferedBytes = 0;
|
|
99
|
+
let overflowed = false;
|
|
100
|
+
let latest = null;
|
|
101
|
+
const consumeLine = (line) => {
|
|
102
|
+
const trimmed = line.trim();
|
|
103
|
+
if (!trimmed.startsWith("data:"))
|
|
104
|
+
return;
|
|
105
|
+
const payload = trimmed.slice("data:".length).trim();
|
|
106
|
+
if (payload.length === 0 || payload === "[DONE]")
|
|
107
|
+
return;
|
|
108
|
+
let parsed;
|
|
109
|
+
try {
|
|
110
|
+
parsed = JSON.parse(payload);
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
// Prefer terminal events, which carry the authoritative totals, but fall
|
|
116
|
+
// back to any event that reports usage so a stream that ends on a
|
|
117
|
+
// non-standard terminal type still records something.
|
|
118
|
+
const type = isRecord(parsed) ? parsed.type : undefined;
|
|
119
|
+
const isTerminal = typeof type === "string" && TERMINAL_EVENT_TYPES.has(type);
|
|
120
|
+
const counts = extractResponsesUsage(parsed);
|
|
121
|
+
if (counts && (isTerminal || latest === null)) {
|
|
122
|
+
latest = counts;
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
return {
|
|
126
|
+
push: (chunk) => {
|
|
127
|
+
try {
|
|
128
|
+
if (overflowed || chunk.byteLength === 0)
|
|
129
|
+
return;
|
|
130
|
+
bufferedBytes += chunk.byteLength;
|
|
131
|
+
pending += decoder.decode(chunk, { stream: true });
|
|
132
|
+
if (!sse) {
|
|
133
|
+
if (bufferedBytes > MAX_BUFFERED_BYTES) {
|
|
134
|
+
overflowed = true;
|
|
135
|
+
pending = "";
|
|
136
|
+
}
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
let newlineAt = pending.indexOf("\n");
|
|
140
|
+
while (newlineAt !== -1) {
|
|
141
|
+
consumeLine(pending.slice(0, newlineAt));
|
|
142
|
+
pending = pending.slice(newlineAt + 1);
|
|
143
|
+
newlineAt = pending.indexOf("\n");
|
|
144
|
+
}
|
|
145
|
+
// A single unterminated line this long is not SSE any more; stop
|
|
146
|
+
// retaining it rather than growing the buffer for the whole body.
|
|
147
|
+
if (pending.length > MAX_BUFFERED_BYTES) {
|
|
148
|
+
overflowed = true;
|
|
149
|
+
pending = "";
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
// Usage accounting must never break the forwarded response.
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
result: () => {
|
|
157
|
+
try {
|
|
158
|
+
if (overflowed)
|
|
159
|
+
return latest;
|
|
160
|
+
pending += decoder.decode();
|
|
161
|
+
if (sse) {
|
|
162
|
+
if (pending.length > 0)
|
|
163
|
+
consumeLine(pending);
|
|
164
|
+
pending = "";
|
|
165
|
+
return latest;
|
|
166
|
+
}
|
|
167
|
+
if (pending.trim().length === 0)
|
|
168
|
+
return latest;
|
|
169
|
+
const parsed = JSON.parse(pending);
|
|
170
|
+
pending = "";
|
|
171
|
+
return extractResponsesUsage(parsed) ?? latest;
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
return latest;
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codex-multi-auth",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.9.1",
|
|
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",
|