@slkiser/opencode-quota 4.0.1 → 4.1.0
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 +36 -26
- package/dist/bin/opencode-quota.d.ts.map +1 -1
- package/dist/bin/opencode-quota.js +8 -0
- package/dist/bin/opencode-quota.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/lib/anthropic.d.ts.map +1 -1
- package/dist/lib/anthropic.js +78 -72
- package/dist/lib/anthropic.js.map +1 -1
- package/dist/lib/api-key-resolver.d.ts.map +1 -1
- package/dist/lib/api-key-resolver.js +14 -24
- package/dist/lib/api-key-resolver.js.map +1 -1
- package/dist/lib/atomic-json.d.ts +3 -1
- package/dist/lib/atomic-json.d.ts.map +1 -1
- package/dist/lib/atomic-json.js +23 -9
- package/dist/lib/atomic-json.js.map +1 -1
- package/dist/lib/chutes.d.ts.map +1 -1
- package/dist/lib/chutes.js +28 -23
- package/dist/lib/chutes.js.map +1 -1
- package/dist/lib/cli-show.d.ts +9 -0
- package/dist/lib/cli-show.d.ts.map +1 -1
- package/dist/lib/cli-show.js +34 -5
- package/dist/lib/cli-show.js.map +1 -1
- package/dist/lib/cli-status.d.ts +8 -0
- package/dist/lib/cli-status.d.ts.map +1 -0
- package/dist/lib/cli-status.js +125 -0
- package/dist/lib/cli-status.js.map +1 -0
- package/dist/lib/config.d.ts +1 -1
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +47 -12
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/copilot.d.ts +11 -5
- package/dist/lib/copilot.d.ts.map +1 -1
- package/dist/lib/copilot.js +300 -52
- package/dist/lib/copilot.js.map +1 -1
- package/dist/lib/deepseek.d.ts.map +1 -1
- package/dist/lib/deepseek.js +22 -17
- package/dist/lib/deepseek.js.map +1 -1
- package/dist/lib/display-sanitize.d.ts.map +1 -1
- package/dist/lib/display-sanitize.js +8 -0
- package/dist/lib/display-sanitize.js.map +1 -1
- package/dist/lib/entries.d.ts +11 -0
- package/dist/lib/entries.d.ts.map +1 -1
- package/dist/lib/entries.js.map +1 -1
- package/dist/lib/format-utils.d.ts +7 -0
- package/dist/lib/format-utils.d.ts.map +1 -1
- package/dist/lib/format-utils.js +18 -0
- package/dist/lib/format-utils.js.map +1 -1
- package/dist/lib/format.d.ts +1 -0
- package/dist/lib/format.d.ts.map +1 -1
- package/dist/lib/format.js +22 -7
- package/dist/lib/format.js.map +1 -1
- package/dist/lib/google-agy.d.ts.map +1 -1
- package/dist/lib/google-agy.js +203 -92
- package/dist/lib/google-agy.js.map +1 -1
- package/dist/lib/google-gemini-cli.d.ts.map +1 -1
- package/dist/lib/google-gemini-cli.js +53 -41
- package/dist/lib/google-gemini-cli.js.map +1 -1
- package/dist/lib/google-token-cache.d.ts.map +1 -1
- package/dist/lib/google-token-cache.js +87 -34
- package/dist/lib/google-token-cache.js.map +1 -1
- package/dist/lib/google.d.ts.map +1 -1
- package/dist/lib/google.js +57 -39
- package/dist/lib/google.js.map +1 -1
- package/dist/lib/grouped-entry-normalization.d.ts.map +1 -1
- package/dist/lib/grouped-entry-normalization.js +7 -1
- package/dist/lib/grouped-entry-normalization.js.map +1 -1
- package/dist/lib/http.d.ts +12 -7
- package/dist/lib/http.d.ts.map +1 -1
- package/dist/lib/http.js +28 -15
- package/dist/lib/http.js.map +1 -1
- package/dist/lib/init-installer.d.ts +2 -1
- package/dist/lib/init-installer.d.ts.map +1 -1
- package/dist/lib/init-installer.js +29 -2
- package/dist/lib/init-installer.js.map +1 -1
- package/dist/lib/kimi.d.ts.map +1 -1
- package/dist/lib/kimi.js +20 -15
- package/dist/lib/kimi.js.map +1 -1
- package/dist/lib/maintainer-announcements.d.ts.map +1 -1
- package/dist/lib/maintainer-announcements.js +5 -0
- package/dist/lib/maintainer-announcements.js.map +1 -1
- package/dist/lib/mimo-config.d.ts +29 -0
- package/dist/lib/mimo-config.d.ts.map +1 -0
- package/dist/lib/mimo-config.js +147 -0
- package/dist/lib/mimo-config.js.map +1 -0
- package/dist/lib/mimo.d.ts +35 -0
- package/dist/lib/mimo.d.ts.map +1 -0
- package/dist/lib/mimo.js +236 -0
- package/dist/lib/mimo.js.map +1 -0
- package/dist/lib/modelsdev-pricing.d.ts.map +1 -1
- package/dist/lib/modelsdev-pricing.js +39 -27
- package/dist/lib/modelsdev-pricing.js.map +1 -1
- package/dist/lib/nanogpt.d.ts.map +1 -1
- package/dist/lib/nanogpt.js +40 -30
- package/dist/lib/nanogpt.js.map +1 -1
- package/dist/lib/ollama-cloud.d.ts +1 -1
- package/dist/lib/ollama-cloud.d.ts.map +1 -1
- package/dist/lib/ollama-cloud.js +73 -68
- package/dist/lib/ollama-cloud.js.map +1 -1
- package/dist/lib/openai.d.ts.map +1 -1
- package/dist/lib/openai.js +53 -48
- package/dist/lib/openai.js.map +1 -1
- package/dist/lib/opencode-config-providers.d.ts.map +1 -1
- package/dist/lib/opencode-config-providers.js +13 -16
- package/dist/lib/opencode-config-providers.js.map +1 -1
- package/dist/lib/opencode-config-read.d.ts +23 -0
- package/dist/lib/opencode-config-read.d.ts.map +1 -0
- package/dist/lib/opencode-config-read.js +30 -0
- package/dist/lib/opencode-config-read.js.map +1 -0
- package/dist/lib/opencode-go.d.ts.map +1 -1
- package/dist/lib/opencode-go.js +44 -39
- package/dist/lib/opencode-go.js.map +1 -1
- package/dist/lib/opencode-zen-config.d.ts +34 -0
- package/dist/lib/opencode-zen-config.d.ts.map +1 -0
- package/dist/lib/opencode-zen-config.js +103 -0
- package/dist/lib/opencode-zen-config.js.map +1 -0
- package/dist/lib/opencode-zen.d.ts +27 -0
- package/dist/lib/opencode-zen.d.ts.map +1 -0
- package/dist/lib/opencode-zen.js +143 -0
- package/dist/lib/opencode-zen.js.map +1 -0
- package/dist/lib/provider-add-command.d.ts.map +1 -1
- package/dist/lib/provider-add-command.js +4 -12
- package/dist/lib/provider-add-command.js.map +1 -1
- package/dist/lib/provider-add-json-v1-questionnaire.d.ts +32 -0
- package/dist/lib/provider-add-json-v1-questionnaire.d.ts.map +1 -0
- package/dist/lib/provider-add-json-v1-questionnaire.js +336 -0
- package/dist/lib/provider-add-json-v1-questionnaire.js.map +1 -0
- package/dist/lib/provider-availability.d.ts +2 -2
- package/dist/lib/provider-availability.d.ts.map +1 -1
- package/dist/lib/provider-availability.js +2 -3
- package/dist/lib/provider-availability.js.map +1 -1
- package/dist/lib/provider-metadata.d.ts +289 -1
- package/dist/lib/provider-metadata.d.ts.map +1 -1
- package/dist/lib/provider-metadata.js +321 -227
- package/dist/lib/provider-metadata.js.map +1 -1
- package/dist/lib/quota-command-format.d.ts.map +1 -1
- package/dist/lib/quota-command-format.js +12 -16
- package/dist/lib/quota-command-format.js.map +1 -1
- package/dist/lib/quota-dialog-commands.d.ts +59 -2
- package/dist/lib/quota-dialog-commands.d.ts.map +1 -1
- package/dist/lib/quota-dialog-commands.js +71 -30
- package/dist/lib/quota-dialog-commands.js.map +1 -1
- package/dist/lib/quota-entry-display.d.ts +2 -0
- package/dist/lib/quota-entry-display.d.ts.map +1 -1
- package/dist/lib/quota-entry-display.js +27 -13
- package/dist/lib/quota-entry-display.js.map +1 -1
- package/dist/lib/quota-render-data.d.ts +4 -0
- package/dist/lib/quota-render-data.d.ts.map +1 -1
- package/dist/lib/quota-render-data.js +101 -121
- package/dist/lib/quota-render-data.js.map +1 -1
- package/dist/lib/quota-runtime-context.d.ts +9 -1
- package/dist/lib/quota-runtime-context.d.ts.map +1 -1
- package/dist/lib/quota-runtime-context.js +5 -0
- package/dist/lib/quota-runtime-context.js.map +1 -1
- package/dist/lib/quota-state.d.ts.map +1 -1
- package/dist/lib/quota-state.js +25 -1
- package/dist/lib/quota-state.js.map +1 -1
- package/dist/lib/quota-stats.d.ts +1 -0
- package/dist/lib/quota-stats.d.ts.map +1 -1
- package/dist/lib/quota-stats.js +14 -4
- package/dist/lib/quota-stats.js.map +1 -1
- package/dist/lib/quota-status.d.ts.map +1 -1
- package/dist/lib/quota-status.js +96 -1
- package/dist/lib/quota-status.js.map +1 -1
- package/dist/lib/runtime-provider-ids.d.ts +4 -0
- package/dist/lib/runtime-provider-ids.d.ts.map +1 -0
- package/dist/lib/runtime-provider-ids.js +10 -0
- package/dist/lib/runtime-provider-ids.js.map +1 -0
- package/dist/lib/session-tokens.d.ts +2 -0
- package/dist/lib/session-tokens.d.ts.map +1 -1
- package/dist/lib/session-tokens.js +4 -2
- package/dist/lib/session-tokens.js.map +1 -1
- package/dist/lib/synthetic.d.ts.map +1 -1
- package/dist/lib/synthetic.js +37 -32
- package/dist/lib/synthetic.js.map +1 -1
- package/dist/lib/toast-format-grouped.d.ts +1 -0
- package/dist/lib/toast-format-grouped.d.ts.map +1 -1
- package/dist/lib/toast-format-grouped.js +33 -29
- package/dist/lib/toast-format-grouped.js.map +1 -1
- package/dist/lib/tui-refresh-lifecycle.d.ts +17 -0
- package/dist/lib/tui-refresh-lifecycle.d.ts.map +1 -0
- package/dist/lib/tui-refresh-lifecycle.js +83 -0
- package/dist/lib/tui-refresh-lifecycle.js.map +1 -0
- package/dist/lib/tui-runtime.d.ts +0 -5
- package/dist/lib/tui-runtime.d.ts.map +1 -1
- package/dist/lib/tui-runtime.js +1 -6
- package/dist/lib/tui-runtime.js.map +1 -1
- package/dist/lib/tui-sidebar-format.d.ts +1 -1
- package/dist/lib/tui-sidebar-format.d.ts.map +1 -1
- package/dist/lib/tui-sidebar-format.js +1 -0
- package/dist/lib/tui-sidebar-format.js.map +1 -1
- package/dist/lib/types.d.ts +61 -7
- package/dist/lib/types.d.ts.map +1 -1
- package/dist/lib/types.js +2 -0
- package/dist/lib/types.js.map +1 -1
- package/dist/lib/xai.d.ts +39 -0
- package/dist/lib/xai.d.ts.map +1 -0
- package/dist/lib/xai.js +153 -0
- package/dist/lib/xai.js.map +1 -0
- package/dist/lib/zai.d.ts.map +1 -1
- package/dist/lib/zai.js +62 -56
- package/dist/lib/zai.js.map +1 -1
- package/dist/lib/zhipu.d.ts.map +1 -1
- package/dist/lib/zhipu.js +55 -50
- package/dist/lib/zhipu.js.map +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +20 -145
- package/dist/plugin.js.map +1 -1
- package/dist/providers/copilot.d.ts.map +1 -1
- package/dist/providers/copilot.js +24 -3
- package/dist/providers/copilot.js.map +1 -1
- package/dist/providers/glm-coding-plan-provider.d.ts +35 -0
- package/dist/providers/glm-coding-plan-provider.d.ts.map +1 -0
- package/dist/providers/glm-coding-plan-provider.js +40 -0
- package/dist/providers/glm-coding-plan-provider.js.map +1 -0
- package/dist/providers/google-agy.d.ts.map +1 -1
- package/dist/providers/google-agy.js +60 -36
- package/dist/providers/google-agy.js.map +1 -1
- package/dist/providers/mimo.d.ts +3 -0
- package/dist/providers/mimo.d.ts.map +1 -0
- package/dist/providers/mimo.js +120 -0
- package/dist/providers/mimo.js.map +1 -0
- package/dist/providers/minimax-coding-plan.d.ts.map +1 -1
- package/dist/providers/minimax-coding-plan.js +31 -26
- package/dist/providers/minimax-coding-plan.js.map +1 -1
- package/dist/providers/opencode-zen.d.ts +3 -0
- package/dist/providers/opencode-zen.d.ts.map +1 -0
- package/dist/providers/opencode-zen.js +94 -0
- package/dist/providers/opencode-zen.js.map +1 -0
- package/dist/providers/quota-providers.d.ts.map +1 -1
- package/dist/providers/quota-providers.js +1 -2
- package/dist/providers/quota-providers.js.map +1 -1
- package/dist/providers/registry.d.ts.map +1 -1
- package/dist/providers/registry.js +6 -0
- package/dist/providers/registry.js.map +1 -1
- package/dist/providers/xai.d.ts +6 -0
- package/dist/providers/xai.d.ts.map +1 -0
- package/dist/providers/xai.js +51 -0
- package/dist/providers/xai.js.map +1 -0
- package/dist/providers/zai.d.ts +1 -7
- package/dist/providers/zai.d.ts.map +1 -1
- package/dist/providers/zai.js +9 -45
- package/dist/providers/zai.js.map +1 -1
- package/dist/providers/zhipu.d.ts +1 -7
- package/dist/providers/zhipu.d.ts.map +1 -1
- package/dist/providers/zhipu.js +9 -45
- package/dist/providers/zhipu.js.map +1 -1
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +47 -136
- package/dist/tui.tsx +62 -179
- package/package.json +1 -1
package/dist/lib/copilot.js
CHANGED
|
@@ -15,6 +15,7 @@ const GITHUB_API_BASE_URL = "https://api.github.com";
|
|
|
15
15
|
const GITHUB_API_VERSION = "2026-03-10";
|
|
16
16
|
const COPILOT_QUOTA_CONFIG_FILENAME = "copilot-quota-token.json";
|
|
17
17
|
const USER_AGENT = "opencode-quota/copilot-billing";
|
|
18
|
+
const COPILOT_INTERNAL_USER_PATH = "/copilot_internal/user";
|
|
18
19
|
const PERSONAL_AI_CREDIT_TOTALS = {
|
|
19
20
|
pro: 1500,
|
|
20
21
|
"pro+": 7000,
|
|
@@ -36,6 +37,61 @@ function dedupeStrings(values) {
|
|
|
36
37
|
}
|
|
37
38
|
return out;
|
|
38
39
|
}
|
|
40
|
+
function validateEnterpriseHost(value) {
|
|
41
|
+
if (value === undefined || value === null)
|
|
42
|
+
return {};
|
|
43
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
44
|
+
return { error: "enterpriseUrl must be a non-empty string when provided" };
|
|
45
|
+
}
|
|
46
|
+
const raw = value.trim();
|
|
47
|
+
const invalid = {
|
|
48
|
+
error: "enterpriseUrl must be a hostname or host-only HTTPS URL ending in .ghe.com (without api. prefix, port, path, query, fragment, or userinfo)",
|
|
49
|
+
};
|
|
50
|
+
if (!/^[\x21-\x7e]+$/.test(raw) || raw.includes("*"))
|
|
51
|
+
return invalid;
|
|
52
|
+
let host;
|
|
53
|
+
if (raw.includes("://")) {
|
|
54
|
+
const authority = raw.slice(raw.indexOf("//") + 2).split(/[/?#]/u, 1)[0] ?? "";
|
|
55
|
+
if (authority.includes(":"))
|
|
56
|
+
return invalid;
|
|
57
|
+
let url;
|
|
58
|
+
try {
|
|
59
|
+
url = new URL(raw);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return invalid;
|
|
63
|
+
}
|
|
64
|
+
if (url.protocol !== "https:" ||
|
|
65
|
+
url.username ||
|
|
66
|
+
url.password ||
|
|
67
|
+
url.port ||
|
|
68
|
+
(url.pathname !== "" && url.pathname !== "/") ||
|
|
69
|
+
url.search ||
|
|
70
|
+
url.hash) {
|
|
71
|
+
return invalid;
|
|
72
|
+
}
|
|
73
|
+
host = url.hostname.toLowerCase();
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
if (/[\/:@?#]/.test(raw))
|
|
77
|
+
return invalid;
|
|
78
|
+
host = raw.toLowerCase();
|
|
79
|
+
}
|
|
80
|
+
if (host.length > 253 || host.startsWith("api.") || !host.endsWith(".ghe.com")) {
|
|
81
|
+
return invalid;
|
|
82
|
+
}
|
|
83
|
+
const labels = host.split(".");
|
|
84
|
+
if (labels.some((label) => label.length === 0 || label.length > 63 || !/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(label))) {
|
|
85
|
+
return invalid;
|
|
86
|
+
}
|
|
87
|
+
return { host };
|
|
88
|
+
}
|
|
89
|
+
function getApiBaseUrl(enterpriseHost) {
|
|
90
|
+
return enterpriseHost ? `https://api.${enterpriseHost}` : GITHUB_API_BASE_URL;
|
|
91
|
+
}
|
|
92
|
+
function getApiHost(apiBaseUrl) {
|
|
93
|
+
return new URL(apiBaseUrl).hostname;
|
|
94
|
+
}
|
|
39
95
|
function classifyPatTokenKind(token) {
|
|
40
96
|
if (token.startsWith("github_pat_"))
|
|
41
97
|
return "github_pat";
|
|
@@ -203,6 +259,9 @@ function validateQuotaConfig(raw) {
|
|
|
203
259
|
return value.trim();
|
|
204
260
|
};
|
|
205
261
|
try {
|
|
262
|
+
const enterpriseHost = validateEnterpriseHost(obj.enterpriseUrl);
|
|
263
|
+
if (enterpriseHost.error)
|
|
264
|
+
throw new Error(enterpriseHost.error);
|
|
206
265
|
const config = {
|
|
207
266
|
token,
|
|
208
267
|
tier: tier,
|
|
@@ -210,6 +269,7 @@ function validateQuotaConfig(raw) {
|
|
|
210
269
|
username: readOptionalString("username"),
|
|
211
270
|
organization: readOptionalString("organization"),
|
|
212
271
|
enterprise: readOptionalString("enterprise"),
|
|
272
|
+
enterpriseUrl: enterpriseHost.host,
|
|
213
273
|
};
|
|
214
274
|
const resolved = resolvePatBillingTarget(config);
|
|
215
275
|
if (!resolved.target) {
|
|
@@ -256,6 +316,9 @@ export function readQuotaConfigWithMeta() {
|
|
|
256
316
|
}
|
|
257
317
|
return { state: "absent", checkedPaths };
|
|
258
318
|
}
|
|
319
|
+
function getCopilotOAuthToken(auth) {
|
|
320
|
+
return auth?.access?.trim() || auth?.refresh?.trim() || null;
|
|
321
|
+
}
|
|
259
322
|
function selectCopilotAuth(authData) {
|
|
260
323
|
if (!authData)
|
|
261
324
|
return { auth: null, keyName: null };
|
|
@@ -268,7 +331,7 @@ function selectCopilotAuth(authData) {
|
|
|
268
331
|
for (const [keyName, auth] of candidates) {
|
|
269
332
|
if (!auth || auth.type !== "oauth")
|
|
270
333
|
continue;
|
|
271
|
-
if (!auth
|
|
334
|
+
if (!getCopilotOAuthToken(auth))
|
|
272
335
|
continue;
|
|
273
336
|
return { auth, keyName };
|
|
274
337
|
}
|
|
@@ -289,6 +352,7 @@ function getRemainingTotalsState(target, config) {
|
|
|
289
352
|
export function getCopilotQuotaAuthDiagnostics(authData) {
|
|
290
353
|
const pat = readQuotaConfigWithMeta();
|
|
291
354
|
const { auth, keyName } = selectCopilotAuth(authData);
|
|
355
|
+
const oauthEnterpriseHost = validateEnterpriseHost(auth?.enterpriseUrl);
|
|
292
356
|
const resolved = pat.state === "valid" && pat.config
|
|
293
357
|
? resolvePatBillingTarget(pat.config)
|
|
294
358
|
: { target: null };
|
|
@@ -301,46 +365,88 @@ export function getCopilotQuotaAuthDiagnostics(authData) {
|
|
|
301
365
|
: auth
|
|
302
366
|
? "oauth"
|
|
303
367
|
: "none";
|
|
304
|
-
const
|
|
305
|
-
|
|
368
|
+
const effectiveEnterpriseHost = effectiveSource === "pat"
|
|
369
|
+
? pat.config?.enterpriseUrl
|
|
370
|
+
: effectiveSource === "oauth"
|
|
371
|
+
? oauthEnterpriseHost.host
|
|
372
|
+
: undefined;
|
|
373
|
+
const enterpriseHostError = effectiveSource === "pat" && pat.error?.startsWith("enterpriseUrl")
|
|
374
|
+
? pat.error
|
|
375
|
+
: effectiveSource === "oauth"
|
|
376
|
+
? oauthEnterpriseHost.error
|
|
377
|
+
: undefined;
|
|
378
|
+
const deployment = effectiveSource === "none"
|
|
306
379
|
? "none"
|
|
307
|
-
:
|
|
380
|
+
: enterpriseHostError
|
|
381
|
+
? "invalid"
|
|
382
|
+
: effectiveEnterpriseHost
|
|
383
|
+
? "ghe.com"
|
|
384
|
+
: effectiveSource === "pat" && pat.state !== "valid"
|
|
385
|
+
? "none"
|
|
386
|
+
: "github.com";
|
|
387
|
+
const apiBaseUrl = deployment === "github.com" || deployment === "ghe.com"
|
|
388
|
+
? getApiBaseUrl(effectiveEnterpriseHost)
|
|
389
|
+
: null;
|
|
390
|
+
const billingModel = pat.config?.billingModel ?? "ai_credits";
|
|
391
|
+
const oauthAvailable = effectiveSource === "oauth" && Boolean(auth) && !oauthEnterpriseHost.error;
|
|
392
|
+
const quotaApi = pat.state === "valid"
|
|
393
|
+
? billingModel === "legacy_premium_requests"
|
|
308
394
|
? "github_legacy_premium_request_api"
|
|
309
|
-
: "github_ai_credit_api"
|
|
395
|
+
: "github_ai_credit_api"
|
|
396
|
+
: oauthAvailable
|
|
397
|
+
? "copilot_internal_user"
|
|
398
|
+
: "none";
|
|
399
|
+
const billingMode = resolved.target?.scope === "organization"
|
|
400
|
+
? "organization_usage"
|
|
401
|
+
: resolved.target?.scope === "enterprise"
|
|
402
|
+
? "enterprise_usage"
|
|
403
|
+
: resolved.target?.scope === "user" || oauthAvailable
|
|
404
|
+
? "user_quota"
|
|
405
|
+
: "none";
|
|
310
406
|
return {
|
|
311
407
|
pat,
|
|
312
408
|
oauth: {
|
|
313
409
|
configured: Boolean(auth),
|
|
314
410
|
keyName,
|
|
315
|
-
hasRefreshToken: Boolean(auth?.refresh),
|
|
411
|
+
hasRefreshToken: Boolean(auth?.refresh?.trim()),
|
|
316
412
|
hasAccessToken: Boolean(auth?.access?.trim()),
|
|
413
|
+
hasEnterpriseUrl: auth?.enterpriseUrl !== undefined,
|
|
317
414
|
},
|
|
415
|
+
deployment,
|
|
416
|
+
apiHost: apiBaseUrl ? getApiHost(apiBaseUrl) : null,
|
|
417
|
+
enterpriseHostSource: effectiveSource === "pat" && pat.config?.enterpriseUrl
|
|
418
|
+
? "pat"
|
|
419
|
+
: effectiveSource === "oauth" && oauthEnterpriseHost.host
|
|
420
|
+
? "oauth"
|
|
421
|
+
: "none",
|
|
422
|
+
enterpriseHostError,
|
|
318
423
|
effectiveSource,
|
|
319
424
|
override: patBlocksOAuth && auth ? "pat_overrides_oauth" : "none",
|
|
320
425
|
quotaApi,
|
|
321
|
-
billingMode
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
billingScope: resolved.target?.scope ?? "none",
|
|
329
|
-
billingApiAccessLikely: pat.state === "valid" && Boolean(resolved.target) && !resolved.error && !compatibilityError,
|
|
330
|
-
remainingTotalsState: getRemainingTotalsState(resolved.target, pat.config),
|
|
426
|
+
billingMode,
|
|
427
|
+
billingScope: resolved.target?.scope ?? (oauthAvailable ? "user" : "none"),
|
|
428
|
+
billingApiAccessLikely: oauthAvailable ||
|
|
429
|
+
(pat.state === "valid" && Boolean(resolved.target) && !resolved.error && !compatibilityError),
|
|
430
|
+
remainingTotalsState: oauthAvailable
|
|
431
|
+
? "reported_by_copilot_internal_user"
|
|
432
|
+
: getRemainingTotalsState(resolved.target, pat.config),
|
|
331
433
|
queryPeriod: resolved.target?.billingPeriod,
|
|
332
434
|
usernameFilter: pat.config?.username,
|
|
333
435
|
billingTargetError: resolved.error,
|
|
334
436
|
tokenCompatibilityError: compatibilityError ?? undefined,
|
|
335
437
|
billingModel,
|
|
336
|
-
budgetApi: billingModel !== "ai_credits"
|
|
438
|
+
budgetApi: billingModel !== "ai_credits" || oauthAvailable
|
|
337
439
|
? "not_available"
|
|
338
440
|
: resolved.target?.scope === "organization"
|
|
339
441
|
? "organization_budgets"
|
|
340
442
|
: resolved.target?.scope === "enterprise"
|
|
341
443
|
? "enterprise_budgets"
|
|
342
444
|
: "not_available",
|
|
343
|
-
oauthAccountingState: auth
|
|
445
|
+
oauthAccountingState: !auth
|
|
446
|
+
? "not_configured"
|
|
447
|
+
: oauthEnterpriseHost.error
|
|
448
|
+
? "invalid_enterprise_host"
|
|
449
|
+
: "available_via_copilot_internal_user",
|
|
344
450
|
};
|
|
345
451
|
}
|
|
346
452
|
function buildGitHubRestHeaders(token) {
|
|
@@ -351,41 +457,59 @@ function buildGitHubRestHeaders(token) {
|
|
|
351
457
|
"User-Agent": USER_AGENT,
|
|
352
458
|
};
|
|
353
459
|
}
|
|
354
|
-
|
|
460
|
+
function redactGitHubErrorText(text, secrets) {
|
|
461
|
+
let sanitized = sanitizeDisplayText(text);
|
|
462
|
+
for (const secret of secrets) {
|
|
463
|
+
if (!secret)
|
|
464
|
+
continue;
|
|
465
|
+
sanitized = sanitized.split(secret).join("[redacted]");
|
|
466
|
+
const sanitizedSecret = sanitizeDisplayText(secret);
|
|
467
|
+
if (sanitizedSecret) {
|
|
468
|
+
sanitized = sanitized.split(sanitizedSecret).join("[redacted]");
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
return sanitized;
|
|
472
|
+
}
|
|
473
|
+
async function readGitHubRestErrorMessage(response, secrets) {
|
|
355
474
|
const text = await response.text();
|
|
356
475
|
try {
|
|
357
476
|
const parsed = JSON.parse(text);
|
|
358
477
|
const message = typeof parsed.message === "string" ? parsed.message : null;
|
|
359
478
|
const documentationUrl = typeof parsed.documentation_url === "string" ? parsed.documentation_url : null;
|
|
360
479
|
if (message && documentationUrl) {
|
|
361
|
-
return
|
|
480
|
+
return redactGitHubErrorText(`${message} (${documentationUrl})`, secrets);
|
|
362
481
|
}
|
|
363
482
|
if (message)
|
|
364
|
-
return
|
|
483
|
+
return redactGitHubErrorText(message, secrets);
|
|
365
484
|
}
|
|
366
485
|
catch {
|
|
367
486
|
// Fall through to a bounded plain-text snippet.
|
|
368
487
|
}
|
|
369
|
-
return sanitizeDisplaySnippet(text, 160);
|
|
488
|
+
return sanitizeDisplaySnippet(redactGitHubErrorText(text, secrets), 160);
|
|
370
489
|
}
|
|
371
490
|
async function fetchGitHubRestJson(url, token, requestTimeoutMs) {
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
491
|
+
return await fetchWithTimeout(url, {
|
|
492
|
+
request: { headers: buildGitHubRestHeaders(token) },
|
|
493
|
+
timeoutMs: requestTimeoutMs,
|
|
494
|
+
consume: async (response) => {
|
|
495
|
+
if (!response.ok) {
|
|
496
|
+
const message = await readGitHubRestErrorMessage(response, [token]);
|
|
497
|
+
const rateLimit = response.status === 403 && response.headers.get("x-ratelimit-remaining") === "0"
|
|
498
|
+
? " (GitHub API rate limit exhausted)"
|
|
499
|
+
: "";
|
|
500
|
+
throw new Error(`GitHub API error ${response.status}: ${message}${rateLimit}`);
|
|
501
|
+
}
|
|
502
|
+
try {
|
|
503
|
+
return (await response.json());
|
|
504
|
+
}
|
|
505
|
+
catch {
|
|
506
|
+
throw new Error("GitHub API returned malformed JSON");
|
|
507
|
+
}
|
|
508
|
+
},
|
|
509
|
+
});
|
|
386
510
|
}
|
|
387
|
-
async function resolveGitHubUsername(token, requestTimeoutMs) {
|
|
388
|
-
const response = await fetchGitHubRestJson(`${
|
|
511
|
+
async function resolveGitHubUsername(apiBaseUrl, token, requestTimeoutMs) {
|
|
512
|
+
const response = await fetchGitHubRestJson(`${apiBaseUrl}/user`, token, requestTimeoutMs);
|
|
389
513
|
const login = response.login?.trim();
|
|
390
514
|
if (!login) {
|
|
391
515
|
throw new Error("GitHub /user response did not include a login");
|
|
@@ -404,27 +528,27 @@ function buildBillingQuery(target) {
|
|
|
404
528
|
}
|
|
405
529
|
return query;
|
|
406
530
|
}
|
|
407
|
-
function getBillingUsageUrl(target, billingModel) {
|
|
531
|
+
function getBillingUsageUrl(apiBaseUrl, target, billingModel) {
|
|
408
532
|
const report = billingModel === "legacy_premium_requests" ? "premium_request" : "ai_credit";
|
|
409
533
|
const query = buildBillingQuery(target);
|
|
410
534
|
if (target.scope === "enterprise") {
|
|
411
|
-
return `${
|
|
535
|
+
return `${apiBaseUrl}/enterprises/${encodeURIComponent(target.enterprise)}/settings/billing/${report}/usage?${query}`;
|
|
412
536
|
}
|
|
413
537
|
if (target.scope === "organization") {
|
|
414
|
-
return `${
|
|
538
|
+
return `${apiBaseUrl}/organizations/${encodeURIComponent(target.organization)}/settings/billing/${report}/usage?${query}`;
|
|
415
539
|
}
|
|
416
|
-
return `${
|
|
540
|
+
return `${apiBaseUrl}/users/${encodeURIComponent(target.username)}/settings/billing/${report}/usage?${query}`;
|
|
417
541
|
}
|
|
418
542
|
async function fetchBillingUsage(params) {
|
|
419
543
|
const target = params.target.scope === "user"
|
|
420
544
|
? {
|
|
421
545
|
...params.target,
|
|
422
546
|
username: params.target.username ??
|
|
423
|
-
(await resolveGitHubUsername(params.token, params.requestTimeoutMs)),
|
|
547
|
+
(await resolveGitHubUsername(params.apiBaseUrl, params.token, params.requestTimeoutMs)),
|
|
424
548
|
}
|
|
425
549
|
: params.target;
|
|
426
550
|
return {
|
|
427
|
-
response: await fetchGitHubRestJson(getBillingUsageUrl(target, params.billingModel), params.token, params.requestTimeoutMs),
|
|
551
|
+
response: await fetchGitHubRestJson(getBillingUsageUrl(params.apiBaseUrl, target, params.billingModel), params.token, params.requestTimeoutMs),
|
|
428
552
|
target,
|
|
429
553
|
};
|
|
430
554
|
}
|
|
@@ -562,10 +686,10 @@ function budgetSpecificity(budget) {
|
|
|
562
686
|
return 0;
|
|
563
687
|
}
|
|
564
688
|
}
|
|
565
|
-
function getBudgetsUrl(target, page) {
|
|
689
|
+
function getBudgetsUrl(apiBaseUrl, target, page) {
|
|
566
690
|
const base = target.scope === "organization"
|
|
567
|
-
? `${
|
|
568
|
-
: `${
|
|
691
|
+
? `${apiBaseUrl}/organizations/${encodeURIComponent(target.organization)}/settings/billing/budgets`
|
|
692
|
+
: `${apiBaseUrl}/enterprises/${encodeURIComponent(target.enterprise)}/settings/billing/budgets`;
|
|
569
693
|
const query = new URLSearchParams({ page: String(page), per_page: "100" });
|
|
570
694
|
if (target.username)
|
|
571
695
|
query.set("user", target.username);
|
|
@@ -574,7 +698,7 @@ function getBudgetsUrl(target, page) {
|
|
|
574
698
|
async function fetchApplicableBudget(params) {
|
|
575
699
|
const budgets = [];
|
|
576
700
|
for (let page = 1; page <= 100; page += 1) {
|
|
577
|
-
const response = await fetchGitHubRestJson(getBudgetsUrl(params.target, page), params.token, params.requestTimeoutMs);
|
|
701
|
+
const response = await fetchGitHubRestJson(getBudgetsUrl(params.apiBaseUrl, params.target, page), params.token, params.requestTimeoutMs);
|
|
578
702
|
if (!Array.isArray(response.budgets)) {
|
|
579
703
|
throw new Error("GitHub budgets response did not include a budgets array");
|
|
580
704
|
}
|
|
@@ -617,6 +741,7 @@ async function toAiCreditResult(params) {
|
|
|
617
741
|
if (params.target.scope !== "user") {
|
|
618
742
|
try {
|
|
619
743
|
budget = await fetchApplicableBudget({
|
|
744
|
+
apiBaseUrl: params.apiBaseUrl,
|
|
620
745
|
token: params.token,
|
|
621
746
|
target: params.target,
|
|
622
747
|
spentUsd: totals.billedAmountUsd,
|
|
@@ -685,12 +810,112 @@ function toLegacyResult(response, config) {
|
|
|
685
810
|
function toQuotaError(message) {
|
|
686
811
|
return { success: false, error: message };
|
|
687
812
|
}
|
|
813
|
+
function buildCopilotInternalHeaders(token) {
|
|
814
|
+
return {
|
|
815
|
+
Accept: "application/json",
|
|
816
|
+
Authorization: `token ${token}`,
|
|
817
|
+
"Editor-Version": "vscode/1.96.2",
|
|
818
|
+
"Editor-Plugin-Version": "copilot-chat/0.26.7",
|
|
819
|
+
"User-Agent": "GitHubCopilotChat/0.26.7",
|
|
820
|
+
"X-GitHub-Api-Version": "2025-04-01",
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
function normalizeCopilotPlan(value) {
|
|
824
|
+
if (typeof value !== "string")
|
|
825
|
+
return undefined;
|
|
826
|
+
const plan = sanitizeDisplayText(value).replace(/\s+/gu, " ").trim().slice(0, 64);
|
|
827
|
+
return plan || undefined;
|
|
828
|
+
}
|
|
829
|
+
function normalizeResetTime(value) {
|
|
830
|
+
if (typeof value !== "string" || !value.trim())
|
|
831
|
+
return undefined;
|
|
832
|
+
const timestamp = Date.parse(value);
|
|
833
|
+
return Number.isFinite(timestamp) ? new Date(timestamp).toISOString() : undefined;
|
|
834
|
+
}
|
|
835
|
+
function parseCopilotInternalUser(response) {
|
|
836
|
+
const plan = normalizeCopilotPlan(response.copilot_plan);
|
|
837
|
+
const resetTimeIso = normalizeResetTime(response.quota_reset_date_utc ?? response.quota_reset_date);
|
|
838
|
+
const snapshot = response.quota_snapshots?.premium_interactions;
|
|
839
|
+
const entitlement = readFiniteNumber(snapshot?.entitlement);
|
|
840
|
+
const remaining = readFiniteNumber(snapshot?.quota_remaining ?? snapshot?.remaining);
|
|
841
|
+
const reportedPercentRemaining = readFiniteNumber(snapshot?.percent_remaining);
|
|
842
|
+
const hasPercentRemaining = snapshot?.percent_remaining !== undefined;
|
|
843
|
+
const hasReportedPercent = reportedPercentRemaining !== undefined &&
|
|
844
|
+
reportedPercentRemaining >= 0 &&
|
|
845
|
+
reportedPercentRemaining <= 100;
|
|
846
|
+
const isPlaceholder = response.token_based_billing === true &&
|
|
847
|
+
(!snapshot ||
|
|
848
|
+
(snapshot.unlimited !== true &&
|
|
849
|
+
entitlement === 0 &&
|
|
850
|
+
remaining === 0 &&
|
|
851
|
+
(!hasReportedPercent || reportedPercentRemaining === 0)));
|
|
852
|
+
if (isPlaceholder) {
|
|
853
|
+
return { success: true, mode: "user_plan", plan, resetTimeIso };
|
|
854
|
+
}
|
|
855
|
+
if (snapshot?.unlimited === true) {
|
|
856
|
+
return {
|
|
857
|
+
success: true,
|
|
858
|
+
mode: "user_quota",
|
|
859
|
+
unit: "ai_credits",
|
|
860
|
+
used: 0,
|
|
861
|
+
unlimited: true,
|
|
862
|
+
plan,
|
|
863
|
+
resetTimeIso,
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
if (entitlement !== undefined &&
|
|
867
|
+
entitlement > 0 &&
|
|
868
|
+
remaining !== undefined &&
|
|
869
|
+
remaining >= 0 &&
|
|
870
|
+
remaining <= entitlement) {
|
|
871
|
+
return {
|
|
872
|
+
success: true,
|
|
873
|
+
mode: "user_quota",
|
|
874
|
+
unit: "ai_credits",
|
|
875
|
+
used: entitlement - remaining,
|
|
876
|
+
total: entitlement,
|
|
877
|
+
percentRemaining: hasReportedPercent
|
|
878
|
+
? reportedPercentRemaining
|
|
879
|
+
: !hasPercentRemaining
|
|
880
|
+
? Math.min(100, Math.max(0, (remaining / entitlement) * 100))
|
|
881
|
+
: undefined,
|
|
882
|
+
plan,
|
|
883
|
+
resetTimeIso,
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
if (response.token_based_billing === true) {
|
|
887
|
+
return { success: true, mode: "user_plan", plan, resetTimeIso };
|
|
888
|
+
}
|
|
889
|
+
throw new Error("GitHub Copilot user response did not include a usable quota snapshot");
|
|
890
|
+
}
|
|
891
|
+
async function fetchCopilotInternalUser(params) {
|
|
892
|
+
return await fetchWithTimeout(`${params.apiBaseUrl}${COPILOT_INTERNAL_USER_PATH}`, {
|
|
893
|
+
request: { headers: buildCopilotInternalHeaders(params.token) },
|
|
894
|
+
timeoutMs: params.requestTimeoutMs,
|
|
895
|
+
consume: async (response) => {
|
|
896
|
+
if (!response.ok) {
|
|
897
|
+
const message = await readGitHubRestErrorMessage(response, [params.token]);
|
|
898
|
+
throw new Error(`GitHub Copilot API error ${response.status}: ${message}`);
|
|
899
|
+
}
|
|
900
|
+
let parsed;
|
|
901
|
+
try {
|
|
902
|
+
parsed = (await response.json());
|
|
903
|
+
}
|
|
904
|
+
catch {
|
|
905
|
+
throw new Error("GitHub Copilot API returned malformed JSON");
|
|
906
|
+
}
|
|
907
|
+
if (!parsed || typeof parsed !== "object") {
|
|
908
|
+
throw new Error("GitHub Copilot API returned malformed JSON");
|
|
909
|
+
}
|
|
910
|
+
return parseCopilotInternalUser(parsed);
|
|
911
|
+
},
|
|
912
|
+
});
|
|
913
|
+
}
|
|
688
914
|
/**
|
|
689
915
|
* Query GitHub Copilot accounting.
|
|
690
916
|
*
|
|
691
|
-
* A
|
|
692
|
-
*
|
|
693
|
-
* because its billing permissions are not part of the documented contract.
|
|
917
|
+
* A trusted local PAT config remains authoritative. When it is absent, the
|
|
918
|
+
* OpenCode-managed Copilot OAuth token can query the per-user internal quota endpoint.
|
|
694
919
|
*/
|
|
695
920
|
export async function queryCopilotQuota(options = {}) {
|
|
696
921
|
const pat = readQuotaConfigWithMeta();
|
|
@@ -698,7 +923,24 @@ export async function queryCopilotQuota(options = {}) {
|
|
|
698
923
|
return toQuotaError(`Invalid copilot-quota-token.json: ${pat.error ?? "unknown error"}${pat.selectedPath ? ` (${pat.selectedPath})` : ""}`);
|
|
699
924
|
}
|
|
700
925
|
if (pat.state === "absent" || !pat.config) {
|
|
701
|
-
|
|
926
|
+
const { auth } = selectCopilotAuth(await readAuthFile());
|
|
927
|
+
const token = getCopilotOAuthToken(auth);
|
|
928
|
+
if (!auth || !token)
|
|
929
|
+
return null;
|
|
930
|
+
const enterpriseHost = validateEnterpriseHost(auth.enterpriseUrl);
|
|
931
|
+
if (enterpriseHost.error) {
|
|
932
|
+
return toQuotaError(`Invalid OpenCode Copilot enterpriseUrl: ${enterpriseHost.error}`);
|
|
933
|
+
}
|
|
934
|
+
try {
|
|
935
|
+
return await fetchCopilotInternalUser({
|
|
936
|
+
apiBaseUrl: getApiBaseUrl(enterpriseHost.host),
|
|
937
|
+
token,
|
|
938
|
+
requestTimeoutMs: options.requestTimeoutMs,
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
catch (error) {
|
|
942
|
+
return toQuotaError(error instanceof Error ? error.message : String(error));
|
|
943
|
+
}
|
|
702
944
|
}
|
|
703
945
|
const resolved = resolvePatBillingTarget(pat.config);
|
|
704
946
|
if (!resolved.target) {
|
|
@@ -709,7 +951,9 @@ export async function queryCopilotQuota(options = {}) {
|
|
|
709
951
|
return toQuotaError(compatibilityError);
|
|
710
952
|
try {
|
|
711
953
|
const billingModel = pat.config.billingModel ?? "ai_credits";
|
|
954
|
+
const apiBaseUrl = getApiBaseUrl(pat.config.enterpriseUrl);
|
|
712
955
|
const { response, target } = await fetchBillingUsage({
|
|
956
|
+
apiBaseUrl,
|
|
713
957
|
token: pat.config.token,
|
|
714
958
|
target: resolved.target,
|
|
715
959
|
billingModel,
|
|
@@ -718,6 +962,7 @@ export async function queryCopilotQuota(options = {}) {
|
|
|
718
962
|
return billingModel === "legacy_premium_requests"
|
|
719
963
|
? toLegacyResult(response, pat.config)
|
|
720
964
|
: await toAiCreditResult({
|
|
965
|
+
apiBaseUrl,
|
|
721
966
|
response,
|
|
722
967
|
target,
|
|
723
968
|
config: pat.config,
|
|
@@ -736,6 +981,9 @@ export async function hasCopilotQuotaRuntimeAvailable() {
|
|
|
736
981
|
export function formatCopilotQuota(result) {
|
|
737
982
|
if (!result || !result.success)
|
|
738
983
|
return null;
|
|
984
|
+
if (result.mode === "user_plan") {
|
|
985
|
+
return result.plan ? `Copilot Plan ${result.plan}` : "Copilot Plan available";
|
|
986
|
+
}
|
|
739
987
|
const unit = result.unit === "ai_credits" ? "AI Credits" : "Premium Requests";
|
|
740
988
|
if (result.mode === "organization_usage") {
|
|
741
989
|
return `Copilot Org (${result.organization}) ${result.used} ${unit} | ${formatBillingPeriod(result.period)}`;
|