@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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@narumitw/pi-usage",
|
|
3
|
-
"version": "0.52.
|
|
3
|
+
"version": "0.52.3",
|
|
4
4
|
"description": "Pi extension that shows current-account usage for Codex, GitHub Copilot, OpenRouter, and OpenCode Zen.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -43,11 +43,11 @@
|
|
|
43
43
|
"@earendil-works/pi-coding-agent": "*"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
|
-
"@biomejs/biome": "2.5.
|
|
47
|
-
"@earendil-works/pi-ai": "0.84.
|
|
48
|
-
"@earendil-works/pi-coding-agent": "0.84.
|
|
46
|
+
"@biomejs/biome": "2.5.10",
|
|
47
|
+
"@earendil-works/pi-ai": "0.84.3",
|
|
48
|
+
"@earendil-works/pi-coding-agent": "0.84.3",
|
|
49
49
|
"@types/node": "26.2.0",
|
|
50
|
-
"esbuild": "0.28.
|
|
50
|
+
"esbuild": "0.28.2",
|
|
51
51
|
"typescript": "7.0.2"
|
|
52
52
|
},
|
|
53
53
|
"repository": {
|
package/src/codex-resets.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { readStoredCredential } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { fingerprintResolvedAuth, sanitizeDisplayText } from "./core.js";
|
|
4
|
+
import {
|
|
5
|
+
fallbackOAuthCredentialCandidates,
|
|
6
|
+
type OAuthCredentialCandidateReader,
|
|
7
|
+
} from "./oauth-credential-source.js";
|
|
4
8
|
import {
|
|
5
9
|
AUTH_FINGERPRINT_SALT,
|
|
6
10
|
adapterForProvider,
|
|
@@ -98,6 +102,7 @@ export async function resolveCodexResetAuth(
|
|
|
98
102
|
ctx: ExtensionContext,
|
|
99
103
|
salt: Uint8Array = AUTH_FINGERPRINT_SALT,
|
|
100
104
|
credentialReader: StoredCredentialReader = readStoredCredential,
|
|
105
|
+
candidateReader?: OAuthCredentialCandidateReader,
|
|
101
106
|
): Promise<ResolvedUsageAuth> {
|
|
102
107
|
const model = ctx.model;
|
|
103
108
|
if (model?.provider !== "openai-codex") {
|
|
@@ -112,26 +117,22 @@ export async function resolveCodexResetAuth(
|
|
|
112
117
|
}
|
|
113
118
|
if (!auth) throw new Error("No runtime credential is configured for OpenAI Codex.");
|
|
114
119
|
|
|
115
|
-
const credential = asObject(credentialReader("openai-codex"));
|
|
116
|
-
if (credential?.type !== "oauth") {
|
|
117
|
-
throw new Error(
|
|
118
|
-
"Usage limit resets require the OpenAI Codex OAuth account configured through Pi /login.",
|
|
119
|
-
);
|
|
120
|
-
}
|
|
121
|
-
const storedAccess = asNonemptyString(credential.access);
|
|
122
120
|
const resolvedAccess = bearerToken(headerValue(auth.headers, "Authorization")) ?? auth.apiKey;
|
|
123
|
-
if (!
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
throw new Error(
|
|
128
|
-
"The active OpenAI Codex runtime account does not match Pi's stored OAuth account.",
|
|
129
|
-
);
|
|
121
|
+
if (!resolvedAccess) throw new Error("OpenAI Codex OAuth credentials were incomplete.");
|
|
122
|
+
const resolvedAccountId = codexAccountIdFromAccessToken(resolvedAccess);
|
|
123
|
+
if (!resolvedAccountId) {
|
|
124
|
+
throw new Error("The active OpenAI Codex access token did not contain a valid account ID.");
|
|
130
125
|
}
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
126
|
+
const offered = candidateReader
|
|
127
|
+
? candidateReader(ctx, "openai-codex")
|
|
128
|
+
: fallbackOAuthCredentialCandidates("openai-codex", credentialReader);
|
|
129
|
+
if (!offered.ok) throw new Error("OpenAI Codex OAuth credential discovery failed closed.");
|
|
130
|
+
const { accountId, storedAccess } = selectCodexResetCredential(
|
|
131
|
+
offered.candidates,
|
|
132
|
+
resolvedAccess,
|
|
133
|
+
resolvedAccountId,
|
|
134
|
+
offered.offeredCount === 0,
|
|
135
|
+
);
|
|
135
136
|
const authorization = `Bearer ${resolvedAccess}`;
|
|
136
137
|
const headers = {
|
|
137
138
|
Authorization: authorization,
|
|
@@ -226,6 +227,70 @@ export function normalizeCodexResetCreditsPayload(
|
|
|
226
227
|
return { availableCount, options };
|
|
227
228
|
}
|
|
228
229
|
|
|
230
|
+
function selectCodexResetCredential(
|
|
231
|
+
candidates: readonly unknown[],
|
|
232
|
+
resolvedAccess: string,
|
|
233
|
+
resolvedAccountId: string,
|
|
234
|
+
standaloneFallback: boolean,
|
|
235
|
+
): { accountId: string; storedAccess: string } {
|
|
236
|
+
let sawOAuth = false;
|
|
237
|
+
let sawMatchingAccess = false;
|
|
238
|
+
let sawInvalidAccountId = false;
|
|
239
|
+
const matches = new Map<string, { accountId: string; storedAccess: string }>();
|
|
240
|
+
for (const candidate of candidates) {
|
|
241
|
+
try {
|
|
242
|
+
const credential = asObject(candidate);
|
|
243
|
+
if (credential?.type !== "oauth") continue;
|
|
244
|
+
sawOAuth = true;
|
|
245
|
+
const storedAccess = asNonemptyString(credential.access);
|
|
246
|
+
if (storedAccess !== resolvedAccess) continue;
|
|
247
|
+
sawMatchingAccess = true;
|
|
248
|
+
const accountId = validHeaderValue(credential.accountId);
|
|
249
|
+
const refresh = asNonemptyString(credential.refresh);
|
|
250
|
+
if (!accountId || accountId !== resolvedAccountId || !refresh) {
|
|
251
|
+
sawInvalidAccountId = true;
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
matches.set(refresh, { accountId, storedAccess });
|
|
255
|
+
} catch {
|
|
256
|
+
// Malformed candidates never authorize a reset request.
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
if (sawInvalidAccountId) {
|
|
260
|
+
throw new Error("The OpenAI Codex OAuth credential did not include a valid account ID.");
|
|
261
|
+
}
|
|
262
|
+
if (matches.size > 1) {
|
|
263
|
+
throw new Error("Conflicting OAuth credentials match the active OpenAI Codex runtime account.");
|
|
264
|
+
}
|
|
265
|
+
const match = matches.values().next().value;
|
|
266
|
+
if (match) return match;
|
|
267
|
+
if (!sawOAuth) {
|
|
268
|
+
throw new Error(
|
|
269
|
+
standaloneFallback
|
|
270
|
+
? "Usage limit resets require the OpenAI Codex OAuth account configured through Pi /login."
|
|
271
|
+
: "Usage limit resets require an OpenAI Codex OAuth account configured through Pi /login or a compatible credential source.",
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
if (sawMatchingAccess) throw new Error("OpenAI Codex OAuth credentials were incomplete.");
|
|
275
|
+
throw new Error(
|
|
276
|
+
standaloneFallback
|
|
277
|
+
? "The active OpenAI Codex runtime account does not match Pi's stored OAuth account."
|
|
278
|
+
: "The active OpenAI Codex runtime account does not match any available OAuth account.",
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function codexAccountIdFromAccessToken(access: string): string | undefined {
|
|
283
|
+
try {
|
|
284
|
+
const parts = access.split(".");
|
|
285
|
+
if (parts.length !== 3 || !parts[1]) return undefined;
|
|
286
|
+
const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8")) as unknown;
|
|
287
|
+
const claims = asObject(asObject(payload)?.["https://api.openai.com/auth"]);
|
|
288
|
+
return validHeaderValue(claims?.chatgpt_account_id);
|
|
289
|
+
} catch {
|
|
290
|
+
return undefined;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
229
294
|
function normalizeResetOption(credit: Record<string, unknown>): CodexResetOption {
|
|
230
295
|
const creditId = asOpaqueId(credit.id);
|
|
231
296
|
if (!creditId) throw new Error("Codex reset credits response returned an invalid credit ID.");
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { OAuthCredential } from "@earendil-works/pi-ai";
|
|
2
|
+
import {
|
|
3
|
+
type ExtensionAPI,
|
|
4
|
+
type ExtensionContext,
|
|
5
|
+
readStoredCredential,
|
|
6
|
+
} from "@earendil-works/pi-coding-agent";
|
|
7
|
+
|
|
8
|
+
export const OAUTH_CREDENTIAL_SOURCE_CHANNEL = "oauth:credential-source:v1";
|
|
9
|
+
|
|
10
|
+
export type StoredCredentialReader = (providerId: string) => unknown;
|
|
11
|
+
|
|
12
|
+
export type OAuthCredentialCandidates =
|
|
13
|
+
| { ok: true; candidates: readonly OAuthCredential[]; offeredCount?: number }
|
|
14
|
+
| { ok: false };
|
|
15
|
+
|
|
16
|
+
export type OAuthCredentialCandidateReader = (
|
|
17
|
+
ctx: ExtensionContext,
|
|
18
|
+
providerId: string,
|
|
19
|
+
) => OAuthCredentialCandidates;
|
|
20
|
+
|
|
21
|
+
export function createOAuthCredentialCandidateReader(
|
|
22
|
+
pi: ExtensionAPI,
|
|
23
|
+
credentialReader: StoredCredentialReader = readStoredCredential,
|
|
24
|
+
): OAuthCredentialCandidateReader {
|
|
25
|
+
return (ctx, providerId) =>
|
|
26
|
+
collectOAuthCredentialCandidates(pi, ctx, providerId, credentialReader);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function collectOAuthCredentialCandidates(
|
|
30
|
+
pi: Pick<ExtensionAPI, "events">,
|
|
31
|
+
ctx: ExtensionContext,
|
|
32
|
+
providerId: string,
|
|
33
|
+
credentialReader: StoredCredentialReader = readStoredCredential,
|
|
34
|
+
): OAuthCredentialCandidates {
|
|
35
|
+
const candidates: OAuthCredential[] = [];
|
|
36
|
+
let collecting = true;
|
|
37
|
+
const request = Object.freeze({
|
|
38
|
+
session: ctx.sessionManager,
|
|
39
|
+
provider: providerId,
|
|
40
|
+
offer(candidate: unknown) {
|
|
41
|
+
if (!collecting) return;
|
|
42
|
+
const clone = cloneOAuthCredential(candidate);
|
|
43
|
+
if (clone) candidates.push(clone);
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
try {
|
|
47
|
+
pi.events.emit(OAUTH_CREDENTIAL_SOURCE_CHANNEL, request);
|
|
48
|
+
} catch {
|
|
49
|
+
return { ok: false };
|
|
50
|
+
} finally {
|
|
51
|
+
collecting = false;
|
|
52
|
+
}
|
|
53
|
+
const offeredCount = candidates.length;
|
|
54
|
+
try {
|
|
55
|
+
const fallback = cloneOAuthCredential(credentialReader(providerId));
|
|
56
|
+
if (fallback) candidates.push(fallback);
|
|
57
|
+
} catch {
|
|
58
|
+
// A malformed or unavailable standalone credential is equivalent to no fallback.
|
|
59
|
+
}
|
|
60
|
+
return { ok: true, candidates, offeredCount };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function fallbackOAuthCredentialCandidates(
|
|
64
|
+
providerId: string,
|
|
65
|
+
credentialReader: StoredCredentialReader,
|
|
66
|
+
): OAuthCredentialCandidates {
|
|
67
|
+
try {
|
|
68
|
+
const credential = cloneOAuthCredential(credentialReader(providerId));
|
|
69
|
+
return { ok: true, candidates: credential ? [credential] : [], offeredCount: 0 };
|
|
70
|
+
} catch {
|
|
71
|
+
return { ok: true, candidates: [], offeredCount: 0 };
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function cloneOAuthCredential(value: unknown): OAuthCredential | undefined {
|
|
76
|
+
try {
|
|
77
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
78
|
+
const clone = structuredClone(value) as OAuthCredential;
|
|
79
|
+
if (!clone || typeof clone !== "object" || Array.isArray(clone)) return undefined;
|
|
80
|
+
if (clone.type !== "oauth") return undefined;
|
|
81
|
+
if (typeof clone.access !== "string" || !clone.access) return undefined;
|
|
82
|
+
if (typeof clone.refresh !== "string" || !clone.refresh) return undefined;
|
|
83
|
+
if (typeof clone.expires !== "number" || !Number.isFinite(clone.expires)) return undefined;
|
|
84
|
+
return clone;
|
|
85
|
+
} catch {
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
}
|
package/src/query.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
2
|
import { type ExtensionContext, readStoredCredential } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { errorMessage, fingerprintResolvedAuth, redactUsageError } from "./core.js";
|
|
4
|
+
import {
|
|
5
|
+
fallbackOAuthCredentialCandidates,
|
|
6
|
+
type OAuthCredentialCandidateReader,
|
|
7
|
+
} from "./oauth-credential-source.js";
|
|
4
8
|
import { normalizeCodexBackendPayload } from "./providers/codex.js";
|
|
5
9
|
import { normalizeGitHubCopilotUsagePayload } from "./providers/github-copilot.js";
|
|
6
10
|
import { normalizeOpenCodeZenPayload } from "./providers/opencode-zen.js";
|
|
@@ -19,6 +23,7 @@ import type {
|
|
|
19
23
|
const CODEX_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
|
|
20
24
|
const GITHUB_COPILOT_USAGE_URL = "https://api.github.com/copilot_internal/user";
|
|
21
25
|
const OPENROUTER_KEY_URL = "https://openrouter.ai/api/v1/key";
|
|
26
|
+
const OPENCODE_GO_USAGE_URL = "https://opencode.ai/zen/go/v1/usage";
|
|
22
27
|
const MAX_SUCCESS_BODY_BYTES = 64 * 1024;
|
|
23
28
|
const MAX_ERROR_BODY_BYTES = 4 * 1024;
|
|
24
29
|
|
|
@@ -82,7 +87,7 @@ export const SUPPORTED_ADAPTERS: readonly UsageProviderAdapter[] = [
|
|
|
82
87
|
semantics: { kind: "consumer-subscription", label: "OpenCode Zen plan usage" },
|
|
83
88
|
async query(auth, signal, timeoutMs) {
|
|
84
89
|
const payload = await fetchProviderJson(
|
|
85
|
-
|
|
90
|
+
OPENCODE_GO_USAGE_URL,
|
|
86
91
|
auth,
|
|
87
92
|
signal,
|
|
88
93
|
timeoutMs,
|
|
@@ -111,6 +116,7 @@ export async function resolveUsageAuth(
|
|
|
111
116
|
adapter: UsageProviderAdapter,
|
|
112
117
|
salt: Uint8Array = AUTH_FINGERPRINT_SALT,
|
|
113
118
|
credentialReader: StoredCredentialReader = readStoredCredential,
|
|
119
|
+
candidateReader?: OAuthCredentialCandidateReader,
|
|
114
120
|
): Promise<ResolvedUsageAuth | undefined> {
|
|
115
121
|
if (ctx.model?.provider === adapter.id && !hasOfficialOrigin(ctx.model, adapter.id)) {
|
|
116
122
|
throw new Error(
|
|
@@ -122,6 +128,7 @@ export async function resolveUsageAuth(
|
|
|
122
128
|
hasOfficialOrigin(candidate, adapter.id),
|
|
123
129
|
);
|
|
124
130
|
if (!model) return undefined;
|
|
131
|
+
// SAFETY: Pi exposes the required auth methods at runtime, and checks below narrow them before use.
|
|
125
132
|
const registry = ctx.modelRegistry as unknown as UsageAuthRegistry;
|
|
126
133
|
let modelAuth: RequestAuth | undefined;
|
|
127
134
|
if (ctx.model?.provider === adapter.id && typeof registry.getApiKeyAndHeaders === "function") {
|
|
@@ -144,7 +151,19 @@ export async function resolveUsageAuth(
|
|
|
144
151
|
const auth = modelAuth ?? providerResult?.auth;
|
|
145
152
|
if (!auth) return undefined;
|
|
146
153
|
if (adapter.id === "github-copilot") {
|
|
147
|
-
|
|
154
|
+
const offered = candidateReader
|
|
155
|
+
? candidateReader(ctx, adapter.id)
|
|
156
|
+
: fallbackOAuthCredentialCandidates(adapter.id, credentialReader);
|
|
157
|
+
if (!offered.ok) {
|
|
158
|
+
throw new Error("GitHub Copilot OAuth credential discovery failed closed.");
|
|
159
|
+
}
|
|
160
|
+
return resolveGitHubCopilotUsageAuth(
|
|
161
|
+
auth,
|
|
162
|
+
model,
|
|
163
|
+
salt,
|
|
164
|
+
offered.candidates,
|
|
165
|
+
offered.offeredCount === 0,
|
|
166
|
+
);
|
|
148
167
|
}
|
|
149
168
|
const authorization = authorizationFrom(auth);
|
|
150
169
|
if (!authorization) return undefined;
|
|
@@ -334,33 +353,73 @@ function resolveGitHubCopilotUsageAuth(
|
|
|
334
353
|
auth: RequestAuth,
|
|
335
354
|
model: PiModel,
|
|
336
355
|
salt: Uint8Array,
|
|
337
|
-
|
|
356
|
+
candidates: readonly unknown[],
|
|
357
|
+
standaloneFallback: boolean,
|
|
338
358
|
): ResolvedUsageAuth {
|
|
339
|
-
const
|
|
340
|
-
if (
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
359
|
+
const resolvedAccess = bearerToken(headerValue(auth.headers, "Authorization")) ?? auth.apiKey;
|
|
360
|
+
if (!resolvedAccess) throw new Error("GitHub Copilot OAuth credentials were incomplete.");
|
|
361
|
+
let sawOAuth = false;
|
|
362
|
+
let sawMatchingAccess = false;
|
|
363
|
+
let sawIncompleteMatch = false;
|
|
364
|
+
let sawEnterpriseMatch = false;
|
|
365
|
+
const matches = new Map<string, { refresh: string; storedAccess: string }>();
|
|
366
|
+
for (const candidate of candidates) {
|
|
367
|
+
try {
|
|
368
|
+
const credential = asObject(candidate);
|
|
369
|
+
if (credential?.type !== "oauth") continue;
|
|
370
|
+
sawOAuth = true;
|
|
371
|
+
const storedAccess =
|
|
372
|
+
typeof credential.access === "string" && credential.access ? credential.access : undefined;
|
|
373
|
+
if (storedAccess !== resolvedAccess) continue;
|
|
374
|
+
sawMatchingAccess = true;
|
|
375
|
+
const enterpriseUrl = credential.enterpriseUrl;
|
|
376
|
+
if (
|
|
377
|
+
typeof enterpriseUrl === "string" &&
|
|
378
|
+
enterpriseUrl &&
|
|
379
|
+
!isPublicGitHubDomain(enterpriseUrl)
|
|
380
|
+
) {
|
|
381
|
+
sawEnterpriseMatch = true;
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
const refresh =
|
|
385
|
+
typeof credential.refresh === "string" && credential.refresh
|
|
386
|
+
? credential.refresh
|
|
387
|
+
: undefined;
|
|
388
|
+
if (!refresh) {
|
|
389
|
+
sawIncompleteMatch = true;
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
matches.set(`${storedAccess.length}:${storedAccess}${refresh}`, { refresh, storedAccess });
|
|
393
|
+
} catch {
|
|
394
|
+
// Malformed candidates never authorize a provider request.
|
|
395
|
+
}
|
|
344
396
|
}
|
|
345
|
-
if (
|
|
346
|
-
typeof credential.enterpriseUrl === "string" &&
|
|
347
|
-
credential.enterpriseUrl &&
|
|
348
|
-
!isPublicGitHubDomain(credential.enterpriseUrl)
|
|
349
|
-
) {
|
|
397
|
+
if (sawEnterpriseMatch) {
|
|
350
398
|
throw new Error("GitHub Copilot usage does not yet support GitHub Enterprise accounts.");
|
|
351
399
|
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
400
|
+
if (sawIncompleteMatch) throw new Error("GitHub Copilot OAuth credentials were incomplete.");
|
|
401
|
+
if (matches.size > 1) {
|
|
402
|
+
throw new Error(
|
|
403
|
+
"Conflicting OAuth credentials match the active GitHub Copilot runtime account.",
|
|
404
|
+
);
|
|
357
405
|
}
|
|
358
|
-
|
|
406
|
+
const match = matches.values().next().value;
|
|
407
|
+
if (!match) {
|
|
408
|
+
if (!sawOAuth) {
|
|
409
|
+
throw new Error(
|
|
410
|
+
standaloneFallback
|
|
411
|
+
? "GitHub Copilot usage requires the OAuth account configured through Pi /login."
|
|
412
|
+
: "GitHub Copilot usage requires an OAuth account configured through Pi /login or a compatible credential source.",
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
if (sawMatchingAccess) throw new Error("GitHub Copilot OAuth credentials were incomplete.");
|
|
359
416
|
throw new Error(
|
|
360
|
-
|
|
417
|
+
standaloneFallback
|
|
418
|
+
? "The active GitHub Copilot runtime account does not match Pi's stored OAuth account."
|
|
419
|
+
: "The active GitHub Copilot runtime account does not match any available OAuth account.",
|
|
361
420
|
);
|
|
362
421
|
}
|
|
363
|
-
|
|
422
|
+
const { refresh, storedAccess } = match;
|
|
364
423
|
const authorization = `Bearer ${refresh}`;
|
|
365
424
|
const headers = {
|
|
366
425
|
Authorization: authorization,
|
|
@@ -436,12 +495,6 @@ function hasHeader(headers: Record<string, string>, name: string): boolean {
|
|
|
436
495
|
return Object.keys(headers).some((key) => key.toLowerCase() === name.toLowerCase());
|
|
437
496
|
}
|
|
438
497
|
|
|
439
|
-
function opencodeUsageUrl(baseUrl: string | undefined): string {
|
|
440
|
-
const base = baseUrl?.trim().replace(/\/+$/u, "");
|
|
441
|
-
if (!base) throw new Error("OpenCode Go model base URL is unavailable.");
|
|
442
|
-
return `${base}/usage`;
|
|
443
|
-
}
|
|
444
|
-
|
|
445
498
|
function isAbortError(error: unknown): boolean {
|
|
446
499
|
return error instanceof Error && error.name === "AbortError";
|
|
447
500
|
}
|
package/src/usage.ts
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
} from "./codex-resets.js";
|
|
23
23
|
import { awaitWithDeadline, errorMessage, runWithConcurrency, UsageCache } from "./core.js";
|
|
24
24
|
import { formatProviderStates, formatUsageStatusline } from "./format.js";
|
|
25
|
+
import { createOAuthCredentialCandidateReader } from "./oauth-credential-source.js";
|
|
25
26
|
import {
|
|
26
27
|
adapterForProvider,
|
|
27
28
|
isStaleExtensionContextError,
|
|
@@ -80,6 +81,7 @@ export default function usageExtension(
|
|
|
80
81
|
dependencies: UsageExtensionDependencies = {},
|
|
81
82
|
) {
|
|
82
83
|
const credentialReader = dependencies.credentialReader;
|
|
84
|
+
const credentialCandidates = createOAuthCredentialCandidateReader(pi, credentialReader);
|
|
83
85
|
const createRedemptionId = dependencies.createRedemptionId ?? randomUUID;
|
|
84
86
|
const settingsRuntime = dependencies.settingsRuntime ?? createUsageSettingsRuntime();
|
|
85
87
|
const cache = new UsageCache(CACHE_TTL_MS);
|
|
@@ -189,7 +191,7 @@ export default function usageExtension(
|
|
|
189
191
|
let auth: ResolvedUsageAuth | undefined;
|
|
190
192
|
try {
|
|
191
193
|
auth = await awaitWithDeadline(
|
|
192
|
-
resolveUsageAuth(ctx, adapter),
|
|
194
|
+
resolveUsageAuth(ctx, adapter, undefined, credentialReader, credentialCandidates),
|
|
193
195
|
signal,
|
|
194
196
|
DEFAULT_TIMEOUT_MS,
|
|
195
197
|
`resolving ${adapter.displayName} runtime auth`,
|
|
@@ -419,7 +421,7 @@ export default function usageExtension(
|
|
|
419
421
|
if (!adapter) return false;
|
|
420
422
|
try {
|
|
421
423
|
const auth = await awaitWithDeadline(
|
|
422
|
-
resolveUsageAuth(ctx, adapter),
|
|
424
|
+
resolveUsageAuth(ctx, adapter, undefined, credentialReader, credentialCandidates),
|
|
423
425
|
signal,
|
|
424
426
|
DEFAULT_TIMEOUT_MS,
|
|
425
427
|
`revalidating ${adapter.displayName} runtime auth`,
|
|
@@ -438,7 +440,7 @@ export default function usageExtension(
|
|
|
438
440
|
if (!adapter) return false;
|
|
439
441
|
try {
|
|
440
442
|
const auth = await awaitWithDeadline(
|
|
441
|
-
resolveUsageAuth(ctx, adapter),
|
|
443
|
+
resolveUsageAuth(ctx, adapter, undefined, credentialReader, credentialCandidates),
|
|
442
444
|
signal,
|
|
443
445
|
DEFAULT_TIMEOUT_MS,
|
|
444
446
|
`revalidating ${adapter.displayName} runtime auth`,
|
|
@@ -677,7 +679,7 @@ export default function usageExtension(
|
|
|
677
679
|
async (signal) => {
|
|
678
680
|
const expectedModel = modelIdentity(ctx.model);
|
|
679
681
|
const auth = await awaitWithDeadline(
|
|
680
|
-
resolveCodexResetAuth(ctx, undefined, credentialReader),
|
|
682
|
+
resolveCodexResetAuth(ctx, undefined, credentialReader, credentialCandidates),
|
|
681
683
|
signal,
|
|
682
684
|
DEFAULT_TIMEOUT_MS,
|
|
683
685
|
"resolving current Codex reset authentication",
|
|
@@ -695,7 +697,7 @@ export default function usageExtension(
|
|
|
695
697
|
};
|
|
696
698
|
}
|
|
697
699
|
const revalidated = await awaitWithDeadline(
|
|
698
|
-
resolveCodexResetAuth(ctx, undefined, credentialReader),
|
|
700
|
+
resolveCodexResetAuth(ctx, undefined, credentialReader, credentialCandidates),
|
|
699
701
|
signal,
|
|
700
702
|
DEFAULT_TIMEOUT_MS,
|
|
701
703
|
"revalidating current Codex reset authentication",
|
|
@@ -759,7 +761,7 @@ export default function usageExtension(
|
|
|
759
761
|
controller.signal,
|
|
760
762
|
async (signal) => {
|
|
761
763
|
const auth = await awaitWithDeadline(
|
|
762
|
-
resolveCodexResetAuth(ctx, undefined, credentialReader),
|
|
764
|
+
resolveCodexResetAuth(ctx, undefined, credentialReader, credentialCandidates),
|
|
763
765
|
signal,
|
|
764
766
|
DEFAULT_TIMEOUT_MS,
|
|
765
767
|
"revalidating current Codex reset authentication",
|