codex-multi-auth 2.8.7 → 2.9.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 +1 -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/codex-manager/login-flow.js +7 -0
- package/dist/lib/codex-manager/login-menu-actions.d.ts.map +1 -1
- package/dist/lib/codex-manager/login-menu-actions.js +7 -1
- 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/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/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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codex-multi-auth",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.9.0",
|
|
4
4
|
"description": "Install and operate codex-multi-auth for the official @openai/codex CLI with multi-account OAuth rotation, switching, health checks, and recovery tools.",
|
|
5
5
|
"interface": {
|
|
6
6
|
"composerIcon": "./assets/codex-multi-auth-icon.svg"
|
package/README.md
CHANGED
|
@@ -392,7 +392,7 @@ codex-multi-auth doctor --json
|
|
|
392
392
|
|
|
393
393
|
## Release Notes
|
|
394
394
|
|
|
395
|
-
- Current stable: [docs/releases/v2.
|
|
395
|
+
- Current stable: [docs/releases/v2.9.0.md](docs/releases/v2.9.0.md) — install via `npm i -g codex-multi-auth`
|
|
396
396
|
- Previous stable: [docs/releases/v2.8.3.md](docs/releases/v2.8.3.md)
|
|
397
397
|
- Previous stable: [docs/releases/v2.8.2.md](docs/releases/v2.8.2.md)
|
|
398
398
|
- Previous stable: [docs/releases/v2.7.1.md](docs/releases/v2.7.1.md)
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAe,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAe,MAAM,kBAAkB,CAAC;AAoW5D;;;;;;;;;;;;;;;GAeG;AAEH,eAAO,MAAM,iBAAiB,EAAE,MAuwI/B,CAAC;AAEF,eAAO,MAAM,gBAAgB,KAAoB,CAAC;AAElD,eAAe,iBAAiB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -38,6 +38,14 @@ import { LiveAccountSync } from "./lib/live-account-sync.js";
|
|
|
38
38
|
import { clearCorrelationId, initLogger, logDebug, logError, logInfo, logRequest, logWarn, setCorrelationId, } from "./lib/logger.js";
|
|
39
39
|
import { PreemptiveQuotaScheduler, readQuotaSchedulerSnapshot, } from "./lib/preemptive-quota-scheduler.js";
|
|
40
40
|
import { createRuntimeUsageRecorder, evaluateRuntimePolicy, loadRuntimePolicyState, } from "./lib/policy/runtime-policy.js";
|
|
41
|
+
import { createStreamUsageDeferral } from "./lib/usage/stream-usage-deferral.js";
|
|
42
|
+
/**
|
|
43
|
+
* How long a streaming request's usage-ledger row may wait for the upstream
|
|
44
|
+
* `usage` event before it is written without token counts. Long enough not to
|
|
45
|
+
* truncate a genuinely slow completion; the timer is unref'd so it never holds
|
|
46
|
+
* the process open. See createStreamUsageDeferral.
|
|
47
|
+
*/
|
|
48
|
+
const USAGE_STREAM_RECORD_FALLBACK_MS = 15 * 60_000;
|
|
41
49
|
import { getModelFamily, MODEL_FAMILIES, prewarmCodexInstructions, } from "./lib/prompts/codex.js";
|
|
42
50
|
import { prewarmHostCodexPrompt } from "./lib/prompts/host-codex-prompt.js";
|
|
43
51
|
import { fetchCodexQuotaSnapshot, formatQuotaSnapshotLine, } from "./lib/quota-probe.js";
|
|
@@ -546,6 +554,18 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
546
554
|
async fetch(input, init) {
|
|
547
555
|
let usageRecorder = null;
|
|
548
556
|
let usageCompletion = null;
|
|
557
|
+
// A non-streaming response reports its token counts while
|
|
558
|
+
// handleSuccessResponse is still assembling the body, so they
|
|
559
|
+
// are folded straight into usageCompletion below. A streaming
|
|
560
|
+
// one reports them only as the client drains the body, long
|
|
561
|
+
// after the `finally` that writes the row — so that row is
|
|
562
|
+
// handed to the deferral instead of written empty.
|
|
563
|
+
const usageDeferral = createStreamUsageDeferral({
|
|
564
|
+
record: (completion) => {
|
|
565
|
+
void usageRecorder?.record(completion);
|
|
566
|
+
},
|
|
567
|
+
fallbackMs: USAGE_STREAM_RECORD_FALLBACK_MS,
|
|
568
|
+
});
|
|
549
569
|
try {
|
|
550
570
|
if (cachedAccountManager &&
|
|
551
571
|
cachedAccountManager !== accountManager) {
|
|
@@ -1594,6 +1614,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1594
1614
|
sessionAffinityStore?.updateLastResponseId(sessionAffinityKey, responseId, Date.now(), sessionAffinityVersion);
|
|
1595
1615
|
storedResponseIdForSuccess = true;
|
|
1596
1616
|
},
|
|
1617
|
+
onUsage: usageDeferral.onUsage,
|
|
1597
1618
|
streamStallTimeoutMs,
|
|
1598
1619
|
});
|
|
1599
1620
|
if (!isStreaming && emptyResponseMaxRetries > 0) {
|
|
@@ -1679,7 +1700,20 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1679
1700
|
accountId: successAccountForResponse.accountId ?? null,
|
|
1680
1701
|
email: successAccountForResponse.email ?? null,
|
|
1681
1702
|
},
|
|
1703
|
+
// Already known for a non-streaming response, which
|
|
1704
|
+
// reported its usage while the body was being
|
|
1705
|
+
// assembled above.
|
|
1706
|
+
...(usageDeferral.captured() ?? {}),
|
|
1682
1707
|
};
|
|
1708
|
+
if (isStreaming && !usageDeferral.captured()) {
|
|
1709
|
+
// Writing the row in the `finally` below would stamp
|
|
1710
|
+
// zero tokens — the client has not read a byte yet —
|
|
1711
|
+
// and the ledger is append-only, so the real counts
|
|
1712
|
+
// could never correct it. Zero tokens is exactly what
|
|
1713
|
+
// left maxTokens / maxCostUsd unenforceable here.
|
|
1714
|
+
usageDeferral.defer(usageCompletion);
|
|
1715
|
+
usageCompletion = null;
|
|
1716
|
+
}
|
|
1683
1717
|
if (lastCodexCliActiveSyncIndex !==
|
|
1684
1718
|
successAccountForResponse.index) {
|
|
1685
1719
|
void accountManager.syncCodexCliActiveSelectionForIndex(successAccountForResponse.index);
|
|
@@ -13,7 +13,9 @@ export interface AccountPolicyStore {
|
|
|
13
13
|
accounts: Record<string, AccountPolicy>;
|
|
14
14
|
}
|
|
15
15
|
export declare function getAccountPolicyPath(): string;
|
|
16
|
-
export declare function getAccountPolicyKey(account: Pick<AccountMetadataV3, "accountId" | "email"
|
|
16
|
+
export declare function getAccountPolicyKey(account: Pick<AccountMetadataV3, "accountId" | "email"> & {
|
|
17
|
+
refreshToken?: string | null;
|
|
18
|
+
}, _index?: number): string;
|
|
17
19
|
export declare function loadAccountPolicyStore(): Promise<AccountPolicyStore>;
|
|
18
20
|
export declare function saveAccountPolicyStore(store: AccountPolicyStore): Promise<void>;
|
|
19
21
|
export declare function upsertAccountPolicy(store: AccountPolicyStore, accountKey: string, mutate: (policy: AccountPolicy) => void, now?: number): AccountPolicy;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"account-policy.d.ts","sourceRoot":"","sources":["../../lib/account-policy.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAItD,MAAM,WAAW,aAAa;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,kBAAkB;IAClC,OAAO,EAAE,CAAC,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;CACxC;AAkFD,wBAAgB,oBAAoB,IAAI,MAAM,CAE7C;AAED,wBAAgB,mBAAmB,CAClC,OAAO,EAAE,IAAI,CAAC,iBAAiB,EAAE,WAAW,GAAG,OAAO,CAAC,
|
|
1
|
+
{"version":3,"file":"account-policy.d.ts","sourceRoot":"","sources":["../../lib/account-policy.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAItD,MAAM,WAAW,aAAa;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,kBAAkB;IAClC,OAAO,EAAE,CAAC,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;CACxC;AAkFD,wBAAgB,oBAAoB,IAAI,MAAM,CAE7C;AAED,wBAAgB,mBAAmB,CAClC,OAAO,EAAE,IAAI,CAAC,iBAAiB,EAAE,WAAW,GAAG,OAAO,CAAC,GAAG;IACzD,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B,EACD,MAAM,CAAC,EAAE,MAAM,GACb,MAAM,CAmBR;AAED,wBAAsB,sBAAsB,IAAI,OAAO,CAAC,kBAAkB,CAAC,CAa1E;AAED,wBAAsB,sBAAsB,CAC3C,KAAK,EAAE,kBAAkB,GACvB,OAAO,CAAC,IAAI,CAAC,CAsCf;AAED,wBAAgB,mBAAmB,CAClC,KAAK,EAAE,kBAAkB,EACzB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,CAAC,MAAM,EAAE,aAAa,KAAK,IAAI,EACvC,GAAG,SAAa,GACd,aAAa,CAgBf;AAED,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAEtE;AAED,wBAAgB,oCAAoC,IAAI,IAAI,CAE3D"}
|
|
@@ -81,9 +81,22 @@ export function getAccountPolicyPath() {
|
|
|
81
81
|
return join(getCodexMultiAuthDir(), ACCOUNT_POLICY_FILE_NAME);
|
|
82
82
|
}
|
|
83
83
|
export function getAccountPolicyKey(account, _index) {
|
|
84
|
+
// The key must be stable across processes and independent of the account's
|
|
85
|
+
// slot (policy state is persisted and survives reordering), so it is derived
|
|
86
|
+
// from identity, never from `_index`.
|
|
87
|
+
//
|
|
88
|
+
// An account whose accountId AND email are both missing — freshly imported,
|
|
89
|
+
// or not yet hydrated from its token — used to hash the literal "unknown",
|
|
90
|
+
// which gave EVERY such account the same key: pausing, draining or tagging
|
|
91
|
+
// one silently applied to all of them. Fall back to the refresh token
|
|
92
|
+
// instead, which every account has and no two accounts share. It is
|
|
93
|
+
// namespaced before hashing so a token value can never collide with an
|
|
94
|
+
// email or accountId of the same text, and only its digest is persisted.
|
|
95
|
+
// Mirrors getAccountIdentityKey's `allowRefreshFallback`.
|
|
96
|
+
const refreshFallback = account.refreshToken?.trim();
|
|
84
97
|
const identity = account.accountId?.trim() ||
|
|
85
98
|
account.email?.trim().toLowerCase() ||
|
|
86
|
-
"unknown";
|
|
99
|
+
(refreshFallback ? `refresh:${refreshFallback}` : "unknown");
|
|
87
100
|
return `sha256:${createHash("sha256").update(identity).digest("hex")}`;
|
|
88
101
|
}
|
|
89
102
|
export async function loadAccountPolicyStore() {
|
|
@@ -290,6 +290,13 @@ async function runAuthLoginFlow(loginOptions, deps) {
|
|
|
290
290
|
parsed: targetIndex + 1,
|
|
291
291
|
switchReason: "restore",
|
|
292
292
|
preserveActiveIndexByFamily: true,
|
|
293
|
+
// A restore replaces the whole account list, so every
|
|
294
|
+
// index moves. Session affinity remembers accounts BY
|
|
295
|
+
// INDEX, so without this a live proxy keeps routing
|
|
296
|
+
// in-flight sessions to whatever now occupies the old
|
|
297
|
+
// slot. `switch`/`best` already bump for a single
|
|
298
|
+
// move; a restore moves everything. See issue #474.
|
|
299
|
+
bumpAffinityGeneration: true,
|
|
293
300
|
});
|
|
294
301
|
console.log(UI_COPY.oauth.restoreBackupLoaded(selectedBackup.fileName, restoredStorage.accounts.length));
|
|
295
302
|
if (!synced) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"login-menu-actions.d.ts","sourceRoot":"","sources":["../../../lib/codex-manager/login-menu-actions.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAEjD,OAAO,EAEN,KAAK,gBAAgB,
|
|
1
|
+
{"version":3,"file":"login-menu-actions.d.ts","sourceRoot":"","sources":["../../../lib/codex-manager/login-menu-actions.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAEjD,OAAO,EAEN,KAAK,gBAAgB,EAKrB,KAAK,kBAAkB,EAGvB,MAAM,eAAe,CAAC;AASvB,OAAO,EACN,KAAK,eAAe,EAKpB,MAAM,kBAAkB,CAAC;AAG1B;;;;;GAKG;AAEH,gBAAgB;AAChB,MAAM,MAAM,iBAAiB,GAAG,QAAQ,GAAG,QAAQ,GAAG,MAAM,CAAC;AAE7D,gBAAgB;AAChB,wBAAsB,qBAAqB,CAC1C,YAAY,EAAE,kBAAkB,GAAG,IAAI,EACvC,sBAAsB,GAAE,MAAM,GAAG,IAAW,GAC1C,OAAO,CAAC,eAAe,CAAC,CA6D1B;AAED,gBAAgB;AAChB,wBAAsB,uBAAuB,CAC5C,YAAY,EAAE,kBAAkB,GAC9B,OAAO,CAAC,iBAAiB,CAAC,CA0C5B;AAED,gBAAgB;AAChB,wBAAsB,2BAA2B,CAChD,OAAO,EAAE,kBAAkB,EAAE,GAC3B,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAkCpC;AA8GD,gBAAgB;AAChB,wBAAsB,kBAAkB,CACvC,OAAO,EAAE,gBAAgB,EACzB,UAAU,EAAE,OAAO,CAAC,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC,GACrD,OAAO,CAAC,IAAI,CAAC,CAgIf"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { stdin as input, stdout as output } from "node:process";
|
|
2
2
|
import { sanitizeEmail } from "../accounts.js";
|
|
3
3
|
import { MODEL_FAMILIES } from "../prompts/codex.js";
|
|
4
|
-
import { findMatchingAccountIndex, loadAccounts, reconcilePinnedAccountIndex, setStoragePath, withAccountStorageTransaction, } from "../storage.js";
|
|
4
|
+
import { bumpStorageAffinityGeneration, findMatchingAccountIndex, loadAccounts, reconcilePinnedAccountIndex, setStoragePath, withAccountStorageTransaction, } from "../storage.js";
|
|
5
5
|
import { UI_COPY } from "../ui/ui-copy.js";
|
|
6
6
|
import { getUiRuntimeOptions } from "../ui/runtime.js";
|
|
7
7
|
import { select } from "../ui/select.js";
|
|
@@ -249,6 +249,12 @@ export async function handleManageAction(storage, menuResult) {
|
|
|
249
249
|
nextStorage.accounts.splice(nextIndex, 1);
|
|
250
250
|
resetManageActionSelection(nextStorage, nextIndex);
|
|
251
251
|
nextStorage.pinnedAccountIndex = reconcilePinnedAccountIndex(pinnedAccount, nextStorage.accounts);
|
|
252
|
+
// The splice shifts every index above the removed slot, and session
|
|
253
|
+
// affinity remembers accounts BY INDEX. Without a generation bump a
|
|
254
|
+
// live proxy keeps routing in-flight sessions to the account that
|
|
255
|
+
// slid into the old slot. Reconciling the pin is not enough — the
|
|
256
|
+
// affinity map is separate state. See issue #474.
|
|
257
|
+
bumpStorageAffinityGeneration(nextStorage);
|
|
252
258
|
await persist(nextStorage);
|
|
253
259
|
replaceManageActionStorage(storage, nextStorage);
|
|
254
260
|
deleted = true;
|
|
@@ -7,6 +7,13 @@ export interface RuntimePolicyAccount {
|
|
|
7
7
|
index: number;
|
|
8
8
|
accountId?: string | null;
|
|
9
9
|
email?: string | null;
|
|
10
|
+
/**
|
|
11
|
+
* Only used to key account policy for an account with no accountId and no
|
|
12
|
+
* email. Without it the runtime would key such an account as "unknown"
|
|
13
|
+
* while the CLI keys it by refresh-token digest, and pause/drain/tag state
|
|
14
|
+
* written by one would be invisible to the other.
|
|
15
|
+
*/
|
|
16
|
+
refreshToken?: string | null;
|
|
10
17
|
}
|
|
11
18
|
export interface RuntimePolicyDecision {
|
|
12
19
|
allowed: boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime-policy.d.ts","sourceRoot":"","sources":["../../../lib/policy/runtime-policy.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAErE,OAAO,EAGN,KAAK,kBAAkB,EACvB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAKN,KAAK,qBAAqB,EAC1B,KAAK,gBAAgB,EACrB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAEN,KAAK,4BAA4B,EACjC,MAAM,wBAAwB,CAAC;AAChC,OAAO,EACN,oBAAoB,EAGpB,KAAK,oBAAoB,EACzB,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,EACtB,MAAM,mBAAmB,CAAC;AAE3B,MAAM,WAAW,oBAAoB;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"runtime-policy.d.ts","sourceRoot":"","sources":["../../../lib/policy/runtime-policy.ts"],"names":[],"mappings":"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"}
|
|
@@ -98,6 +98,7 @@ export async function evaluateRuntimePolicy(input) {
|
|
|
98
98
|
const accountKey = getAccountPolicyKey({
|
|
99
99
|
accountId: account.accountId ?? undefined,
|
|
100
100
|
email: account.email ?? undefined,
|
|
101
|
+
refreshToken: account.refreshToken ?? undefined,
|
|
101
102
|
}, account.index);
|
|
102
103
|
const accountPolicy = input.state.accountPolicies.accounts[accountKey];
|
|
103
104
|
let boost = 0;
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { type FastSessionInputTrimPlan } from "./request-transformer.js";
|
|
6
6
|
import type { UserConfig, RequestBody } from "../types.js";
|
|
7
|
+
import type { UsageTokenCounts } from "../usage/types.js";
|
|
7
8
|
export { shouldRefreshToken, refreshAndUpdateToken } from "./token-refresh.js";
|
|
8
9
|
export { DEFAULT_UNSUPPORTED_CODEX_FALLBACK_CHAIN, extractUnsupportedCodexModelFromText, getUnsupportedCodexModelInfo, resolveUnsupportedCodexFallbackModel, shouldFallbackToGpt52OnUnsupportedGpt53, isEntitlementError, isWorkspaceDisabledError, createEntitlementErrorResponse, } from "./error-classification.js";
|
|
9
10
|
export type { EntitlementError, UnsupportedCodexModelInfo, ResolveUnsupportedCodexFallbackOptions, } from "./error-classification.js";
|
|
@@ -70,6 +71,12 @@ export declare function handleErrorResponse(response: Response, options?: ErrorH
|
|
|
70
71
|
*/
|
|
71
72
|
export declare function handleSuccessResponse(response: Response, isStreaming: boolean, options?: {
|
|
72
73
|
onResponseId?: (responseId: string) => void;
|
|
74
|
+
/**
|
|
75
|
+
* Reports the upstream token counts once they are known, for the usage
|
|
76
|
+
* ledger. Fires from the SSE tee on the streaming path and from the
|
|
77
|
+
* assembled response on the non-streaming path.
|
|
78
|
+
*/
|
|
79
|
+
onUsage?: (usage: UsageTokenCounts) => void;
|
|
73
80
|
streamStallTimeoutMs?: number;
|
|
74
81
|
}): Promise<Response>;
|
|
75
82
|
//# sourceMappingURL=fetch-helpers.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fetch-helpers.d.ts","sourceRoot":"","sources":["../../../lib/request/fetch-helpers.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,OAAO,EAIN,KAAK,wBAAwB,EAC7B,MAAM,0BAA0B,CAAC;AAMlC,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"fetch-helpers.d.ts","sourceRoot":"","sources":["../../../lib/request/fetch-helpers.ts"],"names":[],"mappings":"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"}
|
|
@@ -155,7 +155,7 @@ export async function handleSuccessResponse(response, isStreaming, options) {
|
|
|
155
155
|
return await convertSseToJson(response, responseHeaders, options);
|
|
156
156
|
}
|
|
157
157
|
// For streaming requests (streamText), return stream as-is
|
|
158
|
-
return attachResponseIdCapture(response, responseHeaders, options?.onResponseId);
|
|
158
|
+
return attachResponseIdCapture(response, responseHeaders, options?.onResponseId, options?.onUsage);
|
|
159
159
|
}
|
|
160
160
|
async function safeReadBody(response) {
|
|
161
161
|
try {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { UsageTokenCounts } from "../usage/types.js";
|
|
1
2
|
/**
|
|
2
3
|
* Convert SSE stream response to JSON for generateText()
|
|
3
4
|
* @param response - Fetch response with SSE stream
|
|
@@ -6,6 +7,7 @@
|
|
|
6
7
|
*/
|
|
7
8
|
export declare function convertSseToJson(response: Response, headers: Headers, options?: {
|
|
8
9
|
onResponseId?: (responseId: string) => void;
|
|
10
|
+
onUsage?: (usage: UsageTokenCounts) => void;
|
|
9
11
|
streamStallTimeoutMs?: number;
|
|
10
12
|
}): Promise<Response>;
|
|
11
13
|
/**
|
|
@@ -21,5 +23,5 @@ export declare function ensureContentType(headers: Headers): Headers;
|
|
|
21
23
|
* @returns True if response should be considered empty/malformed
|
|
22
24
|
*/
|
|
23
25
|
export declare function isEmptyResponse(body: unknown): boolean;
|
|
24
|
-
export declare function attachResponseIdCapture(response: Response, headers: Headers, onResponseId?: (responseId: string) => void): Response;
|
|
26
|
+
export declare function attachResponseIdCapture(response: Response, headers: Headers, onResponseId?: (responseId: string) => void, onUsage?: (usage: UsageTokenCounts) => void): Response;
|
|
25
27
|
//# sourceMappingURL=response-handler.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"response-handler.d.ts","sourceRoot":"","sources":["../../../lib/request/response-handler.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"response-handler.d.ts","sourceRoot":"","sources":["../../../lib/request/response-handler.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAqwB1D;;;;;GAKG;AACH,wBAAsB,gBAAgB,CACrC,QAAQ,EAAE,QAAQ,EAClB,OAAO,EAAE,OAAO,EAChB,OAAO,CAAC,EAAE;IACT,YAAY,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5C,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,gBAAgB,KAAK,IAAI,CAAC;IAC5C,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC9B,GACC,OAAO,CAAC,QAAQ,CAAC,CAkInB;AAwED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAQ3D;AA2BD;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CA4CtD;AAED,wBAAgB,uBAAuB,CACtC,QAAQ,EAAE,QAAQ,EAClB,OAAO,EAAE,OAAO,EAChB,YAAY,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,EAC3C,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,gBAAgB,KAAK,IAAI,GACzC,QAAQ,CAiBV"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createLogger, logRequest, LOGGING_ENABLED } from "../logger.js";
|
|
2
2
|
import { HTTP_STATUS, PLUGIN_NAME } from "../constants.js";
|
|
3
3
|
import { isRecord } from "../utils.js";
|
|
4
|
+
import { extractResponsesUsage } from "../usage/usage-extraction.js";
|
|
4
5
|
const log = createLogger("response-handler");
|
|
5
6
|
const MAX_SSE_SIZE = 10 * 1024 * 1024; // 10MB limit to prevent memory exhaustion
|
|
6
7
|
const DEFAULT_STREAM_STALL_TIMEOUT_MS = 45_000;
|
|
@@ -663,6 +664,15 @@ export async function convertSseToJson(response, headers, options) {
|
|
|
663
664
|
});
|
|
664
665
|
}
|
|
665
666
|
logStreamDiagnostics(finalResponse);
|
|
667
|
+
if (options?.onUsage) {
|
|
668
|
+
// The non-streaming branch already holds the assembled response, so
|
|
669
|
+
// the token counts come straight off it. Reported even though the
|
|
670
|
+
// caller sees JSON rather than SSE: the usage ledger (and therefore
|
|
671
|
+
// the maxTokens / maxCostUsd budget caps) needs both shapes.
|
|
672
|
+
const usage = extractResponsesUsage(finalResponse);
|
|
673
|
+
if (usage)
|
|
674
|
+
options.onUsage(usage);
|
|
675
|
+
}
|
|
666
676
|
// Return as plain JSON (not SSE)
|
|
667
677
|
const jsonHeaders = new Headers(headers);
|
|
668
678
|
jsonHeaders.set("content-type", "application/json; charset=utf-8");
|
|
@@ -685,7 +695,7 @@ export async function convertSseToJson(response, headers, options) {
|
|
|
685
695
|
reader.releaseLock();
|
|
686
696
|
}
|
|
687
697
|
}
|
|
688
|
-
function createResponseIdCapturingStream(body, onResponseId) {
|
|
698
|
+
function createResponseIdCapturingStream(body, onResponseId, onUsage) {
|
|
689
699
|
const decoder = new TextDecoder();
|
|
690
700
|
let bufferedText = "";
|
|
691
701
|
let loggedMalformedStreamChunk = false;
|
|
@@ -709,7 +719,18 @@ function createResponseIdCapturingStream(body, onResponseId) {
|
|
|
709
719
|
continue;
|
|
710
720
|
try {
|
|
711
721
|
const data = JSON.parse(payload);
|
|
712
|
-
|
|
722
|
+
if (onResponseId) {
|
|
723
|
+
maybeCaptureResponseEvent(state, data, onResponseId);
|
|
724
|
+
}
|
|
725
|
+
if (onUsage) {
|
|
726
|
+
// Reuse the JSON this loop already parsed rather than scanning
|
|
727
|
+
// the stream twice. Without these counts every usage-ledger row
|
|
728
|
+
// lands with zero tokens and zero cost, which silently disables
|
|
729
|
+
// the maxTokens / maxCostUsd budget caps.
|
|
730
|
+
const usage = extractResponsesUsage(data);
|
|
731
|
+
if (usage)
|
|
732
|
+
onUsage(usage);
|
|
733
|
+
}
|
|
713
734
|
if (state.encounteredError)
|
|
714
735
|
break;
|
|
715
736
|
}
|
|
@@ -812,15 +833,15 @@ export function isEmptyResponse(body) {
|
|
|
812
833
|
}
|
|
813
834
|
return false;
|
|
814
835
|
}
|
|
815
|
-
export function attachResponseIdCapture(response, headers, onResponseId) {
|
|
816
|
-
if (!response.body || !onResponseId) {
|
|
836
|
+
export function attachResponseIdCapture(response, headers, onResponseId, onUsage) {
|
|
837
|
+
if (!response.body || (!onResponseId && !onUsage)) {
|
|
817
838
|
return new Response(response.body, {
|
|
818
839
|
status: response.status,
|
|
819
840
|
statusText: response.statusText,
|
|
820
841
|
headers,
|
|
821
842
|
});
|
|
822
843
|
}
|
|
823
|
-
return new Response(createResponseIdCapturingStream(response.body, onResponseId), {
|
|
844
|
+
return new Response(createResponseIdCapturingStream(response.body, onResponseId, onUsage), {
|
|
824
845
|
status: response.status,
|
|
825
846
|
statusText: response.statusText,
|
|
826
847
|
headers,
|
|
@@ -4,5 +4,12 @@ export declare const HOP_BY_HOP_HEADERS: Set<string>;
|
|
|
4
4
|
export declare function responseHeadersForClient(upstreamHeaders: Headers): Record<string, string>;
|
|
5
5
|
export declare function withTimeout<T>(promise: Promise<T>, timeoutMs: number, onTimeout: () => void, message: string): Promise<T>;
|
|
6
6
|
export declare function readErrorBody(response: Response, timeoutMs: number, maxBytes?: number): Promise<string>;
|
|
7
|
-
export declare function forwardStreamingResponse(upstream: Response, res: ServerResponse, status: RuntimeRotationProxyStatus, onStreamError: () => void, streamStallTimeoutMs: number
|
|
7
|
+
export declare function forwardStreamingResponse(upstream: Response, res: ServerResponse, status: RuntimeRotationProxyStatus, onStreamError: () => void, streamStallTimeoutMs: number,
|
|
8
|
+
/**
|
|
9
|
+
* Observe each forwarded chunk. Used to recover the upstream `usage` totals
|
|
10
|
+
* for the usage ledger without buffering the body; it must not mutate the
|
|
11
|
+
* chunk and is called before the write, so a slow observer delays the
|
|
12
|
+
* forward. Implementations are expected to swallow their own errors.
|
|
13
|
+
*/
|
|
14
|
+
onChunk?: (chunk: Uint8Array) => void): Promise<boolean>;
|
|
8
15
|
//# sourceMappingURL=stream-failover-runtime.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stream-failover-runtime.d.ts","sourceRoot":"","sources":["../../../lib/request/stream-failover-runtime.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAChD,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,qCAAqC,CAAC;AAEtF,eAAO,MAAM,kBAAkB,aAW7B,CAAC;AAWH,wBAAgB,wBAAwB,CAAC,eAAe,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAUzF;AAED,wBAAsB,WAAW,CAAC,CAAC,EAClC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,EACnB,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,IAAI,EACrB,OAAO,EAAE,MAAM,GACb,OAAO,CAAC,CAAC,CAAC,CAmBZ;AAED,wBAAsB,aAAa,CAClC,QAAQ,EAAE,QAAQ,EAClB,SAAS,EAAE,MAAM,EACjB,QAAQ,SAAc,GACpB,OAAO,CAAC,MAAM,CAAC,CAuDjB;AAoBD,wBAAsB,wBAAwB,CAC7C,QAAQ,EAAE,QAAQ,EAClB,GAAG,EAAE,cAAc,EACnB,MAAM,EAAE,0BAA0B,EAClC,aAAa,EAAE,MAAM,IAAI,EACzB,oBAAoB,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"stream-failover-runtime.d.ts","sourceRoot":"","sources":["../../../lib/request/stream-failover-runtime.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAChD,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,qCAAqC,CAAC;AAEtF,eAAO,MAAM,kBAAkB,aAW7B,CAAC;AAWH,wBAAgB,wBAAwB,CAAC,eAAe,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAUzF;AAED,wBAAsB,WAAW,CAAC,CAAC,EAClC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,EACnB,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,IAAI,EACrB,OAAO,EAAE,MAAM,GACb,OAAO,CAAC,CAAC,CAAC,CAmBZ;AAED,wBAAsB,aAAa,CAClC,QAAQ,EAAE,QAAQ,EAClB,SAAS,EAAE,MAAM,EACjB,QAAQ,SAAc,GACpB,OAAO,CAAC,MAAM,CAAC,CAuDjB;AAoBD,wBAAsB,wBAAwB,CAC7C,QAAQ,EAAE,QAAQ,EAClB,GAAG,EAAE,cAAc,EACnB,MAAM,EAAE,0BAA0B,EAClC,aAAa,EAAE,MAAM,IAAI,EACzB,oBAAoB,EAAE,MAAM;AAC5B;;;;;GAKG;AACH,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,GACnC,OAAO,CAAC,OAAO,CAAC,CAsDlB"}
|
|
@@ -128,7 +128,14 @@ function waitForDrain(res) {
|
|
|
128
128
|
res.once("error", settle);
|
|
129
129
|
});
|
|
130
130
|
}
|
|
131
|
-
export async function forwardStreamingResponse(upstream, res, status, onStreamError, streamStallTimeoutMs
|
|
131
|
+
export async function forwardStreamingResponse(upstream, res, status, onStreamError, streamStallTimeoutMs,
|
|
132
|
+
/**
|
|
133
|
+
* Observe each forwarded chunk. Used to recover the upstream `usage` totals
|
|
134
|
+
* for the usage ledger without buffering the body; it must not mutate the
|
|
135
|
+
* chunk and is called before the write, so a slow observer delays the
|
|
136
|
+
* forward. Implementations are expected to swallow their own errors.
|
|
137
|
+
*/
|
|
138
|
+
onChunk) {
|
|
132
139
|
status.streamsStarted += 1;
|
|
133
140
|
res.writeHead(upstream.status, responseHeadersForClient(upstream.headers));
|
|
134
141
|
if (!upstream.body) {
|
|
@@ -149,6 +156,7 @@ export async function forwardStreamingResponse(upstream, res, status, onStreamEr
|
|
|
149
156
|
if (done)
|
|
150
157
|
break;
|
|
151
158
|
if (value && value.byteLength > 0) {
|
|
159
|
+
onChunk?.(value);
|
|
152
160
|
// If the response is already finished (clean end by a concurrent
|
|
153
161
|
// close-then-reader-cancel path), stop writing. Do NOT guard on
|
|
154
162
|
// res.destroyed here: a socket-error-during-backpressure scenario sets
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stream-failover.d.ts","sourceRoot":"","sources":["../../../lib/request/stream-failover.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,qBAAqB;IACrC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAkGD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,qBAAqB,CACpC,eAAe,EAAE,QAAQ,EACzB,mBAAmB,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,EACxF,OAAO,GAAE,qBAA0B,GACjC,QAAQ,
|
|
1
|
+
{"version":3,"file":"stream-failover.d.ts","sourceRoot":"","sources":["../../../lib/request/stream-failover.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,qBAAqB;IACrC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAkGD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,qBAAqB,CACpC,eAAe,EAAE,QAAQ,EACzB,mBAAmB,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,EACxF,OAAO,GAAE,qBAA0B,GACjC,QAAQ,CAuKV"}
|
|
@@ -117,6 +117,34 @@ export function withStreamingFailover(initialResponse, getFallbackResponse, opti
|
|
|
117
117
|
}
|
|
118
118
|
let closed = false;
|
|
119
119
|
let releaseCurrentReaderForCancel = null;
|
|
120
|
+
// Backpressure. `pump()` runs from `start()` as a free-running loop, so
|
|
121
|
+
// without a demand signal it enqueues every chunk upstream delivers as fast
|
|
122
|
+
// as it arrives and the whole stream buffers in memory for a slow consumer.
|
|
123
|
+
// `pull()` (and cancellation) release the loop; the sibling forwarder in
|
|
124
|
+
// stream-failover-runtime.ts gets the same guarantee from waitForDrain.
|
|
125
|
+
let demandWaiter = null;
|
|
126
|
+
let demandPending = false;
|
|
127
|
+
const signalDemand = () => {
|
|
128
|
+
if (demandWaiter) {
|
|
129
|
+
demandWaiter();
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
// `pull()` can fire before the loop parks. Latch it, or the next
|
|
133
|
+
// awaitDemand() would block on a signal that already happened.
|
|
134
|
+
demandPending = true;
|
|
135
|
+
};
|
|
136
|
+
const awaitDemand = () => {
|
|
137
|
+
if (demandPending || closed) {
|
|
138
|
+
demandPending = false;
|
|
139
|
+
return Promise.resolve();
|
|
140
|
+
}
|
|
141
|
+
return new Promise((resolve) => {
|
|
142
|
+
demandWaiter = () => {
|
|
143
|
+
demandWaiter = null;
|
|
144
|
+
resolve();
|
|
145
|
+
};
|
|
146
|
+
});
|
|
147
|
+
};
|
|
120
148
|
const body = new ReadableStream({
|
|
121
149
|
start(controller) {
|
|
122
150
|
let currentReader = initialResponse.body?.getReader() ?? null;
|
|
@@ -174,6 +202,13 @@ export function withStreamingFailover(initialResponse, getFallbackResponse, opti
|
|
|
174
202
|
if (result.value && result.value.byteLength > 0) {
|
|
175
203
|
emittedBytes += result.value.byteLength;
|
|
176
204
|
controller.enqueue(result.value);
|
|
205
|
+
// Park until the consumer asks for more. desiredSize is
|
|
206
|
+
// null once the controller is errored or closed, and the
|
|
207
|
+
// loop's own `closed` checks cover that case.
|
|
208
|
+
if (typeof controller.desiredSize === "number" &&
|
|
209
|
+
controller.desiredSize <= 0) {
|
|
210
|
+
await awaitDemand();
|
|
211
|
+
}
|
|
177
212
|
}
|
|
178
213
|
}
|
|
179
214
|
catch (error) {
|
|
@@ -206,8 +241,14 @@ export function withStreamingFailover(initialResponse, getFallbackResponse, opti
|
|
|
206
241
|
}
|
|
207
242
|
});
|
|
208
243
|
},
|
|
244
|
+
pull() {
|
|
245
|
+
signalDemand();
|
|
246
|
+
},
|
|
209
247
|
cancel() {
|
|
210
248
|
closed = true;
|
|
249
|
+
// Release a parked pump so it observes `closed` and unwinds instead
|
|
250
|
+
// of holding the upstream reader open for a demand that never comes.
|
|
251
|
+
signalDemand();
|
|
211
252
|
if (releaseCurrentReaderForCancel) {
|
|
212
253
|
void releaseCurrentReaderForCancel();
|
|
213
254
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"account-check.d.ts","sourceRoot":"","sources":["../../../lib/runtime/account-check.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AACvD,OAAO,EACN,KAAK,gBAAgB,
|
|
1
|
+
{"version":3,"file":"account-check.d.ts","sourceRoot":"","sources":["../../../lib/runtime/account-check.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { maskEmail } from "../logger.js";
|
|
2
2
|
import { CODEX_UNAVAILABLE_PROBE_NOTE } from "../quota-probe.js";
|
|
3
3
|
import { isCodexUnavailableError } from "../errors.js";
|
|
4
|
-
import { reconcilePinnedAccountIndex, } from "../storage.js";
|
|
4
|
+
import { bumpStorageAffinityGeneration, reconcilePinnedAccountIndex, } from "../storage.js";
|
|
5
5
|
export async function runRuntimeAccountCheck(deepProbe, deps) {
|
|
6
6
|
const loadedStorage = await deps.hydrateEmails(await deps.loadAccounts());
|
|
7
7
|
const workingStorage = loadedStorage
|
|
@@ -195,6 +195,13 @@ export async function runRuntimeAccountCheck(deepProbe, deps) {
|
|
|
195
195
|
workingStorage.accounts = workingStorage.accounts.filter((account) => !state.removeFromActive.has(account.refreshToken));
|
|
196
196
|
deps.clampRuntimeActiveIndices(workingStorage, deps.MODEL_FAMILIES);
|
|
197
197
|
workingStorage.pinnedAccountIndex = reconcilePinnedAccountIndex(pinnedAccount, workingStorage.accounts);
|
|
198
|
+
// The filter can drop several accounts at once, shifting every index
|
|
199
|
+
// after each hole. Session affinity remembers accounts BY INDEX, so
|
|
200
|
+
// without a generation bump a live proxy keeps routing in-flight
|
|
201
|
+
// sessions to whatever now occupies the old slot — and this path is
|
|
202
|
+
// AUTOMATIC (revoked-token accounts are removed without a prompt), so
|
|
203
|
+
// nothing else invalidates the affinity map. See issue #474.
|
|
204
|
+
bumpStorageAffinityGeneration(workingStorage);
|
|
198
205
|
state.storageChanged = true;
|
|
199
206
|
}
|
|
200
207
|
const persistAccountAndFlaggedStorage = deps.persistAccountAndFlaggedStorage;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime-rotation-proxy.d.ts","sourceRoot":"","sources":["../../lib/runtime-rotation-proxy.ts"],"names":[],"mappings":"AAGA,OAAO,EAIN,KAAK,cAAc,EACnB,MAAM,eAAe,CAAC;AA8BvB,OAAO,EAAkB,KAAK,WAAW,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"runtime-rotation-proxy.d.ts","sourceRoot":"","sources":["../../lib/runtime-rotation-proxy.ts"],"names":[],"mappings":"AAGA,OAAO,EAIN,KAAK,cAAc,EACnB,MAAM,eAAe,CAAC;AA8BvB,OAAO,EAAkB,KAAK,WAAW,EAAE,MAAM,oBAAoB,CAAC;AA6CtE,OAAO,KAAK,EAKX,2BAA2B,EAC3B,0BAA0B,EAE1B,MAAM,oCAAoC,CAAC;AAe5C,YAAY,EACX,2BAA2B,EAC3B,0BAA0B,EAC1B,0BAA0B,GAC1B,MAAM,oCAAoC,CAAC;AAC5C,OAAO,EACN,+BAA+B,EAC/B,0BAA0B,GAC1B,MAAM,kCAAkC,CAAC;AAC1C,YAAY,EAAE,0BAA0B,EAAE,MAAM,kCAAkC,CAAC;AACnF,OAAO,EAAE,aAAa,EAAE,MAAM,yCAAyC,CAAC;AACxE,OAAO,EACN,+BAA+B,EAC/B,8BAA8B,EAC9B,uBAAuB,EACvB,uBAAuB,GACvB,MAAM,oCAAoC,CAAC;AAC5C,YAAY,EAAE,WAAW,EAAE,MAAM,oCAAoC,CAAC;AA8DtE,kFAAkF;AAClF,wBAAgB,qBAAqB,CACpC,OAAO,EAAE,IAAI,CAAC,cAAc,EAAE,WAAW,GAAG,OAAO,GAAG,cAAc,CAAC,GAAG;IACvE,gFAAgF;IAChF,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB,EACD,MAAM,EAAE,WAAW,EACnB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,GACnB,MAAM,CAmBR;AA8bD;;;;;;;;GAQG;AACH,wBAAgB,2BAA2B,CAC1C,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,GACvC,MAAM,GAAG,IAAI,CAUf;AAED,wBAAsB,yBAAyB,CAC9C,OAAO,EAAE,2BAA2B,GAClC,OAAO,CAAC,0BAA0B,CAAC,CA4KrC"}
|
|
@@ -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":"AA+BA,OAAO,EACN,KAAK,cAAc,EAGnB,KAAK,iBAAiB,EAEtB,MAAM,8BAA8B,CAAC;AAkBtC,OAAO,EAEN,KAAK,oBAAoB,EACzB,MAAM,qBAAqB,CAAC;AAe7B,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EACN,sBAAsB,EACtB,cAAc,GACd,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACN,qBAAqB,EACrB,iBAAiB,GACjB,MAAM,uBAAuB,CAAC;AAa/B,OAAO,KAAK,EACX,iBAAiB,EACjB,gBAAgB,EAChB,cAAc,EACd,wBAAwB,EACxB,uBAAuB,EACvB,gBAAgB,EAChB,MAAM,2BAA2B,CAAC;AAEnC,OAAO,EAEN,KAAK,kBAAkB,EACvB,MAAM,4BAA4B,CAAC;AA2CpC,YAAY,EACX,oBAAoB,EACpB,cAAc,EACd,gBAAgB,EAChB,iBAAiB,EACjB,gBAAgB,EAChB,wBAAwB,EACxB,uBAAuB,EACvB,cAAc,EACd,iBAAiB,EACjB,kBAAkB,GAClB,CAAC;AAcF,KAAK,WAAW,GAAG;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAiBF,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAE9D;AAuWD,wBAAgB,4BAA4B,IAAI,MAAM,CAErD;AAED,wBAAgB,cAAc,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CA4C/D;AAED,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAO9D;AAED;;;GAGG;AACH,wBAAgB,cAAc,IAAI,MAAM,CAMvC;AAED,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEzD;AAED,wBAAsB,eAAe,IAAI,OAAO,CAAC,kBAAkB,EAAE,CAAC,CAarE;AAED,wBAAsB,yBAAyB,CAC9C,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,GAC7B,OAAO,CAAC,gBAAgB,CAAC,CAe3B;AAED,wBAAsB,iBAAiB,CACtC,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,GAC3B,OAAO,CAAC,MAAM,CAAC,CAQjB;AAED,wBAAgB,sBAAsB,IAAI,MAAM,CAE/C;AA6LD,KAAK,mBAAmB,GAAG;IAC1B,wCAAwC,CAAC,EAAE,OAAO,CAAC;CACnD,CAAC;AA8IF,wBAAgB,wBAAwB,CACvC,CAAC,SAAS,IAAI,CAAC,WAAW,EAAE,WAAW,GAAG,OAAO,GAAG,cAAc,CAAC,EAEnE,QAAQ,EAAE,SAAS,CAAC,EAAE,EACtB,SAAS,EAAE,IAAI,CAAC,WAAW,EAAE,WAAW,GAAG,OAAO,GAAG,cAAc,CAAC,EACpE,OAAO,GAAE,mBAAwB,GAC/B,MAAM,GAAG,SAAS,CAkBpB;AAED;;;;;;;;GAQG;AACH,wBAAgB,2BAA2B,CAC1C,aAAa,EACV,IAAI,CAAC,WAAW,EAAE,WAAW,GAAG,OAAO,GAAG,cAAc,CAAC,GACzD,SAAS,EACZ,YAAY,EAAE,SAAS,IAAI,CAC1B,WAAW,EACX,WAAW,GAAG,OAAO,GAAG,cAAc,CACtC,EAAE,GACD,MAAM,GAAG,SAAS,CAGpB;AAED,wBAAgB,4BAA4B,CAC3C,CAAC,SAAS,IAAI,CAAC,WAAW,EAAE,WAAW,GAAG,OAAO,GAAG,cAAc,CAAC,EAEnE,QAAQ,EAAE,SAAS,CAAC,EAAE,EACtB,SAAS,EAAE,IAAI,CAAC,WAAW,EAAE,WAAW,GAAG,OAAO,GAAG,cAAc,CAAC,EACpE,aAAa,SAAI,GACf,MAAM,CAOR;AAsCD;;;GAGG;AACH,wBAAgB,mBAAmB,CAClC,CAAC,SAAS;IACT,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB,EACA,QAAQ,EAAE,CAAC,EAAE,GAAG,CAAC,EAAE,CAEpB;AAED,wBAAgB,0BAA0B,CACzC,CAAC,SAAS;IACT,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB,EACA,QAAQ,EAAE,CAAC,EAAE,GAAG,CAAC,EAAE,CAEpB;AAoBD;;;;;GAKG;AACH,wBAAgB,uBAAuB,CACtC,IAAI,EAAE,OAAO,GACX,gBAAgB,GAAG,IAAI,CAoJzB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,8BAA8B,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAoBnE;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,6BAA6B,CAC5C,OAAO,EAAE;IAAE,kBAAkB,CAAC,EAAE,MAAM,CAAA;CAAE,EACxC,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,GACzB,MAAM,CAaR;AAED;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG;IACpD,kBAAkB,EAAE,MAAM,GAAG,SAAS,CAAC;IACvC,kBAAkB,EAAE,MAAM,CAAC;CAC3B,CA8BA;AAED;;;;GAIG;AACH,wBAAsB,YAAY,IAAI,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAErE;AAED,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,cAAc,CAAC,CAajE;AAED,wBAAsB,oBAAoB,IAAI,OAAO,CAAC,iBAAiB,CAAC,CASvE;AAED,wBAAsB,oBAAoB,IAAI,OAAO,CAAC,oBAAoB,CAAC,CAiG1E;AAyaD,wBAAsB,6BAA6B,CAAC,CAAC,EACpD,OAAO,EAAE,CACR,OAAO,EAAE,gBAAgB,GAAG,IAAI,EAChC,OAAO,EAAE,CAAC,OAAO,EAAE,gBAAgB,KAAK,OAAO,CAAC,IAAI,CAAC,KACjD,OAAO,CAAC,CAAC,CAAC,GACb,OAAO,CAAC,CAAC,CAAC,CAMZ;AAED,wBAAsB,uCAAuC,CAAC,CAAC,EAC9D,OAAO,EAAE,CACR,OAAO,EAAE,gBAAgB,GAAG,IAAI,EAChC,OAAO,EAAE,CACR,cAAc,EAAE,gBAAgB,EAChC,cAAc,EAAE,uBAAuB,KACnC,OAAO,CAAC,IAAI,CAAC,EAClB,cAAc,EAAE,uBAAuB,KACnC,OAAO,CAAC,CAAC,CAAC,GACb,OAAO,CAAC,CAAC,CAAC,CAgDZ;AAED,wBAAsB,6BAA6B,CAAC,CAAC,EACpD,OAAO,EAAE,CACR,OAAO,EAAE,uBAAuB,EAChC,OAAO,EAAE,CAAC,OAAO,EAAE,uBAAuB,KAAK,OAAO,CAAC,IAAI,CAAC,KACxD,OAAO,CAAC,CAAC,CAAC,GACb,OAAO,CAAC,CAAC,CAAC,CAiCZ;AAED;;;;;;GAMG;AACH,wBAAsB,YAAY,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAM3E;AAED;;;GAGG;AACH,wBAAsB,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAcnD;AAED,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,uBAAuB,CAAC,CAgD5E;AAyBD,wBAAsB,mBAAmB,CACxC,OAAO,EAAE,uBAAuB,GAC9B,OAAO,CAAC,IAAI,CAAC,CAMf;AAED,wBAAsB,oBAAoB,IAAI,OAAO,CAAC,IAAI,CAAC,CAa1D;AAED;;;;;GAKG;AACH,wBAAsB,cAAc,CACnC,QAAQ,EAAE,MAAM,EAChB,KAAK,UAAO,EACZ,YAAY,CAAC,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,GAC3D,OAAO,CAAC,IAAI,CAAC,CAgBf;AAED;;;;;GAKG;AACH,wBAAsB,cAAc,CACnC,QAAQ,EAAE,MAAM,GACd,OAAO,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CAe/D"}
|
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
|
@@ -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
|
+
}
|
|
@@ -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.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",
|