@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.
Files changed (358) hide show
  1. package/README.md +94 -28
  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 +73 -22
  11. package/dist/lib/api-key-resolver.d.ts.map +1 -1
  12. package/dist/lib/api-key-resolver.js +116 -39
  13. package/dist/lib/api-key-resolver.js.map +1 -1
  14. package/dist/lib/atomic-json.d.ts +3 -1
  15. package/dist/lib/atomic-json.d.ts.map +1 -1
  16. package/dist/lib/atomic-json.js +23 -9
  17. package/dist/lib/atomic-json.js.map +1 -1
  18. package/dist/lib/chutes-config.d.ts.map +1 -1
  19. package/dist/lib/chutes-config.js +2 -4
  20. package/dist/lib/chutes-config.js.map +1 -1
  21. package/dist/lib/chutes.d.ts.map +1 -1
  22. package/dist/lib/chutes.js +28 -23
  23. package/dist/lib/chutes.js.map +1 -1
  24. package/dist/lib/cli-show.d.ts +9 -0
  25. package/dist/lib/cli-show.d.ts.map +1 -1
  26. package/dist/lib/cli-show.js +34 -5
  27. package/dist/lib/cli-show.js.map +1 -1
  28. package/dist/lib/cli-status.d.ts +8 -0
  29. package/dist/lib/cli-status.d.ts.map +1 -0
  30. package/dist/lib/cli-status.js +125 -0
  31. package/dist/lib/cli-status.js.map +1 -0
  32. package/dist/lib/config.d.ts +1 -1
  33. package/dist/lib/config.d.ts.map +1 -1
  34. package/dist/lib/config.js +47 -12
  35. package/dist/lib/config.js.map +1 -1
  36. package/dist/lib/copilot.d.ts +11 -5
  37. package/dist/lib/copilot.d.ts.map +1 -1
  38. package/dist/lib/copilot.js +378 -79
  39. package/dist/lib/copilot.js.map +1 -1
  40. package/dist/lib/deepseek-auth.d.ts.map +1 -1
  41. package/dist/lib/deepseek-auth.js +3 -7
  42. package/dist/lib/deepseek-auth.js.map +1 -1
  43. package/dist/lib/deepseek.d.ts.map +1 -1
  44. package/dist/lib/deepseek.js +22 -17
  45. package/dist/lib/deepseek.js.map +1 -1
  46. package/dist/lib/display-sanitize.d.ts.map +1 -1
  47. package/dist/lib/display-sanitize.js +8 -0
  48. package/dist/lib/display-sanitize.js.map +1 -1
  49. package/dist/lib/entries.d.ts +14 -0
  50. package/dist/lib/entries.d.ts.map +1 -1
  51. package/dist/lib/entries.js.map +1 -1
  52. package/dist/lib/format-utils.d.ts +7 -0
  53. package/dist/lib/format-utils.d.ts.map +1 -1
  54. package/dist/lib/format-utils.js +18 -0
  55. package/dist/lib/format-utils.js.map +1 -1
  56. package/dist/lib/format.d.ts +1 -0
  57. package/dist/lib/format.d.ts.map +1 -1
  58. package/dist/lib/format.js +22 -7
  59. package/dist/lib/format.js.map +1 -1
  60. package/dist/lib/glm-coding-plan.d.ts +18 -0
  61. package/dist/lib/glm-coding-plan.d.ts.map +1 -0
  62. package/dist/lib/glm-coding-plan.js +68 -0
  63. package/dist/lib/glm-coding-plan.js.map +1 -0
  64. package/dist/lib/google-agy-companion.d.ts +4 -25
  65. package/dist/lib/google-agy-companion.d.ts.map +1 -1
  66. package/dist/lib/google-agy-companion.js +47 -272
  67. package/dist/lib/google-agy-companion.js.map +1 -1
  68. package/dist/lib/google-agy.d.ts.map +1 -1
  69. package/dist/lib/google-agy.js +204 -111
  70. package/dist/lib/google-agy.js.map +1 -1
  71. package/dist/lib/google-antigravity-companion.d.ts +4 -25
  72. package/dist/lib/google-antigravity-companion.d.ts.map +1 -1
  73. package/dist/lib/google-antigravity-companion.js +44 -283
  74. package/dist/lib/google-antigravity-companion.js.map +1 -1
  75. package/dist/lib/google-companion-credentials.d.ts +70 -0
  76. package/dist/lib/google-companion-credentials.d.ts.map +1 -0
  77. package/dist/lib/google-companion-credentials.js +265 -0
  78. package/dist/lib/google-companion-credentials.js.map +1 -0
  79. package/dist/lib/google-gemini-cli-companion.d.ts +4 -25
  80. package/dist/lib/google-gemini-cli-companion.d.ts.map +1 -1
  81. package/dist/lib/google-gemini-cli-companion.js +47 -258
  82. package/dist/lib/google-gemini-cli-companion.js.map +1 -1
  83. package/dist/lib/google-gemini-cli.d.ts.map +1 -1
  84. package/dist/lib/google-gemini-cli.js +55 -61
  85. package/dist/lib/google-gemini-cli.js.map +1 -1
  86. package/dist/lib/google-token-cache.d.ts.map +1 -1
  87. package/dist/lib/google-token-cache.js +87 -34
  88. package/dist/lib/google-token-cache.js.map +1 -1
  89. package/dist/lib/google.d.ts.map +1 -1
  90. package/dist/lib/google.js +80 -84
  91. package/dist/lib/google.js.map +1 -1
  92. package/dist/lib/grouped-entry-normalization.d.ts.map +1 -1
  93. package/dist/lib/grouped-entry-normalization.js +7 -1
  94. package/dist/lib/grouped-entry-normalization.js.map +1 -1
  95. package/dist/lib/http.d.ts +12 -7
  96. package/dist/lib/http.d.ts.map +1 -1
  97. package/dist/lib/http.js +28 -15
  98. package/dist/lib/http.js.map +1 -1
  99. package/dist/lib/init-installer.d.ts +2 -1
  100. package/dist/lib/init-installer.d.ts.map +1 -1
  101. package/dist/lib/init-installer.js +29 -2
  102. package/dist/lib/init-installer.js.map +1 -1
  103. package/dist/lib/kimi-auth.d.ts +3 -26
  104. package/dist/lib/kimi-auth.d.ts.map +1 -1
  105. package/dist/lib/kimi-auth.js +24 -91
  106. package/dist/lib/kimi-auth.js.map +1 -1
  107. package/dist/lib/kimi.d.ts.map +1 -1
  108. package/dist/lib/kimi.js +20 -15
  109. package/dist/lib/kimi.js.map +1 -1
  110. package/dist/lib/maintainer-announcements.d.ts.map +1 -1
  111. package/dist/lib/maintainer-announcements.js +5 -0
  112. package/dist/lib/maintainer-announcements.js.map +1 -1
  113. package/dist/lib/map-with-concurrency.d.ts +2 -0
  114. package/dist/lib/map-with-concurrency.d.ts.map +1 -0
  115. package/dist/lib/map-with-concurrency.js +17 -0
  116. package/dist/lib/map-with-concurrency.js.map +1 -0
  117. package/dist/lib/mimo-config.d.ts +29 -0
  118. package/dist/lib/mimo-config.d.ts.map +1 -0
  119. package/dist/lib/mimo-config.js +147 -0
  120. package/dist/lib/mimo-config.js.map +1 -0
  121. package/dist/lib/mimo.d.ts +35 -0
  122. package/dist/lib/mimo.d.ts.map +1 -0
  123. package/dist/lib/mimo.js +236 -0
  124. package/dist/lib/mimo.js.map +1 -0
  125. package/dist/lib/modelsdev-pricing.d.ts.map +1 -1
  126. package/dist/lib/modelsdev-pricing.js +39 -27
  127. package/dist/lib/modelsdev-pricing.js.map +1 -1
  128. package/dist/lib/nanogpt-config.d.ts.map +1 -1
  129. package/dist/lib/nanogpt-config.js +2 -4
  130. package/dist/lib/nanogpt-config.js.map +1 -1
  131. package/dist/lib/nanogpt.d.ts.map +1 -1
  132. package/dist/lib/nanogpt.js +40 -30
  133. package/dist/lib/nanogpt.js.map +1 -1
  134. package/dist/lib/ollama-cloud.d.ts +1 -1
  135. package/dist/lib/ollama-cloud.d.ts.map +1 -1
  136. package/dist/lib/ollama-cloud.js +73 -68
  137. package/dist/lib/ollama-cloud.js.map +1 -1
  138. package/dist/lib/openai.d.ts.map +1 -1
  139. package/dist/lib/openai.js +53 -48
  140. package/dist/lib/openai.js.map +1 -1
  141. package/dist/lib/opencode-config-providers.d.ts.map +1 -1
  142. package/dist/lib/opencode-config-providers.js +13 -16
  143. package/dist/lib/opencode-config-providers.js.map +1 -1
  144. package/dist/lib/opencode-config-read.d.ts +23 -0
  145. package/dist/lib/opencode-config-read.d.ts.map +1 -0
  146. package/dist/lib/opencode-config-read.js +30 -0
  147. package/dist/lib/opencode-config-read.js.map +1 -0
  148. package/dist/lib/opencode-go.d.ts.map +1 -1
  149. package/dist/lib/opencode-go.js +44 -39
  150. package/dist/lib/opencode-go.js.map +1 -1
  151. package/dist/lib/opencode-zen-config.d.ts +34 -0
  152. package/dist/lib/opencode-zen-config.d.ts.map +1 -0
  153. package/dist/lib/opencode-zen-config.js +103 -0
  154. package/dist/lib/opencode-zen-config.js.map +1 -0
  155. package/dist/lib/opencode-zen.d.ts +27 -0
  156. package/dist/lib/opencode-zen.d.ts.map +1 -0
  157. package/dist/lib/opencode-zen.js +143 -0
  158. package/dist/lib/opencode-zen.js.map +1 -0
  159. package/dist/lib/openrouter.d.ts +13 -0
  160. package/dist/lib/openrouter.d.ts.map +1 -0
  161. package/dist/lib/openrouter.js +41 -0
  162. package/dist/lib/openrouter.js.map +1 -0
  163. package/dist/lib/provider-add-command.d.ts.map +1 -1
  164. package/dist/lib/provider-add-command.js +4 -12
  165. package/dist/lib/provider-add-command.js.map +1 -1
  166. package/dist/lib/provider-add-json-v1-questionnaire.d.ts +32 -0
  167. package/dist/lib/provider-add-json-v1-questionnaire.d.ts.map +1 -0
  168. package/dist/lib/provider-add-json-v1-questionnaire.js +336 -0
  169. package/dist/lib/provider-add-json-v1-questionnaire.js.map +1 -0
  170. package/dist/lib/provider-availability.d.ts +2 -2
  171. package/dist/lib/provider-availability.d.ts.map +1 -1
  172. package/dist/lib/provider-availability.js +2 -3
  173. package/dist/lib/provider-availability.js.map +1 -1
  174. package/dist/lib/provider-metadata.d.ts +300 -1
  175. package/dist/lib/provider-metadata.d.ts.map +1 -1
  176. package/dist/lib/provider-metadata.js +332 -227
  177. package/dist/lib/provider-metadata.js.map +1 -1
  178. package/dist/lib/quota-command-format.d.ts.map +1 -1
  179. package/dist/lib/quota-command-format.js +12 -16
  180. package/dist/lib/quota-command-format.js.map +1 -1
  181. package/dist/lib/quota-dialog-commands.d.ts +59 -2
  182. package/dist/lib/quota-dialog-commands.d.ts.map +1 -1
  183. package/dist/lib/quota-dialog-commands.js +73 -39
  184. package/dist/lib/quota-dialog-commands.js.map +1 -1
  185. package/dist/lib/quota-entry-display.d.ts +2 -0
  186. package/dist/lib/quota-entry-display.d.ts.map +1 -1
  187. package/dist/lib/quota-entry-display.js +27 -13
  188. package/dist/lib/quota-entry-display.js.map +1 -1
  189. package/dist/lib/quota-providers-remote.d.ts +0 -1
  190. package/dist/lib/quota-providers-remote.d.ts.map +1 -1
  191. package/dist/lib/quota-providers-remote.js +3 -16
  192. package/dist/lib/quota-providers-remote.js.map +1 -1
  193. package/dist/lib/quota-render-data.d.ts +4 -0
  194. package/dist/lib/quota-render-data.d.ts.map +1 -1
  195. package/dist/lib/quota-render-data.js +118 -127
  196. package/dist/lib/quota-render-data.js.map +1 -1
  197. package/dist/lib/quota-runtime-context.d.ts +9 -1
  198. package/dist/lib/quota-runtime-context.d.ts.map +1 -1
  199. package/dist/lib/quota-runtime-context.js +5 -0
  200. package/dist/lib/quota-runtime-context.js.map +1 -1
  201. package/dist/lib/quota-state.d.ts.map +1 -1
  202. package/dist/lib/quota-state.js +25 -1
  203. package/dist/lib/quota-state.js.map +1 -1
  204. package/dist/lib/quota-stats.d.ts +1 -0
  205. package/dist/lib/quota-stats.d.ts.map +1 -1
  206. package/dist/lib/quota-stats.js +14 -4
  207. package/dist/lib/quota-stats.js.map +1 -1
  208. package/dist/lib/quota-status.d.ts +7 -17
  209. package/dist/lib/quota-status.d.ts.map +1 -1
  210. package/dist/lib/quota-status.js +123 -948
  211. package/dist/lib/quota-status.js.map +1 -1
  212. package/dist/lib/runtime-provider-ids.d.ts +4 -0
  213. package/dist/lib/runtime-provider-ids.d.ts.map +1 -0
  214. package/dist/lib/runtime-provider-ids.js +10 -0
  215. package/dist/lib/runtime-provider-ids.js.map +1 -0
  216. package/dist/lib/session-tokens.d.ts +2 -0
  217. package/dist/lib/session-tokens.d.ts.map +1 -1
  218. package/dist/lib/session-tokens.js +4 -2
  219. package/dist/lib/session-tokens.js.map +1 -1
  220. package/dist/lib/synthetic-config.d.ts.map +1 -1
  221. package/dist/lib/synthetic-config.js +2 -4
  222. package/dist/lib/synthetic-config.js.map +1 -1
  223. package/dist/lib/synthetic.d.ts.map +1 -1
  224. package/dist/lib/synthetic.js +37 -32
  225. package/dist/lib/synthetic.js.map +1 -1
  226. package/dist/lib/toast-format-grouped.d.ts +1 -0
  227. package/dist/lib/toast-format-grouped.d.ts.map +1 -1
  228. package/dist/lib/toast-format-grouped.js +33 -29
  229. package/dist/lib/toast-format-grouped.js.map +1 -1
  230. package/dist/lib/tui-refresh-lifecycle.d.ts +17 -0
  231. package/dist/lib/tui-refresh-lifecycle.d.ts.map +1 -0
  232. package/dist/lib/tui-refresh-lifecycle.js +83 -0
  233. package/dist/lib/tui-refresh-lifecycle.js.map +1 -0
  234. package/dist/lib/tui-runtime.d.ts +0 -5
  235. package/dist/lib/tui-runtime.d.ts.map +1 -1
  236. package/dist/lib/tui-runtime.js +1 -6
  237. package/dist/lib/tui-runtime.js.map +1 -1
  238. package/dist/lib/tui-sidebar-format.d.ts +1 -1
  239. package/dist/lib/tui-sidebar-format.d.ts.map +1 -1
  240. package/dist/lib/tui-sidebar-format.js +1 -0
  241. package/dist/lib/tui-sidebar-format.js.map +1 -1
  242. package/dist/lib/types.d.ts +72 -8
  243. package/dist/lib/types.d.ts.map +1 -1
  244. package/dist/lib/types.js +2 -0
  245. package/dist/lib/types.js.map +1 -1
  246. package/dist/lib/xai.d.ts +39 -0
  247. package/dist/lib/xai.d.ts.map +1 -0
  248. package/dist/lib/xai.js +153 -0
  249. package/dist/lib/xai.js.map +1 -0
  250. package/dist/lib/zai-auth.d.ts +3 -26
  251. package/dist/lib/zai-auth.d.ts.map +1 -1
  252. package/dist/lib/zai-auth.js +24 -94
  253. package/dist/lib/zai-auth.js.map +1 -1
  254. package/dist/lib/zai.d.ts +1 -8
  255. package/dist/lib/zai.d.ts.map +1 -1
  256. package/dist/lib/zai.js +11 -91
  257. package/dist/lib/zai.js.map +1 -1
  258. package/dist/lib/zhipu-auth.d.ts +3 -26
  259. package/dist/lib/zhipu-auth.d.ts.map +1 -1
  260. package/dist/lib/zhipu-auth.js +24 -94
  261. package/dist/lib/zhipu-auth.js.map +1 -1
  262. package/dist/lib/zhipu.d.ts +1 -8
  263. package/dist/lib/zhipu.d.ts.map +1 -1
  264. package/dist/lib/zhipu.js +10 -84
  265. package/dist/lib/zhipu.js.map +1 -1
  266. package/dist/plugin.d.ts.map +1 -1
  267. package/dist/plugin.js +20 -145
  268. package/dist/plugin.js.map +1 -1
  269. package/dist/providers/alibaba-coding-plan.d.ts.map +1 -1
  270. package/dist/providers/alibaba-coding-plan.js +33 -6
  271. package/dist/providers/alibaba-coding-plan.js.map +1 -1
  272. package/dist/providers/anthropic.d.ts.map +1 -1
  273. package/dist/providers/anthropic.js +35 -7
  274. package/dist/providers/anthropic.js.map +1 -1
  275. package/dist/providers/chutes.d.ts.map +1 -1
  276. package/dist/providers/chutes.js +10 -3
  277. package/dist/providers/chutes.js.map +1 -1
  278. package/dist/providers/copilot.d.ts.map +1 -1
  279. package/dist/providers/copilot.js +102 -31
  280. package/dist/providers/copilot.js.map +1 -1
  281. package/dist/providers/cursor.d.ts.map +1 -1
  282. package/dist/providers/cursor.js +34 -6
  283. package/dist/providers/cursor.js.map +1 -1
  284. package/dist/providers/deepseek.d.ts.map +1 -1
  285. package/dist/providers/deepseek.js +10 -3
  286. package/dist/providers/deepseek.js.map +1 -1
  287. package/dist/providers/glm-coding-plan-provider.d.ts +44 -0
  288. package/dist/providers/glm-coding-plan-provider.d.ts.map +1 -0
  289. package/dist/providers/glm-coding-plan-provider.js +62 -0
  290. package/dist/providers/glm-coding-plan-provider.js.map +1 -0
  291. package/dist/providers/google-agy.d.ts.map +1 -1
  292. package/dist/providers/google-agy.js +83 -42
  293. package/dist/providers/google-agy.js.map +1 -1
  294. package/dist/providers/google-antigravity.d.ts.map +1 -1
  295. package/dist/providers/google-antigravity.js +31 -6
  296. package/dist/providers/google-antigravity.js.map +1 -1
  297. package/dist/providers/google-gemini-cli.d.ts.map +1 -1
  298. package/dist/providers/google-gemini-cli.js +24 -6
  299. package/dist/providers/google-gemini-cli.js.map +1 -1
  300. package/dist/providers/kimi-code.d.ts.map +1 -1
  301. package/dist/providers/kimi-code.js +27 -8
  302. package/dist/providers/kimi-code.js.map +1 -1
  303. package/dist/providers/mimo.d.ts +3 -0
  304. package/dist/providers/mimo.d.ts.map +1 -0
  305. package/dist/providers/mimo.js +127 -0
  306. package/dist/providers/mimo.js.map +1 -0
  307. package/dist/providers/minimax-coding-plan.d.ts.map +1 -1
  308. package/dist/providers/minimax-coding-plan.js +68 -32
  309. package/dist/providers/minimax-coding-plan.js.map +1 -1
  310. package/dist/providers/nanogpt.d.ts.map +1 -1
  311. package/dist/providers/nanogpt.js +41 -4
  312. package/dist/providers/nanogpt.js.map +1 -1
  313. package/dist/providers/openai.d.ts.map +1 -1
  314. package/dist/providers/openai.js +20 -3
  315. package/dist/providers/openai.js.map +1 -1
  316. package/dist/providers/opencode-go.d.ts.map +1 -1
  317. package/dist/providers/opencode-go.js +33 -15
  318. package/dist/providers/opencode-go.js.map +1 -1
  319. package/dist/providers/opencode-zen.d.ts +3 -0
  320. package/dist/providers/opencode-zen.d.ts.map +1 -0
  321. package/dist/providers/opencode-zen.js +99 -0
  322. package/dist/providers/opencode-zen.js.map +1 -0
  323. package/dist/providers/openrouter.d.ts +6 -0
  324. package/dist/providers/openrouter.d.ts.map +1 -0
  325. package/dist/providers/openrouter.js +25 -0
  326. package/dist/providers/openrouter.js.map +1 -0
  327. package/dist/providers/quota-providers.d.ts.map +1 -1
  328. package/dist/providers/quota-providers.js +3 -3
  329. package/dist/providers/quota-providers.js.map +1 -1
  330. package/dist/providers/qwen-code.d.ts.map +1 -1
  331. package/dist/providers/qwen-code.js +19 -5
  332. package/dist/providers/qwen-code.js.map +1 -1
  333. package/dist/providers/registry.d.ts.map +1 -1
  334. package/dist/providers/registry.js +8 -0
  335. package/dist/providers/registry.js.map +1 -1
  336. package/dist/providers/result-helpers.d.ts +29 -1
  337. package/dist/providers/result-helpers.d.ts.map +1 -1
  338. package/dist/providers/result-helpers.js +69 -0
  339. package/dist/providers/result-helpers.js.map +1 -1
  340. package/dist/providers/synthetic.d.ts.map +1 -1
  341. package/dist/providers/synthetic.js +10 -3
  342. package/dist/providers/synthetic.js.map +1 -1
  343. package/dist/providers/xai.d.ts +6 -0
  344. package/dist/providers/xai.d.ts.map +1 -0
  345. package/dist/providers/xai.js +51 -0
  346. package/dist/providers/xai.js.map +1 -0
  347. package/dist/providers/zai.d.ts +1 -7
  348. package/dist/providers/zai.d.ts.map +1 -1
  349. package/dist/providers/zai.js +11 -46
  350. package/dist/providers/zai.js.map +1 -1
  351. package/dist/providers/zhipu.d.ts +1 -7
  352. package/dist/providers/zhipu.d.ts.map +1 -1
  353. package/dist/providers/zhipu.js +11 -46
  354. package/dist/providers/zhipu.js.map +1 -1
  355. package/dist/tui.d.ts.map +1 -1
  356. package/dist/tui.js +47 -136
  357. package/dist/tui.tsx +62 -179
  358. package/package.json +1 -1
