codex-multi-auth 2.8.6 → 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/rate-limit-decision.d.ts +27 -0
- package/dist/lib/request/rate-limit-decision.d.ts.map +1 -1
- package/dist/lib/request/rate-limit-decision.js +117 -2
- 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/account-status.d.ts +39 -10
- package/dist/lib/runtime/account-status.d.ts.map +1 -1
- package/dist/lib/runtime/account-status.js +47 -42
- package/dist/lib/runtime-rotation-proxy.d.ts.map +1 -1
- package/dist/lib/runtime-rotation-proxy.js +73 -8
- 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
- package/scripts/codex.js +300 -82
|
@@ -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 {
|
|
@@ -40,6 +40,33 @@ export interface PinnedUnavailableContext {
|
|
|
40
40
|
pinSource?: "forced" | "manual" | null;
|
|
41
41
|
/** Epoch ms when the blocking record ends (rate limit or cooldown). */
|
|
42
42
|
resetAtMs?: number | null;
|
|
43
|
+
/**
|
|
44
|
+
* Which class of record supplies `resetAtMs`. That deadline is the max
|
|
45
|
+
* across every gating record, so a `rate-limited` skip reason can carry a
|
|
46
|
+
* deadline supplied by a breaker or cooldown that ends later, and the
|
|
47
|
+
* quota phrasing is only true when the rate limit itself is that bound.
|
|
48
|
+
*
|
|
49
|
+
* - `"rate-limit"` — the rate-limit records bound it; word it as a reset.
|
|
50
|
+
* - `"other"` — a cooldown or breaker ends later and bounds it instead.
|
|
51
|
+
* - `"unknown"` (the default) — the caller did not measure; trust the
|
|
52
|
+
* skip reason.
|
|
53
|
+
*
|
|
54
|
+
* Deliberately a value rather than the presence of a second deadline
|
|
55
|
+
* field. The previous shape branched on `"rateLimitResetAtMs" in context`,
|
|
56
|
+
* so a caller that spread the key with an `undefined` value got the
|
|
57
|
+
* opposite wording from one that omitted it — a contract no type could
|
|
58
|
+
* express and no compiler could check.
|
|
59
|
+
*/
|
|
60
|
+
recoveryBound?: "rate-limit" | "other" | "unknown";
|
|
61
|
+
/**
|
|
62
|
+
* The pin's CURRENT runtime skip reason, re-read from account state at the
|
|
63
|
+
* moment the 503 is built. `accountSkipReasons` holds the SELECTION
|
|
64
|
+
* verdict, and on the dominant path the retry loop has already overwritten
|
|
65
|
+
* it with its own bookkeeping token ("already-attempted") — wording the
|
|
66
|
+
* sentence from that is what issue #675 reported. Omitted when the caller
|
|
67
|
+
* did not re-read runtime state.
|
|
68
|
+
*/
|
|
69
|
+
currentSkipReason?: string | null;
|
|
43
70
|
now?: number;
|
|
44
71
|
}
|
|
45
72
|
export declare function buildPinnedUnavailableErrorBody(pinnedIndex: number | null | undefined, accountSkipReasons: ReadonlyMap<number, string>, context?: PinnedUnavailableContext): PinnedUnavailableErrorBody;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rate-limit-decision.d.ts","sourceRoot":"","sources":["../../../lib/request/rate-limit-decision.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qCAAqC,CAAC;
|
|
1
|
+
{"version":3,"file":"rate-limit-decision.d.ts","sourceRoot":"","sources":["../../../lib/request/rate-limit-decision.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qCAAqC,CAAC;AAE5E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAe/C,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAGlE;AAED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,OAAO,CAAC,WAAW,EAAE;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,CAAC,GAAG,OAAO,CAWjG;AAED,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAapF;AAED,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAoBlF;AAYD,wBAAgB,0BAA0B,CAAC,gBAAgB,EAAE,MAAM,GAAG,MAAM,CAuB3E;AAED,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAkBxE;AA0BD,wBAAgB,4BAA4B,CAC3C,OAAO,EAAE,OAAO,EAChB,kBAAkB,EAAE,MAAM,EAC1B,GAAG,EAAE,MAAM,GACT,MAAM,CAcR;AAED,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,gBAAgB,GAAG,MAAM,CAE1E;AAED;;;;;;GAMG;AACH,MAAM,WAAW,0BAA0B;IAC1C,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,kCAAkC,CAAC;IACzC,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,mEAAmE;IACnE,UAAU,EAAE,QAAQ,GAAG,QAAQ,GAAG,IAAI,CAAC;IACvC,2EAA2E;IAC3E,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,oBAAoB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC7C;AAQD,MAAM,WAAW,wBAAwB;IACxC;;;;;;OAMG;IACH,SAAS,CAAC,EAAE,QAAQ,GAAG,QAAQ,GAAG,IAAI,CAAC;IACvC,uEAAuE;IACvE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B;;;;;;;;;;;;;;;;OAgBG;IACH,aAAa,CAAC,EAAE,YAAY,GAAG,OAAO,GAAG,SAAS,CAAC;IACnD;;;;;;;OAOG;IACH,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,GAAG,CAAC,EAAE,MAAM,CAAC;CACb;AAwID,wBAAgB,+BAA+B,CAC9C,WAAW,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACtC,kBAAkB,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,EAC/C,OAAO,CAAC,EAAE,wBAAwB,GAChC,0BAA0B,CA8E5B"}
|
|
@@ -177,12 +177,107 @@ export function normalizeExhaustionStatus(reason) {
|
|
|
177
177
|
* throws RangeError (ECMAScript time-value limit, ±100,000,000 days).
|
|
178
178
|
*/
|
|
179
179
|
const MAX_ECMASCRIPT_TIME_VALUE = 8_640_000_000_000_000;
|
|
180
|
+
const DEADLINE_SENTENCES = {
|
|
181
|
+
"available-again": (resetAt) => `the account is expected to be available again at ${resetAt}`,
|
|
182
|
+
"next-attempt": (resetAt) => `the next attempt is allowed at ${resetAt}`,
|
|
183
|
+
"cooldown-ends": (resetAt) => `the cooldown ends at ${resetAt}`,
|
|
184
|
+
"rate-limit-reset": (resetAt) => `the rate limit resets at ${resetAt}`,
|
|
185
|
+
};
|
|
186
|
+
/**
|
|
187
|
+
* Every cooldown variant is bounded by the same `coolingDownUntil` field, so
|
|
188
|
+
* they all word their deadline the same way. Two of them used to say "the next
|
|
189
|
+
* attempt is allowed at" while the rest said "the cooldown ends at", which
|
|
190
|
+
* reads as two different mechanisms to an operator comparing two 503s.
|
|
191
|
+
*
|
|
192
|
+
* `satisfies` is what keeps this honest: a new CooldownReason becomes a
|
|
193
|
+
* compile error here rather than falling through to the verbatim arm below,
|
|
194
|
+
* which would print the raw `cooling-down:<reason>` token in the sentence —
|
|
195
|
+
* the internal-token leak issue #675 was filed about.
|
|
196
|
+
*/
|
|
197
|
+
const COOLDOWN_DESCRIPTIONS = {
|
|
198
|
+
"auth-failure": {
|
|
199
|
+
parenthetical: "cooling down after authentication failures",
|
|
200
|
+
deadlineNoun: "cooldown-ends",
|
|
201
|
+
},
|
|
202
|
+
"network-error": {
|
|
203
|
+
parenthetical: "cooling down after network errors",
|
|
204
|
+
deadlineNoun: "cooldown-ends",
|
|
205
|
+
},
|
|
206
|
+
"server-error": {
|
|
207
|
+
parenthetical: "cooling down after upstream server errors",
|
|
208
|
+
deadlineNoun: "cooldown-ends",
|
|
209
|
+
},
|
|
210
|
+
"rate-limit": {
|
|
211
|
+
parenthetical: "cooling down after a rate limit",
|
|
212
|
+
deadlineNoun: "cooldown-ends",
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
const BLOCKER_DESCRIPTIONS = new Map([
|
|
216
|
+
[
|
|
217
|
+
"rate-limited",
|
|
218
|
+
// Upgraded to "rate-limit-reset" below when the caller confirms the
|
|
219
|
+
// rate limit is what bounds the advertised deadline.
|
|
220
|
+
{ parenthetical: "rate-limited", deadlineNoun: "available-again" },
|
|
221
|
+
],
|
|
222
|
+
[
|
|
223
|
+
"circuit-open",
|
|
224
|
+
{
|
|
225
|
+
parenthetical: "paused after repeated upstream errors",
|
|
226
|
+
deadlineNoun: "next-attempt",
|
|
227
|
+
},
|
|
228
|
+
],
|
|
229
|
+
["cooling-down", { parenthetical: "cooling down", deadlineNoun: "cooldown-ends" }],
|
|
230
|
+
...Object.entries(COOLDOWN_DESCRIPTIONS).map(([reason, description]) => [
|
|
231
|
+
`cooling-down:${reason}`,
|
|
232
|
+
description,
|
|
233
|
+
]),
|
|
234
|
+
]);
|
|
235
|
+
/** Whether the sentence has real wording for this token, or must echo it raw. */
|
|
236
|
+
function isDescribedBlocker(skipReason) {
|
|
237
|
+
return BLOCKER_DESCRIPTIONS.has(skipReason);
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* The human sentence derives its parenthetical and its deadline noun from the
|
|
241
|
+
* blocker class. The deadline is max(rate-limit reset, cooldown end, breaker
|
|
242
|
+
* next-attempt), so calling it a "limit" is only true for the rate-limit
|
|
243
|
+
* class: during a provider outage a tripped breaker or a server-error
|
|
244
|
+
* cooldown printed "the recorded limit resets at …", and operators read a
|
|
245
|
+
* backend incident as a blown subscription quota. Only the message changes —
|
|
246
|
+
* the machine-readable `reason` keeps the raw skip token.
|
|
247
|
+
*/
|
|
248
|
+
function describePinnedBlocker(skipReason, rateLimitBoundsRecovery) {
|
|
249
|
+
if (skipReason === null) {
|
|
250
|
+
return {
|
|
251
|
+
parenthetical: null,
|
|
252
|
+
deadline: DEADLINE_SENTENCES["available-again"],
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
const described = BLOCKER_DESCRIPTIONS.get(skipReason);
|
|
256
|
+
if (described === undefined) {
|
|
257
|
+
// Permanent blockers never reach the deadline clause (the call site
|
|
258
|
+
// suppresses their reset time), and future or internal tokens stay
|
|
259
|
+
// legible verbatim.
|
|
260
|
+
return {
|
|
261
|
+
parenthetical: skipReason,
|
|
262
|
+
deadline: DEADLINE_SENTENCES["available-again"],
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
// A breaker or cooldown can outlive the rate limit; the deadline is the max
|
|
266
|
+
// of every gating record, so it is only worded as the limit's own reset
|
|
267
|
+
// when the rate limit actually supplies it.
|
|
268
|
+
const deadlineNoun = skipReason === "rate-limited" && rateLimitBoundsRecovery
|
|
269
|
+
? "rate-limit-reset"
|
|
270
|
+
: described.deadlineNoun;
|
|
271
|
+
return {
|
|
272
|
+
parenthetical: described.parenthetical,
|
|
273
|
+
deadline: DEADLINE_SENTENCES[deadlineNoun],
|
|
274
|
+
};
|
|
275
|
+
}
|
|
180
276
|
export function buildPinnedUnavailableErrorBody(pinnedIndex, accountSkipReasons, context) {
|
|
181
277
|
const normalizedPinnedIndex = typeof pinnedIndex === "number" ? pinnedIndex : null;
|
|
182
278
|
const skipReason = normalizedPinnedIndex !== null
|
|
183
279
|
? accountSkipReasons.get(normalizedPinnedIndex) ?? null
|
|
184
280
|
: null;
|
|
185
|
-
const reasonSuffix = skipReason ? ` (${skipReason})` : "";
|
|
186
281
|
// On the desync path the pin index is unknown (null); claiming "account 1"
|
|
187
282
|
// there would contradict the machine-readable pinnedAccountIndex: null.
|
|
188
283
|
const accountPhrase = normalizedPinnedIndex === null
|
|
@@ -206,7 +301,27 @@ export function buildPinnedUnavailableErrorBody(pinnedIndex, accountSkipReasons,
|
|
|
206
301
|
const now = context?.now ?? Date.now();
|
|
207
302
|
const retryAfterMs = resetAtMs !== null ? Math.max(0, resetAtMs - now) : null;
|
|
208
303
|
const resetAt = resetAtMs !== null ? new Date(resetAtMs).toISOString() : null;
|
|
209
|
-
|
|
304
|
+
// "unknown" (the default) means the caller did not measure which record
|
|
305
|
+
// bounds the deadline — older callers and unit seams — so trust the skip
|
|
306
|
+
// reason. Only a measured "other" demotes to the neutral phrasing, which
|
|
307
|
+
// stays true for a live rate limit either way.
|
|
308
|
+
const rateLimitBoundsRecovery = (context?.recoveryBound ?? "unknown") !== "other";
|
|
309
|
+
// The machine-readable `reason` below stays the recorded SELECTION verdict;
|
|
310
|
+
// only the human sentence follows the blocker actually gating the pin. A
|
|
311
|
+
// recorded verdict that names a real blocker class wins, because the
|
|
312
|
+
// selection-only classes ("missing", "policy-blocked") have no
|
|
313
|
+
// runtime-state equivalent and would be lost. Otherwise the re-read runtime
|
|
314
|
+
// state is the truth and the recorded token is the retry loop's own
|
|
315
|
+
// bookkeeping — the dominant path in issue #675, where a 503/429 on the pin
|
|
316
|
+
// re-enters selection and records "already-attempted" over the real class.
|
|
317
|
+
const describedSkipReason = skipReason !== null && isDescribedBlocker(skipReason)
|
|
318
|
+
? skipReason
|
|
319
|
+
: context?.currentSkipReason ?? skipReason;
|
|
320
|
+
const blocker = describePinnedBlocker(describedSkipReason, rateLimitBoundsRecovery);
|
|
321
|
+
const reasonSuffix = blocker.parenthetical
|
|
322
|
+
? ` (${blocker.parenthetical})`
|
|
323
|
+
: "";
|
|
324
|
+
const waitSuffix = resetAt !== null ? `; ${blocker.deadline(resetAt)}` : "";
|
|
210
325
|
// A forced pin belongs to the launching process, not to the persisted pin
|
|
211
326
|
// state, so `unpin` would clear nothing — say what actually helps.
|
|
212
327
|
const remedy = pinSource === "forced"
|
|
@@ -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"}
|