@narumitw/pi-usage 0.52.1 β 0.52.3
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/README.md +80 -53
- package/dist/index.ts +192 -49
- package/dist/index.ts.map +4 -4
- package/package.json +5 -5
- package/src/codex-resets.ts +83 -18
- package/src/oauth-credential-source.ts +88 -0
- package/src/query.ts +80 -27
- package/src/usage.ts +8 -6
package/README.md
CHANGED
|
@@ -1,28 +1,26 @@
|
|
|
1
|
-
# π pi-usage β Provider Usage
|
|
1
|
+
# π pi-usage β Check Provider Usage and Codex Fast Mode
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@narumitw/pi-usage) [](https://pi.dev) [](./LICENSE)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Check the limits and usage for the provider account Pi is actually using, and toggle Fast mode for supported OpenAI Codex models.
|
|
6
|
+
|
|
7
|
+
The extension reports each provider's native semantics instead of presenting unlike quotas as equivalent.
|
|
6
8
|
|
|
7
9
|
## β¨ Features
|
|
8
10
|
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
- Supports
|
|
12
|
-
- Toggles persistent Codex Fast routing through `/fast` or the contextual
|
|
13
|
-
- Redeems
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
- Runs manually requested all-provider queries with concurrency limited to two and preserves partial results.
|
|
18
|
-
- Labels only the selected model provider as `Current`; other results are `Configured`.
|
|
19
|
-
- Keeps the compact statusline scoped to the current provider and runtime account.
|
|
20
|
-
- Isolates its five-minute in-memory cache by provider and a process-salted credential fingerprint.
|
|
21
|
-
- Resolves runtime credentials through Pi; for Copilot only, reads Pi's stored OAuth credential through Pi's public credential API and verifies that it matches the active runtime account.
|
|
11
|
+
- Shows current-account usage and next actions through `/usage`.
|
|
12
|
+
- Supports OpenAI Codex subscription windows, credits, resets, and model-specific buckets.
|
|
13
|
+
- Supports GitHub Copilot allowances and OpenRouter per-key limits and spend windows.
|
|
14
|
+
- Toggles persistent Codex Fast routing through `/fast` or the contextual usage menu.
|
|
15
|
+
- Redeems eligible Codex resets only after fresh account matching and explicit confirmation.
|
|
16
|
+
- Refreshes one or all configured providers with bounded concurrency and partial-result preservation.
|
|
17
|
+
- Keeps statusline and cache data scoped to the current provider and runtime account.
|
|
18
|
+
- Resolves credentials through Pi or the process-local OAuth credential-source protocol and validates the effective provider endpoint before sending them.
|
|
22
19
|
|
|
23
20
|
## π¦ Install
|
|
24
21
|
|
|
25
22
|
Requires Pi 0.81.0 or newer so the extension can validate the effective base URL attached to resolved provider auth before sending credentials to an official usage endpoint.
|
|
23
|
+
The v1 credential-source interoperability path is characterized against Pi 0.84.3; other runtimes retain standalone fallback but do not receive the protocol timing guarantee.
|
|
26
24
|
|
|
27
25
|
```bash
|
|
28
26
|
pi install npm:@narumitw/pi-usage
|
|
@@ -34,17 +32,21 @@ Try without installing permanently:
|
|
|
34
32
|
pi -e npm:@narumitw/pi-usage
|
|
35
33
|
```
|
|
36
34
|
|
|
37
|
-
|
|
35
|
+
Build and try this package locally from the repository root:
|
|
38
36
|
|
|
39
37
|
```bash
|
|
38
|
+
npm --workspace @narumitw/pi-usage run build
|
|
40
39
|
pi -e ./packages/pi-usage
|
|
41
40
|
```
|
|
42
41
|
|
|
43
|
-
The package declares `dist/index.ts`, so an unbuilt local checkout must run
|
|
42
|
+
The package declares `dist/index.ts`, so an unbuilt local checkout must run the build before Pi loads the package directory.
|
|
43
|
+
|
|
44
|
+
## π Quick start
|
|
44
45
|
|
|
45
|
-
`
|
|
46
|
+
Run `/usage` in TUI or RPC mode to inspect the current provider, refresh usage, or choose another configured provider.
|
|
47
|
+
Use `/fast` separately to toggle Fast mode for a supported current Codex model.
|
|
46
48
|
|
|
47
|
-
##
|
|
49
|
+
## π¬ Commands
|
|
48
50
|
|
|
49
51
|
Run:
|
|
50
52
|
|
|
@@ -52,8 +54,7 @@ Run:
|
|
|
52
54
|
/usage
|
|
53
55
|
```
|
|
54
56
|
|
|
55
|
-
In TUI or RPC mode, the standard menu first queries the current model provider and presents its state
|
|
56
|
-
with these actions:
|
|
57
|
+
In TUI or RPC mode, the standard menu first queries the current model provider and presents its state with these actions:
|
|
57
58
|
|
|
58
59
|
```text
|
|
59
60
|
Refresh current usage
|
|
@@ -65,19 +66,18 @@ Close
|
|
|
65
66
|
```
|
|
66
67
|
|
|
67
68
|
There are intentionally no `/usage --refresh`, `/usage <provider>`, or `/usage --all` argument paths.
|
|
68
|
-
Cross-provider traffic requires an explicit interactive choice.
|
|
69
|
-
and closes the root menu.
|
|
70
|
-
|
|
71
|
-
provider work and supports in-flight abort rather than presenting a standard menu screen.
|
|
72
|
-
|
|
73
|
-
For the current OpenAI Codex provider, **Redeem usage limit resetβ¦** checks fresh earned-reset
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
statusline are refreshed for the still-current account.
|
|
69
|
+
Cross-provider traffic requires an explicit interactive choice.
|
|
70
|
+
Escape returns from provider selection and closes the root menu.
|
|
71
|
+
Print and JSON modes reject `/usage` observably because they cannot host the interactive flow.
|
|
72
|
+
The cancellable live-query progress view remains extension-owned because it streams provider work and supports in-flight abort rather than presenting a standard menu screen.
|
|
73
|
+
|
|
74
|
+
For the current OpenAI Codex provider, **Redeem usage limit resetβ¦** checks fresh earned-reset details, lets you select a reset when details are available, and shows the exact reset before asking for confirmation.
|
|
75
|
+
**No, go back** is the safe default and cancellation before confirmation sends no mutation.
|
|
76
|
+
After confirmation, the reset operation cannot be cancelled from its progress view; session replacement or shutdown still aborts owned work.
|
|
77
|
+
A transport failure offers **Try again** with the same redemption request ID so the backend can treat an uncertain retry idempotently.
|
|
78
|
+
Successful, already-completed, not-needed, and no-credit outcomes are reported separately, then usage and the statusline are refreshed for the still-current account.
|
|
79
|
+
|
|
80
|
+
## βοΈ Settings
|
|
81
81
|
|
|
82
82
|
### Codex Fast mode
|
|
83
83
|
|
|
@@ -109,14 +109,14 @@ Repair or remove an invalid file, then run `/reload` before trying the toggle ag
|
|
|
109
109
|
- Reset mutation: `POST /wham/rate-limit-reset-credits/consume` with a unique redemption request ID and, when available, the selected opaque credit ID
|
|
110
110
|
- Statusline examples: `codex 59% 5h 61% wk`, `codex fast 59% 5h`, or `codex spark 100% 5h`
|
|
111
111
|
|
|
112
|
-
The statusline selects a returned bucket that matches the current Codex model when one is available.
|
|
112
|
+
The statusline selects a returned bucket that matches the current Codex model when one is available.
|
|
113
|
+
Unlike `pi-codex-usage`, this successor intentionally has no Codex CLI fallback because the CLI may be logged into a different account than Pi's active runtime account.
|
|
113
114
|
|
|
114
|
-
Reset redemption is available only when Codex is the current provider and Pi's freshly resolved access
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
opaque credit and account IDs are never shown or persisted by the extension.
|
|
115
|
+
Reset redemption is available only when Codex is the current provider and Pi's freshly resolved access token exactly matches an OAuth credential from Pi's stored login or a compatible credential source.
|
|
116
|
+
`pi-usage` forwards only the bearer authorization and matching `chatgpt-account-id` to the official ChatGPT origin.
|
|
117
|
+
API-key credentials, configured-but-not-current Codex accounts, account changes during the flow, and custom/proxy origins fail before mutation.
|
|
118
|
+
Backend-provided titles and descriptions are sanitized for terminal display.
|
|
119
|
+
Opaque credit and account IDs are never shown or persisted by the extension.
|
|
120
120
|
|
|
121
121
|
### GitHub Copilot
|
|
122
122
|
|
|
@@ -126,7 +126,12 @@ opaque credit and account IDs are never shown or persisted by the extension.
|
|
|
126
126
|
- Displayed data: entitlement, remaining allowance, percentage, reset time, plan, and any additional usage beyond the included allowance
|
|
127
127
|
- Statusline examples: `copilot credits 1200/1500 80%`, `copilot 245/300 82%`, or `copilot chat 40/50 80%`
|
|
128
128
|
|
|
129
|
-
GitHub's quota endpoint requires the original GitHub OAuth token rather than the short-lived Copilot inference token exposed by runtime auth.
|
|
129
|
+
GitHub's quota endpoint requires the original GitHub OAuth token rather than the short-lived Copilot inference token exposed by runtime auth.
|
|
130
|
+
`pi-usage` supports Copilot accounts created through Pi's `/login` flow and named accounts offered by a compatible `oauth:credential-source:v1` owner.
|
|
131
|
+
It uses a candidate only when its short-lived access token exactly matches the freshly resolved active runtime credential.
|
|
132
|
+
Duplicate equivalent candidates are harmless, while conflicting matches fail closed without choosing by extension load order.
|
|
133
|
+
API-key credentials, account mismatches, GitHub Enterprise accounts, and proxy/custom provider origins fail closed.
|
|
134
|
+
The detailed report follows the endpoint's `token_based_billing` marker so AI credits are not mislabeled as legacy premium requests, and it reports overage without treating a negative included balance as a malformed response.
|
|
130
135
|
|
|
131
136
|
### OpenRouter
|
|
132
137
|
|
|
@@ -136,33 +141,43 @@ GitHub's quota endpoint requires the original GitHub OAuth token rather than the
|
|
|
136
141
|
- Displayed data: key label when safely returned, optional per-key limit and remaining amount, reset period, and daily/weekly/monthly/all-time spend
|
|
137
142
|
- Statusline examples: `openrouter $74.50 left` or `openrouter $25.50 used`
|
|
138
143
|
|
|
139
|
-
The extension does not call OpenRouter's account-level `/credits` endpoint because that operation requires a separate management key.
|
|
144
|
+
The extension does not call OpenRouter's account-level `/credits` endpoint because that operation requires a separate management key.
|
|
145
|
+
OpenRouter documents the distinction between credit and rate limits in its [API limits guide](https://openrouter.ai/docs/api_reference/limits).
|
|
140
146
|
|
|
141
147
|
### OpenCode Go (Zen)
|
|
142
148
|
|
|
143
149
|
- Provider ID: `opencode-go`
|
|
144
150
|
- Semantics: OpenCode Zen plan usage windowsβrolling, weekly, and monthly
|
|
145
|
-
- Source: `GET
|
|
151
|
+
- Source: `GET https://opencode.ai/zen/go/v1/usage` using Pi's resolved inference API key
|
|
146
152
|
- Displayed data: used percentage and reset time for each window; `rate-limited` windows remain visible at their reported usage, while unknown statuses are reported as unavailable notes
|
|
147
153
|
- Statusline examples: `zen 0% r 4% w 2% m`
|
|
148
154
|
|
|
149
|
-
The
|
|
155
|
+
The fixed endpoint is queried only when the candidate OpenCode Go model and the resolved provider-auth base URL, when present, use the official `https://opencode.ai` origin; other origins fail before sending the credential.
|
|
150
156
|
|
|
151
157
|
## π§ Current and configured accounts
|
|
152
158
|
|
|
153
|
-
`Current` means the provider and credential used by Pi's selected model.
|
|
159
|
+
`Current` means the provider and credential used by Pi's selected model.
|
|
160
|
+
`Configured` means Pi reports runtime auth for another supported provider; it does not mean that provider is active.
|
|
154
161
|
|
|
155
|
-
The extension does not enumerate multiple accounts inside one provider and does not switch accounts.
|
|
162
|
+
The extension does not enumerate multiple accounts inside one provider and does not switch accounts.
|
|
163
|
+
Account selection remains owned by Pi or an account-management extension.
|
|
164
|
+
A compatible credential owner may offer the verified active named account through the versioned process-local protocol without exposing its account label or storage.
|
|
165
|
+
Without such an owner, `pi-usage` retains its standalone Pi `auth.json` behavior.
|
|
166
|
+
An older or incompatible owner degrades to the existing authentication-unavailable result when the stored login does not match runtime auth.
|
|
167
|
+
After the active runtime credential changes, the next command, turn, or scheduled refresh resolves auth again and cannot reuse another account's cached report.
|
|
156
168
|
|
|
157
169
|
## π Statusline behavior
|
|
158
170
|
|
|
159
|
-
The `usage` status item is active only for the selected model provider.
|
|
171
|
+
The `usage` status item is active only for the selected model provider.
|
|
172
|
+
It refreshes every five minutes while the session remains on a supported provider and is cleared when the model changes to an unsupported provider.
|
|
160
173
|
|
|
161
|
-
Manual another-provider and all-provider queries never publish to the statusline.
|
|
174
|
+
Manual another-provider and all-provider queries never publish to the statusline.
|
|
175
|
+
`@narumitw/pi-statusline` supplies the default `π` icon; `pi-usage` publishes text-only values.
|
|
162
176
|
|
|
163
177
|
## π Migrating from pi-codex-usage
|
|
164
178
|
|
|
165
|
-
`pi-codex-usage` is deprecated and its source is archived under `deprecated/`.
|
|
179
|
+
`pi-codex-usage` is deprecated and its source is archived under `deprecated/`.
|
|
180
|
+
To migrate one installation:
|
|
166
181
|
|
|
167
182
|
```bash
|
|
168
183
|
pi remove npm:@narumitw/pi-codex-usage
|
|
@@ -178,15 +193,25 @@ Behavior changes:
|
|
|
178
193
|
- Codex CLI fallback is removed to preserve active-runtime-account correctness.
|
|
179
194
|
- The status key changes from `codex-usage` to `usage`.
|
|
180
195
|
|
|
196
|
+
## π Security and privacy
|
|
197
|
+
|
|
198
|
+
Credential candidates are collected synchronously in memory and are not cached, persisted, logged, formatted, or appended to the Pi session.
|
|
199
|
+
The protocol carries no account name or extension identity.
|
|
200
|
+
Only the selected provider's exact runtime match is used, and secrets are sent only to the validated official provider origin.
|
|
201
|
+
Pi extensions run with the user's process privileges, so the shared event bus is not a security boundary between installed extensions.
|
|
202
|
+
Install only trusted extensions because any installed extension may already read user files and process memory.
|
|
203
|
+
Protocol v1 interoperability is characterized for the repository's supported Pi runtime; an absent or incompatible peer preserves standalone fallback and fail-closed mismatch behavior.
|
|
204
|
+
|
|
181
205
|
## π§ Limitations
|
|
182
206
|
|
|
183
207
|
- Only providers with a meaningful usage source and verifiable Pi runtime auth are supported.
|
|
184
208
|
- GitHub Copilot quota and OpenAI Codex reset redemption use undocumented provider endpoints that may change without notice.
|
|
185
|
-
- Codex reset redemption requires a current ChatGPT OAuth login
|
|
209
|
+
- Codex reset redemption requires a current ChatGPT OAuth credential from Pi's login or a compatible credential source; Codex API keys cannot redeem earned subscription resets.
|
|
186
210
|
- Credentials resolved for custom provider base URLs are never forwarded to the providers' official usage endpoints; effective auth origin validation requires Pi 0.81.0 or newer.
|
|
187
211
|
- Provider reports are snapshots and may themselves be delayed by the provider.
|
|
188
212
|
- OpenRouter successful inference responses do not expose proactive request-rate counters; `/usage` reports the documented per-key credit/spend fields instead.
|
|
189
|
-
- A provider may not return a safe human-readable account identity.
|
|
213
|
+
- A provider may not return a safe human-readable account identity.
|
|
214
|
+
In that case the provider and runtime credential state remain visible without exposing secrets.
|
|
190
215
|
- Immediate account-change events are not available from Pi; auth is re-resolved before commands, turns, and scheduled refreshes.
|
|
191
216
|
- Fast model support is intentionally conservative and may require an extension update when Codex adds or removes service tiers.
|
|
192
217
|
- Another later-loaded extension can replace the final provider payload, so arbitrary third-party payload-rewrite conflicts cannot be prevented.
|
|
@@ -206,6 +231,7 @@ packages/pi-usage/
|
|
|
206
231
|
β βββ settings.ts # Validated user settings and atomic persistence
|
|
207
232
|
β βββ usage-helpers.ts # Small orchestration helpers
|
|
208
233
|
β βββ query.ts # Runtime auth resolution and bounded provider queries
|
|
234
|
+
β βββ oauth-credential-source.ts # Ephemeral OAuth candidate collection
|
|
209
235
|
β βββ codex-resets.ts # Codex reset auth, API contracts, and normalization
|
|
210
236
|
β βββ format.ts # Provider-aware notifications and statusline text
|
|
211
237
|
β βββ core.ts # Cache, concurrency, fingerprint, and redaction helpers
|
|
@@ -228,4 +254,5 @@ Pi extension, Pi coding agent, usage, quota, OpenAI Codex usage, ChatGPT subscri
|
|
|
228
254
|
|
|
229
255
|
## π License
|
|
230
256
|
|
|
231
|
-
MIT.
|
|
257
|
+
MIT.
|
|
258
|
+
See [`LICENSE`](./LICENSE).
|
package/dist/index.ts
CHANGED
|
@@ -86,7 +86,7 @@ function costsEqual(left, right) {
|
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
// src/codex-resets.ts
|
|
89
|
-
import { readStoredCredential as
|
|
89
|
+
import { readStoredCredential as readStoredCredential3 } from "@earendil-works/pi-coding-agent";
|
|
90
90
|
|
|
91
91
|
// src/core.ts
|
|
92
92
|
import { createHmac } from "node:crypto";
|
|
@@ -266,9 +266,67 @@ function truncate(value, maxChars) {
|
|
|
266
266
|
return `${value.slice(0, maxChars - 1)}\u2026`;
|
|
267
267
|
}
|
|
268
268
|
|
|
269
|
+
// src/oauth-credential-source.ts
|
|
270
|
+
import {
|
|
271
|
+
readStoredCredential
|
|
272
|
+
} from "@earendil-works/pi-coding-agent";
|
|
273
|
+
var OAUTH_CREDENTIAL_SOURCE_CHANNEL = "oauth:credential-source:v1";
|
|
274
|
+
function createOAuthCredentialCandidateReader(pi, credentialReader = readStoredCredential) {
|
|
275
|
+
return (ctx, providerId) => collectOAuthCredentialCandidates(pi, ctx, providerId, credentialReader);
|
|
276
|
+
}
|
|
277
|
+
function collectOAuthCredentialCandidates(pi, ctx, providerId, credentialReader = readStoredCredential) {
|
|
278
|
+
const candidates = [];
|
|
279
|
+
let collecting = true;
|
|
280
|
+
const request = Object.freeze({
|
|
281
|
+
session: ctx.sessionManager,
|
|
282
|
+
provider: providerId,
|
|
283
|
+
offer(candidate) {
|
|
284
|
+
if (!collecting) return;
|
|
285
|
+
const clone = cloneOAuthCredential(candidate);
|
|
286
|
+
if (clone) candidates.push(clone);
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
try {
|
|
290
|
+
pi.events.emit(OAUTH_CREDENTIAL_SOURCE_CHANNEL, request);
|
|
291
|
+
} catch {
|
|
292
|
+
return { ok: false };
|
|
293
|
+
} finally {
|
|
294
|
+
collecting = false;
|
|
295
|
+
}
|
|
296
|
+
const offeredCount = candidates.length;
|
|
297
|
+
try {
|
|
298
|
+
const fallback = cloneOAuthCredential(credentialReader(providerId));
|
|
299
|
+
if (fallback) candidates.push(fallback);
|
|
300
|
+
} catch {
|
|
301
|
+
}
|
|
302
|
+
return { ok: true, candidates, offeredCount };
|
|
303
|
+
}
|
|
304
|
+
function fallbackOAuthCredentialCandidates(providerId, credentialReader) {
|
|
305
|
+
try {
|
|
306
|
+
const credential = cloneOAuthCredential(credentialReader(providerId));
|
|
307
|
+
return { ok: true, candidates: credential ? [credential] : [], offeredCount: 0 };
|
|
308
|
+
} catch {
|
|
309
|
+
return { ok: true, candidates: [], offeredCount: 0 };
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
function cloneOAuthCredential(value) {
|
|
313
|
+
try {
|
|
314
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
315
|
+
const clone = structuredClone(value);
|
|
316
|
+
if (!clone || typeof clone !== "object" || Array.isArray(clone)) return void 0;
|
|
317
|
+
if (clone.type !== "oauth") return void 0;
|
|
318
|
+
if (typeof clone.access !== "string" || !clone.access) return void 0;
|
|
319
|
+
if (typeof clone.refresh !== "string" || !clone.refresh) return void 0;
|
|
320
|
+
if (typeof clone.expires !== "number" || !Number.isFinite(clone.expires)) return void 0;
|
|
321
|
+
return clone;
|
|
322
|
+
} catch {
|
|
323
|
+
return void 0;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
269
327
|
// src/query.ts
|
|
270
328
|
import { randomBytes } from "node:crypto";
|
|
271
|
-
import { readStoredCredential } from "@earendil-works/pi-coding-agent";
|
|
329
|
+
import { readStoredCredential as readStoredCredential2 } from "@earendil-works/pi-coding-agent";
|
|
272
330
|
|
|
273
331
|
// src/providers/codex.ts
|
|
274
332
|
function normalizeCodexBackendPayload(payload, capturedAt) {
|
|
@@ -628,6 +686,7 @@ function asNonnegativeNumber3(value) {
|
|
|
628
686
|
var CODEX_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
|
|
629
687
|
var GITHUB_COPILOT_USAGE_URL = "https://api.github.com/copilot_internal/user";
|
|
630
688
|
var OPENROUTER_KEY_URL = "https://openrouter.ai/api/v1/key";
|
|
689
|
+
var OPENCODE_GO_USAGE_URL = "https://opencode.ai/zen/go/v1/usage";
|
|
631
690
|
var MAX_SUCCESS_BODY_BYTES = 64 * 1024;
|
|
632
691
|
var MAX_ERROR_BODY_BYTES = 4 * 1024;
|
|
633
692
|
var AUTH_FINGERPRINT_SALT = randomBytes(32);
|
|
@@ -689,7 +748,7 @@ var SUPPORTED_ADAPTERS = [
|
|
|
689
748
|
semantics: { kind: "consumer-subscription", label: "OpenCode Zen plan usage" },
|
|
690
749
|
async query(auth, signal, timeoutMs) {
|
|
691
750
|
const payload = await fetchProviderJson(
|
|
692
|
-
|
|
751
|
+
OPENCODE_GO_USAGE_URL,
|
|
693
752
|
auth,
|
|
694
753
|
signal,
|
|
695
754
|
timeoutMs,
|
|
@@ -705,7 +764,7 @@ function adapterForProvider(providerId) {
|
|
|
705
764
|
function isStaleExtensionContextError(error) {
|
|
706
765
|
return error instanceof Error && error.message.includes("This extension ctx is stale after session replacement or reload");
|
|
707
766
|
}
|
|
708
|
-
async function resolveUsageAuth(ctx, adapter, salt = AUTH_FINGERPRINT_SALT, credentialReader =
|
|
767
|
+
async function resolveUsageAuth(ctx, adapter, salt = AUTH_FINGERPRINT_SALT, credentialReader = readStoredCredential2, candidateReader) {
|
|
709
768
|
if (ctx.model?.provider === adapter.id && !hasOfficialOrigin(ctx.model, adapter.id)) {
|
|
710
769
|
throw new Error(
|
|
711
770
|
`${adapter.displayName} usage cannot send a custom provider base URL credential to the official usage endpoint.`
|
|
@@ -734,7 +793,17 @@ async function resolveUsageAuth(ctx, adapter, salt = AUTH_FINGERPRINT_SALT, cred
|
|
|
734
793
|
const auth = modelAuth ?? providerResult?.auth;
|
|
735
794
|
if (!auth) return void 0;
|
|
736
795
|
if (adapter.id === "github-copilot") {
|
|
737
|
-
|
|
796
|
+
const offered = candidateReader ? candidateReader(ctx, adapter.id) : fallbackOAuthCredentialCandidates(adapter.id, credentialReader);
|
|
797
|
+
if (!offered.ok) {
|
|
798
|
+
throw new Error("GitHub Copilot OAuth credential discovery failed closed.");
|
|
799
|
+
}
|
|
800
|
+
return resolveGitHubCopilotUsageAuth(
|
|
801
|
+
auth,
|
|
802
|
+
model,
|
|
803
|
+
salt,
|
|
804
|
+
offered.candidates,
|
|
805
|
+
offered.offeredCount === 0
|
|
806
|
+
);
|
|
738
807
|
}
|
|
739
808
|
const authorization = authorizationFrom(auth);
|
|
740
809
|
if (!authorization) return void 0;
|
|
@@ -875,27 +944,58 @@ async function readBoundedResponse(response, maxBytes, truncateOverflow, descrip
|
|
|
875
944
|
const text = new TextDecoder().decode(body);
|
|
876
945
|
return truncated ? `${text}\u2026` : text;
|
|
877
946
|
}
|
|
878
|
-
function resolveGitHubCopilotUsageAuth(auth, model, salt,
|
|
879
|
-
const
|
|
880
|
-
if (
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
947
|
+
function resolveGitHubCopilotUsageAuth(auth, model, salt, candidates, standaloneFallback) {
|
|
948
|
+
const resolvedAccess = bearerToken(headerValue(auth.headers, "Authorization")) ?? auth.apiKey;
|
|
949
|
+
if (!resolvedAccess) throw new Error("GitHub Copilot OAuth credentials were incomplete.");
|
|
950
|
+
let sawOAuth = false;
|
|
951
|
+
let sawMatchingAccess = false;
|
|
952
|
+
let sawIncompleteMatch = false;
|
|
953
|
+
let sawEnterpriseMatch = false;
|
|
954
|
+
const matches = /* @__PURE__ */ new Map();
|
|
955
|
+
for (const candidate of candidates) {
|
|
956
|
+
try {
|
|
957
|
+
const credential = asObject5(candidate);
|
|
958
|
+
if (credential?.type !== "oauth") continue;
|
|
959
|
+
sawOAuth = true;
|
|
960
|
+
const storedAccess2 = typeof credential.access === "string" && credential.access ? credential.access : void 0;
|
|
961
|
+
if (storedAccess2 !== resolvedAccess) continue;
|
|
962
|
+
sawMatchingAccess = true;
|
|
963
|
+
const enterpriseUrl = credential.enterpriseUrl;
|
|
964
|
+
if (typeof enterpriseUrl === "string" && enterpriseUrl && !isPublicGitHubDomain(enterpriseUrl)) {
|
|
965
|
+
sawEnterpriseMatch = true;
|
|
966
|
+
continue;
|
|
967
|
+
}
|
|
968
|
+
const refresh2 = typeof credential.refresh === "string" && credential.refresh ? credential.refresh : void 0;
|
|
969
|
+
if (!refresh2) {
|
|
970
|
+
sawIncompleteMatch = true;
|
|
971
|
+
continue;
|
|
972
|
+
}
|
|
973
|
+
matches.set(`${storedAccess2.length}:${storedAccess2}${refresh2}`, { refresh: refresh2, storedAccess: storedAccess2 });
|
|
974
|
+
} catch {
|
|
975
|
+
}
|
|
884
976
|
}
|
|
885
|
-
if (
|
|
977
|
+
if (sawEnterpriseMatch) {
|
|
886
978
|
throw new Error("GitHub Copilot usage does not yet support GitHub Enterprise accounts.");
|
|
887
979
|
}
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
980
|
+
if (sawIncompleteMatch) throw new Error("GitHub Copilot OAuth credentials were incomplete.");
|
|
981
|
+
if (matches.size > 1) {
|
|
982
|
+
throw new Error(
|
|
983
|
+
"Conflicting OAuth credentials match the active GitHub Copilot runtime account."
|
|
984
|
+
);
|
|
893
985
|
}
|
|
894
|
-
|
|
986
|
+
const match = matches.values().next().value;
|
|
987
|
+
if (!match) {
|
|
988
|
+
if (!sawOAuth) {
|
|
989
|
+
throw new Error(
|
|
990
|
+
standaloneFallback ? "GitHub Copilot usage requires the OAuth account configured through Pi /login." : "GitHub Copilot usage requires an OAuth account configured through Pi /login or a compatible credential source."
|
|
991
|
+
);
|
|
992
|
+
}
|
|
993
|
+
if (sawMatchingAccess) throw new Error("GitHub Copilot OAuth credentials were incomplete.");
|
|
895
994
|
throw new Error(
|
|
896
|
-
"The active GitHub Copilot runtime account does not match Pi's stored OAuth account."
|
|
995
|
+
standaloneFallback ? "The active GitHub Copilot runtime account does not match Pi's stored OAuth account." : "The active GitHub Copilot runtime account does not match any available OAuth account."
|
|
897
996
|
);
|
|
898
997
|
}
|
|
998
|
+
const { refresh, storedAccess } = match;
|
|
899
999
|
const authorization = `Bearer ${refresh}`;
|
|
900
1000
|
const headers = {
|
|
901
1001
|
Authorization: authorization,
|
|
@@ -954,11 +1054,6 @@ function headerValue(headers, name) {
|
|
|
954
1054
|
function hasHeader(headers, name) {
|
|
955
1055
|
return Object.keys(headers).some((key) => key.toLowerCase() === name.toLowerCase());
|
|
956
1056
|
}
|
|
957
|
-
function opencodeUsageUrl(baseUrl) {
|
|
958
|
-
const base = baseUrl?.trim().replace(/\/+$/u, "");
|
|
959
|
-
if (!base) throw new Error("OpenCode Go model base URL is unavailable.");
|
|
960
|
-
return `${base}/usage`;
|
|
961
|
-
}
|
|
962
1057
|
function isAbortError(error) {
|
|
963
1058
|
return error instanceof Error && error.name === "AbortError";
|
|
964
1059
|
}
|
|
@@ -1014,7 +1109,7 @@ function formatCodexResetOutcome(outcome, remainingCount) {
|
|
|
1014
1109
|
function resetLabel(count) {
|
|
1015
1110
|
return count === 1 ? "usage limit reset" : "usage limit resets";
|
|
1016
1111
|
}
|
|
1017
|
-
async function resolveCodexResetAuth(ctx, salt = AUTH_FINGERPRINT_SALT, credentialReader =
|
|
1112
|
+
async function resolveCodexResetAuth(ctx, salt = AUTH_FINGERPRINT_SALT, credentialReader = readStoredCredential3, candidateReader) {
|
|
1018
1113
|
const model = ctx.model;
|
|
1019
1114
|
if (model?.provider !== "openai-codex") {
|
|
1020
1115
|
throw new Error("Usage limit resets require the current model to use OpenAI Codex.");
|
|
@@ -1027,25 +1122,20 @@ async function resolveCodexResetAuth(ctx, salt = AUTH_FINGERPRINT_SALT, credenti
|
|
|
1027
1122
|
throw new Error("The current model changed while resolving Codex reset authentication.");
|
|
1028
1123
|
}
|
|
1029
1124
|
if (!auth) throw new Error("No runtime credential is configured for OpenAI Codex.");
|
|
1030
|
-
const credential = asObject6(credentialReader("openai-codex"));
|
|
1031
|
-
if (credential?.type !== "oauth") {
|
|
1032
|
-
throw new Error(
|
|
1033
|
-
"Usage limit resets require the OpenAI Codex OAuth account configured through Pi /login."
|
|
1034
|
-
);
|
|
1035
|
-
}
|
|
1036
|
-
const storedAccess = asNonemptyString(credential.access);
|
|
1037
1125
|
const resolvedAccess = bearerToken2(headerValue2(auth.headers, "Authorization")) ?? auth.apiKey;
|
|
1038
|
-
if (!
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
}
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1126
|
+
if (!resolvedAccess) throw new Error("OpenAI Codex OAuth credentials were incomplete.");
|
|
1127
|
+
const resolvedAccountId = codexAccountIdFromAccessToken(resolvedAccess);
|
|
1128
|
+
if (!resolvedAccountId) {
|
|
1129
|
+
throw new Error("The active OpenAI Codex access token did not contain a valid account ID.");
|
|
1130
|
+
}
|
|
1131
|
+
const offered = candidateReader ? candidateReader(ctx, "openai-codex") : fallbackOAuthCredentialCandidates("openai-codex", credentialReader);
|
|
1132
|
+
if (!offered.ok) throw new Error("OpenAI Codex OAuth credential discovery failed closed.");
|
|
1133
|
+
const { accountId, storedAccess } = selectCodexResetCredential(
|
|
1134
|
+
offered.candidates,
|
|
1135
|
+
resolvedAccess,
|
|
1136
|
+
resolvedAccountId,
|
|
1137
|
+
offered.offeredCount === 0
|
|
1138
|
+
);
|
|
1049
1139
|
const authorization = `Bearer ${resolvedAccess}`;
|
|
1050
1140
|
const headers = {
|
|
1051
1141
|
Authorization: authorization,
|
|
@@ -1114,6 +1204,58 @@ function normalizeCodexResetCreditsPayload(payload) {
|
|
|
1114
1204
|
}
|
|
1115
1205
|
return { availableCount, options };
|
|
1116
1206
|
}
|
|
1207
|
+
function selectCodexResetCredential(candidates, resolvedAccess, resolvedAccountId, standaloneFallback) {
|
|
1208
|
+
let sawOAuth = false;
|
|
1209
|
+
let sawMatchingAccess = false;
|
|
1210
|
+
let sawInvalidAccountId = false;
|
|
1211
|
+
const matches = /* @__PURE__ */ new Map();
|
|
1212
|
+
for (const candidate of candidates) {
|
|
1213
|
+
try {
|
|
1214
|
+
const credential = asObject6(candidate);
|
|
1215
|
+
if (credential?.type !== "oauth") continue;
|
|
1216
|
+
sawOAuth = true;
|
|
1217
|
+
const storedAccess = asNonemptyString(credential.access);
|
|
1218
|
+
if (storedAccess !== resolvedAccess) continue;
|
|
1219
|
+
sawMatchingAccess = true;
|
|
1220
|
+
const accountId = validHeaderValue(credential.accountId);
|
|
1221
|
+
const refresh = asNonemptyString(credential.refresh);
|
|
1222
|
+
if (!accountId || accountId !== resolvedAccountId || !refresh) {
|
|
1223
|
+
sawInvalidAccountId = true;
|
|
1224
|
+
continue;
|
|
1225
|
+
}
|
|
1226
|
+
matches.set(refresh, { accountId, storedAccess });
|
|
1227
|
+
} catch {
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
if (sawInvalidAccountId) {
|
|
1231
|
+
throw new Error("The OpenAI Codex OAuth credential did not include a valid account ID.");
|
|
1232
|
+
}
|
|
1233
|
+
if (matches.size > 1) {
|
|
1234
|
+
throw new Error("Conflicting OAuth credentials match the active OpenAI Codex runtime account.");
|
|
1235
|
+
}
|
|
1236
|
+
const match = matches.values().next().value;
|
|
1237
|
+
if (match) return match;
|
|
1238
|
+
if (!sawOAuth) {
|
|
1239
|
+
throw new Error(
|
|
1240
|
+
standaloneFallback ? "Usage limit resets require the OpenAI Codex OAuth account configured through Pi /login." : "Usage limit resets require an OpenAI Codex OAuth account configured through Pi /login or a compatible credential source."
|
|
1241
|
+
);
|
|
1242
|
+
}
|
|
1243
|
+
if (sawMatchingAccess) throw new Error("OpenAI Codex OAuth credentials were incomplete.");
|
|
1244
|
+
throw new Error(
|
|
1245
|
+
standaloneFallback ? "The active OpenAI Codex runtime account does not match Pi's stored OAuth account." : "The active OpenAI Codex runtime account does not match any available OAuth account."
|
|
1246
|
+
);
|
|
1247
|
+
}
|
|
1248
|
+
function codexAccountIdFromAccessToken(access) {
|
|
1249
|
+
try {
|
|
1250
|
+
const parts = access.split(".");
|
|
1251
|
+
if (parts.length !== 3 || !parts[1]) return void 0;
|
|
1252
|
+
const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
|
|
1253
|
+
const claims = asObject6(asObject6(payload)?.["https://api.openai.com/auth"]);
|
|
1254
|
+
return validHeaderValue(claims?.chatgpt_account_id);
|
|
1255
|
+
} catch {
|
|
1256
|
+
return void 0;
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1117
1259
|
function normalizeResetOption(credit) {
|
|
1118
1260
|
const creditId = asOpaqueId(credit.id);
|
|
1119
1261
|
if (!creditId) throw new Error("Codex reset credits response returned an invalid credit ID.");
|
|
@@ -1784,6 +1926,7 @@ var CLOSE = "Close";
|
|
|
1784
1926
|
var REDEEM_CODEX_RESET = "Redeem usage limit reset\u2026";
|
|
1785
1927
|
function usageExtension(pi, dependencies = {}) {
|
|
1786
1928
|
const credentialReader = dependencies.credentialReader;
|
|
1929
|
+
const credentialCandidates = createOAuthCredentialCandidateReader(pi, credentialReader);
|
|
1787
1930
|
const createRedemptionId = dependencies.createRedemptionId ?? randomUUID2;
|
|
1788
1931
|
const settingsRuntime = dependencies.settingsRuntime ?? createUsageSettingsRuntime();
|
|
1789
1932
|
const cache = new UsageCache(CACHE_TTL_MS);
|
|
@@ -1872,7 +2015,7 @@ function usageExtension(pi, dependencies = {}) {
|
|
|
1872
2015
|
let auth;
|
|
1873
2016
|
try {
|
|
1874
2017
|
auth = await awaitWithDeadline(
|
|
1875
|
-
resolveUsageAuth(ctx, adapter),
|
|
2018
|
+
resolveUsageAuth(ctx, adapter, void 0, credentialReader, credentialCandidates),
|
|
1876
2019
|
signal,
|
|
1877
2020
|
DEFAULT_TIMEOUT_MS,
|
|
1878
2021
|
`resolving ${adapter.displayName} runtime auth`
|
|
@@ -2067,7 +2210,7 @@ function usageExtension(pi, dependencies = {}) {
|
|
|
2067
2210
|
if (!adapter) return false;
|
|
2068
2211
|
try {
|
|
2069
2212
|
const auth = await awaitWithDeadline(
|
|
2070
|
-
resolveUsageAuth(ctx, adapter),
|
|
2213
|
+
resolveUsageAuth(ctx, adapter, void 0, credentialReader, credentialCandidates),
|
|
2071
2214
|
signal,
|
|
2072
2215
|
DEFAULT_TIMEOUT_MS,
|
|
2073
2216
|
`revalidating ${adapter.displayName} runtime auth`
|
|
@@ -2082,7 +2225,7 @@ function usageExtension(pi, dependencies = {}) {
|
|
|
2082
2225
|
if (!adapter) return false;
|
|
2083
2226
|
try {
|
|
2084
2227
|
const auth = await awaitWithDeadline(
|
|
2085
|
-
resolveUsageAuth(ctx, adapter),
|
|
2228
|
+
resolveUsageAuth(ctx, adapter, void 0, credentialReader, credentialCandidates),
|
|
2086
2229
|
signal,
|
|
2087
2230
|
DEFAULT_TIMEOUT_MS,
|
|
2088
2231
|
`revalidating ${adapter.displayName} runtime auth`
|
|
@@ -2269,7 +2412,7 @@ function usageExtension(pi, dependencies = {}) {
|
|
|
2269
2412
|
async (signal) => {
|
|
2270
2413
|
const expectedModel = modelIdentity(ctx.model);
|
|
2271
2414
|
const auth = await awaitWithDeadline(
|
|
2272
|
-
resolveCodexResetAuth(ctx, void 0, credentialReader),
|
|
2415
|
+
resolveCodexResetAuth(ctx, void 0, credentialReader, credentialCandidates),
|
|
2273
2416
|
signal,
|
|
2274
2417
|
DEFAULT_TIMEOUT_MS,
|
|
2275
2418
|
"resolving current Codex reset authentication"
|
|
@@ -2287,7 +2430,7 @@ function usageExtension(pi, dependencies = {}) {
|
|
|
2287
2430
|
};
|
|
2288
2431
|
}
|
|
2289
2432
|
const revalidated = await awaitWithDeadline(
|
|
2290
|
-
resolveCodexResetAuth(ctx, void 0, credentialReader),
|
|
2433
|
+
resolveCodexResetAuth(ctx, void 0, credentialReader, credentialCandidates),
|
|
2291
2434
|
signal,
|
|
2292
2435
|
DEFAULT_TIMEOUT_MS,
|
|
2293
2436
|
"revalidating current Codex reset authentication"
|
|
@@ -2346,7 +2489,7 @@ function usageExtension(pi, dependencies = {}) {
|
|
|
2346
2489
|
controller.signal,
|
|
2347
2490
|
async (signal) => {
|
|
2348
2491
|
const auth = await awaitWithDeadline(
|
|
2349
|
-
resolveCodexResetAuth(ctx, void 0, credentialReader),
|
|
2492
|
+
resolveCodexResetAuth(ctx, void 0, credentialReader, credentialCandidates),
|
|
2350
2493
|
signal,
|
|
2351
2494
|
DEFAULT_TIMEOUT_MS,
|
|
2352
2495
|
"revalidating current Codex reset authentication"
|