@slkiser/opencode-quota 4.0.1 → 4.2.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 +94 -28
- 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 +73 -22
- package/dist/lib/api-key-resolver.d.ts.map +1 -1
- package/dist/lib/api-key-resolver.js +116 -39
- 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-config.d.ts.map +1 -1
- package/dist/lib/chutes-config.js +2 -4
- package/dist/lib/chutes-config.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 +378 -79
- package/dist/lib/copilot.js.map +1 -1
- package/dist/lib/deepseek-auth.d.ts.map +1 -1
- package/dist/lib/deepseek-auth.js +3 -7
- package/dist/lib/deepseek-auth.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 +14 -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/glm-coding-plan.d.ts +18 -0
- package/dist/lib/glm-coding-plan.d.ts.map +1 -0
- package/dist/lib/glm-coding-plan.js +68 -0
- package/dist/lib/glm-coding-plan.js.map +1 -0
- package/dist/lib/google-agy-companion.d.ts +4 -25
- package/dist/lib/google-agy-companion.d.ts.map +1 -1
- package/dist/lib/google-agy-companion.js +47 -272
- package/dist/lib/google-agy-companion.js.map +1 -1
- package/dist/lib/google-agy.d.ts.map +1 -1
- package/dist/lib/google-agy.js +204 -111
- package/dist/lib/google-agy.js.map +1 -1
- package/dist/lib/google-antigravity-companion.d.ts +4 -25
- package/dist/lib/google-antigravity-companion.d.ts.map +1 -1
- package/dist/lib/google-antigravity-companion.js +44 -283
- package/dist/lib/google-antigravity-companion.js.map +1 -1
- package/dist/lib/google-companion-credentials.d.ts +70 -0
- package/dist/lib/google-companion-credentials.d.ts.map +1 -0
- package/dist/lib/google-companion-credentials.js +265 -0
- package/dist/lib/google-companion-credentials.js.map +1 -0
- package/dist/lib/google-gemini-cli-companion.d.ts +4 -25
- package/dist/lib/google-gemini-cli-companion.d.ts.map +1 -1
- package/dist/lib/google-gemini-cli-companion.js +47 -258
- package/dist/lib/google-gemini-cli-companion.js.map +1 -1
- package/dist/lib/google-gemini-cli.d.ts.map +1 -1
- package/dist/lib/google-gemini-cli.js +55 -61
- 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 +80 -84
- 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-auth.d.ts +3 -26
- package/dist/lib/kimi-auth.d.ts.map +1 -1
- package/dist/lib/kimi-auth.js +24 -91
- package/dist/lib/kimi-auth.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/map-with-concurrency.d.ts +2 -0
- package/dist/lib/map-with-concurrency.d.ts.map +1 -0
- package/dist/lib/map-with-concurrency.js +17 -0
- package/dist/lib/map-with-concurrency.js.map +1 -0
- 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-config.d.ts.map +1 -1
- package/dist/lib/nanogpt-config.js +2 -4
- package/dist/lib/nanogpt-config.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/openrouter.d.ts +13 -0
- package/dist/lib/openrouter.d.ts.map +1 -0
- package/dist/lib/openrouter.js +41 -0
- package/dist/lib/openrouter.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 +300 -1
- package/dist/lib/provider-metadata.d.ts.map +1 -1
- package/dist/lib/provider-metadata.js +332 -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 +73 -39
- 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-providers-remote.d.ts +0 -1
- package/dist/lib/quota-providers-remote.d.ts.map +1 -1
- package/dist/lib/quota-providers-remote.js +3 -16
- package/dist/lib/quota-providers-remote.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 +118 -127
- 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 +7 -17
- package/dist/lib/quota-status.d.ts.map +1 -1
- package/dist/lib/quota-status.js +123 -948
- 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-config.d.ts.map +1 -1
- package/dist/lib/synthetic-config.js +2 -4
- package/dist/lib/synthetic-config.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 +72 -8
- 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-auth.d.ts +3 -26
- package/dist/lib/zai-auth.d.ts.map +1 -1
- package/dist/lib/zai-auth.js +24 -94
- package/dist/lib/zai-auth.js.map +1 -1
- package/dist/lib/zai.d.ts +1 -8
- package/dist/lib/zai.d.ts.map +1 -1
- package/dist/lib/zai.js +11 -91
- package/dist/lib/zai.js.map +1 -1
- package/dist/lib/zhipu-auth.d.ts +3 -26
- package/dist/lib/zhipu-auth.d.ts.map +1 -1
- package/dist/lib/zhipu-auth.js +24 -94
- package/dist/lib/zhipu-auth.js.map +1 -1
- package/dist/lib/zhipu.d.ts +1 -8
- package/dist/lib/zhipu.d.ts.map +1 -1
- package/dist/lib/zhipu.js +10 -84
- 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/alibaba-coding-plan.d.ts.map +1 -1
- package/dist/providers/alibaba-coding-plan.js +33 -6
- package/dist/providers/alibaba-coding-plan.js.map +1 -1
- package/dist/providers/anthropic.d.ts.map +1 -1
- package/dist/providers/anthropic.js +35 -7
- package/dist/providers/anthropic.js.map +1 -1
- package/dist/providers/chutes.d.ts.map +1 -1
- package/dist/providers/chutes.js +10 -3
- package/dist/providers/chutes.js.map +1 -1
- package/dist/providers/copilot.d.ts.map +1 -1
- package/dist/providers/copilot.js +102 -31
- package/dist/providers/copilot.js.map +1 -1
- package/dist/providers/cursor.d.ts.map +1 -1
- package/dist/providers/cursor.js +34 -6
- package/dist/providers/cursor.js.map +1 -1
- package/dist/providers/deepseek.d.ts.map +1 -1
- package/dist/providers/deepseek.js +10 -3
- package/dist/providers/deepseek.js.map +1 -1
- package/dist/providers/glm-coding-plan-provider.d.ts +44 -0
- package/dist/providers/glm-coding-plan-provider.d.ts.map +1 -0
- package/dist/providers/glm-coding-plan-provider.js +62 -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 +83 -42
- 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 +31 -6
- 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 +24 -6
- 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 +27 -8
- package/dist/providers/kimi-code.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 +127 -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 +68 -32
- package/dist/providers/minimax-coding-plan.js.map +1 -1
- package/dist/providers/nanogpt.d.ts.map +1 -1
- package/dist/providers/nanogpt.js +41 -4
- package/dist/providers/nanogpt.js.map +1 -1
- package/dist/providers/openai.d.ts.map +1 -1
- package/dist/providers/openai.js +20 -3
- package/dist/providers/openai.js.map +1 -1
- package/dist/providers/opencode-go.d.ts.map +1 -1
- package/dist/providers/opencode-go.js +33 -15
- package/dist/providers/opencode-go.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 +99 -0
- package/dist/providers/opencode-zen.js.map +1 -0
- package/dist/providers/openrouter.d.ts +6 -0
- package/dist/providers/openrouter.d.ts.map +1 -0
- package/dist/providers/openrouter.js +25 -0
- package/dist/providers/openrouter.js.map +1 -0
- package/dist/providers/quota-providers.d.ts.map +1 -1
- package/dist/providers/quota-providers.js +3 -3
- package/dist/providers/quota-providers.js.map +1 -1
- package/dist/providers/qwen-code.d.ts.map +1 -1
- package/dist/providers/qwen-code.js +19 -5
- package/dist/providers/qwen-code.js.map +1 -1
- package/dist/providers/registry.d.ts.map +1 -1
- package/dist/providers/registry.js +8 -0
- package/dist/providers/registry.js.map +1 -1
- package/dist/providers/result-helpers.d.ts +29 -1
- package/dist/providers/result-helpers.d.ts.map +1 -1
- package/dist/providers/result-helpers.js +69 -0
- package/dist/providers/result-helpers.js.map +1 -1
- package/dist/providers/synthetic.d.ts.map +1 -1
- package/dist/providers/synthetic.js +10 -3
- package/dist/providers/synthetic.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 +11 -46
- 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 +11 -46
- 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,11 +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
|
|
19
|
-
pro: 1500,
|
|
20
|
-
"pro+": 7000,
|
|
21
|
-
max: 20000,
|
|
22
|
-
};
|
|
18
|
+
const COPILOT_INTERNAL_USER_PATH = "/copilot_internal/user";
|
|
23
19
|
const LEGACY_PREMIUM_REQUEST_TOTALS = {
|
|
24
20
|
pro: 300,
|
|
25
21
|
"pro+": 1500,
|
|
@@ -36,6 +32,61 @@ function dedupeStrings(values) {
|
|
|
36
32
|
}
|
|
37
33
|
return out;
|
|
38
34
|
}
|
|
35
|
+
function validateEnterpriseHost(value) {
|
|
36
|
+
if (value === undefined || value === null)
|
|
37
|
+
return {};
|
|
38
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
39
|
+
return { error: "enterpriseUrl must be a non-empty string when provided" };
|
|
40
|
+
}
|
|
41
|
+
const raw = value.trim();
|
|
42
|
+
const invalid = {
|
|
43
|
+
error: "enterpriseUrl must be a hostname or host-only HTTPS URL ending in .ghe.com (without api. prefix, port, path, query, fragment, or userinfo)",
|
|
44
|
+
};
|
|
45
|
+
if (!/^[\x21-\x7e]+$/.test(raw) || raw.includes("*"))
|
|
46
|
+
return invalid;
|
|
47
|
+
let host;
|
|
48
|
+
if (raw.includes("://")) {
|
|
49
|
+
const authority = raw.slice(raw.indexOf("//") + 2).split(/[/?#]/u, 1)[0] ?? "";
|
|
50
|
+
if (authority.includes(":"))
|
|
51
|
+
return invalid;
|
|
52
|
+
let url;
|
|
53
|
+
try {
|
|
54
|
+
url = new URL(raw);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return invalid;
|
|
58
|
+
}
|
|
59
|
+
if (url.protocol !== "https:" ||
|
|
60
|
+
url.username ||
|
|
61
|
+
url.password ||
|
|
62
|
+
url.port ||
|
|
63
|
+
(url.pathname !== "" && url.pathname !== "/") ||
|
|
64
|
+
url.search ||
|
|
65
|
+
url.hash) {
|
|
66
|
+
return invalid;
|
|
67
|
+
}
|
|
68
|
+
host = url.hostname.toLowerCase();
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
if (/[\/:@?#]/.test(raw))
|
|
72
|
+
return invalid;
|
|
73
|
+
host = raw.toLowerCase();
|
|
74
|
+
}
|
|
75
|
+
if (host.length > 253 || host.startsWith("api.") || !host.endsWith(".ghe.com")) {
|
|
76
|
+
return invalid;
|
|
77
|
+
}
|
|
78
|
+
const labels = host.split(".");
|
|
79
|
+
if (labels.some((label) => label.length === 0 || label.length > 63 || !/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(label))) {
|
|
80
|
+
return invalid;
|
|
81
|
+
}
|
|
82
|
+
return { host };
|
|
83
|
+
}
|
|
84
|
+
function getApiBaseUrl(enterpriseHost) {
|
|
85
|
+
return enterpriseHost ? `https://api.${enterpriseHost}` : GITHUB_API_BASE_URL;
|
|
86
|
+
}
|
|
87
|
+
function getApiHost(apiBaseUrl) {
|
|
88
|
+
return new URL(apiBaseUrl).hostname;
|
|
89
|
+
}
|
|
39
90
|
function classifyPatTokenKind(token) {
|
|
40
91
|
if (token.startsWith("github_pat_"))
|
|
41
92
|
return "github_pat";
|
|
@@ -203,6 +254,9 @@ function validateQuotaConfig(raw) {
|
|
|
203
254
|
return value.trim();
|
|
204
255
|
};
|
|
205
256
|
try {
|
|
257
|
+
const enterpriseHost = validateEnterpriseHost(obj.enterpriseUrl);
|
|
258
|
+
if (enterpriseHost.error)
|
|
259
|
+
throw new Error(enterpriseHost.error);
|
|
206
260
|
const config = {
|
|
207
261
|
token,
|
|
208
262
|
tier: tier,
|
|
@@ -210,6 +264,7 @@ function validateQuotaConfig(raw) {
|
|
|
210
264
|
username: readOptionalString("username"),
|
|
211
265
|
organization: readOptionalString("organization"),
|
|
212
266
|
enterprise: readOptionalString("enterprise"),
|
|
267
|
+
enterpriseUrl: enterpriseHost.host,
|
|
213
268
|
};
|
|
214
269
|
const resolved = resolvePatBillingTarget(config);
|
|
215
270
|
if (!resolved.target) {
|
|
@@ -256,6 +311,9 @@ export function readQuotaConfigWithMeta() {
|
|
|
256
311
|
}
|
|
257
312
|
return { state: "absent", checkedPaths };
|
|
258
313
|
}
|
|
314
|
+
function getCopilotOAuthToken(auth) {
|
|
315
|
+
return auth?.access?.trim() || auth?.refresh?.trim() || null;
|
|
316
|
+
}
|
|
259
317
|
function selectCopilotAuth(authData) {
|
|
260
318
|
if (!authData)
|
|
261
319
|
return { auth: null, keyName: null };
|
|
@@ -268,7 +326,7 @@ function selectCopilotAuth(authData) {
|
|
|
268
326
|
for (const [keyName, auth] of candidates) {
|
|
269
327
|
if (!auth || auth.type !== "oauth")
|
|
270
328
|
continue;
|
|
271
|
-
if (!auth
|
|
329
|
+
if (!getCopilotOAuthToken(auth))
|
|
272
330
|
continue;
|
|
273
331
|
return { auth, keyName };
|
|
274
332
|
}
|
|
@@ -281,14 +339,17 @@ function getRemainingTotalsState(target, config) {
|
|
|
281
339
|
return "not_available_from_org_usage";
|
|
282
340
|
if (target.scope === "enterprise")
|
|
283
341
|
return "not_available_from_enterprise_usage";
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
return
|
|
342
|
+
if (config.billingModel !== "legacy_premium_requests") {
|
|
343
|
+
return "value_only_without_denominator";
|
|
344
|
+
}
|
|
345
|
+
return LEGACY_PREMIUM_REQUEST_TOTALS[config.tier]
|
|
346
|
+
? "available"
|
|
347
|
+
: "value_only_without_denominator";
|
|
288
348
|
}
|
|
289
349
|
export function getCopilotQuotaAuthDiagnostics(authData) {
|
|
290
350
|
const pat = readQuotaConfigWithMeta();
|
|
291
351
|
const { auth, keyName } = selectCopilotAuth(authData);
|
|
352
|
+
const oauthEnterpriseHost = validateEnterpriseHost(auth?.enterpriseUrl);
|
|
292
353
|
const resolved = pat.state === "valid" && pat.config
|
|
293
354
|
? resolvePatBillingTarget(pat.config)
|
|
294
355
|
: { target: null };
|
|
@@ -301,46 +362,88 @@ export function getCopilotQuotaAuthDiagnostics(authData) {
|
|
|
301
362
|
: auth
|
|
302
363
|
? "oauth"
|
|
303
364
|
: "none";
|
|
304
|
-
const
|
|
305
|
-
|
|
365
|
+
const effectiveEnterpriseHost = effectiveSource === "pat"
|
|
366
|
+
? pat.config?.enterpriseUrl
|
|
367
|
+
: effectiveSource === "oauth"
|
|
368
|
+
? oauthEnterpriseHost.host
|
|
369
|
+
: undefined;
|
|
370
|
+
const enterpriseHostError = effectiveSource === "pat" && pat.error?.startsWith("enterpriseUrl")
|
|
371
|
+
? pat.error
|
|
372
|
+
: effectiveSource === "oauth"
|
|
373
|
+
? oauthEnterpriseHost.error
|
|
374
|
+
: undefined;
|
|
375
|
+
const deployment = effectiveSource === "none"
|
|
306
376
|
? "none"
|
|
307
|
-
:
|
|
377
|
+
: enterpriseHostError
|
|
378
|
+
? "invalid"
|
|
379
|
+
: effectiveEnterpriseHost
|
|
380
|
+
? "ghe.com"
|
|
381
|
+
: effectiveSource === "pat" && pat.state !== "valid"
|
|
382
|
+
? "none"
|
|
383
|
+
: "github.com";
|
|
384
|
+
const apiBaseUrl = deployment === "github.com" || deployment === "ghe.com"
|
|
385
|
+
? getApiBaseUrl(effectiveEnterpriseHost)
|
|
386
|
+
: null;
|
|
387
|
+
const billingModel = pat.config?.billingModel ?? "ai_credits";
|
|
388
|
+
const oauthAvailable = effectiveSource === "oauth" && Boolean(auth) && !oauthEnterpriseHost.error;
|
|
389
|
+
const quotaApi = pat.state === "valid"
|
|
390
|
+
? billingModel === "legacy_premium_requests"
|
|
308
391
|
? "github_legacy_premium_request_api"
|
|
309
|
-
: "github_ai_credit_api"
|
|
392
|
+
: "github_ai_credit_api"
|
|
393
|
+
: oauthAvailable
|
|
394
|
+
? "copilot_internal_user"
|
|
395
|
+
: "none";
|
|
396
|
+
const billingMode = resolved.target?.scope === "organization"
|
|
397
|
+
? "organization_usage"
|
|
398
|
+
: resolved.target?.scope === "enterprise"
|
|
399
|
+
? "enterprise_usage"
|
|
400
|
+
: resolved.target?.scope === "user" || oauthAvailable
|
|
401
|
+
? "user_quota"
|
|
402
|
+
: "none";
|
|
310
403
|
return {
|
|
311
404
|
pat,
|
|
312
405
|
oauth: {
|
|
313
406
|
configured: Boolean(auth),
|
|
314
407
|
keyName,
|
|
315
|
-
hasRefreshToken: Boolean(auth?.refresh),
|
|
408
|
+
hasRefreshToken: Boolean(auth?.refresh?.trim()),
|
|
316
409
|
hasAccessToken: Boolean(auth?.access?.trim()),
|
|
410
|
+
hasEnterpriseUrl: auth?.enterpriseUrl !== undefined,
|
|
317
411
|
},
|
|
412
|
+
deployment,
|
|
413
|
+
apiHost: apiBaseUrl ? getApiHost(apiBaseUrl) : null,
|
|
414
|
+
enterpriseHostSource: effectiveSource === "pat" && pat.config?.enterpriseUrl
|
|
415
|
+
? "pat"
|
|
416
|
+
: effectiveSource === "oauth" && oauthEnterpriseHost.host
|
|
417
|
+
? "oauth"
|
|
418
|
+
: "none",
|
|
419
|
+
enterpriseHostError,
|
|
318
420
|
effectiveSource,
|
|
319
421
|
override: patBlocksOAuth && auth ? "pat_overrides_oauth" : "none",
|
|
320
422
|
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),
|
|
423
|
+
billingMode,
|
|
424
|
+
billingScope: resolved.target?.scope ?? (oauthAvailable ? "user" : "none"),
|
|
425
|
+
billingApiAccessLikely: oauthAvailable ||
|
|
426
|
+
(pat.state === "valid" && Boolean(resolved.target) && !resolved.error && !compatibilityError),
|
|
427
|
+
remainingTotalsState: oauthAvailable
|
|
428
|
+
? "reported_by_copilot_internal_user"
|
|
429
|
+
: getRemainingTotalsState(resolved.target, pat.config),
|
|
331
430
|
queryPeriod: resolved.target?.billingPeriod,
|
|
332
431
|
usernameFilter: pat.config?.username,
|
|
333
432
|
billingTargetError: resolved.error,
|
|
334
433
|
tokenCompatibilityError: compatibilityError ?? undefined,
|
|
335
434
|
billingModel,
|
|
336
|
-
budgetApi: billingModel !== "ai_credits"
|
|
435
|
+
budgetApi: billingModel !== "ai_credits" || oauthAvailable
|
|
337
436
|
? "not_available"
|
|
338
437
|
: resolved.target?.scope === "organization"
|
|
339
438
|
? "organization_budgets"
|
|
340
439
|
: resolved.target?.scope === "enterprise"
|
|
341
440
|
? "enterprise_budgets"
|
|
342
441
|
: "not_available",
|
|
343
|
-
oauthAccountingState: auth
|
|
442
|
+
oauthAccountingState: !auth
|
|
443
|
+
? "not_configured"
|
|
444
|
+
: oauthEnterpriseHost.error
|
|
445
|
+
? "invalid_enterprise_host"
|
|
446
|
+
: "available_via_copilot_internal_user",
|
|
344
447
|
};
|
|
345
448
|
}
|
|
346
449
|
function buildGitHubRestHeaders(token) {
|
|
@@ -351,41 +454,59 @@ function buildGitHubRestHeaders(token) {
|
|
|
351
454
|
"User-Agent": USER_AGENT,
|
|
352
455
|
};
|
|
353
456
|
}
|
|
354
|
-
|
|
457
|
+
function redactGitHubErrorText(text, secrets) {
|
|
458
|
+
let sanitized = sanitizeDisplayText(text);
|
|
459
|
+
for (const secret of secrets) {
|
|
460
|
+
if (!secret)
|
|
461
|
+
continue;
|
|
462
|
+
sanitized = sanitized.split(secret).join("[redacted]");
|
|
463
|
+
const sanitizedSecret = sanitizeDisplayText(secret);
|
|
464
|
+
if (sanitizedSecret) {
|
|
465
|
+
sanitized = sanitized.split(sanitizedSecret).join("[redacted]");
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
return sanitized;
|
|
469
|
+
}
|
|
470
|
+
async function readGitHubRestErrorMessage(response, secrets) {
|
|
355
471
|
const text = await response.text();
|
|
356
472
|
try {
|
|
357
473
|
const parsed = JSON.parse(text);
|
|
358
474
|
const message = typeof parsed.message === "string" ? parsed.message : null;
|
|
359
475
|
const documentationUrl = typeof parsed.documentation_url === "string" ? parsed.documentation_url : null;
|
|
360
476
|
if (message && documentationUrl) {
|
|
361
|
-
return
|
|
477
|
+
return redactGitHubErrorText(`${message} (${documentationUrl})`, secrets);
|
|
362
478
|
}
|
|
363
479
|
if (message)
|
|
364
|
-
return
|
|
480
|
+
return redactGitHubErrorText(message, secrets);
|
|
365
481
|
}
|
|
366
482
|
catch {
|
|
367
483
|
// Fall through to a bounded plain-text snippet.
|
|
368
484
|
}
|
|
369
|
-
return sanitizeDisplaySnippet(text, 160);
|
|
485
|
+
return sanitizeDisplaySnippet(redactGitHubErrorText(text, secrets), 160);
|
|
370
486
|
}
|
|
371
487
|
async function fetchGitHubRestJson(url, token, requestTimeoutMs) {
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
488
|
+
return await fetchWithTimeout(url, {
|
|
489
|
+
request: { headers: buildGitHubRestHeaders(token) },
|
|
490
|
+
timeoutMs: requestTimeoutMs,
|
|
491
|
+
consume: async (response) => {
|
|
492
|
+
if (!response.ok) {
|
|
493
|
+
const message = await readGitHubRestErrorMessage(response, [token]);
|
|
494
|
+
const rateLimit = response.status === 403 && response.headers.get("x-ratelimit-remaining") === "0"
|
|
495
|
+
? " (GitHub API rate limit exhausted)"
|
|
496
|
+
: "";
|
|
497
|
+
throw new Error(`GitHub API error ${response.status}: ${message}${rateLimit}`);
|
|
498
|
+
}
|
|
499
|
+
try {
|
|
500
|
+
return (await response.json());
|
|
501
|
+
}
|
|
502
|
+
catch {
|
|
503
|
+
throw new Error("GitHub API returned malformed JSON");
|
|
504
|
+
}
|
|
505
|
+
},
|
|
506
|
+
});
|
|
386
507
|
}
|
|
387
|
-
async function resolveGitHubUsername(token, requestTimeoutMs) {
|
|
388
|
-
const response = await fetchGitHubRestJson(`${
|
|
508
|
+
async function resolveGitHubUsername(apiBaseUrl, token, requestTimeoutMs) {
|
|
509
|
+
const response = await fetchGitHubRestJson(`${apiBaseUrl}/user`, token, requestTimeoutMs);
|
|
389
510
|
const login = response.login?.trim();
|
|
390
511
|
if (!login) {
|
|
391
512
|
throw new Error("GitHub /user response did not include a login");
|
|
@@ -404,27 +525,27 @@ function buildBillingQuery(target) {
|
|
|
404
525
|
}
|
|
405
526
|
return query;
|
|
406
527
|
}
|
|
407
|
-
function getBillingUsageUrl(target, billingModel) {
|
|
528
|
+
function getBillingUsageUrl(apiBaseUrl, target, billingModel) {
|
|
408
529
|
const report = billingModel === "legacy_premium_requests" ? "premium_request" : "ai_credit";
|
|
409
530
|
const query = buildBillingQuery(target);
|
|
410
531
|
if (target.scope === "enterprise") {
|
|
411
|
-
return `${
|
|
532
|
+
return `${apiBaseUrl}/enterprises/${encodeURIComponent(target.enterprise)}/settings/billing/${report}/usage?${query}`;
|
|
412
533
|
}
|
|
413
534
|
if (target.scope === "organization") {
|
|
414
|
-
return `${
|
|
535
|
+
return `${apiBaseUrl}/organizations/${encodeURIComponent(target.organization)}/settings/billing/${report}/usage?${query}`;
|
|
415
536
|
}
|
|
416
|
-
return `${
|
|
537
|
+
return `${apiBaseUrl}/users/${encodeURIComponent(target.username)}/settings/billing/${report}/usage?${query}`;
|
|
417
538
|
}
|
|
418
539
|
async function fetchBillingUsage(params) {
|
|
419
540
|
const target = params.target.scope === "user"
|
|
420
541
|
? {
|
|
421
542
|
...params.target,
|
|
422
543
|
username: params.target.username ??
|
|
423
|
-
(await resolveGitHubUsername(params.token, params.requestTimeoutMs)),
|
|
544
|
+
(await resolveGitHubUsername(params.apiBaseUrl, params.token, params.requestTimeoutMs)),
|
|
424
545
|
}
|
|
425
546
|
: params.target;
|
|
426
547
|
return {
|
|
427
|
-
response: await fetchGitHubRestJson(getBillingUsageUrl(target, params.billingModel), params.token, params.requestTimeoutMs),
|
|
548
|
+
response: await fetchGitHubRestJson(getBillingUsageUrl(params.apiBaseUrl, target, params.billingModel), params.token, params.requestTimeoutMs),
|
|
428
549
|
target,
|
|
429
550
|
};
|
|
430
551
|
}
|
|
@@ -445,12 +566,17 @@ function getUsageItems(response) {
|
|
|
445
566
|
function readFiniteNumber(value) {
|
|
446
567
|
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
447
568
|
}
|
|
569
|
+
function quantitiesDiffer(left, right) {
|
|
570
|
+
return (Math.abs(left - right) > Number.EPSILON * 16 * Math.max(1, Math.abs(left), Math.abs(right)));
|
|
571
|
+
}
|
|
448
572
|
function isAiCreditItem(item) {
|
|
449
573
|
const text = [item.product, item.sku, item.unitType, item.unit_type]
|
|
450
574
|
.filter((value) => typeof value === "string")
|
|
451
575
|
.join(" ")
|
|
452
576
|
.toLowerCase();
|
|
453
|
-
|
|
577
|
+
const identifiesCopilot = text.includes("copilot");
|
|
578
|
+
const identifiesAiCredits = text.includes("ai credit") || text.includes("ai-credit");
|
|
579
|
+
return identifiesCopilot && identifiesAiCredits;
|
|
454
580
|
}
|
|
455
581
|
function isPremiumRequestItem(item) {
|
|
456
582
|
const text = [item.product, item.sku, item.unitType, item.unit_type]
|
|
@@ -463,28 +589,64 @@ function parseAiCreditTotals(response) {
|
|
|
463
589
|
const items = getUsageItems(response);
|
|
464
590
|
const matches = items.filter(isAiCreditItem);
|
|
465
591
|
if (matches.length === 0 && items.length > 0) {
|
|
466
|
-
throw new Error("GitHub billing response did not contain
|
|
592
|
+
throw new Error("GitHub billing response did not contain a Copilot AI Credit usage item");
|
|
467
593
|
}
|
|
468
594
|
let used = 0;
|
|
469
595
|
let includedUsed = 0;
|
|
470
596
|
let billedUsed = 0;
|
|
471
597
|
let billedAmountUsd = 0;
|
|
472
598
|
let hasBilledAmount = false;
|
|
599
|
+
let authority = "provider_reported";
|
|
473
600
|
for (const item of matches) {
|
|
474
601
|
const gross = readFiniteNumber(item.grossQuantity ?? item.gross_quantity);
|
|
475
602
|
const discount = readFiniteNumber(item.discountQuantity ?? item.discount_quantity);
|
|
476
603
|
const net = readFiniteNumber(item.netQuantity ?? item.net_quantity);
|
|
477
|
-
|
|
478
|
-
|
|
604
|
+
const quantities = [gross, discount, net].filter((value) => value !== undefined);
|
|
605
|
+
if (quantities.length === 0) {
|
|
606
|
+
throw new Error("GitHub Copilot AI Credit usage item did not include quantity fields");
|
|
607
|
+
}
|
|
608
|
+
if (quantities.some((value) => value < 0)) {
|
|
609
|
+
throw new Error("GitHub Copilot AI Credit usage item contained a negative quantity");
|
|
610
|
+
}
|
|
611
|
+
if (gross !== undefined &&
|
|
612
|
+
discount !== undefined &&
|
|
613
|
+
net !== undefined &&
|
|
614
|
+
quantitiesDiffer(gross, discount + net)) {
|
|
615
|
+
throw new Error("GitHub Copilot AI Credit usage item contained inconsistent quantities");
|
|
479
616
|
}
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
617
|
+
let normalizedGross = gross;
|
|
618
|
+
let normalizedIncluded = discount;
|
|
619
|
+
let normalizedBilled = net;
|
|
620
|
+
if (gross !== undefined && discount !== undefined && net === undefined) {
|
|
621
|
+
if (discount > gross && quantitiesDiffer(gross, discount)) {
|
|
622
|
+
throw new Error("GitHub Copilot AI Credit usage item contained inconsistent quantities");
|
|
623
|
+
}
|
|
624
|
+
normalizedBilled = Math.max(0, gross - discount);
|
|
625
|
+
}
|
|
626
|
+
else if (gross !== undefined && discount === undefined && net !== undefined) {
|
|
627
|
+
if (net > gross && quantitiesDiffer(gross, net)) {
|
|
628
|
+
throw new Error("GitHub Copilot AI Credit usage item contained inconsistent quantities");
|
|
629
|
+
}
|
|
630
|
+
normalizedIncluded = Math.max(0, gross - net);
|
|
631
|
+
}
|
|
632
|
+
else if (gross === undefined && discount !== undefined && net !== undefined) {
|
|
633
|
+
normalizedGross = discount + net;
|
|
634
|
+
}
|
|
635
|
+
normalizedIncluded ??= 0;
|
|
636
|
+
normalizedBilled ??= Math.max(0, (normalizedGross ?? 0) - normalizedIncluded);
|
|
637
|
+
normalizedGross ??= normalizedIncluded + normalizedBilled;
|
|
638
|
+
used += normalizedGross;
|
|
483
639
|
includedUsed += normalizedIncluded;
|
|
484
640
|
billedUsed += normalizedBilled;
|
|
641
|
+
if (gross === undefined || discount === undefined || net === undefined) {
|
|
642
|
+
authority = "locally_derived";
|
|
643
|
+
}
|
|
485
644
|
const netAmount = readFiniteNumber(item.netAmount ?? item.net_amount);
|
|
486
645
|
if (netAmount !== undefined) {
|
|
487
|
-
|
|
646
|
+
if (netAmount < 0) {
|
|
647
|
+
throw new Error("GitHub Copilot AI Credit usage item contained a negative net amount");
|
|
648
|
+
}
|
|
649
|
+
billedAmountUsd += netAmount;
|
|
488
650
|
hasBilledAmount = true;
|
|
489
651
|
}
|
|
490
652
|
}
|
|
@@ -493,6 +655,7 @@ function parseAiCreditTotals(response) {
|
|
|
493
655
|
includedUsed,
|
|
494
656
|
billedUsed,
|
|
495
657
|
billedAmountUsd: hasBilledAmount ? billedAmountUsd : undefined,
|
|
658
|
+
authority,
|
|
496
659
|
};
|
|
497
660
|
}
|
|
498
661
|
function parseLegacyPremiumRequestUsage(response) {
|
|
@@ -562,10 +725,10 @@ function budgetSpecificity(budget) {
|
|
|
562
725
|
return 0;
|
|
563
726
|
}
|
|
564
727
|
}
|
|
565
|
-
function getBudgetsUrl(target, page) {
|
|
728
|
+
function getBudgetsUrl(apiBaseUrl, target, page) {
|
|
566
729
|
const base = target.scope === "organization"
|
|
567
|
-
? `${
|
|
568
|
-
: `${
|
|
730
|
+
? `${apiBaseUrl}/organizations/${encodeURIComponent(target.organization)}/settings/billing/budgets`
|
|
731
|
+
: `${apiBaseUrl}/enterprises/${encodeURIComponent(target.enterprise)}/settings/billing/budgets`;
|
|
569
732
|
const query = new URLSearchParams({ page: String(page), per_page: "100" });
|
|
570
733
|
if (target.username)
|
|
571
734
|
query.set("user", target.username);
|
|
@@ -574,7 +737,7 @@ function getBudgetsUrl(target, page) {
|
|
|
574
737
|
async function fetchApplicableBudget(params) {
|
|
575
738
|
const budgets = [];
|
|
576
739
|
for (let page = 1; page <= 100; page += 1) {
|
|
577
|
-
const response = await fetchGitHubRestJson(getBudgetsUrl(params.target, page), params.token, params.requestTimeoutMs);
|
|
740
|
+
const response = await fetchGitHubRestJson(getBudgetsUrl(params.apiBaseUrl, params.target, page), params.token, params.requestTimeoutMs);
|
|
578
741
|
if (!Array.isArray(response.budgets)) {
|
|
579
742
|
throw new Error("GitHub budgets response did not include a budgets array");
|
|
580
743
|
}
|
|
@@ -602,6 +765,7 @@ async function fetchApplicableBudget(params) {
|
|
|
602
765
|
spentUsd: params.spentUsd,
|
|
603
766
|
scope: selected.budget_scope ?? "unknown",
|
|
604
767
|
percentRemaining,
|
|
768
|
+
authority: percentRemaining === undefined ? "provider_reported" : "locally_derived",
|
|
605
769
|
};
|
|
606
770
|
}
|
|
607
771
|
function makeBudgetWarning(error) {
|
|
@@ -610,13 +774,13 @@ function makeBudgetWarning(error) {
|
|
|
610
774
|
}
|
|
611
775
|
async function toAiCreditResult(params) {
|
|
612
776
|
const totals = parseAiCreditTotals(params.response);
|
|
613
|
-
const resetTimeIso = getApproxNextResetIso();
|
|
614
777
|
const period = getResponsePeriod(params.response, params.target.billingPeriod);
|
|
615
778
|
let budget;
|
|
616
779
|
const warnings = [];
|
|
617
780
|
if (params.target.scope !== "user") {
|
|
618
781
|
try {
|
|
619
782
|
budget = await fetchApplicableBudget({
|
|
783
|
+
apiBaseUrl: params.apiBaseUrl,
|
|
620
784
|
token: params.token,
|
|
621
785
|
target: params.target,
|
|
622
786
|
spentUsd: totals.billedAmountUsd,
|
|
@@ -638,7 +802,6 @@ async function toAiCreditResult(params) {
|
|
|
638
802
|
...totals,
|
|
639
803
|
budget,
|
|
640
804
|
warnings: warnings.length ? warnings : undefined,
|
|
641
|
-
resetTimeIso,
|
|
642
805
|
};
|
|
643
806
|
}
|
|
644
807
|
if (params.target.scope === "enterprise") {
|
|
@@ -653,19 +816,14 @@ async function toAiCreditResult(params) {
|
|
|
653
816
|
...totals,
|
|
654
817
|
budget,
|
|
655
818
|
warnings: warnings.length ? warnings : undefined,
|
|
656
|
-
resetTimeIso,
|
|
657
819
|
};
|
|
658
820
|
}
|
|
659
|
-
const total = PERSONAL_AI_CREDIT_TOTALS[params.config.tier];
|
|
660
821
|
return {
|
|
661
822
|
success: true,
|
|
662
823
|
mode: "user_quota",
|
|
663
824
|
unit: "ai_credits",
|
|
825
|
+
period,
|
|
664
826
|
...totals,
|
|
665
|
-
total,
|
|
666
|
-
percentRemaining: total ? computePercentRemainingFromUsed(totals.used, total) : undefined,
|
|
667
|
-
plan: params.config.tier,
|
|
668
|
-
resetTimeIso,
|
|
669
827
|
};
|
|
670
828
|
}
|
|
671
829
|
function toLegacyResult(response, config) {
|
|
@@ -676,6 +834,7 @@ function toLegacyResult(response, config) {
|
|
|
676
834
|
mode: "user_quota",
|
|
677
835
|
unit: "premium_requests",
|
|
678
836
|
used,
|
|
837
|
+
authority: "locally_derived",
|
|
679
838
|
total,
|
|
680
839
|
percentRemaining: total ? computePercentRemainingFromUsed(used, total) : undefined,
|
|
681
840
|
plan: config.tier,
|
|
@@ -685,12 +844,126 @@ function toLegacyResult(response, config) {
|
|
|
685
844
|
function toQuotaError(message) {
|
|
686
845
|
return { success: false, error: message };
|
|
687
846
|
}
|
|
847
|
+
function buildCopilotInternalHeaders(token) {
|
|
848
|
+
return {
|
|
849
|
+
Accept: "application/json",
|
|
850
|
+
Authorization: `token ${token}`,
|
|
851
|
+
"Editor-Version": "vscode/1.96.2",
|
|
852
|
+
"Editor-Plugin-Version": "copilot-chat/0.26.7",
|
|
853
|
+
"User-Agent": "GitHubCopilotChat/0.26.7",
|
|
854
|
+
"X-GitHub-Api-Version": "2025-04-01",
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
function normalizeCopilotPlan(value) {
|
|
858
|
+
if (typeof value !== "string")
|
|
859
|
+
return undefined;
|
|
860
|
+
const plan = sanitizeDisplayText(value).replace(/\s+/gu, " ").trim().slice(0, 64);
|
|
861
|
+
return plan || undefined;
|
|
862
|
+
}
|
|
863
|
+
function normalizeResetTime(value) {
|
|
864
|
+
if (typeof value !== "string" || !value.trim())
|
|
865
|
+
return undefined;
|
|
866
|
+
const timestamp = Date.parse(value);
|
|
867
|
+
return Number.isFinite(timestamp) ? new Date(timestamp).toISOString() : undefined;
|
|
868
|
+
}
|
|
869
|
+
function parseCopilotInternalUser(response) {
|
|
870
|
+
const plan = normalizeCopilotPlan(response.copilot_plan);
|
|
871
|
+
const resetTimeIso = normalizeResetTime(response.quota_reset_date_utc ?? response.quota_reset_date);
|
|
872
|
+
const snapshot = response.quota_snapshots?.premium_interactions;
|
|
873
|
+
const entitlement = readFiniteNumber(snapshot?.entitlement);
|
|
874
|
+
const remaining = readFiniteNumber(snapshot?.quota_remaining ?? snapshot?.remaining);
|
|
875
|
+
const reportedPercentRemaining = readFiniteNumber(snapshot?.percent_remaining);
|
|
876
|
+
const hasPercentRemaining = snapshot?.percent_remaining !== undefined;
|
|
877
|
+
const hasReportedPercent = reportedPercentRemaining !== undefined &&
|
|
878
|
+
reportedPercentRemaining >= 0 &&
|
|
879
|
+
reportedPercentRemaining <= 100;
|
|
880
|
+
const isPlaceholder = response.token_based_billing === true &&
|
|
881
|
+
(!snapshot ||
|
|
882
|
+
(snapshot.unlimited !== true &&
|
|
883
|
+
entitlement === 0 &&
|
|
884
|
+
remaining === 0 &&
|
|
885
|
+
(!hasReportedPercent || reportedPercentRemaining === 0)));
|
|
886
|
+
if (isPlaceholder) {
|
|
887
|
+
return {
|
|
888
|
+
success: true,
|
|
889
|
+
mode: "user_plan",
|
|
890
|
+
authority: "provider_reported",
|
|
891
|
+
plan,
|
|
892
|
+
resetTimeIso,
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
if (snapshot?.unlimited === true) {
|
|
896
|
+
return {
|
|
897
|
+
success: true,
|
|
898
|
+
mode: "user_quota",
|
|
899
|
+
unit: "premium_interactions",
|
|
900
|
+
used: 0,
|
|
901
|
+
authority: "provider_reported",
|
|
902
|
+
unlimited: true,
|
|
903
|
+
plan,
|
|
904
|
+
resetTimeIso,
|
|
905
|
+
};
|
|
906
|
+
}
|
|
907
|
+
if (entitlement !== undefined &&
|
|
908
|
+
entitlement > 0 &&
|
|
909
|
+
remaining !== undefined &&
|
|
910
|
+
remaining >= 0 &&
|
|
911
|
+
remaining <= entitlement) {
|
|
912
|
+
return {
|
|
913
|
+
success: true,
|
|
914
|
+
mode: "user_quota",
|
|
915
|
+
unit: "premium_interactions",
|
|
916
|
+
used: entitlement - remaining,
|
|
917
|
+
authority: "locally_derived",
|
|
918
|
+
total: entitlement,
|
|
919
|
+
percentRemaining: hasReportedPercent
|
|
920
|
+
? reportedPercentRemaining
|
|
921
|
+
: !hasPercentRemaining
|
|
922
|
+
? Math.min(100, Math.max(0, (remaining / entitlement) * 100))
|
|
923
|
+
: undefined,
|
|
924
|
+
plan,
|
|
925
|
+
resetTimeIso,
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
if (response.token_based_billing === true) {
|
|
929
|
+
return {
|
|
930
|
+
success: true,
|
|
931
|
+
mode: "user_plan",
|
|
932
|
+
authority: "provider_reported",
|
|
933
|
+
plan,
|
|
934
|
+
resetTimeIso,
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
throw new Error("GitHub Copilot user response did not include a usable quota snapshot");
|
|
938
|
+
}
|
|
939
|
+
async function fetchCopilotInternalUser(params) {
|
|
940
|
+
return await fetchWithTimeout(`${params.apiBaseUrl}${COPILOT_INTERNAL_USER_PATH}`, {
|
|
941
|
+
request: { headers: buildCopilotInternalHeaders(params.token) },
|
|
942
|
+
timeoutMs: params.requestTimeoutMs,
|
|
943
|
+
consume: async (response) => {
|
|
944
|
+
if (!response.ok) {
|
|
945
|
+
const message = await readGitHubRestErrorMessage(response, [params.token]);
|
|
946
|
+
throw new Error(`GitHub Copilot API error ${response.status}: ${message}`);
|
|
947
|
+
}
|
|
948
|
+
let parsed;
|
|
949
|
+
try {
|
|
950
|
+
parsed = (await response.json());
|
|
951
|
+
}
|
|
952
|
+
catch {
|
|
953
|
+
throw new Error("GitHub Copilot API returned malformed JSON");
|
|
954
|
+
}
|
|
955
|
+
if (!parsed || typeof parsed !== "object") {
|
|
956
|
+
throw new Error("GitHub Copilot API returned malformed JSON");
|
|
957
|
+
}
|
|
958
|
+
return parseCopilotInternalUser(parsed);
|
|
959
|
+
},
|
|
960
|
+
});
|
|
961
|
+
}
|
|
688
962
|
/**
|
|
689
963
|
* Query GitHub Copilot accounting.
|
|
690
964
|
*
|
|
691
|
-
* A
|
|
692
|
-
*
|
|
693
|
-
* because its billing permissions are not part of the documented contract.
|
|
965
|
+
* A trusted local PAT config remains authoritative. When it is absent, the
|
|
966
|
+
* OpenCode-managed Copilot OAuth token can query the per-user internal quota endpoint.
|
|
694
967
|
*/
|
|
695
968
|
export async function queryCopilotQuota(options = {}) {
|
|
696
969
|
const pat = readQuotaConfigWithMeta();
|
|
@@ -698,7 +971,24 @@ export async function queryCopilotQuota(options = {}) {
|
|
|
698
971
|
return toQuotaError(`Invalid copilot-quota-token.json: ${pat.error ?? "unknown error"}${pat.selectedPath ? ` (${pat.selectedPath})` : ""}`);
|
|
699
972
|
}
|
|
700
973
|
if (pat.state === "absent" || !pat.config) {
|
|
701
|
-
|
|
974
|
+
const { auth } = selectCopilotAuth(await readAuthFile());
|
|
975
|
+
const token = getCopilotOAuthToken(auth);
|
|
976
|
+
if (!auth || !token)
|
|
977
|
+
return null;
|
|
978
|
+
const enterpriseHost = validateEnterpriseHost(auth.enterpriseUrl);
|
|
979
|
+
if (enterpriseHost.error) {
|
|
980
|
+
return toQuotaError(`Invalid OpenCode Copilot enterpriseUrl: ${enterpriseHost.error}`);
|
|
981
|
+
}
|
|
982
|
+
try {
|
|
983
|
+
return await fetchCopilotInternalUser({
|
|
984
|
+
apiBaseUrl: getApiBaseUrl(enterpriseHost.host),
|
|
985
|
+
token,
|
|
986
|
+
requestTimeoutMs: options.requestTimeoutMs,
|
|
987
|
+
});
|
|
988
|
+
}
|
|
989
|
+
catch (error) {
|
|
990
|
+
return toQuotaError(error instanceof Error ? error.message : String(error));
|
|
991
|
+
}
|
|
702
992
|
}
|
|
703
993
|
const resolved = resolvePatBillingTarget(pat.config);
|
|
704
994
|
if (!resolved.target) {
|
|
@@ -709,7 +999,9 @@ export async function queryCopilotQuota(options = {}) {
|
|
|
709
999
|
return toQuotaError(compatibilityError);
|
|
710
1000
|
try {
|
|
711
1001
|
const billingModel = pat.config.billingModel ?? "ai_credits";
|
|
1002
|
+
const apiBaseUrl = getApiBaseUrl(pat.config.enterpriseUrl);
|
|
712
1003
|
const { response, target } = await fetchBillingUsage({
|
|
1004
|
+
apiBaseUrl,
|
|
713
1005
|
token: pat.config.token,
|
|
714
1006
|
target: resolved.target,
|
|
715
1007
|
billingModel,
|
|
@@ -718,9 +1010,9 @@ export async function queryCopilotQuota(options = {}) {
|
|
|
718
1010
|
return billingModel === "legacy_premium_requests"
|
|
719
1011
|
? toLegacyResult(response, pat.config)
|
|
720
1012
|
: await toAiCreditResult({
|
|
1013
|
+
apiBaseUrl,
|
|
721
1014
|
response,
|
|
722
1015
|
target,
|
|
723
|
-
config: pat.config,
|
|
724
1016
|
token: pat.config.token,
|
|
725
1017
|
requestTimeoutMs: options.requestTimeoutMs,
|
|
726
1018
|
});
|
|
@@ -736,7 +1028,14 @@ export async function hasCopilotQuotaRuntimeAvailable() {
|
|
|
736
1028
|
export function formatCopilotQuota(result) {
|
|
737
1029
|
if (!result || !result.success)
|
|
738
1030
|
return null;
|
|
739
|
-
|
|
1031
|
+
if (result.mode === "user_plan") {
|
|
1032
|
+
return result.plan ? `Copilot Plan ${result.plan}` : "Copilot Plan available";
|
|
1033
|
+
}
|
|
1034
|
+
const unit = result.unit === "ai_credits"
|
|
1035
|
+
? "AI Credits"
|
|
1036
|
+
: result.unit === "premium_interactions"
|
|
1037
|
+
? "Premium Interactions"
|
|
1038
|
+
: "Premium Requests";
|
|
740
1039
|
if (result.mode === "organization_usage") {
|
|
741
1040
|
return `Copilot Org (${result.organization}) ${result.used} ${unit} | ${formatBillingPeriod(result.period)}`;
|
|
742
1041
|
}
|