codex-multi-auth 2.9.0 → 2.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-multi-auth",
3
- "version": "2.9.0",
3
+ "version": "2.9.1",
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
@@ -161,6 +161,19 @@ codex-multi-auth forecast --live
161
161
 
162
162
  If browser launch is blocked, use the alternate login paths in [docs/getting-started.md](docs/getting-started.md#alternate-login-paths).
163
163
  For remote or headless shells, prefer `codex-multi-auth login --device-auth`.
164
+ To refresh one saved account without changing the active selection or manual
165
+ pin, run:
166
+
167
+ ```bash
168
+ codex-multi-auth login --account <index|email|account_id> --preserve-selection
169
+ ```
170
+
171
+ Add `--device-auth` on a remote shell. The refreshed OAuth identity must match
172
+ the requested saved account or nothing is written. Refreshing the account Codex
173
+ is currently using still publishes its new tokens to the native CLI; refreshing
174
+ any other account leaves the active selection and the manual pin untouched. A
175
+ disabled account stays disabled. `--account` cannot be combined with `--org`,
176
+ which would rebind the row to a different workspace.
164
177
 
165
178
  ---
166
179
 
@@ -392,7 +405,7 @@ codex-multi-auth doctor --json
392
405
 
393
406
  ## Release Notes
394
407
 
395
- - Current stable: [docs/releases/v2.9.0.md](docs/releases/v2.9.0.md) — install via `npm i -g codex-multi-auth`
408
+ - Current stable: [docs/releases/v2.9.1.md](docs/releases/v2.9.1.md) — install via `npm i -g codex-multi-auth`
396
409
  - Previous stable: [docs/releases/v2.8.3.md](docs/releases/v2.8.3.md)
397
410
  - Previous stable: [docs/releases/v2.8.2.md](docs/releases/v2.8.2.md)
398
411
  - Previous stable: [docs/releases/v2.7.1.md](docs/releases/v2.7.1.md)
@@ -21,6 +21,7 @@ export interface BudgetGuardEvaluation {
21
21
  requests: number;
22
22
  totalTokens: number;
23
23
  costUsd: number;
24
+ unpricedRequests: number;
24
25
  };
25
26
  limits: {
26
27
  maxRequests: number | null;
@@ -1 +1 @@
1
- {"version":3,"file":"budget-guard.d.ts","sourceRoot":"","sources":["../../lib/budget-guard.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAIrD,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,KAAK,GAAG,MAAM,GAAG,OAAO,CAAC;AAE7D,MAAM,WAAW,WAAW;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,YAAY,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,gBAAgB;IAChC,OAAO,EAAE,CAAC,CAAC;IACX,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;CACpC;AAED,MAAM,WAAW,qBAAqB;IACrC,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,YAAY,CAAC;IACrB,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,KAAK,EAAE;QACN,QAAQ,EAAE,MAAM,CAAC;QACjB,WAAW,EAAE,MAAM,CAAC;QACpB,OAAO,EAAE,MAAM,CAAC;KAChB,CAAC;IACF,MAAM,EAAE;QACP,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;QAC3B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;QACzB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;KAC1B,CAAC;CACF;AA6ED,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAE/D;AAED,wBAAsB,oBAAoB,IAAI,OAAO,CAAC,gBAAgB,CAAC,CAatE;AAED,wBAAsB,oBAAoB,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAsCjF;AAED,wBAAgB,iBAAiB,CAChC,KAAK,EAAE,gBAAgB,EACvB,KAAK,EAAE,IAAI,CAAC,WAAW,EAAE,WAAW,CAAC,EACrC,GAAG,SAAa,GACd,WAAW,CAOb;AAED,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,YAAY,EAAE,GAAG,SAAa,GAAG,MAAM,CAoBnF;AAED,wBAAgB,mBAAmB,CAClC,KAAK,EAAE,WAAW,EAClB,OAAO,EAAE,YAAY,GACnB,qBAAqB,CAsCvB;AAED,wBAAgB,kCAAkC,IAAI,IAAI,CAEzD"}
1
+ {"version":3,"file":"budget-guard.d.ts","sourceRoot":"","sources":["../../lib/budget-guard.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAIrD,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,KAAK,GAAG,MAAM,GAAG,OAAO,CAAC;AAE7D,MAAM,WAAW,WAAW;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,YAAY,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,gBAAgB;IAChC,OAAO,EAAE,CAAC,CAAC;IACX,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;CACpC;AAED,MAAM,WAAW,qBAAqB;IACrC,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,YAAY,CAAC;IACrB,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,KAAK,EAAE;QACN,QAAQ,EAAE,MAAM,CAAC;QACjB,WAAW,EAAE,MAAM,CAAC;QACpB,OAAO,EAAE,MAAM,CAAC;QAChB,gBAAgB,EAAE,MAAM,CAAC;KACzB,CAAC;IACF,MAAM,EAAE;QACP,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;QAC3B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;QACzB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;KAC1B,CAAC;CACF;AA6ED,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAE/D;AAED,wBAAsB,oBAAoB,IAAI,OAAO,CAAC,gBAAgB,CAAC,CAatE;AAED,wBAAsB,oBAAoB,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAsCjF;AAED,wBAAgB,iBAAiB,CAChC,KAAK,EAAE,gBAAgB,EACvB,KAAK,EAAE,IAAI,CAAC,WAAW,EAAE,WAAW,CAAC,EACrC,GAAG,SAAa,GACd,WAAW,CAOb;AAED,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,YAAY,EAAE,GAAG,SAAa,GAAG,MAAM,CAoBnF;AAED,wBAAgB,mBAAmB,CAClC,KAAK,EAAE,WAAW,EAClB,OAAO,EAAE,YAAY,GACnB,qBAAqB,CA+CvB;AAED,wBAAgB,kCAAkC,IAAI,IAAI,CAEzD"}
@@ -175,9 +175,18 @@ export function evaluateBudgetGuard(limit, summary) {
175
175
  summary.totals.totalTokens >= limit.maxTokens) {
176
176
  reasons.push(`token limit reached (${summary.totals.totalTokens}/${limit.maxTokens})`);
177
177
  }
178
- if (typeof limit.maxCostUsd === "number" &&
179
- summary.totals.costUsd >= limit.maxCostUsd) {
180
- reasons.push(`cost limit reached (${summary.totals.costUsd.toFixed(6)}/${limit.maxCostUsd.toFixed(6)})`);
178
+ if (typeof limit.maxCostUsd === "number") {
179
+ if (summary.totals.costUsd >= limit.maxCostUsd) {
180
+ reasons.push(`cost limit reached (${summary.totals.costUsd.toFixed(6)}/${limit.maxCostUsd.toFixed(6)})`);
181
+ }
182
+ else if ((summary.totals.unpricedRequests ?? 0) > 0) {
183
+ // The window contains token usage we have no rate for, so `costUsd` is
184
+ // an under-count by an unknown amount and this limit cannot be
185
+ // evaluated. Counting unknown cost as zero is what made a cost budget
186
+ // silently unenforceable for every unpriced model; an unevaluable spend
187
+ // limit fails closed instead. Priced models are unaffected.
188
+ reasons.push(`cost limit cannot be evaluated: ${summary.totals.unpricedRequests} request(s) in this window used a model with no known price, so recorded cost (${summary.totals.costUsd.toFixed(6)}) is incomplete. Price the model in lib/usage/pricing.ts, or budget on --requests/--tokens instead of --cost.`);
189
+ }
181
190
  }
182
191
  return {
183
192
  key: limit.key,
@@ -188,6 +197,7 @@ export function evaluateBudgetGuard(limit, summary) {
188
197
  requests: summary.totals.requests,
189
198
  totalTokens: summary.totals.totalTokens,
190
199
  costUsd: summary.totals.costUsd,
200
+ unpricedRequests: summary.totals.unpricedRequests,
191
201
  },
192
202
  limits: {
193
203
  maxRequests: limit.maxRequests ?? null,
@@ -30,6 +30,14 @@ export interface ResolvedAccountWrite {
30
30
  accessToken?: string;
31
31
  expiresAt?: number;
32
32
  workspaces?: Workspace[];
33
+ /**
34
+ * Keep the saved row's `enabled` flag instead of re-enabling it. A targeted
35
+ * re-authentication only tops up credentials for a row the user named, so it
36
+ * must not pull a deliberately disabled account back into rotation behind
37
+ * their back. A plain login still re-enables: signing in is the user asking
38
+ * for that account to be usable again.
39
+ */
40
+ preserveEnabledState?: boolean;
33
41
  now: number;
34
42
  }
35
43
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"account-pool-write.d.ts","sourceRoot":"","sources":["../../../lib/codex-manager/account-pool-write.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AAEpE;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,uBAAuB,GAAG,UAAU,GAAG,SAAS,GAAG,SAAS,CAAC;AAEzE;;;;;GAKG;AACH,MAAM,WAAW,oBAAoB;IACpC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,iBAAiB,CAAC,iBAAiB,CAAC,CAAC;IACvD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,SAAS,EAAE,CAAC;IACzB,GAAG,EAAE,MAAM,CAAC;CACZ;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CACxC,UAAU,EAAE,SAAS,EAAE,EACvB,SAAS,EAAE,MAAM,GAAG,SAAS,GAC3B,MAAM,CAWR;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACrC,QAAQ,EAAE,IAAI,CAAC,iBAAiB,EAAE,YAAY,CAAC,EAC/C,QAAQ,EAAE,SAAS,EAAE,GAAG,SAAS,GAC/B,SAAS,EAAE,GAAG,SAAS,CAYzB;AAED;;;;GAIG;AACH,wBAAgB,4BAA4B,CAC3C,QAAQ,EAAE,IAAI,CAAC,iBAAiB,EAAE,YAAY,GAAG,uBAAuB,CAAC,EACzE,gBAAgB,EAAE,SAAS,EAAE,GAAG,SAAS,GACvC,MAAM,GAAG,SAAS,CAwBpB;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CACnC,KAAK,EAAE,oBAAoB,GACzB;IAAE,OAAO,EAAE,iBAAiB,CAAC;IAAC,OAAO,EAAE,UAAU,CAAA;CAAE,CAwBrD;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAClC,QAAQ,EAAE,iBAAiB,EAC3B,KAAK,EAAE,oBAAoB,GACzB;IAAE,OAAO,EAAE,iBAAiB,CAAC;IAAC,OAAO,EAAE,SAAS,GAAG,SAAS,CAAA;CAAE,CA+ChE;AAED;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE;IAC/C,QAAQ,EAAE,iBAAiB,EAAE,CAAC;IAC9B,MAAM,EAAE,oBAAoB,EAAE,CAAC;IAC/B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,wBAAwB,EAAE,CACzB,QAAQ,EAAE,iBAAiB,EAAE,EAC7B,QAAQ,EAAE;QACT,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,YAAY,CAAC,EAAE,MAAM,CAAC;KACtB,EACD,OAAO,EAAE;QAAE,wCAAwC,EAAE,OAAO,CAAA;KAAE,KAC1D,MAAM,GAAG,SAAS,CAAC;CACxB,GAAG;IACH,QAAQ,EAAE,iBAAiB,EAAE,CAAC;IAC9B,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,uBAAuB,GAAG,IAAI,CAAC;CACxC,CA6CA"}
1
+ {"version":3,"file":"account-pool-write.d.ts","sourceRoot":"","sources":["../../../lib/codex-manager/account-pool-write.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AAEpE;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,uBAAuB,GAAG,UAAU,GAAG,SAAS,GAAG,SAAS,CAAC;AAEzE;;;;;GAKG;AACH,MAAM,WAAW,oBAAoB;IACpC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,iBAAiB,CAAC,iBAAiB,CAAC,CAAC;IACvD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,SAAS,EAAE,CAAC;IACzB;;;;;;OAMG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,GAAG,EAAE,MAAM,CAAC;CACZ;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CACxC,UAAU,EAAE,SAAS,EAAE,EACvB,SAAS,EAAE,MAAM,GAAG,SAAS,GAC3B,MAAM,CAWR;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACrC,QAAQ,EAAE,IAAI,CAAC,iBAAiB,EAAE,YAAY,CAAC,EAC/C,QAAQ,EAAE,SAAS,EAAE,GAAG,SAAS,GAC/B,SAAS,EAAE,GAAG,SAAS,CAYzB;AAED;;;;GAIG;AACH,wBAAgB,4BAA4B,CAC3C,QAAQ,EAAE,IAAI,CAAC,iBAAiB,EAAE,YAAY,GAAG,uBAAuB,CAAC,EACzE,gBAAgB,EAAE,SAAS,EAAE,GAAG,SAAS,GACvC,MAAM,GAAG,SAAS,CAwBpB;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CACnC,KAAK,EAAE,oBAAoB,GACzB;IAAE,OAAO,EAAE,iBAAiB,CAAC;IAAC,OAAO,EAAE,UAAU,CAAA;CAAE,CAwBrD;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAClC,QAAQ,EAAE,iBAAiB,EAC3B,KAAK,EAAE,oBAAoB,GACzB;IAAE,OAAO,EAAE,iBAAiB,CAAC;IAAC,OAAO,EAAE,SAAS,GAAG,SAAS,CAAA;CAAE,CA+ChE;AAED;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE;IAC/C,QAAQ,EAAE,iBAAiB,EAAE,CAAC;IAC9B,MAAM,EAAE,oBAAoB,EAAE,CAAC;IAC/B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,wBAAwB,EAAE,CACzB,QAAQ,EAAE,iBAAiB,EAAE,EAC7B,QAAQ,EAAE;QACT,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,YAAY,CAAC,EAAE,MAAM,CAAC;KACtB,EACD,OAAO,EAAE;QAAE,wCAAwC,EAAE,OAAO,CAAA;KAAE,KAC1D,MAAM,GAAG,SAAS,CAAC;CACxB,GAAG;IACH,QAAQ,EAAE,iBAAiB,EAAE,CAAC;IAC9B,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,uBAAuB,GAAG,IAAI,CAAC;CACxC,CA6CA"}
@@ -115,7 +115,7 @@ export function buildUpdatedAccount(existing, write) {
115
115
  refreshToken: write.refreshToken,
116
116
  accessToken: write.accessToken,
117
117
  expiresAt: write.expiresAt,
118
- enabled: true,
118
+ enabled: write.preserveEnabledState ? existing.enabled : true,
119
119
  authInvalidatedAt: undefined,
120
120
  authInvalidationErrorCode: undefined,
121
121
  lastUsed: write.now,
@@ -2,7 +2,9 @@ export declare function printUsage(): void;
2
2
  export type AuthLoginOptions = {
3
3
  manual: boolean;
4
4
  deviceAuth: boolean;
5
+ preserveSelection?: boolean;
5
6
  org?: string;
7
+ account?: string;
6
8
  };
7
9
  export type ParsedAuthLoginArgs = {
8
10
  ok: true;
@@ -1 +1 @@
1
- {"version":3,"file":"help.d.ts","sourceRoot":"","sources":["../../../lib/codex-manager/help.ts"],"names":[],"mappings":"AAIA,wBAAgB,UAAU,IAAI,IAAI,CAsDjC;AAED,MAAM,MAAM,gBAAgB,GAAG;IAC9B,MAAM,EAAE,OAAO,CAAC;IAChB,UAAU,EAAE,OAAO,CAAC;IACpB,GAAG,CAAC,EAAE,MAAM,CAAC;CACb,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAC5B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,gBAAgB,CAAA;CAAE,GACvC;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAC7B;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAEnD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,mBAAmB,CAgEtE;AAED,UAAU,cAAc;IACvB,IAAI,EAAE,OAAO,CAAC;IACd,IAAI,EAAE,OAAO,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,OAAO,CAAC;CACvB;AAED,MAAM,MAAM,cAAc,GACvB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,cAAc,CAAA;CAAE,GACrC;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAC7B;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAEnD,wBAAgB,cAAc,IAAI,IAAI,CAgBrC;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,cAAc,CAyD5D"}
1
+ {"version":3,"file":"help.d.ts","sourceRoot":"","sources":["../../../lib/codex-manager/help.ts"],"names":[],"mappings":"AAIA,wBAAgB,UAAU,IAAI,IAAI,CAsDjC;AAED,MAAM,MAAM,gBAAgB,GAAG;IAC9B,MAAM,EAAE,OAAO,CAAC;IAChB,UAAU,EAAE,OAAO,CAAC;IACpB,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAC5B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,gBAAgB,CAAA;CAAE,GACvC;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAC7B;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAEnD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,mBAAmB,CAuGtE;AAED,UAAU,cAAc;IACvB,IAAI,EAAE,OAAO,CAAC;IACd,IAAI,EAAE,OAAO,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,OAAO,CAAC;CACvB;AAED,MAAM,MAAM,cAAc,GACvB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,cAAc,CAAA;CAAE,GACrC;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAC7B;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAEnD,wBAAgB,cAAc,IAAI,IAAI,CAgBrC;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,cAAc,CAyD5D"}
@@ -5,7 +5,7 @@ export function printUsage() {
5
5
  "Codex Multi-Auth CLI",
6
6
  "",
7
7
  "Start here:",
8
- " codex-multi-auth login [--device-auth|--manual|--no-browser] [--org <org_id>]",
8
+ " codex-multi-auth login [--device-auth|--manual|--no-browser] [--org <org_id>] [--preserve-selection] [--account <index|email|account_id>]",
9
9
  " codex-multi-auth status [--json] (list is the same command)",
10
10
  " codex-multi-auth check (always live-probes)",
11
11
  "",
@@ -72,6 +72,33 @@ export function parseAuthLoginArgs(args) {
72
72
  options.deviceAuth = true;
73
73
  continue;
74
74
  }
75
+ if (arg === "--preserve-selection") {
76
+ options.preserveSelection = true;
77
+ continue;
78
+ }
79
+ if (arg === "--account" || arg?.startsWith("--account=")) {
80
+ let value;
81
+ if (arg === "--account") {
82
+ value = args[i + 1];
83
+ i += 1;
84
+ }
85
+ else {
86
+ value = arg.slice("--account=".length);
87
+ }
88
+ const trimmed = value?.trim();
89
+ if (!trimmed || trimmed.startsWith("--")) {
90
+ return {
91
+ ok: false,
92
+ reason: "error",
93
+ message: "Missing value for --account. Usage: codex-multi-auth login --account <index|email|account_id>",
94
+ };
95
+ }
96
+ options.account = trimmed;
97
+ // A targeted re-auth must never select the refreshed account as a
98
+ // side effect, even if the caller omits the redundant safety flag.
99
+ options.preserveSelection = true;
100
+ continue;
101
+ }
75
102
  if (arg === "--org" || arg?.startsWith("--org=")) {
76
103
  // Bind this login to a specific workspace/org id (issue #491). Reuses
77
104
  // the CODEX_AUTH_ACCOUNT_ID override mechanism so the same email's
@@ -112,6 +139,16 @@ export function parseAuthLoginArgs(args) {
112
139
  message: `Cannot combine --device-auth with ${conflict}`,
113
140
  };
114
141
  }
142
+ // --org rebinds a login to a different workspace, which is exactly the
143
+ // identity change a targeted re-auth is built to refuse. Rejecting the pair
144
+ // up front beats failing after the user has already completed a sign-in.
145
+ if (options.account && options.org) {
146
+ return {
147
+ ok: false,
148
+ reason: "error",
149
+ message: "Cannot combine --account with --org. Re-authenticate the saved account on its own workspace, or register the other workspace with --org on its own.",
150
+ };
151
+ }
115
152
  return { ok: true, options };
116
153
  }
117
154
  export function printBestUsage() {
@@ -1,3 +1,4 @@
1
+ import { type AccountMetadataV3, type AccountStorageV3 } from "../storage.js";
1
2
  import { type RepairCommandDeps } from "./repair-commands.js";
2
3
  /**
3
4
  * Interactive control loop for the `login` command: the account dashboard menu
@@ -13,5 +14,7 @@ export interface LoginFlowDeps {
13
14
  createRepairCommandDeps(): RepairCommandDeps;
14
15
  }
15
16
  /** @internal */
17
+ export declare function resolveLoginAccountTarget(storage: AccountStorageV3, identifier: string): AccountMetadataV3 | null;
18
+ /** @internal */
16
19
  export declare function runAuthLogin(args: string[], deps: LoginFlowDeps): Promise<number>;
17
20
  //# sourceMappingURL=login-flow.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"login-flow.d.ts","sourceRoot":"","sources":["../../../lib/codex-manager/login-flow.ts"],"names":[],"mappings":"AAmDA,OAAO,EACN,KAAK,iBAAiB,EAGtB,MAAM,sBAAsB,CAAC;AAM9B;;;;;;;GAOG;AAEH,gBAAgB;AAChB,MAAM,WAAW,aAAa;IAC7B,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC7C,uBAAuB,IAAI,iBAAiB,CAAC;CAC7C;AAkCD,gBAAgB;AAChB,wBAAsB,YAAY,CACjC,IAAI,EAAE,MAAM,EAAE,EACd,IAAI,EAAE,aAAa,GACjB,OAAO,CAAC,MAAM,CAAC,CAqBjB"}
1
+ {"version":3,"file":"login-flow.d.ts","sourceRoot":"","sources":["../../../lib/codex-manager/login-flow.ts"],"names":[],"mappings":"AAQA,OAAO,EACN,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EAUrB,MAAM,eAAe,CAAC;AAmCvB,OAAO,EACN,KAAK,iBAAiB,EAGtB,MAAM,sBAAsB,CAAC;AAM9B;;;;;;;GAOG;AAEH,gBAAgB;AAChB,MAAM,WAAW,aAAa;IAC7B,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC7C,uBAAuB,IAAI,iBAAiB,CAAC;CAC7C;AAkCD,gBAAgB;AAChB,wBAAgB,yBAAyB,CACxC,OAAO,EAAE,gBAAgB,EACzB,UAAU,EAAE,MAAM,GAChB,iBAAiB,GAAG,IAAI,CAkB1B;AAED,gBAAgB;AAChB,wBAAsB,YAAY,CACjC,IAAI,EAAE,MAAM,EAAE,EACd,IAAI,EAAE,aAAa,GACjB,OAAO,CAAC,MAAM,CAAC,CAqBjB"}
@@ -1,3 +1,4 @@
1
+ import { sanitizeEmail } from "../accounts.js";
1
2
  import { isBrowserLaunchSuppressed } from "../auth/browser.js";
2
3
  import { promptAddAnotherAccount, promptLoginMode } from "../cli.js";
3
4
  import { ACCOUNT_LIMITS } from "../constants.js";
@@ -6,6 +7,7 @@ import { createLogger } from "../logger.js";
6
7
  import { loadQuotaCache } from "../quota-cache.js";
7
8
  import { resolveActiveIndex } from "../runtime/account-status.js";
8
9
  import { clearAccounts, formatStorageErrorHint, getNamedBackups, loadAccounts, loadFlaggedAccounts, restoreAccountsFromBackup, setStoragePath, StorageError, } from "../storage.js";
10
+ import { CodexValidationError } from "../errors.js";
9
11
  import { UI_COPY } from "../ui/ui-copy.js";
10
12
  import { confirm } from "../ui/confirm.js";
11
13
  import { stylePromptText } from "./formatters/index.js";
@@ -38,6 +40,23 @@ async function clearAccountsAndReset() {
38
40
  await clearAccounts();
39
41
  }
40
42
  /** @internal */
43
+ export function resolveLoginAccountTarget(storage, identifier) {
44
+ if (/^[1-9]\d*$/.test(identifier)) {
45
+ const byIndex = storage.accounts[Number(identifier) - 1];
46
+ return byIndex ?? null;
47
+ }
48
+ const byAccountId = storage.accounts.filter((account) => account.accountId === identifier);
49
+ if (byAccountId.length === 1)
50
+ return byAccountId[0] ?? null;
51
+ if (byAccountId.length > 1)
52
+ return null;
53
+ const email = sanitizeEmail(identifier);
54
+ if (!email)
55
+ return null;
56
+ const byEmail = storage.accounts.filter((account) => sanitizeEmail(account.email) === email);
57
+ return byEmail.length === 1 ? (byEmail[0] ?? null) : null;
58
+ }
59
+ /** @internal */
41
60
  export async function runAuthLogin(args, deps) {
42
61
  const parsedArgs = parseAuthLoginArgs(args);
43
62
  if (!parsedArgs.ok) {
@@ -197,7 +216,10 @@ async function runAuthLoginFlow(loginOptions, deps) {
197
216
  // (--device-auth, --manual, --no-browser), they want to add a new account
198
217
  // directly. Skipping the dashboard menu keeps `login --device-auth`
199
218
  // usable from scripts and matches the documented behavior of the help text.
200
- const explicitSignInMode = loginOptions.deviceAuth || loginOptions.manual;
219
+ const explicitSignInMode = loginOptions.deviceAuth ||
220
+ loginOptions.manual ||
221
+ loginOptions.preserveSelection ||
222
+ Boolean(loginOptions.account);
201
223
  loginFlow: while (true) {
202
224
  const existingStorage = await loadAccounts();
203
225
  if (!explicitSignInMode &&
@@ -210,6 +232,23 @@ async function runAuthLoginFlow(loginOptions, deps) {
210
232
  }
211
233
  const refreshedStorage = await loadAccounts();
212
234
  let existingCount = refreshedStorage?.accounts.length ?? 0;
235
+ const expectedAccount = loginOptions.account
236
+ ? refreshedStorage
237
+ ? resolveLoginAccountTarget(refreshedStorage, loginOptions.account)
238
+ : null
239
+ : undefined;
240
+ if (loginOptions.account && !expectedAccount) {
241
+ console.error("The requested Codex account is missing or ambiguous. No login was started.");
242
+ return 1;
243
+ }
244
+ // Positional hint for the write, re-verified (never trusted) inside the
245
+ // storage transaction. resolveLoginAccountTarget hands back a row out of
246
+ // this very array, so indexOf is an exact identity lookup.
247
+ const expectedAccountIndex = expectedAccount && refreshedStorage
248
+ ? refreshedStorage.accounts.indexOf(expectedAccount)
249
+ : -1;
250
+ // A non-empty pool already forces a fresh sign-in, and a resolved target
251
+ // implies the pool was non-empty, so it adds no condition of its own.
213
252
  let forceNewLogin = existingCount > 0;
214
253
  let onboardingBackupDiscoveryWarning = null;
215
254
  const loadNamedBackupsForOnboarding = async () => {
@@ -346,9 +385,31 @@ async function runAuthLoginFlow(loginOptions, deps) {
346
385
  console.error(`Login failed: ${tokenResult.message ?? tokenResult.reason ?? "unknown error"}`);
347
386
  return 1;
348
387
  }
349
- const resolved = resolveAccountSelection(tokenResult, loginOptions.org);
350
- const persistOutcome = await persistAccountPool([resolved], false);
351
- await syncSelectionToCodex(resolved);
388
+ const resolved = resolveAccountSelection(tokenResult, loginOptions.org, expectedAccount?.accountId);
389
+ let persistResult;
390
+ try {
391
+ persistResult = await persistAccountPool([resolved], false, {
392
+ preserveSelection: loginOptions.preserveSelection,
393
+ expectedAccount: expectedAccount ?? undefined,
394
+ expectedAccountIndex: expectedAccountIndex >= 0 ? expectedAccountIndex : undefined,
395
+ });
396
+ }
397
+ catch (error) {
398
+ if (error instanceof CodexValidationError) {
399
+ console.error(`Re-authentication failed: ${error.message}`);
400
+ return 1;
401
+ }
402
+ throw error;
403
+ }
404
+ // Publish to ~/.codex/auth.json exactly when this write owns the
405
+ // selection. Skipping the sync whenever --preserve-selection was passed
406
+ // stranded the native CLI on the expired tokens of the very account the
407
+ // user had just refreshed, and nothing downstream repaired it: the
408
+ // drift check compares identity, not tokens, so a same-identity refresh
409
+ // never looks like drift.
410
+ if (!persistResult || persistResult.isActiveAccount) {
411
+ await syncSelectionToCodex(resolved);
412
+ }
352
413
  const latestStorage = await loadAccounts();
353
414
  const count = latestStorage?.accounts.length ?? 1;
354
415
  existingCount = count;
@@ -357,16 +418,22 @@ async function runAuthLoginFlow(loginOptions, deps) {
357
418
  // Only claim a new saved slot when one was actually appended. A
358
419
  // same-email login that maps onto an existing entry updates or
359
420
  // rebinds it instead of growing the pool (issue #512).
360
- const outcomeMessage = persistOutcome === "rebound"
421
+ const outcomeMessage = persistResult?.outcome === "rebound"
361
422
  ? `Rebound workspace for existing account. Total: ${count}`
362
- : persistOutcome === "updated"
423
+ : persistResult?.outcome === "updated"
363
424
  ? `Updated existing account. Total: ${count}`
364
425
  : `Added account. Total: ${count}`;
365
426
  console.log(outcomeMessage);
427
+ if (persistResult && !persistResult.accountEnabled) {
428
+ console.log(`Account ${persistResult.accountIndex + 1} is still disabled and stays out of rotation. Re-enable it from the manage menu in "codex-multi-auth login".`);
429
+ }
366
430
  console.log("Next steps:");
367
431
  console.log(" codex-multi-auth status Check that the wrapper is active.");
368
432
  console.log(" codex-multi-auth check Confirm your saved accounts look healthy.");
369
433
  console.log(" codex-multi-auth list Review saved accounts before switching.");
434
+ if (loginOptions.preserveSelection) {
435
+ return 0;
436
+ }
370
437
  if (count >= ACCOUNT_LIMITS.MAX_ACCOUNTS) {
371
438
  console.log(`Reached maximum account limit (${ACCOUNT_LIMITS.MAX_ACCOUNTS}).`);
372
439
  // Same reasoning as the addAnother=false branch below: in
@@ -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,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
+ {"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;AAGjD,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,CAkKf"}
@@ -1,5 +1,6 @@
1
1
  import { stdin as input, stdout as output } from "node:process";
2
2
  import { sanitizeEmail } from "../accounts.js";
3
+ import { CodexValidationError } from "../errors.js";
3
4
  import { MODEL_FAMILIES } from "../prompts/codex.js";
4
5
  import { bumpStorageAffinityGeneration, findMatchingAccountIndex, loadAccounts, reconcilePinnedAccountIndex, setStoragePath, withAccountStorageTransaction, } from "../storage.js";
5
6
  import { UI_COPY } from "../ui/ui-copy.js";
@@ -312,9 +313,38 @@ export async function handleManageAction(storage, menuResult) {
312
313
  console.error(`Refresh failed: ${tokenResult.message ?? tokenResult.reason ?? "unknown error"}`);
313
314
  return;
314
315
  }
315
- const resolved = resolveAccountSelection(tokenResult);
316
- await persistAccountPool([resolved], false);
317
- await syncSelectionToCodex(resolved);
318
- console.log(`Refreshed account ${idx + 1}.`);
316
+ const resolved = resolveAccountSelection(tokenResult, undefined, existing.accountId);
317
+ let persistResult;
318
+ try {
319
+ persistResult = await persistAccountPool([resolved], false, {
320
+ preserveSelection: true,
321
+ expectedAccount: existing,
322
+ expectedAccountIndex: idx,
323
+ });
324
+ }
325
+ catch (error) {
326
+ // A mismatched sign-in (the user picked a different ChatGPT account in
327
+ // the browser) is a refusal, not a crash. Nothing between here and the
328
+ // process entrypoint catches this, so an uncaught throw would take the
329
+ // whole interactive dashboard down with a raw stack trace.
330
+ if (error instanceof CodexValidationError) {
331
+ console.error(`Refresh failed: ${error.message}`);
332
+ return;
333
+ }
334
+ throw error;
335
+ }
336
+ // Refreshing the account Codex is currently using must publish its new
337
+ // tokens; refreshing any other row must leave the selection alone.
338
+ if (!persistResult || persistResult.isActiveAccount) {
339
+ await syncSelectionToCodex(resolved);
340
+ }
341
+ // Report the row that was actually written. The pool can shift between the
342
+ // dashboard render and this transaction, so the menu index may name a
343
+ // different account by now.
344
+ const refreshedIndex = persistResult?.accountIndex ?? idx;
345
+ console.log(`Refreshed account ${refreshedIndex + 1}.`);
346
+ if (persistResult && !persistResult.accountEnabled) {
347
+ console.log(`Account ${refreshedIndex + 1} is still disabled and stays out of rotation. Toggle it from this menu to bring it back.`);
348
+ }
319
349
  }
320
350
  }
@@ -1,4 +1,5 @@
1
1
  import { type Workspace } from "../accounts.js";
2
+ import { type AccountMetadataV3 } from "../storage.js";
2
3
  import type { AccountIdSource, TokenResult } from "../types.js";
3
4
  import { type AccountPoolWriteOutcome } from "./account-pool-write.js";
4
5
  /**
@@ -35,7 +36,7 @@ export declare function isAbortError(error: unknown): boolean;
35
36
  *
36
37
  * @internal
37
38
  */
38
- export declare function resolveAccountSelection(tokens: TokenSuccess, orgOverride?: string): TokenSuccessWithAccount;
39
+ export declare function resolveAccountSelection(tokens: TokenSuccess, orgOverride?: string, targetAccountId?: string): TokenSuccessWithAccount;
39
40
  /** @internal */
40
41
  export type OAuthSignInMode = "browser" | "manual" | "device" | "restore-backup" | "cancel";
41
42
  /** @internal */
@@ -49,7 +50,42 @@ export declare function runSignInFlow(forceNewLogin: boolean, signInMode: Extrac
49
50
  /** @internal */
50
51
  export type PersistAccountPoolOutcome = AccountPoolWriteOutcome;
51
52
  /** @internal */
52
- export declare function persistAccountPool(results: TokenSuccessWithAccount[], replaceAll: boolean): Promise<PersistAccountPoolOutcome | null>;
53
+ export interface PersistAccountPoolResult {
54
+ /** How the pool absorbed this login (appended, refreshed, or rebound). */
55
+ outcome: PersistAccountPoolOutcome;
56
+ /** Index of the row the login actually wrote. */
57
+ accountIndex: number;
58
+ /** Global selection index after the write. */
59
+ activeIndex: number;
60
+ /**
61
+ * Whether the written row is the one plain `codex` will use. Callers gate the
62
+ * ~/.codex/auth.json sync on this: refreshing the active account must publish
63
+ * its new tokens, and refreshing any other row must not steal the selection.
64
+ */
65
+ isActiveAccount: boolean;
66
+ /** Whether the written row is enabled for rotation after the write. */
67
+ accountEnabled: boolean;
68
+ }
69
+ /** @internal */
70
+ export type ExpectedLoginAccount = Pick<AccountMetadataV3, "recordId" | "accountId" | "email" | "refreshToken">;
71
+ /** @internal */
72
+ export interface PersistAccountPoolOptions {
73
+ /** Keep every global/model-family selection on its pre-login identity. */
74
+ preserveSelection?: boolean;
75
+ /** Refuse the write unless the OAuth result is this saved account. */
76
+ expectedAccount?: ExpectedLoginAccount;
77
+ /**
78
+ * Position {@link expectedAccount} occupied when the caller resolved it.
79
+ * Treated as a hint that is verified against the row actually sitting there,
80
+ * never trusted: the pool can change between the caller's read and this
81
+ * transaction. It exists so an unambiguous positional request such as
82
+ * `login --account 2` stays deterministic when several saved rows share an
83
+ * identity, which identity matching alone cannot tell apart.
84
+ */
85
+ expectedAccountIndex?: number;
86
+ }
87
+ /** @internal */
88
+ export declare function persistAccountPool(results: TokenSuccessWithAccount[], replaceAll: boolean, options?: PersistAccountPoolOptions): Promise<PersistAccountPoolResult | null>;
53
89
  /** @internal */
54
90
  export declare function syncSelectionToCodex(tokens: TokenSuccessWithAccount): Promise<void>;
55
91
  //# sourceMappingURL=login-oauth.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"login-oauth.d.ts","sourceRoot":"","sources":["../../../lib/codex-manager/login-oauth.ts"],"names":[],"mappings":"AAEA,OAAO,EAON,KAAK,SAAS,EACd,MAAM,gBAAgB,CAAC;AAsBxB,OAAO,KAAK,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAEhE,OAAO,EACN,KAAK,uBAAuB,EAG5B,MAAM,yBAAyB,CAAC;AAOjC;;;;;GAKG;AAEH,gBAAgB;AAChB,MAAM,MAAM,YAAY,GAAG,OAAO,CAAC,WAAW,EAAE;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,CAAC,CAAC;AACrE,gBAAgB;AAChB,MAAM,MAAM,uBAAuB,GAAG,YAAY,GAAG;IACpD,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,eAAe,CAAC,EAAE,eAAe,CAAC;IAClC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,SAAS,EAAE,CAAC;CACzB,CAAC;AAIF,gBAAgB;AAChB,wBAAgB,mBAAmB,CAClC,MAAM,EAAE,OAAO,CAAC,WAAW,EAAE;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,CAAC,GAC/C,OAAO,CAGT;AAED,gBAAgB;AAChB,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAIpD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,uBAAuB,CACtC,MAAM,EAAE,YAAY,EACpB,WAAW,CAAC,EAAE,MAAM,GAClB,uBAAuB,CA+DzB;AA0FD,gBAAgB;AAChB,MAAM,MAAM,eAAe,GACxB,SAAS,GACT,QAAQ,GACR,QAAQ,GACR,gBAAgB,GAChB,QAAQ,CAAC;AACZ,gBAAgB;AAChB,MAAM,MAAM,iBAAiB,GAAG;IAC/B,SAAS,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,gBAAgB;AAChB,wBAAsB,YAAY,CACjC,aAAa,EAAE,OAAO,EACtB,UAAU,EAAE,OAAO,CAAC,eAAe,EAAE,SAAS,GAAG,QAAQ,CAAC,GACxD,OAAO,CAAC,WAAW,CAAC,CA6ItB;AAED,gBAAgB;AAChB,wBAAsB,aAAa,CAClC,aAAa,EAAE,OAAO,EACtB,UAAU,EAAE,OAAO,CAAC,eAAe,EAAE,SAAS,GAAG,QAAQ,GAAG,QAAQ,CAAC,EACrE,OAAO,GAAE,iBAAsB,GAC7B,OAAO,CAAC,WAAW,CAAC,CAetB;AAED,gBAAgB;AAChB,MAAM,MAAM,yBAAyB,GAAG,uBAAuB,CAAC;AAEhE,gBAAgB;AAChB,wBAAsB,kBAAkB,CACvC,OAAO,EAAE,uBAAuB,EAAE,EAClC,UAAU,EAAE,OAAO,GACjB,OAAO,CAAC,yBAAyB,GAAG,IAAI,CAAC,CAuD3C;AAED,gBAAgB;AAChB,wBAAsB,oBAAoB,CACzC,MAAM,EAAE,uBAAuB,GAC7B,OAAO,CAAC,IAAI,CAAC,CAkBf"}
1
+ {"version":3,"file":"login-oauth.d.ts","sourceRoot":"","sources":["../../../lib/codex-manager/login-oauth.ts"],"names":[],"mappings":"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"}
@@ -11,6 +11,8 @@ import { setCodexCliActiveSelection } from "../codex-cli/writer.js";
11
11
  import { createLogger } from "../logger.js";
12
12
  import { MODEL_FAMILIES } from "../prompts/codex.js";
13
13
  import { findMatchingAccountIndex, withAccountStorageTransaction, } from "../storage.js";
14
+ import { cloneAccountStorageForPersistence } from "../storage/account-persistence.js";
15
+ import { CodexValidationError } from "../errors.js";
14
16
  import { UI_COPY } from "../ui/ui-copy.js";
15
17
  import { applyAccountPoolResults, } from "./account-pool-write.js";
16
18
  import { stylePromptText } from "./formatters/index.js";
@@ -39,7 +41,7 @@ export function isAbortError(error) {
39
41
  *
40
42
  * @internal
41
43
  */
42
- export function resolveAccountSelection(tokens, orgOverride) {
44
+ export function resolveAccountSelection(tokens, orgOverride, targetAccountId) {
43
45
  const candidates = getAccountIdCandidates(tokens.access, tokens.idToken);
44
46
  // Surface every workspace/organization exposed by the token so the saved
45
47
  // account can track them (issue #491/#512). Without this, same-email
@@ -54,18 +56,50 @@ export function resolveAccountSelection(tokens, orgOverride) {
54
56
  isDefault: candidate.isDefault,
55
57
  }))
56
58
  : undefined;
57
- const override = resolveOrgOverride(orgOverride);
58
- if (override) {
59
- // Prefer the token candidate's human label for the chosen org so the
60
- // saved row is identifiable, falling back to a bare manual binding.
61
- const matched = candidates.find((candidate) => candidate.accountId === override);
62
- return {
63
- ...tokens,
64
- accountIdOverride: override,
65
- accountIdSource: "manual",
66
- accountLabel: matched?.label,
67
- workspaces,
68
- };
59
+ // A targeted re-authentication names one saved row, which is a stricter and
60
+ // more specific instruction than the ambient CODEX_AUTH_ACCOUNT_ID env var
61
+ // that resolveOrgOverride also honours. Consulting the override first let an
62
+ // unrelated exported org id hijack `login --account <n>` and then fail the
63
+ // identity guard with a misleading "does not match" message. An explicit
64
+ // `--org` cannot reach here alongside a target: parseAuthLoginArgs rejects
65
+ // that combination outright.
66
+ if (targetAccountId) {
67
+ const targetedCandidate = candidates.find((candidate) => candidate.accountId === targetAccountId);
68
+ if (targetedCandidate) {
69
+ return {
70
+ ...tokens,
71
+ accountIdOverride: targetedCandidate.accountId,
72
+ // `id_token` candidates auto-follow the access token inside
73
+ // resolveRequestAccountId, which would rewrite this binding back to
74
+ // the access-token account and make the caller's own identity guard
75
+ // reject the write. Pin those as an explicit selection; a `token`
76
+ // candidate already IS the access-token account, and an `org`
77
+ // candidate never auto-follows, so both keep their source.
78
+ accountIdSource: targetedCandidate.source === "id_token"
79
+ ? "manual"
80
+ : targetedCandidate.source,
81
+ accountLabel: targetedCandidate.label,
82
+ workspaces,
83
+ };
84
+ }
85
+ // The user signed in as somebody else. Fall through to the generic
86
+ // selection so persistAccountPool refuses the write with the precise
87
+ // identity-mismatch message instead of a confusing override error.
88
+ }
89
+ else {
90
+ const override = resolveOrgOverride(orgOverride);
91
+ if (override) {
92
+ // Prefer the token candidate's human label for the chosen org so the
93
+ // saved row is identifiable, falling back to a bare manual binding.
94
+ const matched = candidates.find((candidate) => candidate.accountId === override);
95
+ return {
96
+ ...tokens,
97
+ accountIdOverride: override,
98
+ accountIdSource: "manual",
99
+ accountLabel: matched?.label,
100
+ workspaces,
101
+ };
102
+ }
69
103
  }
70
104
  if (candidates.length === 0) {
71
105
  return tokens;
@@ -300,8 +334,81 @@ export async function runSignInFlow(forceNewLogin, signInMode, options = {}) {
300
334
  }
301
335
  return runOAuthFlow(forceNewLogin, signInMode);
302
336
  }
337
+ function sameExpectedLoginIdentity(write, expected) {
338
+ const expectedAccountId = expected.accountId?.trim();
339
+ const incomingAccountId = write.accountId?.trim();
340
+ const expectedEmail = sanitizeEmail(expected.email);
341
+ const incomingEmail = sanitizeEmail(write.email);
342
+ // When the saved row already carries the provider identity it is
343
+ // authoritative and the incoming login must match it: the id survives an
344
+ // email change, and falling back to a shared email across workspaces would
345
+ // let a targeted re-auth overwrite the wrong saved account.
346
+ if (expectedAccountId) {
347
+ return incomingAccountId === expectedAccountId;
348
+ }
349
+ // A pre-#491 row saved before workspace tracking carries no account id at
350
+ // all. Fresh tokens essentially always carry one, so demanding the id on both
351
+ // sides made every legacy row permanently un-refreshable through --account.
352
+ // Email is the only identity such a row has, and the caller already resolved
353
+ // it to a single unambiguous row before starting the sign-in.
354
+ return Boolean(expectedEmail && incomingEmail === expectedEmail);
355
+ }
356
+ function matchesExpectedAccount(account, expected) {
357
+ if (!account)
358
+ return false;
359
+ if (expected.recordId && account.recordId) {
360
+ return account.recordId === expected.recordId;
361
+ }
362
+ const expectedAccountId = expected.accountId?.trim();
363
+ const candidateAccountId = account.accountId?.trim();
364
+ if (expectedAccountId || candidateAccountId) {
365
+ return Boolean(expectedAccountId &&
366
+ candidateAccountId &&
367
+ expectedAccountId === candidateAccountId);
368
+ }
369
+ const expectedEmail = sanitizeEmail(expected.email);
370
+ const candidateEmail = sanitizeEmail(account.email);
371
+ if (expectedEmail || candidateEmail) {
372
+ return Boolean(expectedEmail && candidateEmail === expectedEmail);
373
+ }
374
+ return account.refreshToken === expected.refreshToken;
375
+ }
376
+ /**
377
+ * Locate the saved row a targeted re-authentication must write.
378
+ *
379
+ * Resolution is deliberately identity-first and refresh-token-last. The runtime
380
+ * rotation proxy rewrites a stored refreshToken whenever it refreshes an account
381
+ * (AccountManager.commitRefreshedAuth), so a sign-in that overlaps a live codex
382
+ * session would otherwise "lose" its own target and throw away a completed
383
+ * OAuth. `recordId` is exact when both sides have one, the caller's position is
384
+ * honoured only while the row sitting there is still the same identity, and
385
+ * findMatchingAccountIndex supplies the same composite/email/refresh-token
386
+ * ladder the rest of the pool already resolves identities with.
387
+ */
388
+ function resolveExpectedAccountIndex(accounts, expected, hintedIndex) {
389
+ if (expected.recordId) {
390
+ const byRecordId = accounts.findIndex((account) => account?.recordId === expected.recordId);
391
+ if (byRecordId >= 0)
392
+ return byRecordId;
393
+ }
394
+ if (validSelectionIndex(hintedIndex, accounts) &&
395
+ matchesExpectedAccount(accounts[hintedIndex], expected)) {
396
+ return hintedIndex;
397
+ }
398
+ return findMatchingAccountIndex(accounts, {
399
+ accountId: expected.accountId,
400
+ email: expected.email,
401
+ refreshToken: expected.refreshToken,
402
+ }, { allowUniqueAccountIdFallbackWithoutEmail: true });
403
+ }
404
+ function validSelectionIndex(index, accounts) {
405
+ return (typeof index === "number" &&
406
+ Number.isInteger(index) &&
407
+ index >= 0 &&
408
+ index < accounts.length);
409
+ }
303
410
  /** @internal */
304
- export async function persistAccountPool(results, replaceAll) {
411
+ export async function persistAccountPool(results, replaceAll, options = {}) {
305
412
  if (results.length === 0)
306
413
  return null;
307
414
  return await withAccountStorageTransaction(async (loadedStorage, persist) => {
@@ -324,26 +431,101 @@ export async function persistAccountPool(results, replaceAll) {
324
431
  accessToken: result.access,
325
432
  expiresAt: result.expires,
326
433
  workspaces: result.workspaces,
434
+ // A targeted re-auth only tops up credentials for the row the user
435
+ // named; it must not silently re-enable an account they disabled.
436
+ preserveEnabledState: Boolean(options.expectedAccount),
327
437
  now,
328
438
  };
329
439
  });
440
+ if (options.expectedAccount && writes.length !== 1) {
441
+ throw new CodexValidationError("Targeted re-authentication accepts exactly one OAuth result. No saved credentials were changed.");
442
+ }
443
+ const expectedAccountIndex = options.expectedAccount && stored
444
+ ? resolveExpectedAccountIndex(stored.accounts, options.expectedAccount, options.expectedAccountIndex)
445
+ : undefined;
446
+ if (options.expectedAccount && expectedAccountIndex === undefined) {
447
+ throw new CodexValidationError("The requested Codex account is no longer present. No saved credentials were changed.");
448
+ }
449
+ const expectedAccount = options.expectedAccount;
450
+ if (expectedAccount &&
451
+ writes.some((write) => !sameExpectedLoginIdentity(write, expectedAccount))) {
452
+ throw new CodexValidationError("The authenticated Codex identity does not match the account requested for re-authentication. No saved credentials were changed.");
453
+ }
330
454
  const { accounts, activeIndex, outcome } = applyAccountPoolResults({
331
455
  existing,
332
456
  writes,
333
457
  priorActiveIndex: stored?.activeIndex,
334
- findMatchingAccountIndex,
458
+ findMatchingAccountIndex: expectedAccountIndex === undefined
459
+ ? findMatchingAccountIndex
460
+ : () => expectedAccountIndex,
335
461
  });
462
+ const nextActiveIndex = options.preserveSelection &&
463
+ validSelectionIndex(stored?.activeIndex, accounts)
464
+ ? stored.activeIndex
465
+ : activeIndex;
466
+ const priorActiveIndex = stored?.activeIndex;
336
467
  const activeIndexByFamily = {};
337
468
  for (const family of MODEL_FAMILIES) {
338
- activeIndexByFamily[family] = activeIndex;
469
+ const storedFamilyIndex = stored?.activeIndexByFamily?.[family];
470
+ const priorFamilyIndex = storedFamilyIndex ?? priorActiveIndex;
471
+ if (!validSelectionIndex(priorFamilyIndex, accounts)) {
472
+ activeIndexByFamily[family] = nextActiveIndex;
473
+ continue;
474
+ }
475
+ if (options.preserveSelection) {
476
+ activeIndexByFamily[family] = priorFamilyIndex;
477
+ continue;
478
+ }
479
+ // A family the user deliberately pointed at a DIFFERENT account than
480
+ // the global selection is a setting of its own, and a plain login must
481
+ // not silently drag it onto the account just signed in: adding a third
482
+ // account used to move a deliberate `codex-max -> account 2` binding
483
+ // onto account 3. Families that were merely following the global
484
+ // selection keep following it, which is what makes a plain login still
485
+ // feel like "this new account is now the one in use".
486
+ //
487
+ // Indices stay meaningful here because login only updates a row in
488
+ // place or appends one; it never reorders or removes the pool.
489
+ const followedGlobalSelection = storedFamilyIndex === undefined || storedFamilyIndex === priorActiveIndex;
490
+ activeIndexByFamily[family] = followedGlobalSelection
491
+ ? nextActiveIndex
492
+ : priorFamilyIndex;
339
493
  }
340
- await persist({
341
- version: 3,
342
- accounts,
343
- activeIndex,
344
- activeIndexByFamily,
345
- });
346
- return outcome;
494
+ // Reuse the shared persistence clone so the pin/affinity carry-over rule
495
+ // (issue #474) keeps living in exactly one place instead of being
496
+ // re-derived at every write site with a slightly different validity check.
497
+ const nextStorage = cloneAccountStorageForPersistence(stored);
498
+ nextStorage.accounts = accounts;
499
+ nextStorage.activeIndex = nextActiveIndex;
500
+ nextStorage.activeIndexByFamily = activeIndexByFamily;
501
+ // A selection-preserving login leaves the pool exactly where it was, so a
502
+ // manual `switch <n>` pin survives it: login only updates a row in place
503
+ // or appends one and never reorders, so the raw position still points at
504
+ // the row the user pinned even when two rows share an identity.
505
+ //
506
+ // A plain login is the opposite. It moves activeIndex onto the account
507
+ // just signed in and publishes that account to ~/.codex/auth.json.
508
+ // Carrying an old pin through it would leave storage claiming one account
509
+ // is active while the runtime proxy routes every request to another
510
+ // (runtime-rotation-proxy resolves pinnedAccountIndex first) -- the
511
+ // contradiction `status` reports as "runtime using N but pin requests M".
512
+ if (!options.preserveSelection ||
513
+ !validSelectionIndex(nextStorage.pinnedAccountIndex, accounts)) {
514
+ delete nextStorage.pinnedAccountIndex;
515
+ }
516
+ await persist(nextStorage);
517
+ if (outcome === null)
518
+ return null;
519
+ return {
520
+ outcome,
521
+ accountIndex: activeIndex,
522
+ activeIndex: nextActiveIndex,
523
+ // The Codex CLI reads the `codex` family selection, so that -- not the
524
+ // bare global index -- decides whether this write owns the native
525
+ // ~/.codex/auth.json credentials.
526
+ isActiveAccount: activeIndex === (activeIndexByFamily.codex ?? nextActiveIndex),
527
+ accountEnabled: accounts[activeIndex]?.enabled !== false,
528
+ };
347
529
  });
348
530
  }
349
531
  /** @internal */
@@ -1 +1 @@
1
- {"version":3,"file":"ledger.d.ts","sourceRoot":"","sources":["../../../lib/usage/ledger.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EACX,sBAAsB,EAEtB,gBAAgB,EAChB,gBAAgB,EAEhB,cAAc,EAEd,YAAY,EAGZ,iBAAiB,EAEjB,MAAM,YAAY,CAAC;AAqOpB,wBAAgB,mBAAmB,IAAI,gBAAgB,CAMtD;AAED,wBAAsB,oBAAoB,CACzC,KAAK,EAAE,sBAAsB,GAC3B,OAAO,CAAC,cAAc,CAAC,CASzB;AA+JD,wBAAsB,mBAAmB,CACxC,KAAK,GAAE,gBAAqB,GAC1B,OAAO,CAAC,cAAc,EAAE,CAAC,CAc3B;AAgDD,wBAAgB,kBAAkB,CACjC,IAAI,EAAE,cAAc,EAAE,EACtB,KAAK,GAAE,iBAAsB,GAC3B,YAAY,CAwBd;AAED,wBAAsB,oBAAoB,CACzC,KAAK,GAAE,iBAAsB,GAC3B,OAAO,CAAC,YAAY,CAAC,CAEvB;AAED,wBAAsB,iBAAiB,CAAC,OAAO,GAAE;IAChD,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,iBAAiB,CAAC,EAAE,MAAM,CAAC;CACtB,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CA2B9B;AAED,wBAAgB,6BAA6B,IAAI,IAAI,CAEpD"}
1
+ {"version":3,"file":"ledger.d.ts","sourceRoot":"","sources":["../../../lib/usage/ledger.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EACX,sBAAsB,EAEtB,gBAAgB,EAChB,gBAAgB,EAEhB,cAAc,EAEd,YAAY,EAGZ,iBAAiB,EAEjB,MAAM,YAAY,CAAC;AAqOpB,wBAAgB,mBAAmB,IAAI,gBAAgB,CAMtD;AAED,wBAAsB,oBAAoB,CACzC,KAAK,EAAE,sBAAsB,GAC3B,OAAO,CAAC,cAAc,CAAC,CASzB;AA+JD,wBAAsB,mBAAmB,CACxC,KAAK,GAAE,gBAAqB,GAC1B,OAAO,CAAC,cAAc,EAAE,CAAC,CAc3B;AAuDD,wBAAgB,kBAAkB,CACjC,IAAI,EAAE,cAAc,EAAE,EACtB,KAAK,GAAE,iBAAsB,GAC3B,YAAY,CAwBd;AAED,wBAAsB,oBAAoB,CACzC,KAAK,GAAE,iBAAsB,GAC3B,OAAO,CAAC,YAAY,CAAC,CAEvB;AAED,wBAAsB,iBAAiB,CAAC,OAAO,GAAE;IAChD,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,iBAAiB,CAAC,EAAE,MAAM,CAAC;CACtB,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CA2B9B;AAED,wBAAgB,6BAA6B,IAAI,IAAI,CAEpD"}
@@ -405,6 +405,7 @@ function createBucket(key) {
405
405
  reasoningTokens: 0,
406
406
  totalTokens: 0,
407
407
  costUsd: 0,
408
+ unpricedRequests: 0,
408
409
  };
409
410
  }
410
411
  function getBucketKey(row, by) {
@@ -437,6 +438,12 @@ function addRowToBucket(bucket, row) {
437
438
  bucket.reasoningTokens += row.tokens.reasoningTokens;
438
439
  bucket.totalTokens += row.tokens.totalTokens;
439
440
  bucket.costUsd = Number((bucket.costUsd + (row.costUsd ?? 0)).toFixed(8));
441
+ // Only rows that actually consumed tokens count as unpriced spend. A blocked
442
+ // or cancelled request carries no tokens and no cost, and must not make a
443
+ // cost budget unevaluable.
444
+ if (row.costUsd === null && row.tokens.totalTokens > 0) {
445
+ bucket.unpricedRequests += 1;
446
+ }
440
447
  }
441
448
  export function summarizeUsageRows(rows, query = {}) {
442
449
  const by = query.by ?? "model";
@@ -5,6 +5,21 @@ export interface UsageModelPricing {
5
5
  cachedInputUsdPerMillion?: number;
6
6
  reasoningUsdPerMillion?: number;
7
7
  }
8
+ /**
9
+ * Models the router can normalize to that have no published rate here yet.
10
+ *
11
+ * Their cost is deliberately reported as unknown (`null`) rather than guessed:
12
+ * a wrong dollar figure is worse than no figure, and it would silently move a
13
+ * `maxCostUsd` budget's trip point. Nothing may treat an unknown cost as zero —
14
+ * `evaluateBudgetGuard` refuses a cost budget while unknown-cost usage is in
15
+ * the window instead of counting it as free (that under-count made cost caps
16
+ * unenforceable for exactly these models, every `pro` tier among them).
17
+ *
18
+ * Add a real rate to MODEL_PRICING and delete the entry here once published;
19
+ * `test/usage-pricing-coverage.test.ts` fails if a NEW routable model appears
20
+ * in neither list.
21
+ */
22
+ export declare const UNPRICED_ROUTABLE_MODELS: readonly ["gpt-5.1", "gpt-5.2-pro", "gpt-5.4-mini", "gpt-5.4-nano", "gpt-5.4-pro", "gpt-5.5-pro", "gpt-5-mini", "gpt-5-nano"];
8
23
  export declare function getUsageModelPricing(model: string | null | undefined): UsageModelPricing | null;
9
24
  export declare function estimateUsageCostUsd(model: string | null | undefined, tokens: UsageTokenCounts): number | null;
10
25
  export declare function listUsageModelPricing(): Record<string, UsageModelPricing>;
@@ -1 +1 @@
1
- {"version":3,"file":"pricing.d.ts","sourceRoot":"","sources":["../../../lib/usage/pricing.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEnD,MAAM,WAAW,iBAAiB;IACjC,kBAAkB,EAAE,MAAM,CAAC;IAC3B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,wBAAwB,CAAC,EAAE,MAAM,CAAC;IAClC,sBAAsB,CAAC,EAAE,MAAM,CAAC;CAChC;AAgED,wBAAgB,oBAAoB,CACnC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAC9B,iBAAiB,GAAG,IAAI,CAM1B;AAED,wBAAgB,oBAAoB,CACnC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAChC,MAAM,EAAE,gBAAgB,GACtB,MAAM,GAAG,IAAI,CAqBf;AAED,wBAAgB,qBAAqB,IAAI,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAEzE"}
1
+ {"version":3,"file":"pricing.d.ts","sourceRoot":"","sources":["../../../lib/usage/pricing.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEnD,MAAM,WAAW,iBAAiB;IACjC,kBAAkB,EAAE,MAAM,CAAC;IAC3B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,wBAAwB,CAAC,EAAE,MAAM,CAAC;IAClC,sBAAsB,CAAC,EAAE,MAAM,CAAC;CAChC;AA2DD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,wBAAwB,+HAS3B,CAAC;AAOX,wBAAgB,oBAAoB,CACnC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAC9B,iBAAiB,GAAG,IAAI,CAM1B;AAED,wBAAgB,oBAAoB,CACnC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAChC,MAAM,EAAE,gBAAgB,GACtB,MAAM,GAAG,IAAI,CAqBf;AAED,wBAAgB,qBAAqB,IAAI,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAEzE"}
@@ -54,6 +54,30 @@ const MODEL_PRICING = {
54
54
  reasoningUsdPerMillion: 6,
55
55
  },
56
56
  };
57
+ /**
58
+ * Models the router can normalize to that have no published rate here yet.
59
+ *
60
+ * Their cost is deliberately reported as unknown (`null`) rather than guessed:
61
+ * a wrong dollar figure is worse than no figure, and it would silently move a
62
+ * `maxCostUsd` budget's trip point. Nothing may treat an unknown cost as zero —
63
+ * `evaluateBudgetGuard` refuses a cost budget while unknown-cost usage is in
64
+ * the window instead of counting it as free (that under-count made cost caps
65
+ * unenforceable for exactly these models, every `pro` tier among them).
66
+ *
67
+ * Add a real rate to MODEL_PRICING and delete the entry here once published;
68
+ * `test/usage-pricing-coverage.test.ts` fails if a NEW routable model appears
69
+ * in neither list.
70
+ */
71
+ export const UNPRICED_ROUTABLE_MODELS = [
72
+ "gpt-5.1",
73
+ "gpt-5.2-pro",
74
+ "gpt-5.4-mini",
75
+ "gpt-5.4-nano",
76
+ "gpt-5.4-pro",
77
+ "gpt-5.5-pro",
78
+ "gpt-5-mini",
79
+ "gpt-5-nano",
80
+ ];
57
81
  function normalizeModelName(model) {
58
82
  const trimmed = model?.trim().toLowerCase();
59
83
  return trimmed && trimmed.length > 0 ? trimmed : null;
@@ -74,6 +74,12 @@ export interface UsageSummaryBucket {
74
74
  reasoningTokens: number;
75
75
  totalTokens: number;
76
76
  costUsd: number;
77
+ /**
78
+ * Requests that consumed tokens but could not be priced, so `costUsd` is an
79
+ * UNDER-count by an unknown amount. A cost budget cannot be evaluated while
80
+ * this is above zero; see `evaluateBudgetGuard`.
81
+ */
82
+ unpricedRequests: number;
77
83
  }
78
84
  export interface UsageSummary {
79
85
  since: number | null;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../lib/usage/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../lib/usage/types.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-multi-auth",
3
- "version": "2.9.0",
3
+ "version": "2.9.1",
4
4
  "description": "Codex CLI multi-account OAuth manager with account switching, health checks, runtime rotation, diagnostics, and recovery tools for @openai/codex",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",