@slkiser/opencode-quota 3.11.1 → 4.0.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 +103 -57
- package/dist/bin/opencode-quota.d.ts.map +1 -1
- package/dist/bin/opencode-quota.js +24 -7
- package/dist/bin/opencode-quota.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/anthropic.d.ts.map +1 -1
- package/dist/lib/anthropic.js +106 -3
- package/dist/lib/anthropic.js.map +1 -1
- package/dist/lib/atomic-json.d.ts +1 -0
- package/dist/lib/atomic-json.d.ts.map +1 -1
- package/dist/lib/atomic-json.js +5 -2
- package/dist/lib/atomic-json.js.map +1 -1
- package/dist/lib/cli-show.d.ts.map +1 -1
- package/dist/lib/cli-show.js +11 -7
- package/dist/lib/cli-show.js.map +1 -1
- package/dist/lib/command-surfaces.d.ts +10 -0
- package/dist/lib/command-surfaces.d.ts.map +1 -0
- package/dist/lib/command-surfaces.js +9 -0
- package/dist/lib/command-surfaces.js.map +1 -0
- package/dist/lib/config-file-utils.d.ts +5 -0
- package/dist/lib/config-file-utils.d.ts.map +1 -1
- package/dist/lib/config-file-utils.js +20 -3
- package/dist/lib/config-file-utils.js.map +1 -1
- package/dist/lib/config.d.ts +5 -3
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +124 -32
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/copilot.d.ts +19 -12
- package/dist/lib/copilot.d.ts.map +1 -1
- package/dist/lib/copilot.js +462 -566
- package/dist/lib/copilot.js.map +1 -1
- package/dist/lib/display-sanitize.d.ts.map +1 -1
- package/dist/lib/display-sanitize.js +13 -0
- package/dist/lib/display-sanitize.js.map +1 -1
- package/dist/lib/entries.d.ts +53 -4
- package/dist/lib/entries.d.ts.map +1 -1
- package/dist/lib/entries.js.map +1 -1
- package/dist/lib/init-installer.d.ts +11 -1
- package/dist/lib/init-installer.d.ts.map +1 -1
- package/dist/lib/init-installer.js +542 -154
- package/dist/lib/init-installer.js.map +1 -1
- package/dist/lib/openai.d.ts +9 -12
- package/dist/lib/openai.d.ts.map +1 -1
- package/dist/lib/openai.js +69 -40
- package/dist/lib/openai.js.map +1 -1
- package/dist/lib/opencode-config-editor.d.ts +53 -0
- package/dist/lib/opencode-config-editor.d.ts.map +1 -0
- package/dist/lib/opencode-config-editor.js +274 -0
- package/dist/lib/opencode-config-editor.js.map +1 -0
- package/dist/lib/opencode-config-providers.d.ts +18 -0
- package/dist/lib/opencode-config-providers.d.ts.map +1 -1
- package/dist/lib/opencode-config-providers.js +75 -4
- package/dist/lib/opencode-config-providers.js.map +1 -1
- package/dist/lib/opencode-runtime-paths.d.ts.map +1 -1
- package/dist/lib/opencode-runtime-paths.js +10 -3
- package/dist/lib/opencode-runtime-paths.js.map +1 -1
- package/dist/lib/opencode-storage.d.ts +10 -0
- package/dist/lib/opencode-storage.d.ts.map +1 -1
- package/dist/lib/opencode-storage.js +75 -0
- package/dist/lib/opencode-storage.js.map +1 -1
- package/dist/lib/provider-add-command.d.ts +35 -0
- package/dist/lib/provider-add-command.d.ts.map +1 -0
- package/dist/lib/provider-add-command.js +185 -0
- package/dist/lib/provider-add-command.js.map +1 -0
- package/dist/lib/provider-add.d.ts +21 -0
- package/dist/lib/provider-add.d.ts.map +1 -0
- package/dist/lib/provider-add.js +210 -0
- package/dist/lib/provider-add.js.map +1 -0
- package/dist/lib/provider-metadata.d.ts +1 -1
- package/dist/lib/provider-metadata.d.ts.map +1 -1
- package/dist/lib/provider-metadata.js +11 -5
- package/dist/lib/provider-metadata.js.map +1 -1
- package/dist/lib/quota-command-format.d.ts +2 -0
- package/dist/lib/quota-command-format.d.ts.map +1 -1
- package/dist/lib/quota-command-format.js +94 -26
- package/dist/lib/quota-command-format.js.map +1 -1
- package/dist/lib/quota-dialog-commands.d.ts.map +1 -1
- package/dist/lib/quota-dialog-commands.js +8 -4
- package/dist/lib/quota-dialog-commands.js.map +1 -1
- package/dist/lib/quota-export-types.d.ts +48 -27
- package/dist/lib/quota-export-types.d.ts.map +1 -1
- package/dist/lib/quota-export-types.js +0 -7
- package/dist/lib/quota-export-types.js.map +1 -1
- package/dist/lib/quota-export.d.ts.map +1 -1
- package/dist/lib/quota-export.js +63 -16
- package/dist/lib/quota-export.js.map +1 -1
- package/dist/lib/quota-providers-local.d.ts +55 -0
- package/dist/lib/quota-providers-local.d.ts.map +1 -0
- package/dist/lib/quota-providers-local.js +313 -0
- package/dist/lib/quota-providers-local.js.map +1 -0
- package/dist/lib/quota-providers-remote.d.ts +23 -0
- package/dist/lib/quota-providers-remote.d.ts.map +1 -0
- package/dist/lib/quota-providers-remote.js +318 -0
- package/dist/lib/quota-providers-remote.js.map +1 -0
- package/dist/lib/quota-providers.d.ts +77 -0
- package/dist/lib/quota-providers.d.ts.map +1 -0
- package/dist/lib/quota-providers.js +630 -0
- package/dist/lib/quota-providers.js.map +1 -0
- package/dist/lib/quota-render-data.d.ts +1 -0
- package/dist/lib/quota-render-data.d.ts.map +1 -1
- package/dist/lib/quota-render-data.js +35 -16
- package/dist/lib/quota-render-data.js.map +1 -1
- package/dist/lib/quota-runtime-context.d.ts.map +1 -1
- package/dist/lib/quota-runtime-context.js +3 -4
- package/dist/lib/quota-runtime-context.js.map +1 -1
- package/dist/lib/quota-state.d.ts +5 -2
- package/dist/lib/quota-state.d.ts.map +1 -1
- package/dist/lib/quota-state.js +268 -51
- package/dist/lib/quota-state.js.map +1 -1
- package/dist/lib/quota-status.d.ts +2 -1
- package/dist/lib/quota-status.d.ts.map +1 -1
- package/dist/lib/quota-status.js +247 -69
- package/dist/lib/quota-status.js.map +1 -1
- package/dist/lib/qwen-local-quota.d.ts +12 -11
- package/dist/lib/qwen-local-quota.d.ts.map +1 -1
- package/dist/lib/qwen-local-quota.js +82 -87
- package/dist/lib/qwen-local-quota.js.map +1 -1
- package/dist/lib/scoped-update.d.ts +63 -0
- package/dist/lib/scoped-update.d.ts.map +1 -0
- package/dist/lib/scoped-update.js +311 -0
- package/dist/lib/scoped-update.js.map +1 -0
- package/dist/lib/tui-runtime.d.ts +2 -0
- package/dist/lib/tui-runtime.d.ts.map +1 -1
- package/dist/lib/tui-runtime.js +33 -9
- package/dist/lib/tui-runtime.js.map +1 -1
- package/dist/lib/types.d.ts +51 -15
- package/dist/lib/types.d.ts.map +1 -1
- package/dist/lib/types.js +2 -1
- package/dist/lib/types.js.map +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +75 -100
- package/dist/plugin.js.map +1 -1
- package/dist/providers/alibaba-coding-plan.d.ts.map +1 -1
- package/dist/providers/alibaba-coding-plan.js +35 -4
- package/dist/providers/alibaba-coding-plan.js.map +1 -1
- package/dist/providers/anthropic.d.ts.map +1 -1
- package/dist/providers/anthropic.js +13 -1
- package/dist/providers/anthropic.js.map +1 -1
- package/dist/providers/chutes.d.ts.map +1 -1
- package/dist/providers/chutes.js +6 -0
- package/dist/providers/chutes.js.map +1 -1
- package/dist/providers/copilot.d.ts +2 -1
- package/dist/providers/copilot.d.ts.map +1 -1
- package/dist/providers/copilot.js +143 -60
- package/dist/providers/copilot.js.map +1 -1
- package/dist/providers/cursor.d.ts.map +1 -1
- package/dist/providers/cursor.js +30 -1
- package/dist/providers/cursor.js.map +1 -1
- package/dist/providers/deepseek.d.ts.map +1 -1
- package/dist/providers/deepseek.js +13 -1
- package/dist/providers/deepseek.js.map +1 -1
- package/dist/providers/google-agy.d.ts.map +1 -1
- package/dist/providers/google-agy.js +7 -1
- package/dist/providers/google-agy.js.map +1 -1
- package/dist/providers/google-antigravity.d.ts.map +1 -1
- package/dist/providers/google-antigravity.js +10 -2
- package/dist/providers/google-antigravity.js.map +1 -1
- package/dist/providers/google-gemini-cli.d.ts.map +1 -1
- package/dist/providers/google-gemini-cli.js +8 -2
- package/dist/providers/google-gemini-cli.js.map +1 -1
- package/dist/providers/kimi-code.d.ts.map +1 -1
- package/dist/providers/kimi-code.js +6 -0
- package/dist/providers/kimi-code.js.map +1 -1
- package/dist/providers/minimax-coding-plan.d.ts.map +1 -1
- package/dist/providers/minimax-coding-plan.js +12 -4
- package/dist/providers/minimax-coding-plan.js.map +1 -1
- package/dist/providers/nanogpt.d.ts.map +1 -1
- package/dist/providers/nanogpt.js +19 -1
- package/dist/providers/nanogpt.js.map +1 -1
- package/dist/providers/ollama-cloud.d.ts.map +1 -1
- package/dist/providers/ollama-cloud.js +12 -0
- package/dist/providers/ollama-cloud.js.map +1 -1
- package/dist/providers/openai.d.ts.map +1 -1
- package/dist/providers/openai.js +7 -0
- package/dist/providers/openai.js.map +1 -1
- package/dist/providers/opencode-go.d.ts.map +1 -1
- package/dist/providers/opencode-go.js +9 -1
- package/dist/providers/opencode-go.js.map +1 -1
- package/dist/providers/quota-providers.d.ts +6 -0
- package/dist/providers/quota-providers.d.ts.map +1 -0
- package/dist/providers/quota-providers.js +276 -0
- package/dist/providers/quota-providers.js.map +1 -0
- package/dist/providers/qwen-code.d.ts.map +1 -1
- package/dist/providers/qwen-code.js +29 -3
- package/dist/providers/qwen-code.js.map +1 -1
- package/dist/providers/registry.d.ts.map +1 -1
- package/dist/providers/registry.js +2 -0
- package/dist/providers/registry.js.map +1 -1
- package/dist/providers/result-helpers.d.ts +2 -1
- package/dist/providers/result-helpers.d.ts.map +1 -1
- package/dist/providers/result-helpers.js +6 -1
- package/dist/providers/result-helpers.js.map +1 -1
- package/dist/providers/synthetic.d.ts.map +1 -1
- package/dist/providers/synthetic.js +7 -1
- package/dist/providers/synthetic.js.map +1 -1
- package/dist/providers/zai.d.ts.map +1 -1
- package/dist/providers/zai.js +7 -1
- package/dist/providers/zai.js.map +1 -1
- package/dist/providers/zhipu.d.ts.map +1 -1
- package/dist/providers/zhipu.js +7 -1
- package/dist/providers/zhipu.js.map +1 -1
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +148 -122
- package/dist/tui.tsx +122 -114
- package/package.json +29 -21
package/dist/lib/copilot.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* GitHub Copilot
|
|
2
|
+
* GitHub Copilot accounting fetcher.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Current usage is read from GitHub's public AI Credit billing reports.
|
|
5
|
+
* Legacy premium-request reports are available only when explicitly selected
|
|
6
|
+
* for an eligible Copilot Pro or Pro+ annual plan.
|
|
6
7
|
*/
|
|
7
8
|
import { existsSync, readFileSync } from "fs";
|
|
8
9
|
import { join } from "path";
|
|
@@ -11,89 +12,17 @@ import { fetchWithTimeout } from "./http.js";
|
|
|
11
12
|
import { readAuthFile } from "./opencode-auth.js";
|
|
12
13
|
import { getOpencodeRuntimeDirCandidates } from "./opencode-runtime-paths.js";
|
|
13
14
|
const GITHUB_API_BASE_URL = "https://api.github.com";
|
|
14
|
-
const
|
|
15
|
-
const GITHUB_API_VERSION = "2022-11-28";
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
current = current[key];
|
|
25
|
-
}
|
|
26
|
-
return current;
|
|
27
|
-
}
|
|
28
|
-
function getFirstNestedValue(source, paths) {
|
|
29
|
-
for (const path of paths) {
|
|
30
|
-
const value = getNestedValue(source, path);
|
|
31
|
-
if (value !== undefined) {
|
|
32
|
-
return value;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
return undefined;
|
|
36
|
-
}
|
|
37
|
-
function coerceFiniteNumber(value) {
|
|
38
|
-
if (typeof value === "number" && Number.isFinite(value)) {
|
|
39
|
-
return value;
|
|
40
|
-
}
|
|
41
|
-
if (typeof value === "string") {
|
|
42
|
-
const trimmed = value.trim();
|
|
43
|
-
if (!trimmed)
|
|
44
|
-
return undefined;
|
|
45
|
-
const parsed = Number(trimmed);
|
|
46
|
-
if (Number.isFinite(parsed)) {
|
|
47
|
-
return parsed;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
return undefined;
|
|
51
|
-
}
|
|
52
|
-
function coerceNonEmptyString(value) {
|
|
53
|
-
if (typeof value !== "string") {
|
|
54
|
-
return undefined;
|
|
55
|
-
}
|
|
56
|
-
const trimmed = value.trim();
|
|
57
|
-
return trimmed || undefined;
|
|
58
|
-
}
|
|
59
|
-
function getFirstNestedNumber(source, paths) {
|
|
60
|
-
return coerceFiniteNumber(getFirstNestedValue(source, paths));
|
|
61
|
-
}
|
|
62
|
-
function getFirstNestedString(source, paths) {
|
|
63
|
-
return coerceNonEmptyString(getFirstNestedValue(source, paths));
|
|
64
|
-
}
|
|
65
|
-
function getFirstNestedBoolean(source, paths) {
|
|
66
|
-
const value = getFirstNestedValue(source, paths);
|
|
67
|
-
return typeof value === "boolean" ? value : undefined;
|
|
68
|
-
}
|
|
69
|
-
function normalizeExplicitPercentRemaining(value) {
|
|
70
|
-
if (value === undefined || !Number.isFinite(value))
|
|
71
|
-
return undefined;
|
|
72
|
-
return Math.min(100, Math.floor(value));
|
|
73
|
-
}
|
|
74
|
-
function normalizeCopilotTier(value) {
|
|
75
|
-
const normalized = value?.trim().toLowerCase();
|
|
76
|
-
if (!normalized)
|
|
77
|
-
return undefined;
|
|
78
|
-
if (normalized === "free")
|
|
79
|
-
return "free";
|
|
80
|
-
if (normalized === "pro")
|
|
81
|
-
return "pro";
|
|
82
|
-
if (normalized === "pro+" || normalized === "pro_plus" || normalized === "pro-plus") {
|
|
83
|
-
return "pro+";
|
|
84
|
-
}
|
|
85
|
-
if (normalized === "business")
|
|
86
|
-
return "business";
|
|
87
|
-
if (normalized === "enterprise")
|
|
88
|
-
return "enterprise";
|
|
89
|
-
return undefined;
|
|
90
|
-
}
|
|
91
|
-
const COPILOT_PLAN_LIMITS = {
|
|
92
|
-
free: 50,
|
|
18
|
+
const PERSONAL_AI_CREDIT_TOTALS = {
|
|
19
|
+
pro: 1500,
|
|
20
|
+
"pro+": 7000,
|
|
21
|
+
max: 20000,
|
|
22
|
+
};
|
|
23
|
+
const LEGACY_PREMIUM_REQUEST_TOTALS = {
|
|
93
24
|
pro: 300,
|
|
94
25
|
"pro+": 1500,
|
|
95
|
-
business: 300,
|
|
96
|
-
enterprise: 1000,
|
|
97
26
|
};
|
|
98
27
|
function dedupeStrings(values) {
|
|
99
28
|
const out = [];
|
|
@@ -120,59 +49,44 @@ function classifyPatTokenKind(token) {
|
|
|
120
49
|
}
|
|
121
50
|
function getCurrentBillingPeriod(now = new Date()) {
|
|
122
51
|
return {
|
|
123
|
-
year: now.
|
|
124
|
-
month: now.
|
|
52
|
+
year: now.getUTCFullYear(),
|
|
53
|
+
month: now.getUTCMonth() + 1,
|
|
125
54
|
};
|
|
126
55
|
}
|
|
127
|
-
function
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
searchParams.set("month", String(period.month));
|
|
131
|
-
if (options?.includeDay && typeof period.day === "number") {
|
|
132
|
-
searchParams.set("day", String(period.day));
|
|
133
|
-
}
|
|
134
|
-
if (options?.organization) {
|
|
135
|
-
searchParams.set("organization", options.organization);
|
|
136
|
-
}
|
|
137
|
-
if (options?.username) {
|
|
138
|
-
searchParams.set("user", options.username);
|
|
139
|
-
}
|
|
140
|
-
return searchParams;
|
|
141
|
-
}
|
|
142
|
-
function getBillingModeForTarget(target) {
|
|
143
|
-
if (!target)
|
|
144
|
-
return "none";
|
|
145
|
-
if (target.scope === "organization")
|
|
146
|
-
return "organization_usage";
|
|
147
|
-
if (target.scope === "enterprise")
|
|
148
|
-
return "enterprise_usage";
|
|
149
|
-
return "user_quota";
|
|
56
|
+
function getApproxNextResetIso(nowMs = Date.now()) {
|
|
57
|
+
const now = new Date(nowMs);
|
|
58
|
+
return new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + 1, 1)).toISOString();
|
|
150
59
|
}
|
|
151
|
-
function
|
|
152
|
-
return
|
|
60
|
+
function formatBillingPeriod(period) {
|
|
61
|
+
return `${period.year}-${String(period.month).padStart(2, "0")}`;
|
|
153
62
|
}
|
|
154
|
-
function
|
|
155
|
-
if (!
|
|
156
|
-
return
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
if (target.scope === "enterprise")
|
|
160
|
-
return "not_available_from_enterprise_usage";
|
|
161
|
-
return "available";
|
|
63
|
+
function computePercentRemainingFromUsed(used, total) {
|
|
64
|
+
if (!Number.isFinite(used) || used < 0 || !Number.isFinite(total) || total <= 0) {
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
return Math.max(0, Math.min(100, Math.floor(((total - used) * 100) / total)));
|
|
162
68
|
}
|
|
163
69
|
function resolvePatBillingTarget(config) {
|
|
164
70
|
const billingPeriod = getCurrentBillingPeriod();
|
|
165
|
-
if (config.
|
|
166
|
-
if (config.
|
|
71
|
+
if (config.billingModel === "legacy_premium_requests") {
|
|
72
|
+
if (config.tier !== "pro" && config.tier !== "pro+") {
|
|
167
73
|
return {
|
|
168
74
|
target: null,
|
|
169
|
-
error:
|
|
75
|
+
error: "Legacy premium-request billing is only available to Copilot Pro or Pro+ subscribers on an existing annual plan that remained on legacy billing after June 1, 2026.",
|
|
170
76
|
};
|
|
171
77
|
}
|
|
172
|
-
if (
|
|
78
|
+
if (config.organization || config.enterprise) {
|
|
79
|
+
return {
|
|
80
|
+
target: null,
|
|
81
|
+
error: "Legacy premium-request billing is personal-only. Remove organization and enterprise from copilot-quota-token.json.",
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (config.tier === "business") {
|
|
86
|
+
if (!config.organization || config.enterprise) {
|
|
173
87
|
return {
|
|
174
88
|
target: null,
|
|
175
|
-
error: 'Copilot
|
|
89
|
+
error: 'Copilot Business AI Credit usage requires "organization" and does not accept "enterprise" in copilot-quota-token.json.',
|
|
176
90
|
};
|
|
177
91
|
}
|
|
178
92
|
return {
|
|
@@ -208,32 +122,34 @@ function resolvePatBillingTarget(config) {
|
|
|
208
122
|
}
|
|
209
123
|
return {
|
|
210
124
|
target: null,
|
|
211
|
-
error: 'Copilot
|
|
125
|
+
error: 'Copilot Enterprise AI Credit usage requires "enterprise" or "organization" in copilot-quota-token.json.',
|
|
212
126
|
};
|
|
213
127
|
}
|
|
214
128
|
if (config.organization || config.enterprise) {
|
|
215
129
|
return {
|
|
216
130
|
target: null,
|
|
217
|
-
error: `Copilot ${config.tier} usage is
|
|
131
|
+
error: `Copilot ${config.tier} AI Credit usage is personal. Remove organization and enterprise from copilot-quota-token.json.`,
|
|
218
132
|
};
|
|
219
133
|
}
|
|
220
134
|
return {
|
|
221
135
|
target: {
|
|
222
136
|
scope: "user",
|
|
223
137
|
username: config.username,
|
|
138
|
+
billingPeriod,
|
|
224
139
|
},
|
|
225
140
|
};
|
|
226
141
|
}
|
|
227
142
|
function validatePatTargetCompatibility(target, tokenKind) {
|
|
228
|
-
if (
|
|
143
|
+
if (!tokenKind)
|
|
229
144
|
return null;
|
|
145
|
+
if (target.scope === "user" && tokenKind === "ghs") {
|
|
146
|
+
return ("GitHub's personal AI Credit report supports GitHub App user access tokens, " +
|
|
147
|
+
"but not GitHub App installation access tokens. Use a GitHub App user token, fine-grained PAT with Plan (read), or supported classic credential.");
|
|
230
148
|
}
|
|
231
|
-
if (
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
if (tokenKind === "ghu" || tokenKind === "ghs") {
|
|
236
|
-
return ("GitHub's enterprise premium usage endpoint does not support GitHub App user or installation access tokens.");
|
|
149
|
+
if (target.scope === "enterprise" &&
|
|
150
|
+
(tokenKind === "github_pat" || tokenKind === "ghu" || tokenKind === "ghs")) {
|
|
151
|
+
return ("GitHub's enterprise billing reports do not support fine-grained PATs or GitHub App access tokens. " +
|
|
152
|
+
"Use a classic PAT held by an enterprise admin or billing manager.");
|
|
237
153
|
}
|
|
238
154
|
return null;
|
|
239
155
|
}
|
|
@@ -247,50 +163,63 @@ function validateQuotaConfig(raw) {
|
|
|
247
163
|
}
|
|
248
164
|
const obj = raw;
|
|
249
165
|
const token = typeof obj.token === "string" ? obj.token.trim() : "";
|
|
250
|
-
const tier = typeof obj.tier === "string" ? obj.tier.trim() : "";
|
|
166
|
+
const tier = typeof obj.tier === "string" ? obj.tier.trim().toLowerCase() : "";
|
|
167
|
+
const billingModel = obj.billingModel === undefined
|
|
168
|
+
? "ai_credits"
|
|
169
|
+
: typeof obj.billingModel === "string"
|
|
170
|
+
? obj.billingModel.trim()
|
|
171
|
+
: "";
|
|
251
172
|
if (!token) {
|
|
252
173
|
return { config: null, error: "Missing required string field: token" };
|
|
253
174
|
}
|
|
254
|
-
const validTiers = [
|
|
175
|
+
const validTiers = [
|
|
176
|
+
"free",
|
|
177
|
+
"student",
|
|
178
|
+
"pro",
|
|
179
|
+
"pro+",
|
|
180
|
+
"max",
|
|
181
|
+
"business",
|
|
182
|
+
"enterprise",
|
|
183
|
+
];
|
|
255
184
|
if (!validTiers.includes(tier)) {
|
|
256
185
|
return {
|
|
257
186
|
config: null,
|
|
258
|
-
error: "Invalid tier; expected one of: free, pro, pro+, business, enterprise",
|
|
187
|
+
error: "Invalid tier; expected one of: free, student, pro, pro+, max, business, enterprise",
|
|
259
188
|
};
|
|
260
189
|
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
}
|
|
267
|
-
username = usernameRaw.trim();
|
|
268
|
-
}
|
|
269
|
-
const organizationRaw = obj.organization;
|
|
270
|
-
let organization;
|
|
271
|
-
if (organizationRaw != null) {
|
|
272
|
-
if (typeof organizationRaw !== "string" || !organizationRaw.trim()) {
|
|
273
|
-
return { config: null, error: "organization must be a non-empty string when provided" };
|
|
274
|
-
}
|
|
275
|
-
organization = organizationRaw.trim();
|
|
190
|
+
if (billingModel !== "ai_credits" && billingModel !== "legacy_premium_requests") {
|
|
191
|
+
return {
|
|
192
|
+
config: null,
|
|
193
|
+
error: "Invalid billingModel; expected ai_credits or legacy_premium_requests",
|
|
194
|
+
};
|
|
276
195
|
}
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
196
|
+
const readOptionalString = (key) => {
|
|
197
|
+
const value = obj[key];
|
|
198
|
+
if (value == null)
|
|
199
|
+
return undefined;
|
|
200
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
201
|
+
throw new Error(`${key} must be a non-empty string when provided`);
|
|
282
202
|
}
|
|
283
|
-
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
config
|
|
203
|
+
return value.trim();
|
|
204
|
+
};
|
|
205
|
+
try {
|
|
206
|
+
const config = {
|
|
287
207
|
token,
|
|
288
208
|
tier: tier,
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
209
|
+
billingModel,
|
|
210
|
+
username: readOptionalString("username"),
|
|
211
|
+
organization: readOptionalString("organization"),
|
|
212
|
+
enterprise: readOptionalString("enterprise"),
|
|
213
|
+
};
|
|
214
|
+
const resolved = resolvePatBillingTarget(config);
|
|
215
|
+
if (!resolved.target) {
|
|
216
|
+
return { config: null, error: resolved.error };
|
|
217
|
+
}
|
|
218
|
+
return { config };
|
|
219
|
+
}
|
|
220
|
+
catch (error) {
|
|
221
|
+
return { config: null, error: error instanceof Error ? error.message : String(error) };
|
|
222
|
+
}
|
|
294
223
|
}
|
|
295
224
|
export function readQuotaConfigWithMeta() {
|
|
296
225
|
const checkedPaths = getCopilotPatConfigCandidatePaths();
|
|
@@ -298,8 +227,7 @@ export function readQuotaConfigWithMeta() {
|
|
|
298
227
|
if (!existsSync(path))
|
|
299
228
|
continue;
|
|
300
229
|
try {
|
|
301
|
-
const
|
|
302
|
-
const parsed = JSON.parse(content);
|
|
230
|
+
const parsed = JSON.parse(readFileSync(path, "utf-8"));
|
|
303
231
|
const validated = validateQuotaConfig(parsed);
|
|
304
232
|
if (!validated.config) {
|
|
305
233
|
return {
|
|
@@ -329,9 +257,8 @@ export function readQuotaConfigWithMeta() {
|
|
|
329
257
|
return { state: "absent", checkedPaths };
|
|
330
258
|
}
|
|
331
259
|
function selectCopilotAuth(authData) {
|
|
332
|
-
if (!authData)
|
|
260
|
+
if (!authData)
|
|
333
261
|
return { auth: null, keyName: null };
|
|
334
|
-
}
|
|
335
262
|
const candidates = [
|
|
336
263
|
["github-copilot", authData["github-copilot"]],
|
|
337
264
|
["copilot", authData.copilot],
|
|
@@ -347,72 +274,83 @@ function selectCopilotAuth(authData) {
|
|
|
347
274
|
}
|
|
348
275
|
return { auth: null, keyName: null };
|
|
349
276
|
}
|
|
277
|
+
function getRemainingTotalsState(target, config) {
|
|
278
|
+
if (!target || !config)
|
|
279
|
+
return "unavailable";
|
|
280
|
+
if (target.scope === "organization")
|
|
281
|
+
return "not_available_from_org_usage";
|
|
282
|
+
if (target.scope === "enterprise")
|
|
283
|
+
return "not_available_from_enterprise_usage";
|
|
284
|
+
const total = config.billingModel === "legacy_premium_requests"
|
|
285
|
+
? LEGACY_PREMIUM_REQUEST_TOTALS[config.tier]
|
|
286
|
+
: PERSONAL_AI_CREDIT_TOTALS[config.tier];
|
|
287
|
+
return total ? "available" : "value_only_without_denominator";
|
|
288
|
+
}
|
|
350
289
|
export function getCopilotQuotaAuthDiagnostics(authData) {
|
|
351
290
|
const pat = readQuotaConfigWithMeta();
|
|
352
291
|
const { auth, keyName } = selectCopilotAuth(authData);
|
|
353
|
-
const
|
|
354
|
-
|
|
355
|
-
|
|
292
|
+
const resolved = pat.state === "valid" && pat.config
|
|
293
|
+
? resolvePatBillingTarget(pat.config)
|
|
294
|
+
: { target: null };
|
|
295
|
+
const compatibilityError = resolved.target && pat.state === "valid"
|
|
296
|
+
? validatePatTargetCompatibility(resolved.target, pat.tokenKind)
|
|
356
297
|
: null;
|
|
357
298
|
const patBlocksOAuth = pat.state !== "absent";
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
effectiveSource = "oauth";
|
|
363
|
-
const billingTarget = pat.state === "valid"
|
|
364
|
-
? resolvedPatTarget.target
|
|
365
|
-
: !patBlocksOAuth && auth
|
|
366
|
-
? { scope: "user" }
|
|
367
|
-
: null;
|
|
368
|
-
const billingMode = getBillingModeForTarget(billingTarget);
|
|
369
|
-
const oauthHasAccessToken = Boolean(auth?.access?.trim());
|
|
370
|
-
const quotaApi = effectiveSource === "pat"
|
|
371
|
-
? pat.state === "valid"
|
|
372
|
-
? "github_billing_api"
|
|
373
|
-
: "none"
|
|
374
|
-
: effectiveSource === "oauth" && oauthHasAccessToken
|
|
375
|
-
? "copilot_internal_user"
|
|
299
|
+
const effectiveSource = patBlocksOAuth
|
|
300
|
+
? "pat"
|
|
301
|
+
: auth
|
|
302
|
+
? "oauth"
|
|
376
303
|
: "none";
|
|
304
|
+
const billingModel = pat.config?.billingModel ?? "ai_credits";
|
|
305
|
+
const quotaApi = pat.state !== "valid"
|
|
306
|
+
? "none"
|
|
307
|
+
: billingModel === "legacy_premium_requests"
|
|
308
|
+
? "github_legacy_premium_request_api"
|
|
309
|
+
: "github_ai_credit_api";
|
|
377
310
|
return {
|
|
378
311
|
pat,
|
|
379
312
|
oauth: {
|
|
380
313
|
configured: Boolean(auth),
|
|
381
314
|
keyName,
|
|
382
315
|
hasRefreshToken: Boolean(auth?.refresh),
|
|
383
|
-
hasAccessToken:
|
|
316
|
+
hasAccessToken: Boolean(auth?.access?.trim()),
|
|
384
317
|
},
|
|
385
318
|
effectiveSource,
|
|
386
319
|
override: patBlocksOAuth && auth ? "pat_overrides_oauth" : "none",
|
|
387
320
|
quotaApi,
|
|
388
|
-
billingMode
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
321
|
+
billingMode: resolved.target?.scope === "organization"
|
|
322
|
+
? "organization_usage"
|
|
323
|
+
: resolved.target?.scope === "enterprise"
|
|
324
|
+
? "enterprise_usage"
|
|
325
|
+
: resolved.target?.scope === "user"
|
|
326
|
+
? "user_quota"
|
|
327
|
+
: "none",
|
|
328
|
+
billingScope: resolved.target?.scope ?? "none",
|
|
329
|
+
billingApiAccessLikely: pat.state === "valid" && Boolean(resolved.target) && !resolved.error && !compatibilityError,
|
|
330
|
+
remainingTotalsState: getRemainingTotalsState(resolved.target, pat.config),
|
|
331
|
+
queryPeriod: resolved.target?.billingPeriod,
|
|
332
|
+
usernameFilter: pat.config?.username,
|
|
333
|
+
billingTargetError: resolved.error,
|
|
334
|
+
tokenCompatibilityError: compatibilityError ?? undefined,
|
|
335
|
+
billingModel,
|
|
336
|
+
budgetApi: billingModel !== "ai_credits"
|
|
337
|
+
? "not_available"
|
|
338
|
+
: resolved.target?.scope === "organization"
|
|
339
|
+
? "organization_budgets"
|
|
340
|
+
: resolved.target?.scope === "enterprise"
|
|
341
|
+
? "enterprise_budgets"
|
|
342
|
+
: "not_available",
|
|
343
|
+
oauthAccountingState: auth ? "not_supported_by_public_billing_api" : "not_configured",
|
|
400
344
|
};
|
|
401
345
|
}
|
|
402
|
-
function buildGitHubRestHeaders(token
|
|
346
|
+
function buildGitHubRestHeaders(token) {
|
|
403
347
|
return {
|
|
404
348
|
Accept: "application/vnd.github+json",
|
|
405
|
-
Authorization:
|
|
349
|
+
Authorization: `Bearer ${token}`,
|
|
406
350
|
"X-GitHub-Api-Version": GITHUB_API_VERSION,
|
|
407
351
|
"User-Agent": USER_AGENT,
|
|
408
352
|
};
|
|
409
353
|
}
|
|
410
|
-
function preferredSchemesForToken(token) {
|
|
411
|
-
if (token.startsWith("ghp_")) {
|
|
412
|
-
return ["token", "bearer"];
|
|
413
|
-
}
|
|
414
|
-
return ["bearer", "token"];
|
|
415
|
-
}
|
|
416
354
|
async function readGitHubRestErrorMessage(response) {
|
|
417
355
|
const text = await response.text();
|
|
418
356
|
try {
|
|
@@ -422,336 +360,325 @@ async function readGitHubRestErrorMessage(response) {
|
|
|
422
360
|
if (message && documentationUrl) {
|
|
423
361
|
return sanitizeDisplayText(`${message} (${documentationUrl})`);
|
|
424
362
|
}
|
|
425
|
-
if (message)
|
|
363
|
+
if (message)
|
|
426
364
|
return sanitizeDisplayText(message);
|
|
427
|
-
}
|
|
428
365
|
}
|
|
429
366
|
catch {
|
|
430
|
-
//
|
|
367
|
+
// Fall through to a bounded plain-text snippet.
|
|
431
368
|
}
|
|
432
369
|
return sanitizeDisplaySnippet(text, 160);
|
|
433
370
|
}
|
|
434
|
-
async function
|
|
435
|
-
const response = await fetchWithTimeout(url, {
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
371
|
+
async function fetchGitHubRestJson(url, token, requestTimeoutMs) {
|
|
372
|
+
const response = await fetchWithTimeout(url, { headers: buildGitHubRestHeaders(token) }, requestTimeoutMs);
|
|
373
|
+
if (!response.ok) {
|
|
374
|
+
const message = await readGitHubRestErrorMessage(response);
|
|
375
|
+
const rateLimit = response.status === 403 && response.headers.get("x-ratelimit-remaining") === "0"
|
|
376
|
+
? " (GitHub API rate limit exhausted)"
|
|
377
|
+
: "";
|
|
378
|
+
throw new Error(`GitHub API error ${response.status}: ${message}${rateLimit}`);
|
|
379
|
+
}
|
|
380
|
+
try {
|
|
381
|
+
return (await response.json());
|
|
382
|
+
}
|
|
383
|
+
catch {
|
|
384
|
+
throw new Error("GitHub API returned malformed JSON");
|
|
440
385
|
}
|
|
441
|
-
return {
|
|
442
|
-
ok: false,
|
|
443
|
-
status: response.status,
|
|
444
|
-
message: await readGitHubRestErrorMessage(response),
|
|
445
|
-
};
|
|
446
386
|
}
|
|
447
387
|
async function resolveGitHubUsername(token, requestTimeoutMs) {
|
|
448
|
-
const
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
if (result.ok) {
|
|
453
|
-
const login = result.data.login?.trim();
|
|
454
|
-
if (login)
|
|
455
|
-
return login;
|
|
456
|
-
throw new Error("GitHub /user response did not include a login");
|
|
457
|
-
}
|
|
458
|
-
if (result.status === 401) {
|
|
459
|
-
unauthorized = { status: result.status, message: result.message };
|
|
460
|
-
continue;
|
|
461
|
-
}
|
|
462
|
-
throw new Error(`GitHub API error ${result.status}: ${result.message}`);
|
|
388
|
+
const response = await fetchGitHubRestJson(`${GITHUB_API_BASE_URL}/user`, token, requestTimeoutMs);
|
|
389
|
+
const login = response.login?.trim();
|
|
390
|
+
if (!login) {
|
|
391
|
+
throw new Error("GitHub /user response did not include a login");
|
|
463
392
|
}
|
|
464
|
-
|
|
465
|
-
|
|
393
|
+
return login;
|
|
394
|
+
}
|
|
395
|
+
function buildBillingQuery(target) {
|
|
396
|
+
const query = new URLSearchParams({
|
|
397
|
+
year: String(target.billingPeriod.year),
|
|
398
|
+
month: String(target.billingPeriod.month),
|
|
399
|
+
});
|
|
400
|
+
if (target.scope !== "user" && target.username)
|
|
401
|
+
query.set("user", target.username);
|
|
402
|
+
if (target.scope === "enterprise" && target.organization) {
|
|
403
|
+
query.set("organization", target.organization);
|
|
466
404
|
}
|
|
467
|
-
|
|
405
|
+
return query;
|
|
468
406
|
}
|
|
469
|
-
function
|
|
407
|
+
function getBillingUsageUrl(target, billingModel) {
|
|
408
|
+
const report = billingModel === "legacy_premium_requests" ? "premium_request" : "ai_credit";
|
|
409
|
+
const query = buildBillingQuery(target);
|
|
470
410
|
if (target.scope === "enterprise") {
|
|
471
|
-
|
|
472
|
-
const searchParams = buildBillingPeriodQueryParams(target.billingPeriod, {
|
|
473
|
-
organization: target.organization,
|
|
474
|
-
username: target.username,
|
|
475
|
-
});
|
|
476
|
-
return `${base}?${searchParams.toString()}`;
|
|
411
|
+
return `${GITHUB_API_BASE_URL}/enterprises/${encodeURIComponent(target.enterprise)}/settings/billing/${report}/usage?${query}`;
|
|
477
412
|
}
|
|
478
413
|
if (target.scope === "organization") {
|
|
479
|
-
|
|
480
|
-
const searchParams = buildBillingPeriodQueryParams(target.billingPeriod, {
|
|
481
|
-
username: target.username,
|
|
482
|
-
});
|
|
483
|
-
return `${base}?${searchParams.toString()}`;
|
|
414
|
+
return `${GITHUB_API_BASE_URL}/organizations/${encodeURIComponent(target.organization)}/settings/billing/${report}/usage?${query}`;
|
|
484
415
|
}
|
|
485
|
-
return `${GITHUB_API_BASE_URL}/users/${encodeURIComponent(target.username)}/settings/billing/
|
|
416
|
+
return `${GITHUB_API_BASE_URL}/users/${encodeURIComponent(target.username)}/settings/billing/${report}/usage?${query}`;
|
|
486
417
|
}
|
|
487
|
-
async function
|
|
488
|
-
const
|
|
418
|
+
async function fetchBillingUsage(params) {
|
|
419
|
+
const target = params.target.scope === "user"
|
|
489
420
|
? {
|
|
490
|
-
|
|
491
|
-
username: params.target.username ??
|
|
421
|
+
...params.target,
|
|
422
|
+
username: params.target.username ??
|
|
423
|
+
(await resolveGitHubUsername(params.token, params.requestTimeoutMs)),
|
|
492
424
|
}
|
|
493
425
|
: params.target;
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
426
|
+
return {
|
|
427
|
+
response: await fetchGitHubRestJson(getBillingUsageUrl(target, params.billingModel), params.token, params.requestTimeoutMs),
|
|
428
|
+
target,
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
function getResponsePeriod(response, fallback) {
|
|
432
|
+
const period = response.timePeriod ?? response.time_period;
|
|
433
|
+
return {
|
|
434
|
+
year: typeof period?.year === "number" ? period.year : fallback.year,
|
|
435
|
+
month: typeof period?.month === "number" ? period.month : fallback.month,
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
function getUsageItems(response) {
|
|
439
|
+
if (Array.isArray(response.usageItems))
|
|
440
|
+
return response.usageItems;
|
|
441
|
+
if (Array.isArray(response.usage_items))
|
|
442
|
+
return response.usage_items;
|
|
443
|
+
throw new Error("GitHub billing response did not include a usageItems array");
|
|
444
|
+
}
|
|
445
|
+
function readFiniteNumber(value) {
|
|
446
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
447
|
+
}
|
|
448
|
+
function isAiCreditItem(item) {
|
|
449
|
+
const text = [item.product, item.sku, item.unitType, item.unit_type]
|
|
450
|
+
.filter((value) => typeof value === "string")
|
|
451
|
+
.join(" ")
|
|
452
|
+
.toLowerCase();
|
|
453
|
+
return text.includes("ai credit") || text.includes("ai-credit");
|
|
454
|
+
}
|
|
455
|
+
function isPremiumRequestItem(item) {
|
|
456
|
+
const text = [item.product, item.sku, item.unitType, item.unit_type]
|
|
457
|
+
.filter((value) => typeof value === "string")
|
|
458
|
+
.join(" ")
|
|
459
|
+
.toLowerCase();
|
|
460
|
+
return text.includes("premium request");
|
|
461
|
+
}
|
|
462
|
+
function parseAiCreditTotals(response) {
|
|
463
|
+
const items = getUsageItems(response);
|
|
464
|
+
const matches = items.filter(isAiCreditItem);
|
|
465
|
+
if (matches.length === 0 && items.length > 0) {
|
|
466
|
+
throw new Error("GitHub billing response did not contain an AI Credit usage item");
|
|
467
|
+
}
|
|
468
|
+
let used = 0;
|
|
469
|
+
let includedUsed = 0;
|
|
470
|
+
let billedUsed = 0;
|
|
471
|
+
let billedAmountUsd = 0;
|
|
472
|
+
let hasBilledAmount = false;
|
|
473
|
+
for (const item of matches) {
|
|
474
|
+
const gross = readFiniteNumber(item.grossQuantity ?? item.gross_quantity);
|
|
475
|
+
const discount = readFiniteNumber(item.discountQuantity ?? item.discount_quantity);
|
|
476
|
+
const net = readFiniteNumber(item.netQuantity ?? item.net_quantity);
|
|
477
|
+
if (gross === undefined && discount === undefined && net === undefined) {
|
|
478
|
+
throw new Error("GitHub AI Credit usage item did not include quantity fields");
|
|
503
479
|
}
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
480
|
+
const normalizedIncluded = Math.max(0, discount ?? 0);
|
|
481
|
+
const normalizedBilled = Math.max(0, net ?? Math.max(0, (gross ?? 0) - normalizedIncluded));
|
|
482
|
+
used += Math.max(0, gross ?? normalizedIncluded + normalizedBilled);
|
|
483
|
+
includedUsed += normalizedIncluded;
|
|
484
|
+
billedUsed += normalizedBilled;
|
|
485
|
+
const netAmount = readFiniteNumber(item.netAmount ?? item.net_amount);
|
|
486
|
+
if (netAmount !== undefined) {
|
|
487
|
+
billedAmountUsd += Math.max(0, netAmount);
|
|
488
|
+
hasBilledAmount = true;
|
|
507
489
|
}
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
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
|
-
const
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
const
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
const
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
if (
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
490
|
+
}
|
|
491
|
+
return {
|
|
492
|
+
used,
|
|
493
|
+
includedUsed,
|
|
494
|
+
billedUsed,
|
|
495
|
+
billedAmountUsd: hasBilledAmount ? billedAmountUsd : undefined,
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
function parseLegacyPremiumRequestUsage(response) {
|
|
499
|
+
const items = getUsageItems(response);
|
|
500
|
+
const matches = items.filter(isPremiumRequestItem);
|
|
501
|
+
if (matches.length === 0 && items.length > 0) {
|
|
502
|
+
throw new Error("GitHub billing response did not contain a premium-request usage item");
|
|
503
|
+
}
|
|
504
|
+
return matches.reduce((sum, item) => {
|
|
505
|
+
const gross = readFiniteNumber(item.grossQuantity ?? item.gross_quantity);
|
|
506
|
+
const net = readFiniteNumber(item.netQuantity ?? item.net_quantity);
|
|
507
|
+
if (gross === undefined && net === undefined) {
|
|
508
|
+
throw new Error("GitHub premium-request usage item did not include quantity fields");
|
|
509
|
+
}
|
|
510
|
+
return sum + Math.max(0, gross ?? net ?? 0);
|
|
511
|
+
}, 0);
|
|
512
|
+
}
|
|
513
|
+
function isAiCreditBudget(budget) {
|
|
514
|
+
const skus = [
|
|
515
|
+
budget.budget_product_sku,
|
|
516
|
+
...(Array.isArray(budget.budget_product_skus) ? budget.budget_product_skus : []),
|
|
517
|
+
]
|
|
518
|
+
.filter((value) => typeof value === "string")
|
|
519
|
+
.map((value) => value.toLowerCase());
|
|
520
|
+
return budget.budget_type === "BundlePricing" && skus.includes("ai_credits");
|
|
521
|
+
}
|
|
522
|
+
function budgetMatchesTarget(budget, target) {
|
|
523
|
+
const scope = budget.budget_scope;
|
|
524
|
+
const entity = budget.budget_entity_name?.toLowerCase();
|
|
525
|
+
const user = (budget.user ?? budget.budget_entity_name)?.toLowerCase();
|
|
526
|
+
if (target.scope === "organization") {
|
|
527
|
+
if (target.username && scope === "user") {
|
|
528
|
+
return !user || user === target.username.toLowerCase();
|
|
529
|
+
}
|
|
530
|
+
if (target.username && scope === "multi_user_customer")
|
|
531
|
+
return true;
|
|
532
|
+
return scope === "organization";
|
|
533
|
+
}
|
|
534
|
+
if (target.scope === "enterprise") {
|
|
535
|
+
if (target.username && scope === "user") {
|
|
536
|
+
return !user || user === target.username.toLowerCase();
|
|
537
|
+
}
|
|
538
|
+
if (target.username &&
|
|
539
|
+
(scope === "multi_user_customer" || scope === "multi_user_cost_center")) {
|
|
540
|
+
return true;
|
|
541
|
+
}
|
|
542
|
+
if (target.organization && scope === "organization") {
|
|
543
|
+
return !entity || entity === target.organization.toLowerCase();
|
|
544
|
+
}
|
|
545
|
+
return scope === "enterprise";
|
|
546
|
+
}
|
|
547
|
+
return false;
|
|
548
|
+
}
|
|
549
|
+
function budgetSpecificity(budget) {
|
|
550
|
+
switch (budget.budget_scope) {
|
|
551
|
+
case "user":
|
|
552
|
+
return 5;
|
|
553
|
+
case "multi_user_cost_center":
|
|
554
|
+
return 4;
|
|
555
|
+
case "multi_user_customer":
|
|
556
|
+
return 3;
|
|
557
|
+
case "organization":
|
|
558
|
+
return 2;
|
|
559
|
+
case "enterprise":
|
|
560
|
+
return 1;
|
|
561
|
+
default:
|
|
562
|
+
return 0;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
function getBudgetsUrl(target, page) {
|
|
566
|
+
const base = target.scope === "organization"
|
|
567
|
+
? `${GITHUB_API_BASE_URL}/organizations/${encodeURIComponent(target.organization)}/settings/billing/budgets`
|
|
568
|
+
: `${GITHUB_API_BASE_URL}/enterprises/${encodeURIComponent(target.enterprise)}/settings/billing/budgets`;
|
|
569
|
+
const query = new URLSearchParams({ page: String(page), per_page: "100" });
|
|
570
|
+
if (target.username)
|
|
571
|
+
query.set("user", target.username);
|
|
572
|
+
return `${base}?${query}`;
|
|
573
|
+
}
|
|
574
|
+
async function fetchApplicableBudget(params) {
|
|
575
|
+
const budgets = [];
|
|
576
|
+
for (let page = 1; page <= 100; page += 1) {
|
|
577
|
+
const response = await fetchGitHubRestJson(getBudgetsUrl(params.target, page), params.token, params.requestTimeoutMs);
|
|
578
|
+
if (!Array.isArray(response.budgets)) {
|
|
579
|
+
throw new Error("GitHub budgets response did not include a budgets array");
|
|
580
|
+
}
|
|
581
|
+
budgets.push(...response.budgets);
|
|
582
|
+
if (!response.has_next_page)
|
|
583
|
+
break;
|
|
584
|
+
if (page === 100) {
|
|
585
|
+
throw new Error("GitHub budgets response exceeded 100 pages");
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
const selected = budgets
|
|
589
|
+
.filter((budget) => isAiCreditBudget(budget) &&
|
|
590
|
+
budgetMatchesTarget(budget, params.target) &&
|
|
591
|
+
typeof budget.budget_amount === "number" &&
|
|
592
|
+
Number.isFinite(budget.budget_amount) &&
|
|
593
|
+
budget.budget_amount >= 0)
|
|
594
|
+
.sort((a, b) => budgetSpecificity(b) - budgetSpecificity(a))[0];
|
|
595
|
+
if (!selected || selected.budget_amount === undefined)
|
|
596
|
+
return undefined;
|
|
597
|
+
const percentRemaining = params.spentUsd === undefined
|
|
598
|
+
? undefined
|
|
599
|
+
: computePercentRemainingFromUsed(params.spentUsd, selected.budget_amount);
|
|
600
|
+
return {
|
|
601
|
+
amountUsd: selected.budget_amount,
|
|
602
|
+
spentUsd: params.spentUsd,
|
|
603
|
+
scope: selected.budget_scope ?? "unknown",
|
|
604
|
+
percentRemaining,
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
function makeBudgetWarning(error) {
|
|
608
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
609
|
+
return `AI Credit usage loaded, but the budget report failed: ${message}`;
|
|
610
|
+
}
|
|
611
|
+
async function toAiCreditResult(params) {
|
|
612
|
+
const totals = parseAiCreditTotals(params.response);
|
|
613
|
+
const resetTimeIso = getApproxNextResetIso();
|
|
614
|
+
const period = getResponsePeriod(params.response, params.target.billingPeriod);
|
|
615
|
+
let budget;
|
|
616
|
+
const warnings = [];
|
|
617
|
+
if (params.target.scope !== "user") {
|
|
618
|
+
try {
|
|
619
|
+
budget = await fetchApplicableBudget({
|
|
620
|
+
token: params.token,
|
|
621
|
+
target: params.target,
|
|
622
|
+
spentUsd: totals.billedAmountUsd,
|
|
623
|
+
requestTimeoutMs: params.requestTimeoutMs,
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
catch (error) {
|
|
627
|
+
warnings.push(makeBudgetWarning(error));
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
if (params.target.scope === "organization") {
|
|
620
631
|
return {
|
|
621
632
|
success: true,
|
|
622
|
-
mode: "
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
633
|
+
mode: "organization_usage",
|
|
634
|
+
organization: params.target.organization,
|
|
635
|
+
username: params.target.username,
|
|
636
|
+
period,
|
|
637
|
+
unit: "ai_credits",
|
|
638
|
+
...totals,
|
|
639
|
+
budget,
|
|
640
|
+
warnings: warnings.length ? warnings : undefined,
|
|
627
641
|
resetTimeIso,
|
|
628
642
|
};
|
|
629
643
|
}
|
|
630
|
-
if (
|
|
631
|
-
|
|
644
|
+
if (params.target.scope === "enterprise") {
|
|
645
|
+
return {
|
|
646
|
+
success: true,
|
|
647
|
+
mode: "enterprise_usage",
|
|
648
|
+
enterprise: params.target.enterprise,
|
|
649
|
+
organization: params.target.organization,
|
|
650
|
+
username: params.target.username,
|
|
651
|
+
period,
|
|
652
|
+
unit: "ai_credits",
|
|
653
|
+
...totals,
|
|
654
|
+
budget,
|
|
655
|
+
warnings: warnings.length ? warnings : undefined,
|
|
656
|
+
resetTimeIso,
|
|
657
|
+
};
|
|
632
658
|
}
|
|
659
|
+
const total = PERSONAL_AI_CREDIT_TOTALS[params.config.tier];
|
|
633
660
|
return {
|
|
634
661
|
success: true,
|
|
635
662
|
mode: "user_quota",
|
|
636
|
-
|
|
663
|
+
unit: "ai_credits",
|
|
664
|
+
...totals,
|
|
637
665
|
total,
|
|
638
|
-
percentRemaining:
|
|
666
|
+
percentRemaining: total ? computePercentRemainingFromUsed(totals.used, total) : undefined,
|
|
667
|
+
plan: params.config.tier,
|
|
639
668
|
resetTimeIso,
|
|
640
669
|
};
|
|
641
670
|
}
|
|
642
|
-
function
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
const trimmed = value.trim();
|
|
646
|
-
if (!trimmed)
|
|
647
|
-
return undefined;
|
|
648
|
-
if (/^\d{4}-\d{2}-\d{2}$/.test(trimmed)) {
|
|
649
|
-
return `${trimmed}T00:00:00.000Z`;
|
|
650
|
-
}
|
|
651
|
-
const timestamp = Date.parse(trimmed);
|
|
652
|
-
if (Number.isNaN(timestamp))
|
|
653
|
-
return undefined;
|
|
654
|
-
return new Date(timestamp).toISOString();
|
|
655
|
-
}
|
|
656
|
-
function getApproxNextResetIso(nowMs = Date.now()) {
|
|
657
|
-
const now = new Date(nowMs);
|
|
658
|
-
return new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + 1, 1)).toISOString();
|
|
659
|
-
}
|
|
660
|
-
function computePercentRemainingFromUsed(params) {
|
|
661
|
-
const { used, total } = params;
|
|
662
|
-
if (!Number.isFinite(total) || total <= 0)
|
|
663
|
-
return 0;
|
|
664
|
-
if (!Number.isFinite(used) || used <= 0)
|
|
665
|
-
return 100;
|
|
666
|
-
const normalizedUsed = Math.max(0, used);
|
|
667
|
-
const remaining = total - normalizedUsed;
|
|
668
|
-
return Math.min(100, Math.floor((remaining * 100) / total));
|
|
669
|
-
}
|
|
670
|
-
function getPremiumUsageItems(response, options) {
|
|
671
|
-
const items = Array.isArray(response.usageItems)
|
|
672
|
-
? response.usageItems
|
|
673
|
-
: Array.isArray(response.usage_items)
|
|
674
|
-
? response.usage_items
|
|
675
|
-
: [];
|
|
676
|
-
const premiumItems = items.filter((item) => {
|
|
677
|
-
if (!item || typeof item !== "object")
|
|
678
|
-
return false;
|
|
679
|
-
if (typeof item.sku !== "string")
|
|
680
|
-
return false;
|
|
681
|
-
return item.sku === "Copilot Premium Request" || item.sku.includes("Premium");
|
|
682
|
-
});
|
|
683
|
-
if (premiumItems.length === 0 && items.length > 0) {
|
|
684
|
-
const skus = items.map((item) => (typeof item?.sku === "string" ? item.sku : "?")).join(", ");
|
|
685
|
-
throw new Error(`No premium-request items found in billing response (${items.length} items, SKUs: ${skus}). Expected an item with SKU containing "Premium".`);
|
|
686
|
-
}
|
|
687
|
-
if (premiumItems.length === 0 && options?.allowEmpty) {
|
|
688
|
-
return [];
|
|
689
|
-
}
|
|
690
|
-
if (premiumItems.length === 0) {
|
|
691
|
-
throw new Error("Billing API returned empty usageItems array for Copilot premium requests.");
|
|
692
|
-
}
|
|
693
|
-
return premiumItems;
|
|
694
|
-
}
|
|
695
|
-
function sumUsedUnits(items) {
|
|
696
|
-
return items.reduce((sum, item) => {
|
|
697
|
-
const used = item.grossQuantity ??
|
|
698
|
-
item.gross_quantity ??
|
|
699
|
-
item.netQuantity ??
|
|
700
|
-
item.net_quantity ??
|
|
701
|
-
0;
|
|
702
|
-
return sum + (typeof used === "number" ? used : 0);
|
|
703
|
-
}, 0);
|
|
704
|
-
}
|
|
705
|
-
function formatBillingPeriod(period) {
|
|
706
|
-
return `${period.year}-${String(period.month).padStart(2, "0")}`;
|
|
707
|
-
}
|
|
708
|
-
function getBillingResponsePeriod(response, fallbackPeriod) {
|
|
709
|
-
const timePeriod = response.timePeriod ?? response.time_period;
|
|
710
|
-
const year = typeof timePeriod?.year === "number" ? timePeriod.year : fallbackPeriod.year;
|
|
711
|
-
const month = typeof timePeriod?.month === "number" ? timePeriod.month : fallbackPeriod.month;
|
|
712
|
-
return { year, month };
|
|
713
|
-
}
|
|
714
|
-
function toUserQuotaResultFromBilling(response, fallbackTier) {
|
|
715
|
-
const premiumItems = getPremiumUsageItems(response);
|
|
716
|
-
const used = sumUsedUnits(premiumItems);
|
|
717
|
-
const apiLimits = premiumItems
|
|
718
|
-
.map((item) => item.limit)
|
|
719
|
-
.filter((limit) => typeof limit === "number" && limit > 0);
|
|
720
|
-
const total = apiLimits.length > 0 ? Math.max(...apiLimits) : fallbackTier ? COPILOT_PLAN_LIMITS[fallbackTier] : undefined;
|
|
721
|
-
if (!total || total <= 0) {
|
|
722
|
-
throw new Error("Copilot billing response did not include a limit. Configure copilot-quota-token.json with your tier so the plugin can compute quota totals.");
|
|
723
|
-
}
|
|
671
|
+
function toLegacyResult(response, config) {
|
|
672
|
+
const used = parseLegacyPremiumRequestUsage(response);
|
|
673
|
+
const total = LEGACY_PREMIUM_REQUEST_TOTALS[config.tier];
|
|
724
674
|
return {
|
|
725
675
|
success: true,
|
|
726
676
|
mode: "user_quota",
|
|
677
|
+
unit: "premium_requests",
|
|
727
678
|
used,
|
|
728
679
|
total,
|
|
729
|
-
percentRemaining: computePercentRemainingFromUsed(
|
|
730
|
-
|
|
731
|
-
};
|
|
732
|
-
}
|
|
733
|
-
function toOrganizationUsageResultFromBilling(params) {
|
|
734
|
-
const premiumItems = getPremiumUsageItems(params.response, { allowEmpty: true });
|
|
735
|
-
return {
|
|
736
|
-
success: true,
|
|
737
|
-
mode: "organization_usage",
|
|
738
|
-
organization: params.organization,
|
|
739
|
-
username: params.username,
|
|
740
|
-
period: getBillingResponsePeriod(params.response, params.billingPeriod),
|
|
741
|
-
used: sumUsedUnits(premiumItems),
|
|
742
|
-
resetTimeIso: getApproxNextResetIso(),
|
|
743
|
-
};
|
|
744
|
-
}
|
|
745
|
-
function toEnterpriseUsageResultFromBilling(params) {
|
|
746
|
-
const premiumItems = getPremiumUsageItems(params.response, { allowEmpty: true });
|
|
747
|
-
return {
|
|
748
|
-
success: true,
|
|
749
|
-
mode: "enterprise_usage",
|
|
750
|
-
enterprise: params.enterprise,
|
|
751
|
-
organization: params.organization,
|
|
752
|
-
username: params.username,
|
|
753
|
-
period: getBillingResponsePeriod(params.response, params.billingPeriod),
|
|
754
|
-
used: sumUsedUnits(premiumItems),
|
|
680
|
+
percentRemaining: total ? computePercentRemainingFromUsed(used, total) : undefined,
|
|
681
|
+
plan: config.tier,
|
|
755
682
|
resetTimeIso: getApproxNextResetIso(),
|
|
756
683
|
};
|
|
757
684
|
}
|
|
@@ -759,65 +686,44 @@ function toQuotaError(message) {
|
|
|
759
686
|
return { success: false, error: message };
|
|
760
687
|
}
|
|
761
688
|
/**
|
|
762
|
-
* Query GitHub Copilot
|
|
689
|
+
* Query GitHub Copilot accounting.
|
|
763
690
|
*
|
|
764
|
-
*
|
|
691
|
+
* A valid local billing token config is required. OpenCode OAuth remains
|
|
692
|
+
* visible in diagnostics but is not sent to GitHub's public billing API
|
|
693
|
+
* because its billing permissions are not part of the documented contract.
|
|
765
694
|
*/
|
|
766
695
|
export async function queryCopilotQuota(options = {}) {
|
|
767
696
|
const pat = readQuotaConfigWithMeta();
|
|
768
697
|
if (pat.state === "invalid") {
|
|
769
698
|
return toQuotaError(`Invalid copilot-quota-token.json: ${pat.error ?? "unknown error"}${pat.selectedPath ? ` (${pat.selectedPath})` : ""}`);
|
|
770
699
|
}
|
|
771
|
-
if (pat.state === "
|
|
772
|
-
const resolvedTarget = resolvePatBillingTarget(pat.config);
|
|
773
|
-
if (!resolvedTarget.target) {
|
|
774
|
-
return toQuotaError(resolvedTarget.error ?? "Unable to resolve Copilot billing scope.");
|
|
775
|
-
}
|
|
776
|
-
const tokenCompatibilityError = validatePatTargetCompatibility(resolvedTarget.target, pat.tokenKind);
|
|
777
|
-
if (tokenCompatibilityError) {
|
|
778
|
-
return toQuotaError(tokenCompatibilityError);
|
|
779
|
-
}
|
|
780
|
-
try {
|
|
781
|
-
const { response, billingPeriod } = await fetchPremiumRequestUsage({
|
|
782
|
-
token: pat.config.token,
|
|
783
|
-
target: resolvedTarget.target,
|
|
784
|
-
requestTimeoutMs: options.requestTimeoutMs,
|
|
785
|
-
});
|
|
786
|
-
if (resolvedTarget.target.scope === "organization") {
|
|
787
|
-
return toOrganizationUsageResultFromBilling({
|
|
788
|
-
response,
|
|
789
|
-
organization: resolvedTarget.target.organization,
|
|
790
|
-
username: resolvedTarget.target.username,
|
|
791
|
-
billingPeriod: billingPeriod ?? resolvedTarget.target.billingPeriod,
|
|
792
|
-
});
|
|
793
|
-
}
|
|
794
|
-
if (resolvedTarget.target.scope === "enterprise") {
|
|
795
|
-
return toEnterpriseUsageResultFromBilling({
|
|
796
|
-
response,
|
|
797
|
-
enterprise: resolvedTarget.target.enterprise,
|
|
798
|
-
organization: resolvedTarget.target.organization,
|
|
799
|
-
username: resolvedTarget.target.username,
|
|
800
|
-
billingPeriod: billingPeriod ?? resolvedTarget.target.billingPeriod,
|
|
801
|
-
});
|
|
802
|
-
}
|
|
803
|
-
return toUserQuotaResultFromBilling(response, pat.config.tier);
|
|
804
|
-
}
|
|
805
|
-
catch (error) {
|
|
806
|
-
return toQuotaError(error instanceof Error ? error.message : String(error));
|
|
807
|
-
}
|
|
808
|
-
}
|
|
809
|
-
const authData = await readAuthFile();
|
|
810
|
-
const { auth } = selectCopilotAuth(authData);
|
|
811
|
-
if (!auth) {
|
|
700
|
+
if (pat.state === "absent" || !pat.config) {
|
|
812
701
|
return null;
|
|
813
702
|
}
|
|
814
|
-
const
|
|
815
|
-
if (!
|
|
816
|
-
return toQuotaError(
|
|
703
|
+
const resolved = resolvePatBillingTarget(pat.config);
|
|
704
|
+
if (!resolved.target) {
|
|
705
|
+
return toQuotaError(resolved.error ?? "Unable to resolve Copilot billing scope.");
|
|
817
706
|
}
|
|
707
|
+
const compatibilityError = validatePatTargetCompatibility(resolved.target, pat.tokenKind);
|
|
708
|
+
if (compatibilityError)
|
|
709
|
+
return toQuotaError(compatibilityError);
|
|
818
710
|
try {
|
|
819
|
-
const
|
|
820
|
-
|
|
711
|
+
const billingModel = pat.config.billingModel ?? "ai_credits";
|
|
712
|
+
const { response, target } = await fetchBillingUsage({
|
|
713
|
+
token: pat.config.token,
|
|
714
|
+
target: resolved.target,
|
|
715
|
+
billingModel,
|
|
716
|
+
requestTimeoutMs: options.requestTimeoutMs,
|
|
717
|
+
});
|
|
718
|
+
return billingModel === "legacy_premium_requests"
|
|
719
|
+
? toLegacyResult(response, pat.config)
|
|
720
|
+
: await toAiCreditResult({
|
|
721
|
+
response,
|
|
722
|
+
target,
|
|
723
|
+
config: pat.config,
|
|
724
|
+
token: pat.config.token,
|
|
725
|
+
requestTimeoutMs: options.requestTimeoutMs,
|
|
726
|
+
});
|
|
821
727
|
}
|
|
822
728
|
catch (error) {
|
|
823
729
|
return toQuotaError(error instanceof Error ? error.message : String(error));
|
|
@@ -828,30 +734,20 @@ export async function hasCopilotQuotaRuntimeAvailable() {
|
|
|
828
734
|
return diagnostics.billingApiAccessLikely;
|
|
829
735
|
}
|
|
830
736
|
export function formatCopilotQuota(result) {
|
|
831
|
-
if (!result || !result.success)
|
|
737
|
+
if (!result || !result.success)
|
|
832
738
|
return null;
|
|
833
|
-
|
|
739
|
+
const unit = result.unit === "ai_credits" ? "AI Credits" : "Premium Requests";
|
|
834
740
|
if (result.mode === "organization_usage") {
|
|
835
|
-
|
|
836
|
-
if (result.username) {
|
|
837
|
-
details.push(`user=${result.username}`);
|
|
838
|
-
}
|
|
839
|
-
return `Copilot Org (${result.organization}) ${details.join(" | ")}`;
|
|
741
|
+
return `Copilot Org (${result.organization}) ${result.used} ${unit} | ${formatBillingPeriod(result.period)}`;
|
|
840
742
|
}
|
|
841
743
|
if (result.mode === "enterprise_usage") {
|
|
842
|
-
|
|
843
|
-
if (result.organization) {
|
|
844
|
-
details.push(`org=${result.organization}`);
|
|
845
|
-
}
|
|
846
|
-
if (result.username) {
|
|
847
|
-
details.push(`user=${result.username}`);
|
|
848
|
-
}
|
|
849
|
-
return `Copilot Enterprise (${result.enterprise}) ${details.join(" | ")}`;
|
|
744
|
+
return `Copilot Enterprise (${result.enterprise}) ${result.used} ${unit} | ${formatBillingPeriod(result.period)}`;
|
|
850
745
|
}
|
|
851
|
-
if (result.unlimited)
|
|
852
|
-
return
|
|
746
|
+
if (result.unlimited)
|
|
747
|
+
return `Copilot ${unit} Unlimited`;
|
|
748
|
+
if (result.total !== undefined) {
|
|
749
|
+
return `Copilot ${unit} ${result.used}/${result.total}`;
|
|
853
750
|
}
|
|
854
|
-
|
|
855
|
-
return `Copilot ${result.used}/${result.total} (${percentUsed}%)`;
|
|
751
|
+
return `Copilot ${unit} ${result.used} used`;
|
|
856
752
|
}
|
|
857
753
|
//# sourceMappingURL=copilot.js.map
|