@@ -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 PERSONAL_AI_CREDIT_TOTALS = {
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.access && !auth.refresh)
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
- 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";
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 billingModel = pat.config?.billingModel ?? "ai_credits";
305
- const quotaApi = pat.state !== "valid"
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
- : billingModel === "legacy_premium_requests"
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: 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),
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 ? "not_supported_by_public_billing_api" : "not_configured",
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
- async function readGitHubRestErrorMessage(response) {
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 sanitizeDisplayText(`${message} (${documentationUrl})`);
477
+ return redactGitHubErrorText(`${message} (${documentationUrl})`, secrets);
362
478
  }
363
479
  if (message)
364
- return sanitizeDisplayText(message);
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
- 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
- }
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(`${GITHUB_API_BASE_URL}/user`, token, requestTimeoutMs);
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 `${GITHUB_API_BASE_URL}/enterprises/${encodeURIComponent(target.enterprise)}/settings/billing/${report}/usage?${query}`;
532
+ return `${apiBaseUrl}/enterprises/${encodeURIComponent(target.enterprise)}/settings/billing/${report}/usage?${query}`;
412
533
  }
413
534
  if (target.scope === "organization") {
414
- return `${GITHUB_API_BASE_URL}/organizations/${encodeURIComponent(target.organization)}/settings/billing/${report}/usage?${query}`;
535
+ return `${apiBaseUrl}/organizations/${encodeURIComponent(target.organization)}/settings/billing/${report}/usage?${query}`;
415
536
  }
416
- return `${GITHUB_API_BASE_URL}/users/${encodeURIComponent(target.username)}/settings/billing/${report}/usage?${query}`;
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
- return text.includes("ai credit") || text.includes("ai-credit");
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 an AI Credit usage item");
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
- if (gross === undefined && discount === undefined && net === undefined) {
478
- throw new Error("GitHub AI Credit usage item did not include quantity fields");
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
- 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);
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
- billedAmountUsd += Math.max(0, netAmount);
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
- ? `${GITHUB_API_BASE_URL}/organizations/${encodeURIComponent(target.organization)}/settings/billing/budgets`
568
- : `${GITHUB_API_BASE_URL}/enterprises/${encodeURIComponent(target.enterprise)}/settings/billing/budgets`;
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 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.
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
- return null;
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
- const unit = result.unit === "ai_credits" ? "AI Credits" : "Premium Requests";
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
  }