@narumitw/pi-usage 0.60.7 → 0.60.8
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/dist/index.ts +91 -339
- package/dist/index.ts.map +2 -2
- package/docs/providers.md +2 -1
- package/package.json +73 -74
- package/src/codex-fast-runtime.ts +148 -184
- package/src/codex-fast.ts +74 -83
- package/src/codex-resets.ts +249 -278
- package/src/core.ts +204 -206
- package/src/format.ts +519 -574
- package/src/index.ts +77 -77
- package/src/oauth-credential-source.ts +57 -65
- package/src/providers/baseten.ts +32 -36
- package/src/providers/codex.ts +109 -120
- package/src/providers/deepseek.ts +59 -64
- package/src/providers/fireworks.ts +219 -238
- package/src/providers/github-copilot.ts +89 -95
- package/src/providers/kimi-coding.ts +202 -213
- package/src/providers/minimax.ts +214 -219
- package/src/providers/moonshot.ts +41 -46
- package/src/providers/opencode-zen.ts +56 -59
- package/src/providers/openrouter.ts +50 -53
- package/src/providers/vercel-ai-gateway.ts +33 -33
- package/src/providers/xai.ts +141 -148
- package/src/providers/zai-errors.ts +75 -75
- package/src/providers/zai.ts +173 -189
- package/src/query.ts +887 -967
- package/src/settings.ts +275 -304
- package/src/types.ts +145 -145
- package/src/usage-helpers.ts +18 -18
- package/src/usage-settings-ui.ts +117 -126
- package/src/usage-targets.ts +98 -114
- package/src/usage.ts +1264 -1426
package/src/query.ts
CHANGED
|
@@ -3,21 +3,14 @@
|
|
|
3
3
|
import { randomBytes } from "node:crypto";
|
|
4
4
|
import { type ExtensionContext, readStoredCredential } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import { errorMessage, fingerprintResolvedAuth, redactUsageError } from "./core.js";
|
|
6
|
-
import {
|
|
7
|
-
fallbackOAuthCredentialCandidates,
|
|
8
|
-
type OAuthCredentialCandidateReader,
|
|
9
|
-
} from "./oauth-credential-source.js";
|
|
6
|
+
import { fallbackOAuthCredentialCandidates, type OAuthCredentialCandidateReader } from "./oauth-credential-source.js";
|
|
10
7
|
import { normalizeBasetenBillingUsagePayload } from "./providers/baseten.js";
|
|
11
8
|
import { normalizeCodexBackendPayload } from "./providers/codex.js";
|
|
12
9
|
import { normalizeDeepSeekBalancePayload } from "./providers/deepseek.js";
|
|
13
10
|
import { createFireworksAdapter } from "./providers/fireworks.js";
|
|
14
11
|
import { normalizeGitHubCopilotUsagePayload } from "./providers/github-copilot.js";
|
|
15
12
|
import { normalizeKimiCodingUsagePayload } from "./providers/kimi-coding.js";
|
|
16
|
-
import {
|
|
17
|
-
type MiniMaxProviderId,
|
|
18
|
-
miniMaxUsageKind,
|
|
19
|
-
normalizeMiniMaxUsagePayload,
|
|
20
|
-
} from "./providers/minimax.js";
|
|
13
|
+
import { type MiniMaxProviderId, miniMaxUsageKind, normalizeMiniMaxUsagePayload } from "./providers/minimax.js";
|
|
21
14
|
import { normalizeMoonshotBalancePayload } from "./providers/moonshot.js";
|
|
22
15
|
import { normalizeOpenCodeZenPayload } from "./providers/opencode-zen.js";
|
|
23
16
|
import { normalizeOpenRouterKeyPayload } from "./providers/openrouter.js";
|
|
@@ -26,27 +19,27 @@ import { normalizeXaiBillingPayload } from "./providers/xai.js";
|
|
|
26
19
|
import { normalizeZaiQuotaPayload, normalizeZaiSubscriptionPayload } from "./providers/zai.js";
|
|
27
20
|
import { zaiResponseError } from "./providers/zai-errors.js";
|
|
28
21
|
import type {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
22
|
+
BasetenBillingUsagePayload,
|
|
23
|
+
CodexBackendPayload,
|
|
24
|
+
DeepSeekBalancePayload,
|
|
25
|
+
GitHubCopilotUsagePayload,
|
|
26
|
+
KimiCodingUsagePayload,
|
|
27
|
+
MiniMaxUsagePayload,
|
|
28
|
+
MoonshotBalancePayload,
|
|
29
|
+
OpenCodeZenPayload,
|
|
30
|
+
OpenRouterKeyPayload,
|
|
31
|
+
PiModel,
|
|
32
|
+
ResolvedUsageAuth,
|
|
33
|
+
UsageProviderAdapter,
|
|
34
|
+
UsageQuerySettings,
|
|
35
|
+
UsageReport,
|
|
36
|
+
UsageRequestGuard,
|
|
37
|
+
VercelAIGatewayCreditsPayload,
|
|
38
|
+
XaiBillingPayload,
|
|
39
|
+
XaiUserPayload,
|
|
40
|
+
ZaiPlanInfo,
|
|
41
|
+
ZaiQuotaPayload,
|
|
42
|
+
ZaiSubscriptionPayload,
|
|
50
43
|
} from "./types.js";
|
|
51
44
|
import { resolveUsageTarget } from "./usage-targets.js";
|
|
52
45
|
|
|
@@ -60,20 +53,20 @@ const VERCEL_AI_GATEWAY_CREDITS_URL = "https://ai-gateway.vercel.sh/v1/credits";
|
|
|
60
53
|
const OPENCODE_GO_USAGE_URL = "https://opencode.ai/zen/go/v1/usage";
|
|
61
54
|
const KIMI_CODING_USAGE_URL = "https://api.kimi.com/coding/v1/usages";
|
|
62
55
|
const MINIMAX_API_ROOTS = Object.freeze({
|
|
63
|
-
|
|
64
|
-
|
|
56
|
+
minimax: "https://api.minimax.io",
|
|
57
|
+
"minimax-cn": "https://api.minimaxi.com",
|
|
65
58
|
});
|
|
66
59
|
const MOONSHOT_BALANCE_URLS = Object.freeze({
|
|
67
|
-
|
|
68
|
-
|
|
60
|
+
moonshotai: "https://api.moonshot.ai/v1/users/me/balance",
|
|
61
|
+
"moonshotai-cn": "https://api.moonshot.cn/v1/users/me/balance",
|
|
69
62
|
});
|
|
70
63
|
const SHARED_MOONSHOT_ENV_VAR = "MOONSHOT_API_KEY";
|
|
71
64
|
const XAI_USER_URL = "https://cli-chat-proxy.grok.com/v1/user?include=subscription";
|
|
72
65
|
const XAI_BILLING_URL = "https://cli-chat-proxy.grok.com/v1/billing?format=credits";
|
|
73
66
|
const XAI_CLIENT_HEADERS = Object.freeze({
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
67
|
+
"X-XAI-Token-Auth": "xai-grok-cli",
|
|
68
|
+
"x-grok-client-version": "1.0.10",
|
|
69
|
+
"x-grok-client-mode": "interactive",
|
|
77
70
|
});
|
|
78
71
|
const MAX_SUCCESS_BODY_BYTES = 64 * 1024;
|
|
79
72
|
const MAX_ERROR_BODY_BYTES = 4 * 1024;
|
|
@@ -81,202 +74,189 @@ const MAX_ERROR_BODY_BYTES = 4 * 1024;
|
|
|
81
74
|
export const AUTH_FINGERPRINT_SALT = randomBytes(32);
|
|
82
75
|
|
|
83
76
|
export const SUPPORTED_ADAPTERS: readonly UsageProviderAdapter[] = [
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
async query(auth, signal, timeoutMs, guard) {
|
|
268
|
-
return queryZaiUsage("zai", "Z.AI", auth, signal, timeoutMs, guard);
|
|
269
|
-
},
|
|
270
|
-
},
|
|
271
|
-
{
|
|
272
|
-
id: "zai-coding-cn",
|
|
273
|
-
displayName: "Z.AI Coding CN",
|
|
274
|
-
invalidateCacheOnFailure: true,
|
|
275
|
-
semantics: { kind: "consumer-subscription", label: "GLM Coding Plan usage" },
|
|
276
|
-
async query(auth, signal, timeoutMs, guard) {
|
|
277
|
-
return queryZaiUsage("zai-coding-cn", "Z.AI Coding CN", auth, signal, timeoutMs, guard);
|
|
278
|
-
},
|
|
279
|
-
},
|
|
77
|
+
{
|
|
78
|
+
id: "baseten",
|
|
79
|
+
displayName: "Baseten",
|
|
80
|
+
semantics: { kind: "api-key", label: "Organization Model APIs spend" },
|
|
81
|
+
async query(auth, signal, timeoutMs, guard) {
|
|
82
|
+
if (!guard) throw new Error("Baseten billing usage requires request-boundary revalidation.");
|
|
83
|
+
const startedAt = Date.now();
|
|
84
|
+
await guard();
|
|
85
|
+
const windowAt = Date.now();
|
|
86
|
+
const payload = (await fetchProviderJson(
|
|
87
|
+
basetenBillingUsageUrl(windowAt),
|
|
88
|
+
auth,
|
|
89
|
+
signal,
|
|
90
|
+
remainingTimeout(timeoutMs, startedAt, "fetching Baseten billing usage"),
|
|
91
|
+
"Baseten billing usage endpoint",
|
|
92
|
+
{ redirect: "error" },
|
|
93
|
+
)) as BasetenBillingUsagePayload;
|
|
94
|
+
await guard();
|
|
95
|
+
return normalizeBasetenBillingUsagePayload(payload, Date.now());
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
id: "openai-codex",
|
|
100
|
+
displayName: "OpenAI Codex",
|
|
101
|
+
semantics: {
|
|
102
|
+
kind: "consumer-subscription",
|
|
103
|
+
label: "ChatGPT subscription limits",
|
|
104
|
+
},
|
|
105
|
+
async query(auth, signal, timeoutMs) {
|
|
106
|
+
const payload = await fetchProviderJson(CODEX_USAGE_URL, auth, signal, timeoutMs, "Codex usage endpoint");
|
|
107
|
+
return normalizeCodexBackendPayload(payload as CodexBackendPayload, Date.now());
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
id: "deepseek",
|
|
112
|
+
displayName: "DeepSeek",
|
|
113
|
+
semantics: { kind: "api-key", label: "DeepSeek API balance" },
|
|
114
|
+
async query(auth, signal, timeoutMs, guard) {
|
|
115
|
+
if (!guard) throw new Error("DeepSeek API balance requires request-boundary revalidation.");
|
|
116
|
+
const startedAt = Date.now();
|
|
117
|
+
await guard();
|
|
118
|
+
const remainingMs = timeoutMs - (Date.now() - startedAt);
|
|
119
|
+
if (remainingMs <= 0) throw new Error("Timed out while revalidating DeepSeek runtime auth.");
|
|
120
|
+
const payload = await fetchProviderJson(
|
|
121
|
+
DEEPSEEK_BALANCE_URL,
|
|
122
|
+
auth,
|
|
123
|
+
signal,
|
|
124
|
+
remainingMs,
|
|
125
|
+
"DeepSeek API balance endpoint",
|
|
126
|
+
{ redirect: "error" },
|
|
127
|
+
);
|
|
128
|
+
return normalizeDeepSeekBalancePayload(payload as DeepSeekBalancePayload, Date.now());
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
id: "github-copilot",
|
|
133
|
+
displayName: "GitHub Copilot",
|
|
134
|
+
semantics: {
|
|
135
|
+
kind: "consumer-subscription",
|
|
136
|
+
label: "GitHub Copilot account allowance",
|
|
137
|
+
},
|
|
138
|
+
async query(auth, signal, timeoutMs) {
|
|
139
|
+
const payload = await fetchProviderJson(
|
|
140
|
+
GITHUB_COPILOT_USAGE_URL,
|
|
141
|
+
auth,
|
|
142
|
+
signal,
|
|
143
|
+
timeoutMs,
|
|
144
|
+
"GitHub Copilot usage endpoint",
|
|
145
|
+
);
|
|
146
|
+
return normalizeGitHubCopilotUsagePayload(payload as GitHubCopilotUsagePayload, Date.now());
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
id: "openrouter",
|
|
151
|
+
displayName: "OpenRouter",
|
|
152
|
+
semantics: { kind: "api-key", label: "API-key spend limits" },
|
|
153
|
+
async query(auth, signal, timeoutMs) {
|
|
154
|
+
const payload = await fetchProviderJson(OPENROUTER_KEY_URL, auth, signal, timeoutMs, "OpenRouter key endpoint");
|
|
155
|
+
return normalizeOpenRouterKeyPayload(payload as OpenRouterKeyPayload, Date.now());
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
id: "vercel-ai-gateway",
|
|
160
|
+
displayName: "Vercel AI Gateway",
|
|
161
|
+
semantics: { kind: "api-key", label: "AI Gateway credits and lifetime spend" },
|
|
162
|
+
async query(auth, signal, timeoutMs, guard) {
|
|
163
|
+
if (!guard) throw new Error("Vercel AI Gateway usage requires request-boundary revalidation.");
|
|
164
|
+
const startedAt = Date.now();
|
|
165
|
+
await guard();
|
|
166
|
+
const payload = (await fetchProviderJson(
|
|
167
|
+
VERCEL_AI_GATEWAY_CREDITS_URL,
|
|
168
|
+
auth,
|
|
169
|
+
signal,
|
|
170
|
+
remainingTimeout(timeoutMs, startedAt, "fetching Vercel AI Gateway credits"),
|
|
171
|
+
"Vercel AI Gateway credits endpoint",
|
|
172
|
+
{ redirect: "error" },
|
|
173
|
+
)) as VercelAIGatewayCreditsPayload;
|
|
174
|
+
await guard();
|
|
175
|
+
return normalizeVercelAIGatewayCreditsPayload(payload, Date.now());
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
createFireworksAdapter(fetchProviderJson),
|
|
179
|
+
{
|
|
180
|
+
id: "opencode-go",
|
|
181
|
+
displayName: "OpenCode Go",
|
|
182
|
+
semantics: { kind: "consumer-subscription", label: "OpenCode Zen plan usage" },
|
|
183
|
+
async query(auth, signal, timeoutMs) {
|
|
184
|
+
const payload = await fetchProviderJson(
|
|
185
|
+
OPENCODE_GO_USAGE_URL,
|
|
186
|
+
auth,
|
|
187
|
+
signal,
|
|
188
|
+
timeoutMs,
|
|
189
|
+
"OpenCode Zen usage endpoint",
|
|
190
|
+
);
|
|
191
|
+
return normalizeOpenCodeZenPayload(payload as OpenCodeZenPayload, Date.now());
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
id: "kimi-coding",
|
|
196
|
+
displayName: "Kimi For Coding",
|
|
197
|
+
semantics: { kind: "consumer-subscription", label: "Kimi Coding Plan usage" },
|
|
198
|
+
async query(auth, signal, timeoutMs) {
|
|
199
|
+
const payload = await fetchProviderJson(
|
|
200
|
+
KIMI_CODING_USAGE_URL,
|
|
201
|
+
auth,
|
|
202
|
+
signal,
|
|
203
|
+
timeoutMs,
|
|
204
|
+
"Kimi Coding usage endpoint",
|
|
205
|
+
{ redirect: "error" },
|
|
206
|
+
);
|
|
207
|
+
return normalizeKimiCodingUsagePayload(payload as KimiCodingUsagePayload, Date.now());
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
id: "minimax",
|
|
212
|
+
displayName: "MiniMax",
|
|
213
|
+
semantics: { kind: "consumer-subscription", label: "MiniMax usage" },
|
|
214
|
+
async query(auth, signal, timeoutMs, guard) {
|
|
215
|
+
return queryMiniMaxUsage("minimax", auth, signal, timeoutMs, guard);
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
id: "minimax-cn",
|
|
220
|
+
displayName: "MiniMax CN",
|
|
221
|
+
semantics: { kind: "consumer-subscription", label: "MiniMax usage" },
|
|
222
|
+
async query(auth, signal, timeoutMs, guard) {
|
|
223
|
+
return queryMiniMaxUsage("minimax-cn", auth, signal, timeoutMs, guard);
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
id: "moonshotai",
|
|
228
|
+
displayName: "Moonshot AI",
|
|
229
|
+
semantics: { kind: "api-key", label: "Moonshot API account balance" },
|
|
230
|
+
async query(auth, signal, timeoutMs, guard) {
|
|
231
|
+
return queryMoonshotBalance("moonshotai", auth, signal, timeoutMs, guard);
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
id: "moonshotai-cn",
|
|
236
|
+
displayName: "Moonshot AI CN",
|
|
237
|
+
semantics: { kind: "api-key", label: "Moonshot API account balance" },
|
|
238
|
+
async query(auth, signal, timeoutMs, guard) {
|
|
239
|
+
return queryMoonshotBalance("moonshotai-cn", auth, signal, timeoutMs, guard);
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
id: "zai",
|
|
244
|
+
displayName: "Z.AI",
|
|
245
|
+
invalidateCacheOnFailure: true,
|
|
246
|
+
semantics: { kind: "consumer-subscription", label: "GLM Coding Plan usage" },
|
|
247
|
+
async query(auth, signal, timeoutMs, guard) {
|
|
248
|
+
return queryZaiUsage("zai", "Z.AI", auth, signal, timeoutMs, guard);
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
id: "zai-coding-cn",
|
|
253
|
+
displayName: "Z.AI Coding CN",
|
|
254
|
+
invalidateCacheOnFailure: true,
|
|
255
|
+
semantics: { kind: "consumer-subscription", label: "GLM Coding Plan usage" },
|
|
256
|
+
async query(auth, signal, timeoutMs, guard) {
|
|
257
|
+
return queryZaiUsage("zai-coding-cn", "Z.AI Coding CN", auth, signal, timeoutMs, guard);
|
|
258
|
+
},
|
|
259
|
+
},
|
|
280
260
|
];
|
|
281
261
|
|
|
282
262
|
// Reviewed contract pins:
|
|
@@ -290,857 +270,797 @@ export const SUPPORTED_ADAPTERS: readonly UsageProviderAdapter[] = [
|
|
|
290
270
|
// 723dd2aa22d17be35617463837dc47cda008d90e.
|
|
291
271
|
// x-userid remains attached only to billing to bind the proxy-canonical identity as Grok Build does.
|
|
292
272
|
export const XAI_ADAPTER: UsageProviderAdapter = {
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
273
|
+
id: "xai",
|
|
274
|
+
displayName: "xAI",
|
|
275
|
+
semantics: {
|
|
276
|
+
kind: "consumer-subscription",
|
|
277
|
+
label: "xAI consumer subscription usage",
|
|
278
|
+
},
|
|
279
|
+
publishesStatusline: false,
|
|
280
|
+
async query(auth, signal, timeoutMs, guard) {
|
|
281
|
+
if (!guard) throw new Error("xAI usage requires request-boundary revalidation.");
|
|
282
|
+
const startedAt = Date.now();
|
|
283
|
+
const clientAuth = {
|
|
284
|
+
...auth,
|
|
285
|
+
headers: { ...auth.headers, ...XAI_CLIENT_HEADERS },
|
|
286
|
+
};
|
|
287
|
+
await guard();
|
|
288
|
+
const userPayload = (await fetchProviderJson(
|
|
289
|
+
XAI_USER_URL,
|
|
290
|
+
clientAuth,
|
|
291
|
+
signal,
|
|
292
|
+
remainingTimeout(timeoutMs, startedAt),
|
|
293
|
+
"xAI consumer identity endpoint",
|
|
294
|
+
{ redirect: "error", userAgent: false },
|
|
295
|
+
)) as XaiUserPayload;
|
|
296
|
+
await guard();
|
|
297
|
+
const userId = validatedXaiUserId(userPayload.userId);
|
|
298
|
+
const billingAuth = {
|
|
299
|
+
...clientAuth,
|
|
300
|
+
headers: { ...clientAuth.headers, "x-userid": userId },
|
|
301
|
+
secrets: [...clientAuth.secrets, userId],
|
|
302
|
+
};
|
|
303
|
+
await guard();
|
|
304
|
+
const billingPayload = (await fetchProviderJson(
|
|
305
|
+
XAI_BILLING_URL,
|
|
306
|
+
billingAuth,
|
|
307
|
+
signal,
|
|
308
|
+
remainingTimeout(timeoutMs, startedAt),
|
|
309
|
+
"xAI consumer billing endpoint",
|
|
310
|
+
{ redirect: "error", userAgent: false },
|
|
311
|
+
)) as XaiBillingPayload;
|
|
312
|
+
await guard();
|
|
313
|
+
return normalizeXaiBillingPayload(billingPayload, userPayload.subscriptionTier, Date.now());
|
|
314
|
+
},
|
|
335
315
|
};
|
|
336
316
|
|
|
337
317
|
export function usageAdapters(): readonly UsageProviderAdapter[] {
|
|
338
|
-
|
|
318
|
+
return [...SUPPORTED_ADAPTERS, XAI_ADAPTER];
|
|
339
319
|
}
|
|
340
320
|
|
|
341
|
-
export function adapterForProvider(
|
|
342
|
-
|
|
343
|
-
): UsageProviderAdapter | undefined {
|
|
344
|
-
return usageAdapters().find((adapter) => adapter.id === providerId);
|
|
321
|
+
export function adapterForProvider(providerId: string | undefined): UsageProviderAdapter | undefined {
|
|
322
|
+
return usageAdapters().find((adapter) => adapter.id === providerId);
|
|
345
323
|
}
|
|
346
324
|
|
|
347
325
|
export function isStaleExtensionContextError(error: unknown): boolean {
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
);
|
|
326
|
+
return (
|
|
327
|
+
error instanceof Error && error.message.includes("This extension ctx is stale after session replacement or reload")
|
|
328
|
+
);
|
|
352
329
|
}
|
|
353
330
|
|
|
354
331
|
export async function resolveUsageAuth(
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
332
|
+
ctx: ExtensionContext,
|
|
333
|
+
adapter: UsageProviderAdapter,
|
|
334
|
+
salt: Uint8Array = AUTH_FINGERPRINT_SALT,
|
|
335
|
+
credentialReader: StoredCredentialReader = readStoredCredential,
|
|
336
|
+
candidateReader?: OAuthCredentialCandidateReader,
|
|
360
337
|
): Promise<ResolvedUsageAuth | undefined> {
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
338
|
+
if (ctx.model?.provider === adapter.id && !hasOfficialOrigin(ctx.model, adapter.id)) {
|
|
339
|
+
throw new Error(
|
|
340
|
+
`${adapter.displayName} usage cannot send a custom provider base URL credential to the official usage endpoint.`,
|
|
341
|
+
);
|
|
342
|
+
}
|
|
366
343
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
}
|
|
490
|
-
const authorization = authorizationFrom(auth);
|
|
491
|
-
if (!authorization) return undefined;
|
|
492
|
-
const headers = { Authorization: authorization };
|
|
493
|
-
const secrets = [auth.apiKey, headerValue(auth.headers, "Authorization"), authorization].filter(
|
|
494
|
-
(value): value is string => Boolean(value),
|
|
495
|
-
);
|
|
496
|
-
return finalize({
|
|
497
|
-
apiKey: auth.apiKey,
|
|
498
|
-
headers,
|
|
499
|
-
fingerprint: "",
|
|
500
|
-
secrets,
|
|
501
|
-
model,
|
|
502
|
-
});
|
|
344
|
+
const model = candidateModels(ctx, adapter.id).find((candidate) => hasOfficialOrigin(candidate, adapter.id));
|
|
345
|
+
if (!model) return undefined;
|
|
346
|
+
// SAFETY: Pi exposes the required auth methods at runtime, and checks below narrow them before use.
|
|
347
|
+
const registry = ctx.modelRegistry as unknown as UsageAuthRegistry;
|
|
348
|
+
const provider = registry.getProvider?.(adapter.id);
|
|
349
|
+
if (provider?.baseUrl && !hasOfficialUrlOrigin(provider.baseUrl, adapter.id)) {
|
|
350
|
+
throw new Error(
|
|
351
|
+
`${adapter.displayName} usage cannot send an overridden provider credential to the official usage endpoint.`,
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
let modelAuth: RequestAuth | undefined;
|
|
355
|
+
const currentModel = ctx.model?.provider === adapter.id ? ctx.model : undefined;
|
|
356
|
+
const resolveCurrentModelAuth = async (): Promise<RequestAuth | undefined> => {
|
|
357
|
+
if (!currentModel || typeof registry.getApiKeyAndHeaders !== "function") return undefined;
|
|
358
|
+
const result = await registry.getApiKeyAndHeaders(currentModel);
|
|
359
|
+
if (!result.ok) throw new Error(redactUsageError(result.error));
|
|
360
|
+
return authorizationFrom(result) ? result : undefined;
|
|
361
|
+
};
|
|
362
|
+
const resolveSelectedAuthLast = ["deepseek", "minimax", "minimax-cn"].includes(adapter.id);
|
|
363
|
+
if (!resolveSelectedAuthLast) modelAuth = await resolveCurrentModelAuth();
|
|
364
|
+
if (typeof registry.getProviderAuth !== "function") {
|
|
365
|
+
throw new Error("pi-usage requires Pi 0.81.0 or newer to validate resolved provider auth.");
|
|
366
|
+
}
|
|
367
|
+
if (!moonshotProviderAuthIsAllowed(ctx, adapter.id)) return undefined;
|
|
368
|
+
const providerResult = await registry.getProviderAuth(adapter.id);
|
|
369
|
+
if (!moonshotProviderAuthIsAllowed(ctx, adapter.id)) return undefined;
|
|
370
|
+
if (providerResult?.auth.baseUrl && !hasOfficialUrlOrigin(providerResult.auth.baseUrl, adapter.id)) {
|
|
371
|
+
throw new Error(
|
|
372
|
+
`${adapter.displayName} usage cannot send a proxy-resolved credential to the official usage endpoint.`,
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
// Providers with credential-change retries read selected-model auth last so a rotation during
|
|
376
|
+
// provider-origin validation cannot leave the earlier credential queued for the usage request.
|
|
377
|
+
if (resolveSelectedAuthLast) modelAuth = await resolveCurrentModelAuth();
|
|
378
|
+
if (modelAuth?.baseUrl && !hasOfficialUrlOrigin(modelAuth.baseUrl, adapter.id)) {
|
|
379
|
+
throw new Error(
|
|
380
|
+
`${adapter.displayName} usage cannot send model-resolved proxy credentials to the official usage endpoint.`,
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
const auth = modelAuth ?? providerResult?.auth;
|
|
384
|
+
if (!auth) return undefined;
|
|
385
|
+
const finalize = (resolved: ResolvedUsageAuth): ResolvedUsageAuth => {
|
|
386
|
+
const preservedAuth = { ...(providerResult?.auth ?? auth) };
|
|
387
|
+
const env = providerResult?.env ?? modelAuth?.env;
|
|
388
|
+
const source = providerResult?.source;
|
|
389
|
+
const effectiveBaseUrl = modelAuth?.baseUrl ?? providerResult?.auth.baseUrl ?? provider?.baseUrl ?? model.baseUrl;
|
|
390
|
+
const redactionInputs = [
|
|
391
|
+
preservedAuth.apiKey,
|
|
392
|
+
...Object.values(preservedAuth.headers ?? {}),
|
|
393
|
+
...Object.values(env ?? {}),
|
|
394
|
+
modelAuth?.apiKey,
|
|
395
|
+
...Object.values(modelAuth?.headers ?? {}),
|
|
396
|
+
].filter((value): value is string => typeof value === "string" && value.length > 0);
|
|
397
|
+
return {
|
|
398
|
+
...resolved,
|
|
399
|
+
auth: preservedAuth,
|
|
400
|
+
...(env ? { env: { ...env } } : {}),
|
|
401
|
+
...(source ? { source } : {}),
|
|
402
|
+
effectiveBaseUrl,
|
|
403
|
+
secrets: [...new Set([...resolved.secrets, ...redactionInputs])],
|
|
404
|
+
fingerprint: fingerprintResolvedAuth(
|
|
405
|
+
{
|
|
406
|
+
apiKey: resolved.apiKey,
|
|
407
|
+
headers: resolved.headers,
|
|
408
|
+
baseUrl: effectiveBaseUrl,
|
|
409
|
+
env,
|
|
410
|
+
source,
|
|
411
|
+
providerAuth: preservedAuth,
|
|
412
|
+
},
|
|
413
|
+
salt,
|
|
414
|
+
),
|
|
415
|
+
};
|
|
416
|
+
};
|
|
417
|
+
if (adapter.id === "github-copilot") {
|
|
418
|
+
const offered = candidateReader
|
|
419
|
+
? candidateReader(ctx, adapter.id)
|
|
420
|
+
: fallbackOAuthCredentialCandidates(adapter.id, credentialReader);
|
|
421
|
+
if (!offered.ok) {
|
|
422
|
+
throw new Error("GitHub Copilot OAuth credential discovery failed closed.");
|
|
423
|
+
}
|
|
424
|
+
return finalize(resolveGitHubCopilotUsageAuth(auth, model, salt, offered.candidates, offered.offeredCount === 0));
|
|
425
|
+
}
|
|
426
|
+
if (adapter.id === "xai") {
|
|
427
|
+
const offered = candidateReader
|
|
428
|
+
? candidateReader(ctx, adapter.id)
|
|
429
|
+
: fallbackOAuthCredentialCandidates(adapter.id, credentialReader);
|
|
430
|
+
if (!offered.ok) throw new Error("xAI OAuth credential discovery failed closed.");
|
|
431
|
+
return finalize(resolveXaiUsageAuth(auth, model, salt, offered.candidates));
|
|
432
|
+
}
|
|
433
|
+
if (adapter.id === "deepseek") {
|
|
434
|
+
const resolvedAuthorization = authorizationFrom(auth);
|
|
435
|
+
const access = bearerToken(resolvedAuthorization);
|
|
436
|
+
if (!access) throw new Error("DeepSeek API balance requires Bearer authentication.");
|
|
437
|
+
const authorization = `Bearer ${access}`;
|
|
438
|
+
const headers = { Authorization: authorization };
|
|
439
|
+
return finalize({
|
|
440
|
+
apiKey: access,
|
|
441
|
+
headers,
|
|
442
|
+
fingerprint: "",
|
|
443
|
+
secrets: [
|
|
444
|
+
access,
|
|
445
|
+
auth.apiKey,
|
|
446
|
+
headerValue(auth.headers, "Authorization"),
|
|
447
|
+
resolvedAuthorization,
|
|
448
|
+
authorization,
|
|
449
|
+
].filter((value): value is string => Boolean(value)),
|
|
450
|
+
model,
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
const authorization = authorizationFrom(auth);
|
|
454
|
+
if (!authorization) return undefined;
|
|
455
|
+
const headers = { Authorization: authorization };
|
|
456
|
+
const secrets = [auth.apiKey, headerValue(auth.headers, "Authorization"), authorization].filter(
|
|
457
|
+
(value): value is string => Boolean(value),
|
|
458
|
+
);
|
|
459
|
+
return finalize({
|
|
460
|
+
apiKey: auth.apiKey,
|
|
461
|
+
headers,
|
|
462
|
+
fingerprint: "",
|
|
463
|
+
secrets,
|
|
464
|
+
model,
|
|
465
|
+
});
|
|
503
466
|
}
|
|
504
467
|
|
|
505
468
|
export async function queryProviderUsage(
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
469
|
+
adapter: UsageProviderAdapter,
|
|
470
|
+
auth: ResolvedUsageAuth,
|
|
471
|
+
signal: AbortSignal,
|
|
472
|
+
timeoutMs: number,
|
|
473
|
+
guard?: UsageRequestGuard,
|
|
474
|
+
targetOrSettings?: string | Readonly<UsageQuerySettings>,
|
|
512
475
|
): Promise<UsageReport> {
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
);
|
|
551
|
-
} catch (error) {
|
|
552
|
-
if (isStaleExtensionContextError(error) || isAbortError(error)) throw error;
|
|
553
|
-
throw new Error(redactUsageError(errorMessage(error), auth.secrets));
|
|
554
|
-
}
|
|
476
|
+
const startedAt = Date.now();
|
|
477
|
+
let targetId =
|
|
478
|
+
typeof targetOrSettings === "string"
|
|
479
|
+
? targetOrSettings
|
|
480
|
+
: adapter.id === "fireworks"
|
|
481
|
+
? targetOrSettings?.fireworksAccountId
|
|
482
|
+
: undefined;
|
|
483
|
+
let resolvedLegacyFireworksTarget = false;
|
|
484
|
+
try {
|
|
485
|
+
if (adapter.id === "fireworks" && typeof targetOrSettings !== "string" && adapter.targets && guard) {
|
|
486
|
+
const target = await resolveUsageTarget(
|
|
487
|
+
adapter,
|
|
488
|
+
auth,
|
|
489
|
+
targetId,
|
|
490
|
+
signal,
|
|
491
|
+
remainingTimeout(timeoutMs, startedAt, "resolving the Fireworks account"),
|
|
492
|
+
guard,
|
|
493
|
+
);
|
|
494
|
+
if (target.kind === "selection-required") {
|
|
495
|
+
throw new Error("Fireworks account selection is required.");
|
|
496
|
+
}
|
|
497
|
+
targetId = target.targetId;
|
|
498
|
+
resolvedLegacyFireworksTarget = true;
|
|
499
|
+
}
|
|
500
|
+
return await adapter.query(
|
|
501
|
+
auth,
|
|
502
|
+
signal,
|
|
503
|
+
resolvedLegacyFireworksTarget
|
|
504
|
+
? remainingTimeout(timeoutMs, startedAt, `querying ${adapter.displayName} usage`)
|
|
505
|
+
: timeoutMs,
|
|
506
|
+
guard,
|
|
507
|
+
targetId,
|
|
508
|
+
);
|
|
509
|
+
} catch (error) {
|
|
510
|
+
if (isStaleExtensionContextError(error) || isAbortError(error)) throw error;
|
|
511
|
+
throw new Error(redactUsageError(errorMessage(error), auth.secrets));
|
|
512
|
+
}
|
|
555
513
|
}
|
|
556
514
|
|
|
557
515
|
export function providerIsConfigured(ctx: ExtensionContext, providerId: string): boolean {
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
} catch {
|
|
565
|
-
return (
|
|
566
|
-
!isMoonshotSiblingProvider(ctx, providerId) && candidateModels(ctx, providerId).length > 0
|
|
567
|
-
);
|
|
568
|
-
}
|
|
516
|
+
try {
|
|
517
|
+
const status = ctx.modelRegistry.getProviderAuthStatus(providerId);
|
|
518
|
+
return status.configured && moonshotProviderAuthSourceIsAllowed(ctx, providerId, status.source, status.label);
|
|
519
|
+
} catch {
|
|
520
|
+
return !isMoonshotSiblingProvider(ctx, providerId) && candidateModels(ctx, providerId).length > 0;
|
|
521
|
+
}
|
|
569
522
|
}
|
|
570
523
|
|
|
571
524
|
function moonshotProviderAuthIsAllowed(ctx: ExtensionContext, providerId: string): boolean {
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
525
|
+
if (!isMoonshotSiblingProvider(ctx, providerId)) return true;
|
|
526
|
+
try {
|
|
527
|
+
const status = ctx.modelRegistry.getProviderAuthStatus(providerId);
|
|
528
|
+
return moonshotProviderAuthSourceIsAllowed(ctx, providerId, status.source, status.label);
|
|
529
|
+
} catch {
|
|
530
|
+
return false;
|
|
531
|
+
}
|
|
579
532
|
}
|
|
580
533
|
|
|
581
534
|
function moonshotProviderAuthSourceIsAllowed(
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
535
|
+
ctx: ExtensionContext,
|
|
536
|
+
providerId: string,
|
|
537
|
+
source: string | undefined,
|
|
538
|
+
label: string | undefined,
|
|
586
539
|
): boolean {
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
540
|
+
if (!isMoonshotSiblingProvider(ctx, providerId)) return true;
|
|
541
|
+
if (source === undefined) return false;
|
|
542
|
+
if (source !== "environment") return true;
|
|
543
|
+
return (
|
|
544
|
+
label !== undefined &&
|
|
545
|
+
!label
|
|
546
|
+
.split(",")
|
|
547
|
+
.map((name) => name.trim())
|
|
548
|
+
.includes(SHARED_MOONSHOT_ENV_VAR)
|
|
549
|
+
);
|
|
597
550
|
}
|
|
598
551
|
|
|
599
552
|
function isMoonshotSiblingProvider(ctx: ExtensionContext, providerId: string): boolean {
|
|
600
|
-
|
|
601
|
-
(providerId === "moonshotai" || providerId === "moonshotai-cn") &&
|
|
602
|
-
ctx.model?.provider !== providerId
|
|
603
|
-
);
|
|
553
|
+
return (providerId === "moonshotai" || providerId === "moonshotai-cn") && ctx.model?.provider !== providerId;
|
|
604
554
|
}
|
|
605
555
|
|
|
606
556
|
function candidateModels(ctx: ExtensionContext, providerId: string): PiModel[] {
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
557
|
+
const candidates: PiModel[] = [];
|
|
558
|
+
const seen = new Set<string>();
|
|
559
|
+
const add = (model: PiModel | undefined) => {
|
|
560
|
+
if (!model || model.provider !== providerId) return;
|
|
561
|
+
const key = `${model.provider}/${model.id}`;
|
|
562
|
+
if (seen.has(key)) return;
|
|
563
|
+
seen.add(key);
|
|
564
|
+
candidates.push(model);
|
|
565
|
+
};
|
|
566
|
+
add(ctx.model);
|
|
567
|
+
for (const model of ctx.modelRegistry.getAvailable()) add(model);
|
|
568
|
+
for (const model of ctx.modelRegistry.getAll()) add(model);
|
|
569
|
+
return candidates;
|
|
620
570
|
}
|
|
621
571
|
|
|
622
572
|
export async function fetchProviderJson(
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
573
|
+
url: string,
|
|
574
|
+
auth: ResolvedUsageAuth,
|
|
575
|
+
signal: AbortSignal,
|
|
576
|
+
timeoutMs: number,
|
|
577
|
+
description: string,
|
|
578
|
+
request: {
|
|
579
|
+
method?: "GET" | "POST";
|
|
580
|
+
body?: Record<string, unknown>;
|
|
581
|
+
redirect?: RequestRedirect;
|
|
582
|
+
userAgent?: boolean;
|
|
583
|
+
responseError?: (status: number, text: string) => string | undefined;
|
|
584
|
+
} = {},
|
|
635
585
|
): Promise<Record<string, unknown>> {
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
clearTimeout(timeout);
|
|
698
|
-
signal.removeEventListener("abort", abortFromCaller);
|
|
699
|
-
}
|
|
586
|
+
const controller = new AbortController();
|
|
587
|
+
let timedOut = false;
|
|
588
|
+
const abortFromCaller = () => controller.abort();
|
|
589
|
+
if (signal.aborted) controller.abort();
|
|
590
|
+
else signal.addEventListener("abort", abortFromCaller, { once: true });
|
|
591
|
+
const timeout = setTimeout(() => {
|
|
592
|
+
timedOut = true;
|
|
593
|
+
controller.abort();
|
|
594
|
+
}, timeoutMs);
|
|
595
|
+
try {
|
|
596
|
+
const headers = { ...auth.headers };
|
|
597
|
+
if (request.userAgent !== false && !hasHeader(headers, "User-Agent")) {
|
|
598
|
+
headers["User-Agent"] = "pi-usage";
|
|
599
|
+
}
|
|
600
|
+
if (request.body && !hasHeader(headers, "Content-Type")) {
|
|
601
|
+
headers["Content-Type"] = "application/json";
|
|
602
|
+
}
|
|
603
|
+
const response = await fetch(url, {
|
|
604
|
+
method: request.method ?? "GET",
|
|
605
|
+
headers,
|
|
606
|
+
...(request.body ? { body: JSON.stringify(request.body) } : {}),
|
|
607
|
+
...(request.redirect ? { redirect: request.redirect } : {}),
|
|
608
|
+
signal: controller.signal,
|
|
609
|
+
});
|
|
610
|
+
if (response.redirected) throw new Error(`${description} refused a redirected response.`);
|
|
611
|
+
if (controller.signal.aborted) throw Object.assign(new Error("Usage query aborted."), { name: "AbortError" });
|
|
612
|
+
const text = await readBoundedResponse(
|
|
613
|
+
response,
|
|
614
|
+
response.ok ? MAX_SUCCESS_BODY_BYTES : MAX_ERROR_BODY_BYTES,
|
|
615
|
+
!response.ok,
|
|
616
|
+
description,
|
|
617
|
+
controller.signal,
|
|
618
|
+
);
|
|
619
|
+
if (controller.signal.aborted) throw Object.assign(new Error("Usage query aborted."), { name: "AbortError" });
|
|
620
|
+
const responseError = request.responseError?.(response.status, text);
|
|
621
|
+
if (responseError) throw new Error(responseError);
|
|
622
|
+
if (!response.ok) {
|
|
623
|
+
throw new Error(
|
|
624
|
+
`${description} returned ${response.status} ${response.statusText}: ${redactUsageError(text, auth.secrets)}`,
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
let parsed: unknown;
|
|
628
|
+
try {
|
|
629
|
+
parsed = JSON.parse(text) as unknown;
|
|
630
|
+
} catch (error) {
|
|
631
|
+
throw new Error(`${description} returned invalid JSON: ${errorMessage(error)}`);
|
|
632
|
+
}
|
|
633
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
634
|
+
throw new Error(`${description} response was not an object.`);
|
|
635
|
+
}
|
|
636
|
+
return parsed as Record<string, unknown>;
|
|
637
|
+
} catch (error) {
|
|
638
|
+
if (timedOut) {
|
|
639
|
+
throw new Error(`Timed out after ${Math.round(timeoutMs / 1000)}s while fetching usage.`);
|
|
640
|
+
}
|
|
641
|
+
if (signal.aborted) throw Object.assign(new Error("Usage query aborted."), { name: "AbortError" });
|
|
642
|
+
throw error;
|
|
643
|
+
} finally {
|
|
644
|
+
clearTimeout(timeout);
|
|
645
|
+
signal.removeEventListener("abort", abortFromCaller);
|
|
646
|
+
}
|
|
700
647
|
}
|
|
701
648
|
|
|
702
649
|
async function readBoundedResponse(
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
650
|
+
response: Response,
|
|
651
|
+
maxBytes: number,
|
|
652
|
+
truncateOverflow: boolean,
|
|
653
|
+
description: string,
|
|
654
|
+
signal: AbortSignal,
|
|
708
655
|
): Promise<string> {
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
656
|
+
if (!response.body) return "";
|
|
657
|
+
const reader = response.body.getReader();
|
|
658
|
+
const chunks: Uint8Array[] = [];
|
|
659
|
+
let total = 0;
|
|
660
|
+
let truncated = false;
|
|
661
|
+
const abort = () => void reader.cancel().catch(() => undefined);
|
|
662
|
+
if (signal.aborted) abort();
|
|
663
|
+
else signal.addEventListener("abort", abort, { once: true });
|
|
664
|
+
try {
|
|
665
|
+
while (true) {
|
|
666
|
+
const { done, value } = await reader.read();
|
|
667
|
+
if (done) break;
|
|
668
|
+
const remaining = maxBytes - total;
|
|
669
|
+
if (value.byteLength > remaining) {
|
|
670
|
+
if (remaining > 0) chunks.push(value.subarray(0, remaining));
|
|
671
|
+
total = maxBytes;
|
|
672
|
+
truncated = true;
|
|
673
|
+
await reader.cancel();
|
|
674
|
+
break;
|
|
675
|
+
}
|
|
676
|
+
chunks.push(value);
|
|
677
|
+
total += value.byteLength;
|
|
678
|
+
}
|
|
679
|
+
} finally {
|
|
680
|
+
signal.removeEventListener("abort", abort);
|
|
681
|
+
reader.releaseLock();
|
|
682
|
+
}
|
|
683
|
+
if (truncated && !truncateOverflow) {
|
|
684
|
+
throw new Error(`${description} response exceeded ${maxBytes} bytes.`);
|
|
685
|
+
}
|
|
686
|
+
const body = new Uint8Array(total);
|
|
687
|
+
let offset = 0;
|
|
688
|
+
for (const chunk of chunks) {
|
|
689
|
+
body.set(chunk, offset);
|
|
690
|
+
offset += chunk.byteLength;
|
|
691
|
+
}
|
|
692
|
+
const text = new TextDecoder().decode(body);
|
|
693
|
+
return truncated ? `${text}…` : text;
|
|
747
694
|
}
|
|
748
695
|
|
|
749
696
|
type RequestAuth = {
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
697
|
+
apiKey?: string;
|
|
698
|
+
headers?: Record<string, string | null>;
|
|
699
|
+
baseUrl?: string;
|
|
700
|
+
env?: Record<string, string>;
|
|
754
701
|
};
|
|
755
702
|
|
|
756
703
|
type StoredCredentialReader = (providerId: string) => unknown;
|
|
757
704
|
|
|
758
705
|
type UsageAuthRegistry = {
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
| undefined
|
|
770
|
-
>;
|
|
706
|
+
getProvider?(providerId: string): { baseUrl?: string } | undefined;
|
|
707
|
+
getApiKeyAndHeaders?(model: PiModel): Promise<({ ok: true } & RequestAuth) | { ok: false; error: string }>;
|
|
708
|
+
getProviderAuth?(providerId: string): Promise<
|
|
709
|
+
| {
|
|
710
|
+
auth: RequestAuth;
|
|
711
|
+
env?: Record<string, string>;
|
|
712
|
+
source?: string;
|
|
713
|
+
}
|
|
714
|
+
| undefined
|
|
715
|
+
>;
|
|
771
716
|
};
|
|
772
717
|
|
|
773
718
|
function resolveXaiUsageAuth(
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
719
|
+
auth: RequestAuth,
|
|
720
|
+
model: PiModel,
|
|
721
|
+
salt: Uint8Array,
|
|
722
|
+
candidates: readonly unknown[],
|
|
778
723
|
): ResolvedUsageAuth {
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
724
|
+
const resolvedAccess = bearerToken(headerValue(auth.headers, "Authorization")) ?? auth.apiKey;
|
|
725
|
+
if (!resolvedAccess) throw new Error("xAI runtime authentication was incomplete.");
|
|
726
|
+
let sawOAuth = false;
|
|
727
|
+
let sawMatchingAccess = false;
|
|
728
|
+
let sawIncompleteMatch = false;
|
|
729
|
+
const matches: Array<{ access: string; refresh: string }> = [];
|
|
730
|
+
for (const candidate of candidates) {
|
|
731
|
+
try {
|
|
732
|
+
const credential = asObject(candidate);
|
|
733
|
+
if (credential?.type !== "oauth") continue;
|
|
734
|
+
sawOAuth = true;
|
|
735
|
+
if (credential.access !== resolvedAccess) continue;
|
|
736
|
+
sawMatchingAccess = true;
|
|
737
|
+
if (
|
|
738
|
+
typeof credential.access !== "string" ||
|
|
739
|
+
!credential.access ||
|
|
740
|
+
typeof credential.refresh !== "string" ||
|
|
741
|
+
!credential.refresh ||
|
|
742
|
+
typeof credential.expires !== "number" ||
|
|
743
|
+
!Number.isFinite(credential.expires)
|
|
744
|
+
) {
|
|
745
|
+
sawIncompleteMatch = true;
|
|
746
|
+
continue;
|
|
747
|
+
}
|
|
748
|
+
matches.push({ access: credential.access, refresh: credential.refresh });
|
|
749
|
+
} catch {
|
|
750
|
+
// Malformed candidates never authorize a consumer-proxy request.
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
if (sawIncompleteMatch) throw new Error("The matching xAI OAuth credential was incomplete.");
|
|
754
|
+
if (matches.length > 1) {
|
|
755
|
+
throw new Error("Multiple OAuth credentials match the active xAI runtime account.");
|
|
756
|
+
}
|
|
757
|
+
const match = matches[0];
|
|
758
|
+
if (!match) {
|
|
759
|
+
if (!sawOAuth) {
|
|
760
|
+
throw new Error(
|
|
761
|
+
"xAI consumer usage requires the OAuth subscription account configured through Pi /login; XAI_API_KEY users can review API spend at console.x.ai.",
|
|
762
|
+
);
|
|
763
|
+
}
|
|
764
|
+
if (sawMatchingAccess) throw new Error("The matching xAI OAuth credential was incomplete.");
|
|
765
|
+
throw new Error("The active xAI runtime account does not match Pi's stored OAuth account.");
|
|
766
|
+
}
|
|
767
|
+
const authorization = `Bearer ${match.access}`;
|
|
768
|
+
const headers = { Authorization: authorization };
|
|
769
|
+
return {
|
|
770
|
+
apiKey: match.access,
|
|
771
|
+
headers,
|
|
772
|
+
fingerprint: fingerprintResolvedAuth({ headers }, salt),
|
|
773
|
+
secrets: [
|
|
774
|
+
match.access,
|
|
775
|
+
match.refresh,
|
|
776
|
+
resolvedAccess,
|
|
777
|
+
auth.apiKey,
|
|
778
|
+
headerValue(auth.headers, "Authorization"),
|
|
779
|
+
authorization,
|
|
780
|
+
].filter((value): value is string => Boolean(value)),
|
|
781
|
+
model,
|
|
782
|
+
};
|
|
838
783
|
}
|
|
839
784
|
|
|
840
785
|
function resolveGitHubCopilotUsageAuth(
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
786
|
+
auth: RequestAuth,
|
|
787
|
+
model: PiModel,
|
|
788
|
+
salt: Uint8Array,
|
|
789
|
+
candidates: readonly unknown[],
|
|
790
|
+
standaloneFallback: boolean,
|
|
846
791
|
): ResolvedUsageAuth {
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
Authorization: authorization,
|
|
914
|
-
"X-GitHub-Api-Version": "2025-05-01",
|
|
915
|
-
};
|
|
916
|
-
return {
|
|
917
|
-
apiKey: refresh,
|
|
918
|
-
headers,
|
|
919
|
-
fingerprint: fingerprintResolvedAuth({ headers }, salt),
|
|
920
|
-
secrets: [refresh, storedAccess, resolvedAccess, authorization],
|
|
921
|
-
model,
|
|
922
|
-
};
|
|
792
|
+
const resolvedAccess = bearerToken(headerValue(auth.headers, "Authorization")) ?? auth.apiKey;
|
|
793
|
+
if (!resolvedAccess) throw new Error("GitHub Copilot OAuth credentials were incomplete.");
|
|
794
|
+
let sawOAuth = false;
|
|
795
|
+
let sawMatchingAccess = false;
|
|
796
|
+
let sawIncompleteMatch = false;
|
|
797
|
+
let sawEnterpriseMatch = false;
|
|
798
|
+
const matches = new Map<string, { refresh: string; storedAccess: string }>();
|
|
799
|
+
for (const candidate of candidates) {
|
|
800
|
+
try {
|
|
801
|
+
const credential = asObject(candidate);
|
|
802
|
+
if (credential?.type !== "oauth") continue;
|
|
803
|
+
sawOAuth = true;
|
|
804
|
+
const storedAccess = typeof credential.access === "string" && credential.access ? credential.access : undefined;
|
|
805
|
+
if (storedAccess !== resolvedAccess) continue;
|
|
806
|
+
sawMatchingAccess = true;
|
|
807
|
+
const enterpriseUrl = credential.enterpriseUrl;
|
|
808
|
+
if (typeof enterpriseUrl === "string" && enterpriseUrl && !isPublicGitHubDomain(enterpriseUrl)) {
|
|
809
|
+
sawEnterpriseMatch = true;
|
|
810
|
+
continue;
|
|
811
|
+
}
|
|
812
|
+
const refresh = typeof credential.refresh === "string" && credential.refresh ? credential.refresh : undefined;
|
|
813
|
+
if (!refresh) {
|
|
814
|
+
sawIncompleteMatch = true;
|
|
815
|
+
continue;
|
|
816
|
+
}
|
|
817
|
+
matches.set(`${storedAccess.length}:${storedAccess}${refresh}`, { refresh, storedAccess });
|
|
818
|
+
} catch {
|
|
819
|
+
// Malformed candidates never authorize a provider request.
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
if (sawEnterpriseMatch) {
|
|
823
|
+
throw new Error("GitHub Copilot usage does not yet support GitHub Enterprise accounts.");
|
|
824
|
+
}
|
|
825
|
+
if (sawIncompleteMatch) throw new Error("GitHub Copilot OAuth credentials were incomplete.");
|
|
826
|
+
if (matches.size > 1) {
|
|
827
|
+
throw new Error("Conflicting OAuth credentials match the active GitHub Copilot runtime account.");
|
|
828
|
+
}
|
|
829
|
+
const match = matches.values().next().value;
|
|
830
|
+
if (!match) {
|
|
831
|
+
if (!sawOAuth) {
|
|
832
|
+
throw new Error(
|
|
833
|
+
standaloneFallback
|
|
834
|
+
? "GitHub Copilot usage requires the OAuth account configured through Pi /login."
|
|
835
|
+
: "GitHub Copilot usage requires an OAuth account configured through Pi /login or a compatible credential source.",
|
|
836
|
+
);
|
|
837
|
+
}
|
|
838
|
+
if (sawMatchingAccess) throw new Error("GitHub Copilot OAuth credentials were incomplete.");
|
|
839
|
+
throw new Error(
|
|
840
|
+
standaloneFallback
|
|
841
|
+
? "The active GitHub Copilot runtime account does not match Pi's stored OAuth account."
|
|
842
|
+
: "The active GitHub Copilot runtime account does not match any available OAuth account.",
|
|
843
|
+
);
|
|
844
|
+
}
|
|
845
|
+
const { refresh, storedAccess } = match;
|
|
846
|
+
const authorization = `Bearer ${refresh}`;
|
|
847
|
+
const headers = {
|
|
848
|
+
Authorization: authorization,
|
|
849
|
+
"X-GitHub-Api-Version": "2025-05-01",
|
|
850
|
+
};
|
|
851
|
+
return {
|
|
852
|
+
apiKey: refresh,
|
|
853
|
+
headers,
|
|
854
|
+
fingerprint: fingerprintResolvedAuth({ headers }, salt),
|
|
855
|
+
secrets: [refresh, storedAccess, resolvedAccess, authorization],
|
|
856
|
+
model,
|
|
857
|
+
};
|
|
923
858
|
}
|
|
924
859
|
|
|
925
860
|
function authorizationFrom(auth: RequestAuth): string | undefined {
|
|
926
|
-
|
|
927
|
-
headerValue(auth.headers, "Authorization") ??
|
|
928
|
-
(auth.apiKey ? `Bearer ${auth.apiKey}` : undefined)
|
|
929
|
-
);
|
|
861
|
+
return headerValue(auth.headers, "Authorization") ?? (auth.apiKey ? `Bearer ${auth.apiKey}` : undefined);
|
|
930
862
|
}
|
|
931
863
|
|
|
932
864
|
function bearerToken(authorization: string | undefined): string | undefined {
|
|
933
|
-
|
|
934
|
-
|
|
865
|
+
const match = /^Bearer\s+(.+)$/iu.exec(authorization ?? "");
|
|
866
|
+
return match?.[1];
|
|
935
867
|
}
|
|
936
868
|
|
|
937
869
|
function asObject(value: unknown): Record<string, unknown> | undefined {
|
|
938
|
-
|
|
939
|
-
|
|
870
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
871
|
+
return value as Record<string, unknown>;
|
|
940
872
|
}
|
|
941
873
|
|
|
942
874
|
function isPublicGitHubDomain(value: string): boolean {
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
875
|
+
try {
|
|
876
|
+
const url = new URL(value.includes("://") ? value : `https://${value}`);
|
|
877
|
+
return url.hostname.toLowerCase() === "github.com";
|
|
878
|
+
} catch {
|
|
879
|
+
return false;
|
|
880
|
+
}
|
|
949
881
|
}
|
|
950
882
|
|
|
951
883
|
function hasOfficialOrigin(model: PiModel, providerId: string): boolean {
|
|
952
|
-
|
|
884
|
+
return hasOfficialUrlOrigin(model.baseUrl, providerId);
|
|
953
885
|
}
|
|
954
886
|
|
|
955
887
|
function hasOfficialUrlOrigin(value: string, providerId: string): boolean {
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
return false;
|
|
983
|
-
}
|
|
888
|
+
try {
|
|
889
|
+
const url = new URL(value);
|
|
890
|
+
if (providerId === "baseten") {
|
|
891
|
+
return ["https://inference.baseten.co", "https://api.baseten.co"].includes(url.origin);
|
|
892
|
+
}
|
|
893
|
+
if (providerId === "openai-codex") return url.origin === "https://chatgpt.com";
|
|
894
|
+
if (providerId === "deepseek") return url.origin === "https://api.deepseek.com";
|
|
895
|
+
if (providerId === "fireworks") return url.origin === "https://api.fireworks.ai";
|
|
896
|
+
if (providerId === "openrouter") return url.origin === "https://openrouter.ai";
|
|
897
|
+
if (providerId === "vercel-ai-gateway") return url.origin === "https://ai-gateway.vercel.sh";
|
|
898
|
+
if (providerId === "opencode-go") return url.origin === "https://opencode.ai";
|
|
899
|
+
if (providerId === "kimi-coding") return url.origin === "https://api.kimi.com";
|
|
900
|
+
if (providerId === "minimax") return url.origin === "https://api.minimax.io";
|
|
901
|
+
if (providerId === "minimax-cn") return url.origin === "https://api.minimaxi.com";
|
|
902
|
+
if (providerId === "moonshotai") return url.origin === "https://api.moonshot.ai";
|
|
903
|
+
if (providerId === "moonshotai-cn") return url.origin === "https://api.moonshot.cn";
|
|
904
|
+
if (providerId === "xai") return url.origin === "https://api.x.ai";
|
|
905
|
+
if (providerId === "zai") return url.origin === "https://api.z.ai";
|
|
906
|
+
if (providerId === "zai-coding-cn") return url.origin === "https://open.bigmodel.cn";
|
|
907
|
+
if (providerId === "github-copilot") {
|
|
908
|
+
return url.protocol === "https:" && /^api\.[a-z0-9-]+\.githubcopilot\.com$/u.test(url.hostname);
|
|
909
|
+
}
|
|
910
|
+
return false;
|
|
911
|
+
} catch {
|
|
912
|
+
return false;
|
|
913
|
+
}
|
|
984
914
|
}
|
|
985
915
|
|
|
986
|
-
function headerValue(
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
): string | undefined {
|
|
990
|
-
const entry = Object.entries(headers ?? {}).find(
|
|
991
|
-
([candidate]) => candidate.toLowerCase() === name.toLowerCase(),
|
|
992
|
-
);
|
|
993
|
-
return entry?.[1] ?? undefined;
|
|
916
|
+
function headerValue(headers: Record<string, string | null> | undefined, name: string): string | undefined {
|
|
917
|
+
const entry = Object.entries(headers ?? {}).find(([candidate]) => candidate.toLowerCase() === name.toLowerCase());
|
|
918
|
+
return entry?.[1] ?? undefined;
|
|
994
919
|
}
|
|
995
920
|
|
|
996
921
|
function hasHeader(headers: Record<string, string>, name: string): boolean {
|
|
997
|
-
|
|
922
|
+
return Object.keys(headers).some((key) => key.toLowerCase() === name.toLowerCase());
|
|
998
923
|
}
|
|
999
924
|
|
|
1000
925
|
function validatedXaiUserId(value: unknown): string {
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
926
|
+
if (typeof value !== "string" || !/^[A-Za-z0-9._~-]{1,128}$/u.test(value)) {
|
|
927
|
+
throw new Error("xAI consumer identity returned an unsafe canonical user ID.");
|
|
928
|
+
}
|
|
929
|
+
return value;
|
|
1005
930
|
}
|
|
1006
931
|
|
|
1007
932
|
function basetenBillingUsageUrl(windowAt: number): string {
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
933
|
+
const url = new URL(BASETEN_BILLING_USAGE_URL);
|
|
934
|
+
url.searchParams.set(
|
|
935
|
+
"start_date",
|
|
936
|
+
new Date(windowAt - BASETEN_USAGE_WINDOW_DAYS * 24 * 60 * 60 * 1_000).toISOString(),
|
|
937
|
+
);
|
|
938
|
+
url.searchParams.set("end_date", new Date(windowAt).toISOString());
|
|
939
|
+
return url.toString();
|
|
1015
940
|
}
|
|
1016
941
|
|
|
1017
942
|
async function queryMiniMaxUsage(
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
943
|
+
providerId: MiniMaxProviderId,
|
|
944
|
+
auth: ResolvedUsageAuth,
|
|
945
|
+
signal: AbortSignal,
|
|
946
|
+
timeoutMs: number,
|
|
947
|
+
guard: UsageRequestGuard | undefined,
|
|
1023
948
|
): Promise<UsageReport> {
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
949
|
+
if (!guard) throw new Error("MiniMax usage requires request-boundary revalidation.");
|
|
950
|
+
const apiKey = bearerToken(headerValue(auth.headers, "Authorization")) ?? auth.apiKey;
|
|
951
|
+
if (!apiKey) throw new Error("MiniMax runtime API key was unavailable.");
|
|
952
|
+
const kind = miniMaxUsageKind(apiKey);
|
|
953
|
+
const path = kind === "account-balance" ? "/account/query_balance" : "/v1/token_plan/remains";
|
|
954
|
+
const startedAt = Date.now();
|
|
955
|
+
await guard();
|
|
956
|
+
const payload = (await fetchProviderJson(
|
|
957
|
+
`${MINIMAX_API_ROOTS[providerId]}${path}`,
|
|
958
|
+
auth,
|
|
959
|
+
signal,
|
|
960
|
+
remainingTimeout(timeoutMs, startedAt, "fetching MiniMax usage"),
|
|
961
|
+
"MiniMax usage endpoint",
|
|
962
|
+
{ redirect: "error" },
|
|
963
|
+
)) as MiniMaxUsagePayload;
|
|
964
|
+
await guard();
|
|
965
|
+
return normalizeMiniMaxUsagePayload(providerId, kind, payload, Date.now());
|
|
1041
966
|
}
|
|
1042
967
|
|
|
1043
968
|
async function queryMoonshotBalance(
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
969
|
+
providerId: "moonshotai" | "moonshotai-cn",
|
|
970
|
+
auth: ResolvedUsageAuth,
|
|
971
|
+
signal: AbortSignal,
|
|
972
|
+
timeoutMs: number,
|
|
973
|
+
guard: UsageRequestGuard | undefined,
|
|
1049
974
|
): Promise<UsageReport> {
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
975
|
+
if (!guard) throw new Error("Moonshot AI balance requires request-boundary revalidation.");
|
|
976
|
+
const startedAt = Date.now();
|
|
977
|
+
await guard();
|
|
978
|
+
const payload = (await fetchProviderJson(
|
|
979
|
+
MOONSHOT_BALANCE_URLS[providerId],
|
|
980
|
+
auth,
|
|
981
|
+
signal,
|
|
982
|
+
remainingTimeout(timeoutMs, startedAt, "fetching Moonshot AI balance"),
|
|
983
|
+
"Moonshot AI balance endpoint",
|
|
984
|
+
{ redirect: "error" },
|
|
985
|
+
)) as MoonshotBalancePayload;
|
|
986
|
+
await guard();
|
|
987
|
+
return normalizeMoonshotBalancePayload(providerId, payload, Date.now());
|
|
1063
988
|
}
|
|
1064
989
|
|
|
1065
|
-
function remainingTimeout(
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
): number {
|
|
1070
|
-
const remaining = timeoutMs - (Date.now() - startedAt);
|
|
1071
|
-
if (remaining <= 0) throw new Error(`Timed out while ${description}.`);
|
|
1072
|
-
return remaining;
|
|
990
|
+
function remainingTimeout(timeoutMs: number, startedAt: number, description = "fetching xAI consumer usage"): number {
|
|
991
|
+
const remaining = timeoutMs - (Date.now() - startedAt);
|
|
992
|
+
if (remaining <= 0) throw new Error(`Timed out while ${description}.`);
|
|
993
|
+
return remaining;
|
|
1073
994
|
}
|
|
1074
995
|
|
|
1075
996
|
function zaiOrigin(baseUrl: string | undefined): string {
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
997
|
+
const base = baseUrl?.trim();
|
|
998
|
+
if (!base) throw new Error("Z.AI model base URL is unavailable.");
|
|
999
|
+
return new URL(base).origin;
|
|
1079
1000
|
}
|
|
1080
1001
|
|
|
1081
1002
|
function zaiMonitorUrl(baseUrl: string | undefined): string {
|
|
1082
|
-
|
|
1003
|
+
return `${zaiOrigin(baseUrl)}/api/monitor/usage/quota/limit`;
|
|
1083
1004
|
}
|
|
1084
1005
|
|
|
1085
1006
|
function zaiMonitorAuth(auth: ResolvedUsageAuth): ResolvedUsageAuth {
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
return { ...auth, headers: { ...auth.headers, Authorization: token } };
|
|
1007
|
+
const authorization = headerValue(auth.headers, "Authorization");
|
|
1008
|
+
const token = authorization === undefined ? undefined : (bearerToken(authorization) ?? authorization);
|
|
1009
|
+
if (token === undefined || token === authorization) return auth;
|
|
1010
|
+
return { ...auth, headers: { ...auth.headers, Authorization: token } };
|
|
1091
1011
|
}
|
|
1092
1012
|
|
|
1093
1013
|
async function queryZaiUsage(
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1014
|
+
providerId: "zai" | "zai-coding-cn",
|
|
1015
|
+
providerName: string,
|
|
1016
|
+
auth: ResolvedUsageAuth,
|
|
1017
|
+
signal: AbortSignal,
|
|
1018
|
+
timeoutMs: number,
|
|
1019
|
+
guard: UsageRequestGuard | undefined,
|
|
1100
1020
|
): Promise<UsageReport> {
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1021
|
+
if (!guard) throw new Error("Z.AI usage requires request-boundary revalidation.");
|
|
1022
|
+
const startedAt = Date.now();
|
|
1023
|
+
await guard();
|
|
1024
|
+
const payload = (await fetchProviderJson(
|
|
1025
|
+
zaiMonitorUrl(auth.model.baseUrl),
|
|
1026
|
+
zaiMonitorAuth(auth),
|
|
1027
|
+
signal,
|
|
1028
|
+
remainingTimeout(timeoutMs, startedAt, `fetching ${providerName} quota`),
|
|
1029
|
+
`${providerName} quota endpoint`,
|
|
1030
|
+
{ responseError: zaiResponseError },
|
|
1031
|
+
)) as ZaiQuotaPayload;
|
|
1032
|
+
await guard();
|
|
1033
|
+
const planTimeoutMs = timeoutMs - (Date.now() - startedAt);
|
|
1034
|
+
const plan = await fetchZaiPlan(providerName, auth, signal, planTimeoutMs);
|
|
1035
|
+
return normalizeZaiQuotaPayload(providerId, providerName, payload, Date.now(), plan);
|
|
1116
1036
|
}
|
|
1117
1037
|
|
|
1118
1038
|
// The subscription endpoint is undocumented and may not exist on every official origin. It only
|
|
1119
1039
|
// contributes the plan name and renewal date, so any non-abort failure is swallowed instead of
|
|
1120
1040
|
// blanking the required quota report.
|
|
1121
1041
|
async function fetchZaiPlan(
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1042
|
+
providerName: string,
|
|
1043
|
+
auth: ResolvedUsageAuth,
|
|
1044
|
+
signal: AbortSignal,
|
|
1045
|
+
timeoutMs: number,
|
|
1126
1046
|
): Promise<ZaiPlanInfo | undefined> {
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1047
|
+
if (timeoutMs <= 0 || signal.aborted) return undefined;
|
|
1048
|
+
try {
|
|
1049
|
+
const payload = (await fetchProviderJson(
|
|
1050
|
+
`${zaiOrigin(auth.model.baseUrl)}/api/biz/subscription/list`,
|
|
1051
|
+
zaiMonitorAuth(auth),
|
|
1052
|
+
signal,
|
|
1053
|
+
timeoutMs,
|
|
1054
|
+
`${providerName} plan endpoint`,
|
|
1055
|
+
{ responseError: zaiResponseError },
|
|
1056
|
+
)) as ZaiSubscriptionPayload;
|
|
1057
|
+
return normalizeZaiSubscriptionPayload(payload);
|
|
1058
|
+
} catch (error) {
|
|
1059
|
+
if (isAbortError(error)) throw error;
|
|
1060
|
+
return undefined;
|
|
1061
|
+
}
|
|
1142
1062
|
}
|
|
1143
1063
|
|
|
1144
1064
|
function isAbortError(error: unknown): boolean {
|
|
1145
|
-
|
|
1065
|
+
return error instanceof Error && error.name === "AbortError";
|
|
1146
1066
|
}
|