@slkiser/opencode-quota 4.0.1 → 4.1.0

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