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.
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +14 -1
- package/dist/lib/budget-guard.d.ts +1 -0
- package/dist/lib/budget-guard.d.ts.map +1 -1
- package/dist/lib/budget-guard.js +13 -3
- package/dist/lib/codex-manager/account-pool-write.d.ts +8 -0
- package/dist/lib/codex-manager/account-pool-write.d.ts.map +1 -1
- package/dist/lib/codex-manager/account-pool-write.js +1 -1
- package/dist/lib/codex-manager/help.d.ts +2 -0
- package/dist/lib/codex-manager/help.d.ts.map +1 -1
- package/dist/lib/codex-manager/help.js +38 -1
- package/dist/lib/codex-manager/login-flow.d.ts +3 -0
- package/dist/lib/codex-manager/login-flow.d.ts.map +1 -1
- package/dist/lib/codex-manager/login-flow.js +73 -6
- package/dist/lib/codex-manager/login-menu-actions.d.ts.map +1 -1
- package/dist/lib/codex-manager/login-menu-actions.js +34 -4
- package/dist/lib/codex-manager/login-oauth.d.ts +38 -2
- package/dist/lib/codex-manager/login-oauth.d.ts.map +1 -1
- package/dist/lib/codex-manager/login-oauth.js +205 -23
- package/dist/lib/usage/ledger.d.ts.map +1 -1
- package/dist/lib/usage/ledger.js +7 -0
- package/dist/lib/usage/pricing.d.ts +15 -0
- package/dist/lib/usage/pricing.d.ts.map +1 -1
- package/dist/lib/usage/pricing.js +24 -0
- package/dist/lib/usage/types.d.ts +6 -0
- package/dist/lib/usage/types.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codex-multi-auth",
|
|
3
|
-
"version": "2.9.
|
|
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.
|
|
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)
|
|
@@ -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;
|
|
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"}
|
package/dist/lib/budget-guard.js
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
@@ -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;
|
|
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":"
|
|
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 ||
|
|
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
|
-
|
|
351
|
-
|
|
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 =
|
|
421
|
+
const outcomeMessage = persistResult?.outcome === "rebound"
|
|
361
422
|
? `Rebound workspace for existing account. Total: ${count}`
|
|
362
|
-
:
|
|
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;
|
|
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
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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
|
|
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;
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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]
|
|
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
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
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;
|
|
1
|
+
{"version":3,"file":"ledger.d.ts","sourceRoot":"","sources":["../../../lib/usage/ledger.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EACX,sBAAsB,EAEtB,gBAAgB,EAChB,gBAAgB,EAEhB,cAAc,EAEd,YAAY,EAGZ,iBAAiB,EAEjB,MAAM,YAAY,CAAC;AAqOpB,wBAAgB,mBAAmB,IAAI,gBAAgB,CAMtD;AAED,wBAAsB,oBAAoB,CACzC,KAAK,EAAE,sBAAsB,GAC3B,OAAO,CAAC,cAAc,CAAC,CASzB;AA+JD,wBAAsB,mBAAmB,CACxC,KAAK,GAAE,gBAAqB,GAC1B,OAAO,CAAC,cAAc,EAAE,CAAC,CAc3B;AAuDD,wBAAgB,kBAAkB,CACjC,IAAI,EAAE,cAAc,EAAE,EACtB,KAAK,GAAE,iBAAsB,GAC3B,YAAY,CAwBd;AAED,wBAAsB,oBAAoB,CACzC,KAAK,GAAE,iBAAsB,GAC3B,OAAO,CAAC,YAAY,CAAC,CAEvB;AAED,wBAAsB,iBAAiB,CAAC,OAAO,GAAE;IAChD,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,iBAAiB,CAAC,EAAE,MAAM,CAAC;CACtB,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CA2B9B;AAED,wBAAgB,6BAA6B,IAAI,IAAI,CAEpD"}
|
package/dist/lib/usage/ledger.js
CHANGED
|
@@ -405,6 +405,7 @@ function createBucket(key) {
|
|
|
405
405
|
reasoningTokens: 0,
|
|
406
406
|
totalTokens: 0,
|
|
407
407
|
costUsd: 0,
|
|
408
|
+
unpricedRequests: 0,
|
|
408
409
|
};
|
|
409
410
|
}
|
|
410
411
|
function getBucketKey(row, by) {
|
|
@@ -437,6 +438,12 @@ function addRowToBucket(bucket, row) {
|
|
|
437
438
|
bucket.reasoningTokens += row.tokens.reasoningTokens;
|
|
438
439
|
bucket.totalTokens += row.tokens.totalTokens;
|
|
439
440
|
bucket.costUsd = Number((bucket.costUsd + (row.costUsd ?? 0)).toFixed(8));
|
|
441
|
+
// Only rows that actually consumed tokens count as unpriced spend. A blocked
|
|
442
|
+
// or cancelled request carries no tokens and no cost, and must not make a
|
|
443
|
+
// cost budget unevaluable.
|
|
444
|
+
if (row.costUsd === null && row.tokens.totalTokens > 0) {
|
|
445
|
+
bucket.unpricedRequests += 1;
|
|
446
|
+
}
|
|
440
447
|
}
|
|
441
448
|
export function summarizeUsageRows(rows, query = {}) {
|
|
442
449
|
const by = query.by ?? "model";
|
|
@@ -5,6 +5,21 @@ export interface UsageModelPricing {
|
|
|
5
5
|
cachedInputUsdPerMillion?: number;
|
|
6
6
|
reasoningUsdPerMillion?: number;
|
|
7
7
|
}
|
|
8
|
+
/**
|
|
9
|
+
* Models the router can normalize to that have no published rate here yet.
|
|
10
|
+
*
|
|
11
|
+
* Their cost is deliberately reported as unknown (`null`) rather than guessed:
|
|
12
|
+
* a wrong dollar figure is worse than no figure, and it would silently move a
|
|
13
|
+
* `maxCostUsd` budget's trip point. Nothing may treat an unknown cost as zero —
|
|
14
|
+
* `evaluateBudgetGuard` refuses a cost budget while unknown-cost usage is in
|
|
15
|
+
* the window instead of counting it as free (that under-count made cost caps
|
|
16
|
+
* unenforceable for exactly these models, every `pro` tier among them).
|
|
17
|
+
*
|
|
18
|
+
* Add a real rate to MODEL_PRICING and delete the entry here once published;
|
|
19
|
+
* `test/usage-pricing-coverage.test.ts` fails if a NEW routable model appears
|
|
20
|
+
* in neither list.
|
|
21
|
+
*/
|
|
22
|
+
export declare const UNPRICED_ROUTABLE_MODELS: readonly ["gpt-5.1", "gpt-5.2-pro", "gpt-5.4-mini", "gpt-5.4-nano", "gpt-5.4-pro", "gpt-5.5-pro", "gpt-5-mini", "gpt-5-nano"];
|
|
8
23
|
export declare function getUsageModelPricing(model: string | null | undefined): UsageModelPricing | null;
|
|
9
24
|
export declare function estimateUsageCostUsd(model: string | null | undefined, tokens: UsageTokenCounts): number | null;
|
|
10
25
|
export declare function listUsageModelPricing(): Record<string, UsageModelPricing>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pricing.d.ts","sourceRoot":"","sources":["../../../lib/usage/pricing.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEnD,MAAM,WAAW,iBAAiB;IACjC,kBAAkB,EAAE,MAAM,CAAC;IAC3B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,wBAAwB,CAAC,EAAE,MAAM,CAAC;IAClC,sBAAsB,CAAC,EAAE,MAAM,CAAC;CAChC;
|
|
1
|
+
{"version":3,"file":"pricing.d.ts","sourceRoot":"","sources":["../../../lib/usage/pricing.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEnD,MAAM,WAAW,iBAAiB;IACjC,kBAAkB,EAAE,MAAM,CAAC;IAC3B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,wBAAwB,CAAC,EAAE,MAAM,CAAC;IAClC,sBAAsB,CAAC,EAAE,MAAM,CAAC;CAChC;AA2DD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,wBAAwB,+HAS3B,CAAC;AAOX,wBAAgB,oBAAoB,CACnC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAC9B,iBAAiB,GAAG,IAAI,CAM1B;AAED,wBAAgB,oBAAoB,CACnC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAChC,MAAM,EAAE,gBAAgB,GACtB,MAAM,GAAG,IAAI,CAqBf;AAED,wBAAgB,qBAAqB,IAAI,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAEzE"}
|
|
@@ -54,6 +54,30 @@ const MODEL_PRICING = {
|
|
|
54
54
|
reasoningUsdPerMillion: 6,
|
|
55
55
|
},
|
|
56
56
|
};
|
|
57
|
+
/**
|
|
58
|
+
* Models the router can normalize to that have no published rate here yet.
|
|
59
|
+
*
|
|
60
|
+
* Their cost is deliberately reported as unknown (`null`) rather than guessed:
|
|
61
|
+
* a wrong dollar figure is worse than no figure, and it would silently move a
|
|
62
|
+
* `maxCostUsd` budget's trip point. Nothing may treat an unknown cost as zero —
|
|
63
|
+
* `evaluateBudgetGuard` refuses a cost budget while unknown-cost usage is in
|
|
64
|
+
* the window instead of counting it as free (that under-count made cost caps
|
|
65
|
+
* unenforceable for exactly these models, every `pro` tier among them).
|
|
66
|
+
*
|
|
67
|
+
* Add a real rate to MODEL_PRICING and delete the entry here once published;
|
|
68
|
+
* `test/usage-pricing-coverage.test.ts` fails if a NEW routable model appears
|
|
69
|
+
* in neither list.
|
|
70
|
+
*/
|
|
71
|
+
export const UNPRICED_ROUTABLE_MODELS = [
|
|
72
|
+
"gpt-5.1",
|
|
73
|
+
"gpt-5.2-pro",
|
|
74
|
+
"gpt-5.4-mini",
|
|
75
|
+
"gpt-5.4-nano",
|
|
76
|
+
"gpt-5.4-pro",
|
|
77
|
+
"gpt-5.5-pro",
|
|
78
|
+
"gpt-5-mini",
|
|
79
|
+
"gpt-5-nano",
|
|
80
|
+
];
|
|
57
81
|
function normalizeModelName(model) {
|
|
58
82
|
const trimmed = model?.trim().toLowerCase();
|
|
59
83
|
return trimmed && trimmed.length > 0 ? trimmed : null;
|
|
@@ -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.
|
|
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",
|