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.
Files changed (48) hide show
  1. package/.codex-plugin/plugin.json +1 -1
  2. package/README.md +1 -1
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +34 -0
  5. package/dist/lib/account-policy.d.ts +3 -1
  6. package/dist/lib/account-policy.d.ts.map +1 -1
  7. package/dist/lib/account-policy.js +14 -1
  8. package/dist/lib/codex-manager/login-flow.js +7 -0
  9. package/dist/lib/codex-manager/login-menu-actions.d.ts.map +1 -1
  10. package/dist/lib/codex-manager/login-menu-actions.js +7 -1
  11. package/dist/lib/policy/runtime-policy.d.ts +7 -0
  12. package/dist/lib/policy/runtime-policy.d.ts.map +1 -1
  13. package/dist/lib/policy/runtime-policy.js +1 -0
  14. package/dist/lib/request/fetch-helpers.d.ts +7 -0
  15. package/dist/lib/request/fetch-helpers.d.ts.map +1 -1
  16. package/dist/lib/request/fetch-helpers.js +1 -1
  17. package/dist/lib/request/rate-limit-decision.d.ts +27 -0
  18. package/dist/lib/request/rate-limit-decision.d.ts.map +1 -1
  19. package/dist/lib/request/rate-limit-decision.js +117 -2
  20. package/dist/lib/request/response-handler.d.ts +3 -1
  21. package/dist/lib/request/response-handler.d.ts.map +1 -1
  22. package/dist/lib/request/response-handler.js +26 -5
  23. package/dist/lib/request/stream-failover-runtime.d.ts +8 -1
  24. package/dist/lib/request/stream-failover-runtime.d.ts.map +1 -1
  25. package/dist/lib/request/stream-failover-runtime.js +9 -1
  26. package/dist/lib/request/stream-failover.d.ts.map +1 -1
  27. package/dist/lib/request/stream-failover.js +41 -0
  28. package/dist/lib/runtime/account-check.d.ts.map +1 -1
  29. package/dist/lib/runtime/account-check.js +8 -1
  30. package/dist/lib/runtime/account-status.d.ts +39 -10
  31. package/dist/lib/runtime/account-status.d.ts.map +1 -1
  32. package/dist/lib/runtime/account-status.js +47 -42
  33. package/dist/lib/runtime-rotation-proxy.d.ts.map +1 -1
  34. package/dist/lib/runtime-rotation-proxy.js +73 -8
  35. package/dist/lib/storage.d.ts +19 -0
  36. package/dist/lib/storage.d.ts.map +1 -1
  37. package/dist/lib/storage.js +32 -0
  38. package/dist/lib/usage/index.d.ts +2 -0
  39. package/dist/lib/usage/index.d.ts.map +1 -1
  40. package/dist/lib/usage/index.js +2 -0
  41. package/dist/lib/usage/stream-usage-deferral.d.ts +39 -0
  42. package/dist/lib/usage/stream-usage-deferral.d.ts.map +1 -0
  43. package/dist/lib/usage/stream-usage-deferral.js +54 -0
  44. package/dist/lib/usage/usage-extraction.d.ts +42 -0
  45. package/dist/lib/usage/usage-extraction.d.ts.map +1 -0
  46. package/dist/lib/usage/usage-extraction.js +178 -0
  47. package/package.json +1 -1
  48. package/scripts/codex.js +300 -82
@@ -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.8.6",
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",