@wenathlan/devthink 2.0.13 → 2.0.15

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 (444) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/accounts.ts +2087 -0
  3. package/agent.ts +2331 -0
  4. package/antigravity.ts +2139 -0
  5. package/apifreeze.ts +4146 -0
  6. package/auth.ts +642 -0
  7. package/background.ts +16837 -0
  8. package/bridge.ts +2080 -0
  9. package/build.ts +54 -0
  10. package/bun.ts +26 -0
  11. package/capture.ts +1614 -0
  12. package/checksecrets.ts +72 -0
  13. package/cjs.ts +9 -0
  14. package/cli.ts +3500 -0
  15. package/commands.ts +1101 -0
  16. package/companion.ts +450 -0
  17. package/compatibility.ts +37 -0
  18. package/config.ts +3137 -0
  19. package/constants.ts +943 -0
  20. package/core.ts +1473 -0
  21. package/crossbrowser.ts +1359 -0
  22. package/dashdone.ts +605 -0
  23. package/data.ts +943 -0
  24. package/database.ts +133 -0
  25. package/debug.ts +850 -0
  26. package/deno.ts +27 -0
  27. package/devthink.ts +707 -0
  28. package/dist/accounts.d.ts +409 -0
  29. package/dist/agent.d.ts +837 -0
  30. package/dist/antigravity.d.ts +380 -0
  31. package/dist/apifreeze.d.ts +113 -0
  32. package/dist/auth.d.ts +224 -0
  33. package/{bridge.d.ts → dist/bridge.d.ts} +1 -1
  34. package/dist/capture.d.ts +595 -0
  35. package/dist/cjs.d.ts +6 -0
  36. package/{cli.js → dist/cli.js} +2 -2
  37. package/dist/commands.d.ts +310 -0
  38. package/dist/compatibility.d.ts +17 -0
  39. package/dist/config.d.ts +1853 -0
  40. package/dist/constants.d.ts +637 -0
  41. package/dist/core.d.ts +361 -0
  42. package/dist/data.d.ts +330 -0
  43. package/dist/database.d.ts +24 -0
  44. package/dist/debug.d.ts +270 -0
  45. package/dist/engine.d.ts +74 -0
  46. package/dist/environments.d.ts +244 -0
  47. package/dist/evidence.d.ts +277 -0
  48. package/dist/export.d.ts +311 -0
  49. package/dist/fingerprint.d.ts +594 -0
  50. package/dist/flow.d.ts +300 -0
  51. package/dist/gates.d.ts +103 -0
  52. package/dist/identity.d.ts +51 -0
  53. package/dist/ids.d.ts +7 -0
  54. package/dist/llm.d.ts +652 -0
  55. package/dist/models.d.ts +1807 -0
  56. package/dist/net.d.ts +325 -0
  57. package/dist/neutral.d.ts +41 -0
  58. package/dist/oauth.d.ts +703 -0
  59. package/dist/page.d.ts +1166 -0
  60. package/dist/perf.d.ts +481 -0
  61. package/dist/plan.d.ts +134 -0
  62. package/dist/project.d.ts +395 -0
  63. package/dist/providers.d.ts +37 -0
  64. package/dist/quota.d.ts +453 -0
  65. package/dist/recovery.d.ts +621 -0
  66. package/dist/request.d.ts +1492 -0
  67. package/dist/run.d.ts +779 -0
  68. package/dist/runtime.d.ts +212 -0
  69. package/dist/search.d.ts +375 -0
  70. package/dist/security.d.ts +534 -0
  71. package/dist/serve.d.ts +136 -0
  72. package/dist/session.d.ts +296 -0
  73. package/dist/storage.d.ts +53 -0
  74. package/dist/streaming.d.ts +1119 -0
  75. package/dist/swarm.d.ts +328 -0
  76. package/dist/tools.d.ts +170 -0
  77. package/dist/types.d.ts +8357 -0
  78. package/dist/utils.d.ts +82 -0
  79. package/dist/version.d.ts +7 -0
  80. package/dist/versionregistry.d.ts +406 -0
  81. package/dist/views.d.ts +579 -0
  82. package/dist/workbenchsession.d.ts +69 -0
  83. package/dist/workflow.d.ts +649 -0
  84. package/docs/readme.md +92 -0
  85. package/docs/schemas/antigravity.json +192 -0
  86. package/engine.ts +1524 -0
  87. package/environments.ts +1005 -0
  88. package/evidence.ts +643 -0
  89. package/export.ts +889 -0
  90. package/fingerprint.ts +1458 -0
  91. package/flow.ts +1190 -0
  92. package/gates.ts +307 -0
  93. package/gateway.ts +34 -0
  94. package/hardening.ts +191 -0
  95. package/headless.ts +369 -0
  96. package/http.ts +2110 -0
  97. package/identity.ts +246 -0
  98. package/ids.ts +26 -0
  99. package/index.ts +369 -0
  100. package/llm.ts +2682 -0
  101. package/mcp.ts +1835 -0
  102. package/{memory.js → memory.ts} +3834 -2310
  103. package/models.ts +3805 -0
  104. package/modes.ts +43 -0
  105. package/net.ts +1214 -0
  106. package/neutral.ts +362 -0
  107. package/node.ts +26 -0
  108. package/oauth.ts +2216 -0
  109. package/offscreen.ts +170 -0
  110. package/pack.ts +956 -0
  111. package/package.json +175 -82
  112. package/page.ts +7824 -0
  113. package/pagebridge.ts +1116 -0
  114. package/perf.ts +957 -0
  115. package/plan.ts +672 -0
  116. package/plugin.ts +76 -0
  117. package/{policy.js → policy.ts} +5073 -4679
  118. package/prisma.config.ts +27 -0
  119. package/progress.ts +1308 -0
  120. package/project.ts +1291 -0
  121. package/protocol.ts +4187 -0
  122. package/providers.ts +246 -0
  123. package/quota.ts +1887 -0
  124. package/recovery.ts +2433 -0
  125. package/request.ts +4482 -0
  126. package/run.ts +2135 -0
  127. package/runtime.ts +526 -0
  128. package/search.ts +1760 -0
  129. package/security.ts +1673 -0
  130. package/serve.ts +419 -0
  131. package/server.ts +5538 -0
  132. package/session.ts +1162 -0
  133. package/storage.ts +182 -0
  134. package/streaming.ts +4101 -0
  135. package/swarm.ts +884 -0
  136. package/sync.ts +56 -0
  137. package/tests/agentcert.test.ts +128 -0
  138. package/tests/agentfleet.test.ts +214 -0
  139. package/tests/agentreview.test.ts +241 -0
  140. package/tests/agentstream.test.ts +335 -0
  141. package/tests/agentwork.test.ts +356 -0
  142. package/tests/apifreeze.test.ts +290 -0
  143. package/tests/apimap.test.ts +112 -0
  144. package/tests/approvalgate.test.ts +175 -0
  145. package/tests/artifactmanifest.test.ts +204 -0
  146. package/tests/attentionfeed.test.ts +163 -0
  147. package/tests/backgroundruns.test.ts +100 -0
  148. package/tests/batchscheduling.test.ts +131 -0
  149. package/tests/blackboard.test.ts +238 -0
  150. package/tests/bridge.test.ts +1088 -0
  151. package/tests/browsercoverage.test.ts +134 -0
  152. package/tests/browserpolyfills.test.ts +124 -0
  153. package/tests/build.mjs +1260 -0
  154. package/tests/clientauth.test.ts +223 -0
  155. package/tests/clitools.test.ts +578 -0
  156. package/tests/confirmgates.test.ts +190 -0
  157. package/tests/containerpack.test.ts +143 -0
  158. package/tests/controlflow.test.ts +2120 -0
  159. package/tests/coordination.test.ts +345 -0
  160. package/tests/copyscreen.test.ts +139 -0
  161. package/tests/core.test.ts +417 -0
  162. package/tests/costcert.test.ts +95 -0
  163. package/tests/crossbrowser.test.ts +115 -0
  164. package/tests/cspaudit.test.ts +79 -0
  165. package/tests/dashdone.test.ts +615 -0
  166. package/tests/datagrid.test.ts +137 -0
  167. package/tests/doccheck.test.ts +107 -0
  168. package/tests/dombatch.test.ts +109 -0
  169. package/tests/emulation.test.ts +282 -0
  170. package/tests/environments.test.ts +259 -0
  171. package/tests/evidenceviews.test.ts +88 -0
  172. package/tests/exporttools.test.ts +185 -0
  173. package/tests/firefoxprep.test.ts +134 -0
  174. package/tests/flowlibrary.test.ts +242 -0
  175. package/tests/flowrun.test.ts +303 -0
  176. package/tests/forensics.test.ts +439 -0
  177. package/tests/gateway.test.ts +939 -0
  178. package/tests/headlesslib.test.ts +171 -0
  179. package/tests/httpstream.test.ts +277 -0
  180. package/tests/immutablelog.test.ts +157 -0
  181. package/tests/inboundguard.test.ts +177 -0
  182. package/tests/lazyload.test.ts +72 -0
  183. package/tests/librarymodes.test.ts +623 -0
  184. package/tests/llm.test.ts +696 -0
  185. package/tests/manifest.test.ts +226 -0
  186. package/tests/maskinputs.test.ts +150 -0
  187. package/tests/matrixverify.test.ts +111 -0
  188. package/tests/mavenpack.test.ts +75 -0
  189. package/tests/mcpserver.test.ts +704 -0
  190. package/tests/memory.test.ts +4155 -0
  191. package/tests/memorycare.test.ts +230 -0
  192. package/tests/migrateplan.test.ts +349 -0
  193. package/tests/minimization.test.ts +414 -0
  194. package/tests/modelroute.test.ts +112 -0
  195. package/tests/multiagent.test.ts +440 -0
  196. package/tests/nativebridge.test.ts +807 -0
  197. package/tests/navigation.test.ts +284 -0
  198. package/tests/nugetpack.test.ts +83 -0
  199. package/tests/orchestration.test.ts +449 -0
  200. package/tests/originpolicy.test.ts +405 -0
  201. package/tests/outputcompare.test.ts +97 -0
  202. package/tests/package.test.ts +91 -0
  203. package/tests/pagebridge.test.ts +5909 -0
  204. package/tests/pentest.test.ts +75 -0
  205. package/tests/perfrecords.test.ts +257 -0
  206. package/tests/permdiff.test.ts +85 -0
  207. package/tests/phishguard.test.ts +128 -0
  208. package/tests/pickerviews.test.ts +155 -0
  209. package/tests/pipelines.test.ts +381 -0
  210. package/tests/planlint.test.ts +240 -0
  211. package/tests/planreview.test.ts +383 -0
  212. package/tests/platformtargets.test.ts +56 -0
  213. package/tests/policy.test.ts +8284 -0
  214. package/tests/portability.test.ts +96 -0
  215. package/tests/progress.test.ts +1036 -0
  216. package/tests/promptlibrary.test.ts +116 -0
  217. package/tests/protocol.test.ts +4793 -0
  218. package/tests/publishingpipeline.test.ts +314 -0
  219. package/tests/quickactions.test.ts +164 -0
  220. package/tests/readiness.test.ts +78 -0
  221. package/tests/recipes.test.ts +163 -0
  222. package/tests/redactshots.test.ts +216 -0
  223. package/tests/registernvidiakeys.ts +51 -0
  224. package/tests/relayserve.test.ts +304 -0
  225. package/tests/releasenotes.test.ts +276 -0
  226. package/tests/resilience.test.ts +467 -0
  227. package/tests/resourceaware.test.ts +187 -0
  228. package/tests/runbudget.test.ts +125 -0
  229. package/tests/runhistory.test.ts +299 -0
  230. package/tests/runreplay.test.ts +113 -0
  231. package/tests/runresume.test.ts +184 -0
  232. package/tests/runstate.test.ts +288 -0
  233. package/tests/runtimeadapters.test.ts +251 -0
  234. package/tests/safariskeleton.test.ts +115 -0
  235. package/tests/sandboxframe.test.ts +196 -0
  236. package/tests/sbom.test.ts +105 -0
  237. package/tests/secretvault.test.ts +188 -0
  238. package/tests/selcache.test.ts +111 -0
  239. package/tests/sessioninterface.test.ts +791 -0
  240. package/tests/sessions.test.ts +591 -0
  241. package/tests/siteprefs.test.ts +228 -0
  242. package/tests/snapshotdelta.test.ts +165 -0
  243. package/tests/statusviews.test.ts +237 -0
  244. package/tests/stepmeter.test.ts +200 -0
  245. package/tests/streamparse.test.ts +163 -0
  246. package/tests/stylepruning.test.ts +90 -0
  247. package/tests/surfaces.test.ts +282 -0
  248. package/tests/sweep.test.ts +78 -0
  249. package/tests/syncbridge.test.ts +164 -0
  250. package/tests/taskqueue.test.ts +206 -0
  251. package/tests/toolcalls.test.ts +336 -0
  252. package/tests/toolcatalog.test.ts +201 -0
  253. package/tests/torture-agent.test.ts +706 -0
  254. package/tests/torture-auth.test.ts +461 -0
  255. package/tests/torture-commands.test.ts +895 -0
  256. package/tests/torture-data.test.ts +923 -0
  257. package/tests/torture-environments.test.ts +285 -0
  258. package/tests/torture-export.test.ts +263 -0
  259. package/tests/torture-http.test.ts +1379 -0
  260. package/tests/torture-llm.test.ts +808 -0
  261. package/tests/torture-memory.test.ts +1084 -0
  262. package/tests/torture-plan.test.ts +829 -0
  263. package/tests/torture-run.test.ts +626 -0
  264. package/tests/torture-session.test.ts +928 -0
  265. package/tests/torture-swarm.test.ts +1245 -0
  266. package/tests/torture-tools.test.ts +964 -0
  267. package/tests/torture-workflow.test.ts +2354 -0
  268. package/tests/tourviews.test.ts +52 -0
  269. package/tests/transparency.test.ts +147 -0
  270. package/tests/trigger.test.ts +941 -0
  271. package/tests/v1sunset.test.ts +428 -0
  272. package/tests/virtlist.test.ts +94 -0
  273. package/tests/vision.test.ts +540 -0
  274. package/tests/vsixpack.test.ts +145 -0
  275. package/tests/webapi.test.ts +523 -0
  276. package/tests/workflow.test.ts +1785 -0
  277. package/tests/workfloweditor.test.ts +759 -0
  278. package/tests/xpipack.test.ts +128 -0
  279. package/tools.ts +734 -0
  280. package/types.ts +9908 -0
  281. package/umd.ts +9 -0
  282. package/utils.ts +249 -0
  283. package/version.ts +8 -0
  284. package/versionregistry.ts +1310 -0
  285. package/views.ts +2153 -0
  286. package/vitest.config.ts +12 -0
  287. package/web/App.tsx +78 -0
  288. package/web/README.md +47 -0
  289. package/web/capacitor.config.ts +46 -0
  290. package/web/const.ts +19 -0
  291. package/web/control.shell.tsx +66 -0
  292. package/web/dashboardpage.ts +554 -0
  293. package/web/db.ts +493 -0
  294. package/web/design.html +541 -0
  295. package/web/gatewayview/Gateway.tsx +345 -0
  296. package/web/gatewayview/config.ts +1042 -0
  297. package/web/gatewayview/definition.ts +174 -0
  298. package/web/icons.ts +15 -0
  299. package/{manifest.json → web/manifest.json} +1 -1
  300. package/web/optionspage.ts +691 -0
  301. package/web/popup.ts +1651 -0
  302. package/web/schema.prisma +176 -0
  303. package/web/sidepanel.ts +7941 -0
  304. package/web/sitemanifest.json +13 -0
  305. package/web/terminal-ui.tsx +622 -0
  306. package/web/transparencypage.ts +541 -0
  307. package/web/vite.config.ts +39 -0
  308. package/web/workspace.ts +23 -0
  309. package/workbenchmemory.ts +104 -0
  310. package/workbenchsession.ts +304 -0
  311. package/workflow.ts +4495 -0
  312. package/bridge.d.ts.map +0 -1
  313. package/bridge.js +0 -1877
  314. package/bun.d.ts.map +0 -1
  315. package/bun.js +0 -52329
  316. package/caps/background.json +0 -712
  317. package/caps/cli.json +0 -360
  318. package/caps/library.json +0 -3611
  319. package/caps/mcp.json +0 -126
  320. package/caps/pagebridge.json +0 -377
  321. package/caps/popup.json +0 -51
  322. package/caps/sidepanel.json +0 -522
  323. package/checksums.txt +0 -142
  324. package/cli.d.ts.map +0 -1
  325. package/companion.d.ts.map +0 -1
  326. package/companion.js +0 -389
  327. package/crossbrowser.d.ts.map +0 -1
  328. package/crossbrowser.js +0 -1191
  329. package/dashdone.d.ts.map +0 -1
  330. package/dashdone.js +0 -368
  331. package/deno.d.ts.map +0 -1
  332. package/deno.js +0 -52325
  333. package/devthink.umd.js +0 -52346
  334. package/fixtures/bun.ts +0 -7
  335. package/fixtures/cjs.js +0 -6
  336. package/fixtures/deno.ts +0 -6
  337. package/fixtures/example-org-pagestate.json +0 -22
  338. package/fixtures/importers/automa-workflow.json +0 -18
  339. package/fixtures/importers/selenium-side.json +0 -20
  340. package/fixtures/importers/tabular-plan.csv +0 -7
  341. package/fixtures/importers/uivision-macro.json +0 -10
  342. package/fixtures/importers/v1-plan.json +0 -13
  343. package/fixtures/mcp-client.mjs +0 -14
  344. package/fixtures/pages/checkout.html +0 -192
  345. package/fixtures/pages/dashboard.html +0 -110
  346. package/fixtures/pages/feed.html +0 -113
  347. package/fixtures/pages/product-grid.html +0 -150
  348. package/fixtures/plans/form-inventory-plan.json +0 -11
  349. package/fixtures/plans/release-notes-plan.json +0 -11
  350. package/fixtures/recipes/agentscompete.json +0 -11
  351. package/fixtures/recipes/agentsforms.json +0 -11
  352. package/fixtures/recipes/agentsmonitor.json +0 -11
  353. package/fixtures/recipes/agentsreview.json +0 -29
  354. package/fixtures/recipes/agentswarm.json +0 -11
  355. package/fixtures/recipes/fillcalendar.json +0 -12
  356. package/fixtures/recipes/fillcheckout.json +0 -13
  357. package/fixtures/recipes/filldependentdropdowns.json +0 -12
  358. package/fixtures/recipes/fillfromcsv.json +0 -13
  359. package/fixtures/recipes/fillloginform.json +0 -12
  360. package/fixtures/recipes/fillmultistepwizard.json +0 -13
  361. package/fixtures/recipes/fillregistration.json +0 -12
  362. package/fixtures/recipes/fillsearchfilters.json +0 -12
  363. package/fixtures/recipes/filltypeahead.json +0 -11
  364. package/fixtures/recipes/monitoralerts.json +0 -12
  365. package/fixtures/recipes/monitoravailability.json +0 -11
  366. package/fixtures/recipes/monitornetworkquiet.json +0 -12
  367. package/fixtures/recipes/monitorpagechanges.json +0 -11
  368. package/fixtures/recipes/monitorpricechange.json +0 -11
  369. package/fixtures/recipes/monitorschedule.json +0 -12
  370. package/fixtures/recipes/scrapedynamicprices.json +0 -13
  371. package/fixtures/recipes/scrapeimagegallery.json +0 -12
  372. package/fixtures/recipes/scrapeinfinitefeed.json +0 -13
  373. package/fixtures/recipes/scrapenestedlists.json +0 -12
  374. package/fixtures/recipes/scrapeproductgrid.json +0 -12
  375. package/fixtures/recipes/scrapereviews.json +0 -12
  376. package/fixtures/recipes/scrapesearchresults.json +0 -12
  377. package/fixtures/recipes/scrapetablepagination.json +0 -12
  378. package/fixtures/recipes/testaccessibility.json +0 -34
  379. package/fixtures/recipes/testconsoleerrors.json +0 -12
  380. package/fixtures/recipes/testdeeplinks.json +0 -12
  381. package/fixtures/recipes/testformvalidation.json +0 -13
  382. package/fixtures/recipes/testlayoutshift.json +0 -12
  383. package/fixtures/recipes/testlinkcheck.json +0 -12
  384. package/fixtures/recipes/testloadtiming.json +0 -12
  385. package/fixtures/recipes/testvisualdiff.json +0 -12
  386. package/fixtures/workflows/changelog-digest-workflow.json +0 -13
  387. package/fixtures/workflows/form-survey-workflow.json +0 -13
  388. package/gallery.json +0 -706
  389. package/hardening.d.ts.map +0 -1
  390. package/hardening.js +0 -188
  391. package/headless.d.ts.map +0 -1
  392. package/headless.js +0 -11522
  393. package/http.d.ts.map +0 -1
  394. package/http.js +0 -2165
  395. package/index.cjs +0 -54682
  396. package/index.d.ts.map +0 -1
  397. package/index.js +0 -52310
  398. package/index.neutral.js +0 -46343
  399. package/mcp.d.ts.map +0 -1
  400. package/mcp.js +0 -20515
  401. package/memory.d.ts.map +0 -1
  402. package/nativehost.template.json +0 -10
  403. package/node.cjs +0 -54686
  404. package/node.d.ts.map +0 -1
  405. package/pack.d.ts.map +0 -1
  406. package/pack.js +0 -805
  407. package/policy.d.ts.map +0 -1
  408. package/progress.d.ts.map +0 -1
  409. package/progress.js +0 -645
  410. package/protocol.d.ts.map +0 -1
  411. package/protocol.js +0 -8334
  412. package/schemas/audit.schema.json +0 -226
  413. package/schemas/capability.schema.json +0 -113
  414. package/schemas/envelope.schema.json +0 -162
  415. package/schemas/memory.schema.json +0 -51
  416. package/schemas/observation.schema.json +0 -113
  417. package/schemas/plan.schema.json +0 -208
  418. package/schemas/progress.schema.json +0 -126
  419. package/schemas/proposal.schema.json +0 -68
  420. package/schemas/session.schema.json +0 -82
  421. package/schemas/tool.schema.json +0 -94
  422. package/server.d.ts.map +0 -1
  423. package/server.js +0 -28877
  424. package/umd-example.html +0 -18
  425. package/umd.d.ts.map +0 -1
  426. /package/{bun.d.ts → dist/bun.d.ts} +0 -0
  427. /package/{cli.d.ts → dist/cli.d.ts} +0 -0
  428. /package/{companion.d.ts → dist/companion.d.ts} +0 -0
  429. /package/{crossbrowser.d.ts → dist/crossbrowser.d.ts} +0 -0
  430. /package/{dashdone.d.ts → dist/dashdone.d.ts} +0 -0
  431. /package/{deno.d.ts → dist/deno.d.ts} +0 -0
  432. /package/{hardening.d.ts → dist/hardening.d.ts} +0 -0
  433. /package/{headless.d.ts → dist/headless.d.ts} +0 -0
  434. /package/{http.d.ts → dist/http.d.ts} +0 -0
  435. /package/{index.d.ts → dist/index.d.ts} +0 -0
  436. /package/{mcp.d.ts → dist/mcp.d.ts} +0 -0
  437. /package/{memory.d.ts → dist/memory.d.ts} +0 -0
  438. /package/{node.d.ts → dist/node.d.ts} +0 -0
  439. /package/{pack.d.ts → dist/pack.d.ts} +0 -0
  440. /package/{policy.d.ts → dist/policy.d.ts} +0 -0
  441. /package/{progress.d.ts → dist/progress.d.ts} +0 -0
  442. /package/{protocol.d.ts → dist/protocol.d.ts} +0 -0
  443. /package/{server.d.ts → dist/server.d.ts} +0 -0
  444. /package/{umd.d.ts → dist/umd.d.ts} +0 -0
package/quota.ts ADDED
@@ -0,0 +1,1887 @@
1
+ /**
2
+ * @fileoverview quota.ts — the full quota manager of the merged provider lineage
3
+ * @module auth/quota
4
+ * @description
5
+ * Merged module. Lineage A (dual-source Antigravity + Gemini CLI quota with
6
+ * adaptive TTL cache, soft 90% threshold, background refresh, resilient
7
+ * parsing of v1internal:retrieveUserQuotaSummary) is the canonical surface.
8
+ *
9
+ * v2.1.16 dead-code cleanup: the unused resilient lineage-B quota engine
10
+ * (zero consumers) was deleted. It duplicated lineage A as a second complete
11
+ * engine under aliased names — resolveQuotaPoolGroup (+ resolveGroup /
12
+ * getQuotaGroupForModel / modelToGroup), parseQuotaResponseResilient,
13
+ * retrieveUserQuotaSummaryResilient, checkQuotaFromSummary,
14
+ * isQuotaExhaustedBySummary, shouldSkipAccountBySummary,
15
+ * QuotaManagerResilient (+ getQuotaManager / resetQuotaManager /
16
+ * fetchQuotaWithCache / refreshQuotaBackground), getAdaptiveTtlMs,
17
+ * resolveBaseTtlFromConfig, parseRateLimitHeaders, hashToken's engine
18
+ * cluster, QUOTA_INFO and the Quota facade — every one verified by grep to
19
+ * have zero consumers outside quota.ts before deletion.
20
+ *
21
+ * Canonical (lineage A) surface: OAUTH_CLIENT_ID/OAUTH_CLIENT_SECRET/GEMINI_CLI_SCOPES,
22
+ * PROJECT_FALLBACK, ENDPOINTS/ENDPOINTS_DAILY/QUOTA_ENDPOINTS + QuotaSource,
23
+ * QUOTA_GROUP_DEFS/QuotaGroupId/ResilientQuotaGroupDef, QUOTA_METHOD,
24
+ * QuotaWindow, QuotaBucketRaw/Parsed,
25
+ * QuotaGroupRaw/Parsed, QuotaSummaryRaw/Parsed, computeAdaptiveTtlMs,
26
+ * getQuotaRefreshIntervalMs, listQuotaGroups/getQuotaGroup, buildQuotaHeaders(token,source),
27
+ * parseQuotaBucket/parseQuotaGroup, QuotaFetchError, retrieveUserQuotaSummary,
28
+ * retrieveDualQuota, QuotaCacheManager + getQuotaCacheManager, QuotaManager +
29
+ * getDefaultQuotaManager, canProceedWithRequest, QuotaConfig/DEFAULT_QUOTA_CONFIG/
30
+ * resolveQuotaConfig, QUOTA_CACHE_FILE, hashToken.
31
+ *
32
+ * Production-ready, only node:* builtins + global fetch. Zero external deps.
33
+ *
34
+ * @author ONDA 3 - Core Modules (A) + the provider lineage (B)
35
+ * @license MIT
36
+ * @since 2026
37
+ */
38
+
39
+ import * as fs from "node:fs";
40
+ import * as os from "node:os";
41
+ import * as path from "node:path";
42
+ import * as crypto from "node:crypto";
43
+ import { createHash } from "node:crypto";
44
+
45
+ // Deduplicated against the frozen owners (constants.ts / models.ts) and the
46
+ // canonical in-set file (project.ts): symbols whose local definitions were
47
+ // value-identical are imported and re-exported below, so each has exactly one
48
+ // definition (library dedupe task 2-d1).
49
+ import {
50
+ PROJECT_FALLBACK,
51
+ QUOTA_SOFT_THRESHOLD,
52
+ QUOTA_CACHE_TTL_MIN_MS,
53
+ QUOTA_CACHE_TTL_MS,
54
+ QUOTA_TIMEOUT_MS,
55
+ JITTER_MIN_MS,
56
+ JITTER_MAX_MS,
57
+ ANTIGRAVITY_VERSION_FALLBACK,
58
+ ANTIGRAVITY_USER_AGENT_FALLBACK,
59
+ ANTIGRAVITY_CLIENT_ID_IS_BLINDED,
60
+ ANTIGRAVITY_CLIENT_SECRET_IS_BLINDED,
61
+ ANTIGRAVITY_ENDPOINT_FALLBACKS,
62
+ GEMINI_CLI_ENDPOINT,
63
+ QUOTA_ENDPOINT,
64
+ PROJECT_ID_FALLBACK,
65
+ PROJECT_NAME_FALLBACK,
66
+ } from "./constants.js";
67
+ import { fnv1a32, isRetryableStatus } from "./core.js";
68
+ import { getXGoogApiClient } from "./fingerprint.js";
69
+ import type { QuotaGroup } from "./constants.js";
70
+ import { ANTIGRAVITY_VERSION_MIN, isGeminiCLIOnlyModel, MODELS_2026_LEGACY, UA_DEFAULT_MODEL } from "./models.js";
71
+ import { API_PATHS, API_PATHS_DAILY } from "./request.js";
72
+ import {
73
+ QUOTA_CACHE_TTL_MAX_MS,
74
+ QUOTA_HARD_THRESHOLD,
75
+ CLIENT_METADATA_STRING,
76
+ GEMINI_CLI_OAUTH_CLIENT_ID,
77
+ GEMINI_CLI_OAUTH_CLIENT_SECRET,
78
+ GEMINI_CLI_USER_AGENT,
79
+ } from "./constants.js";
80
+ import { X_GOOG_API_CLIENT_GEMINI_CLI as X_GOOG_API_CLIENT } from "./fingerprint.js";
81
+
82
+ // Re-exports preserve this module's public surface after the dedupe.
83
+ export {
84
+ PROJECT_FALLBACK,
85
+ QUOTA_SOFT_THRESHOLD,
86
+ QUOTA_CACHE_TTL_MIN_MS,
87
+ QUOTA_CACHE_TTL_MS,
88
+ QUOTA_TIMEOUT_MS,
89
+ JITTER_MIN_MS,
90
+ JITTER_MAX_MS,
91
+ ANTIGRAVITY_VERSION_FALLBACK,
92
+ ANTIGRAVITY_USER_AGENT_FALLBACK,
93
+ ANTIGRAVITY_CLIENT_ID_IS_BLINDED,
94
+ ANTIGRAVITY_CLIENT_SECRET_IS_BLINDED,
95
+ ANTIGRAVITY_ENDPOINT_FALLBACKS,
96
+ GEMINI_CLI_ENDPOINT,
97
+ QUOTA_ENDPOINT,
98
+ PROJECT_ID_FALLBACK,
99
+ PROJECT_NAME_FALLBACK,
100
+ getXGoogApiClient,
101
+ ANTIGRAVITY_VERSION_MIN,
102
+ isRetryableStatus,
103
+ X_GOOG_API_CLIENT,
104
+ CLIENT_METADATA_STRING,
105
+ };
106
+ // v2.1.15 Phase C: the historical OAUTH_CLIENT_ID/OAUTH_CLIENT_SECRET names
107
+ // (gemini-cli pair) are re-exported from the constants.js owner.
108
+ export {
109
+ GEMINI_CLI_OAUTH_CLIENT_ID as OAUTH_CLIENT_ID,
110
+ GEMINI_CLI_OAUTH_CLIENT_SECRET as OAUTH_CLIENT_SECRET,
111
+ } from "./constants.js";
112
+ export type { QuotaGroup };
113
+
114
+ // =============================================================================
115
+ // BLINDED CONSTANTS - GEMINI CLI BYPASS
116
+ // =============================================================================
117
+
118
+ // OAUTH_CLIENT_ID / OAUTH_CLIENT_SECRET: static Gemini CLI client values
119
+ // identical to the canonical project.ts definitions — imported and re-exported
120
+ // above (constants.ts exports the same values under the
121
+ // GEMINI_CLI_OAUTH_CLIENT_ID / GEMINI_CLI_OAUTH_CLIENT_SECRET names).
122
+
123
+ // v2.1.15 Phase C: GEMINI_CLI_SCOPES is owned by constants.js
124
+ // (GEMINI_CLI_SCOPES); quota.ts no longer imports it (the v2.1.16 cleanup
125
+ // removed its last dead local reference).
126
+
127
+ // PROJECT_FALLBACK: identical "rising-fact-p41fc" — imported and re-exported above.
128
+
129
+ // local variant: no same-named owner export (constants owns CODE_ASSIST_ENDPOINTS / GEMINI_CLI_ENDPOINT under other names)
130
+
131
+ // v2.1.15 Phase D: the method-path maps moved to request.ts as API_PATHS / API_PATHS_DAILY.
132
+
133
+ /**
134
+ * Dual quota mapping: Antigravity uses the daily endpoint (real quota),
135
+ * Gemini CLI uses the base endpoint (historically returns 1.0 but still queried).
136
+ */
137
+ export const QUOTA_ENDPOINTS = {
138
+ antigravity: API_PATHS_DAILY.retrieveUserQuotaSummary,
139
+ "gemini-cli": API_PATHS.retrieveUserQuotaSummary,
140
+ } as const;
141
+
142
+ export type QuotaSource = keyof typeof QUOTA_ENDPOINTS; // 'antigravity' | 'gemini-cli'
143
+
144
+ // local variant: diverges from constants (static full GeminiCLI UA vs the dynamic GEMINI_CLI_USER_AGENT_FALLBACK "gemini-cli/0.57.0 os/arch")
145
+
146
+ export const GEMINI_CLI_USER_AGENT_ANTIGRAVITY =
147
+ "antigravity/1.12.8 (linux; x64; VSCode/1.90.0; Antigravity) google-api-nodejs-client/9.15.1" as const;
148
+
149
+ // X_GOOG_API_CLIENT: static "gl-node/22.19.0" identical to the canonical
150
+ // project.ts definition — imported and re-exported above (constants.ts's
151
+ // same-named symbol is dynamic and therefore divergent).
152
+
153
+ // local variant: no same-named owner export (Antigravity IDE metadata string)
154
+ export const CLIENT_METADATA_ANTIGRAVITY =
155
+ "ideType=IDE_VSCODE,platform=PLATFORM_UNSPECIFIED,pluginType=ANTIGRAVITY" as const;
156
+
157
+ // CLIENT_METADATA_STRING: identical to the canonical project.ts definition —
158
+ // imported and re-exported above.
159
+
160
+ // v2.1.15 Phase A: the local MODELS_2026 (11 legacy ids) and Model2026 type
161
+ // were byte-identical to models.ts MODELS_2026_LEGACY and were deleted — the
162
+ // canonical family names live only in models.ts.
163
+
164
+ // =============================================================================
165
+ // QUOTA GROUPS & TTL CONSTANTS
166
+ // =============================================================================
167
+
168
+ /**
169
+ * Default interval for quota refresh (minutes).
170
+ * Can be overridden via config or env QUOTA_REFRESH_INTERVAL_MINUTES.
171
+ */
172
+ export const QUOTA_REFRESH_INTERVAL_MINUTES_DEFAULT = 15 as const;
173
+ export const QUOTA_REFRESH_INTERVAL_MS_DEFAULT = QUOTA_REFRESH_INTERVAL_MINUTES_DEFAULT * 60 * 1000;
174
+
175
+ // QUOTA_SOFT_THRESHOLD: identical 0.9 — imported and re-exported above.
176
+ export const QUOTA_SOFT_REMAINING = 1 - QUOTA_SOFT_THRESHOLD; // 0.1
177
+
178
+ // v2.1.15 Phase D: QUOTA_HARD_THRESHOLD (0.98 lineage A) is the canonical
179
+ // value in constants.js — imported from the owner.
180
+ export const QUOTA_CACHE_TTL_MS_DEFAULT = 15 * 60 * 1000; // 15min default
181
+ // QUOTA_CACHE_TTL_MIN_MS: identical 2min — imported and re-exported above.
182
+ // v2.1.15 Phase D: QUOTA_CACHE_TTL_MAX_MS (30min lineage A) is the canonical
183
+ // value in constants.js — imported from the owner.
184
+ export const QUOTA_CACHE_FILE = path.join(os.homedir(), ".config", "opencode", "cache", "antigravity", "quota.json");
185
+
186
+ export const QUOTA_REQUEST_TIMEOUT_MS = 10_000 as const;
187
+
188
+ // Canonical definition of the internal logical groups (used by resolveQuotaGroup)
189
+ export type QuotaGroupId =
190
+ | "gemini-3-pro"
191
+ | "gemini-3.1-pro"
192
+ | "gemini-3-flash"
193
+ | "gemini-2.5-pro"
194
+ | "gemini-2.5-flash"
195
+ | "claude-sonnet"
196
+ | "claude-opus"
197
+ | "gemini" // generic grouping returned by the API (Gemini Models)
198
+ | "claude-3p" // generic Claude+GPT grouping (3p)
199
+ | "unknown";
200
+
201
+ // v2.1.15 Phase C: renamed from QuotaGroupDef (the former constants.js
202
+ // group/models[] shape was deleted in v2.1.16 as dead code) — this is the
203
+ // live quota-group definition used by resolveQuotaGroup/getQuotaGroup.
204
+ export interface ResilientQuotaGroupDef {
205
+ id: QuotaGroupId;
206
+ displayNamePattern: RegExp; // regex para casar displayName da API
207
+ models: readonly string[];
208
+ dailyLimit: number;
209
+ rpm: number;
210
+ tpm: number;
211
+ }
212
+
213
+ /**
214
+ * Quota-group routing table. models.ts owns ALL model identification
215
+ * (catalog, rosters, aliases); this table is quota-group routing derived
216
+ * from the models.ts catalog (v2.1.16 dedup note): the ids below are the
217
+ * quota-specific routing keys, and the antigravity-prefixed claude ids are
218
+ * quota-route spellings with no single-id export in models.ts — kept local
219
+ * so quota routing stays byte-identical (no behavior change).
220
+ */
221
+ export const QUOTA_GROUP_DEFS: readonly ResilientQuotaGroupDef[] = [
222
+ {
223
+ id: "gemini-3-pro",
224
+ displayNamePattern: /gemini.*3.*pro/i,
225
+ models: ["antigravity-gemini-3-pro", "gemini-3-pro-preview"] as const,
226
+ dailyLimit: 1000,
227
+ rpm: 60,
228
+ tpm: 1_000_000,
229
+ },
230
+ {
231
+ id: "gemini-3.1-pro",
232
+ displayNamePattern: /gemini.*3\.1.*pro/i,
233
+ models: ["antigravity-gemini-3.1-pro", "gemini-3.1-pro-preview", "gemini-3.1-pro-preview-customtools"] as const,
234
+ dailyLimit: 500,
235
+ rpm: 30,
236
+ tpm: 2_000_000,
237
+ },
238
+ {
239
+ id: "gemini-3-flash",
240
+ displayNamePattern: /gemini.*3.*flash|flash/i,
241
+ models: ["antigravity-gemini-3-flash", "gemini-3-flash-preview"] as const,
242
+ dailyLimit: 2000,
243
+ rpm: 120,
244
+ tpm: 1_000_000,
245
+ },
246
+ {
247
+ id: "gemini-2.5-pro",
248
+ displayNamePattern: /gemini.*2\.5.*pro/i,
249
+ models: ["gemini-2.5-pro"] as const,
250
+ dailyLimit: 1500,
251
+ rpm: 60,
252
+ tpm: 1_000_000,
253
+ },
254
+ {
255
+ id: "gemini-2.5-flash",
256
+ displayNamePattern: /gemini.*2\.5.*flash/i,
257
+ models: ["gemini-2.5-flash"] as const,
258
+ dailyLimit: 3000,
259
+ rpm: 240,
260
+ tpm: 1_000_000,
261
+ },
262
+ {
263
+ id: "claude-sonnet",
264
+ displayNamePattern: /claude.*sonnet|3p|third.party/i,
265
+ models: ["antigravity-claude-sonnet-4-6"] as const,
266
+ dailyLimit: 500,
267
+ rpm: 30,
268
+ tpm: 200_000,
269
+ },
270
+ {
271
+ id: "claude-opus",
272
+ displayNamePattern: /claude.*opus/i,
273
+ models: ["antigravity-claude-opus-4-6-thinking"] as const,
274
+ dailyLimit: 250,
275
+ rpm: 15,
276
+ tpm: 200_000,
277
+ },
278
+ ] as const;
279
+
280
+ // =============================================================================
281
+ // TYPES - QUOTA API RESPONSE
282
+ // =============================================================================
283
+
284
+ export type QuotaWindow = "weekly" | "5h" | "daily" | "hourly" | "monthly" | string;
285
+
286
+ export interface QuotaBucketRaw {
287
+ bucketId: string; // ex: "gemini-weekly", "gemini-5h", "3p-weekly"
288
+ displayName?: string; // "Weekly Limit", "Five Hour Limit"
289
+ window?: QuotaWindow;
290
+ resetTime?: string; // ISO "2026-07-25T01:43:09Z"
291
+ description?: string;
292
+ remainingFraction?: number; // 0.0 - 1.0
293
+ remaining?: number; // numeric alternative
294
+ limit?: number;
295
+ used?: number;
296
+ }
297
+
298
+ export interface QuotaGroupRaw {
299
+ displayName?: string; // "Gemini Models", "Claude and GPT models"
300
+ description?: string; // "Models within this group: Gemini Flash, Gemini Pro"
301
+ buckets?: QuotaBucketRaw[];
302
+ quotaGroup?: string; // backend sometimes returns an id here
303
+ }
304
+
305
+ export interface QuotaSummaryRaw {
306
+ groups?: QuotaGroupRaw[];
307
+ quotaGroup?: QuotaGroupRaw[]; // fallback variante
308
+ // Alguns payloads retornam diretamente buckets no root
309
+ buckets?: QuotaBucketRaw[];
310
+ }
311
+
312
+ export interface QuotaBucketParsed {
313
+ bucketId: string;
314
+ displayName: string;
315
+ window: QuotaWindow;
316
+ resetTime: string | null; // ISO persistido
317
+ resetTimeMs: number | null; // epoch ms for calculations
318
+ remainingFraction: number; // normalizado 0..1
319
+ remainingPercent: number; // 0..100
320
+ isExhausted: boolean; // remaining <= 0.02
321
+ isSoftExhausted: boolean; // remaining <= 0.1 (90% threshold)
322
+ description: string;
323
+ }
324
+
325
+ export interface QuotaGroupParsed {
326
+ id: QuotaGroupId; // resolvido via mapping ou unknown
327
+ displayName: string;
328
+ description: string;
329
+ rawDisplayName: string;
330
+ source: QuotaSource;
331
+ buckets: QuotaBucketParsed[];
332
+ // Helpers agregados
333
+ weeklyRemaining: number | null;
334
+ fiveHourRemaining: number | null;
335
+ minRemainingFraction: number; // menor remaining entre buckets (pior caso)
336
+ minBucket: QuotaBucketParsed | null;
337
+ isExhausted: boolean;
338
+ isSoftExhausted: boolean;
339
+ }
340
+
341
+ export interface QuotaSummaryParsed {
342
+ groups: QuotaGroupParsed[];
343
+ fetchedAt: number;
344
+ expiresAt: number;
345
+ ttlMs: number;
346
+ source: QuotaSource;
347
+ projectId: string;
348
+ // agregados globais
349
+ globalMinRemaining: number;
350
+ isAnyExhausted: boolean;
351
+ isAnySoftExhausted: boolean;
352
+ }
353
+
354
+ export interface QuotaCacheEntry {
355
+ summary: QuotaSummaryParsed;
356
+ raw: QuotaSummaryRaw;
357
+ tokenHash: string; // token hash used for invalidation
358
+ cachedAt: number;
359
+ expiresAt: number;
360
+ ttlMs: number;
361
+ }
362
+
363
+ export interface CheckQuotaResult {
364
+ allowed: boolean;
365
+ groupId: QuotaGroupId | null;
366
+ bucketId: string | null;
367
+ remainingFraction: number | null;
368
+ remainingPercent: number | null;
369
+ resetTime: string | null;
370
+ resetTimeMs: number | null;
371
+ reason: string;
372
+ isSoftThreshold: boolean;
373
+ isHardExhausted: boolean;
374
+ shouldSkipAccount: boolean;
375
+ source: QuotaSource;
376
+ group?: QuotaGroupParsed | undefined;
377
+ bucket?: QuotaBucketParsed | undefined;
378
+ }
379
+
380
+ // =============================================================================
381
+ // UTILS
382
+ // =============================================================================
383
+
384
+ // hashToken (SHA-256 truncated to 32 hex chars with FNV-1a fallback) is
385
+ // defined below next to the cache layer and keys both cache levels.
386
+
387
+ function parseIsoToMs(iso?: string | null): number | null {
388
+ if (!iso) return null;
389
+ const ms = Date.parse(iso);
390
+ return Number.isNaN(ms) ? null : ms;
391
+ }
392
+
393
+ function clampFraction(n: unknown): number {
394
+ if (typeof n !== "number" || Number.isNaN(n)) return 1.0;
395
+ if (n < 0) return 0;
396
+ if (n > 1) return 1;
397
+ return n;
398
+ }
399
+
400
+ function nowMs(): number {
401
+ return Date.now();
402
+ }
403
+
404
+ /**
405
+ * Adaptive TTL: the lower the remaining fraction, the shorter the TTL so
406
+ * refresh happen sooner.
407
+ * - <10% => 2min
408
+ * - <20% => 5min
409
+ * - <50% => 10min
410
+ * - >=50% => 15min default, still honoring a nearby resetTime
411
+ */
412
+ export function computeAdaptiveTtlMs(
413
+ summary: QuotaSummaryRaw | QuotaSummaryParsed,
414
+ baseTtlMs: number = QUOTA_CACHE_TTL_MS_DEFAULT,
415
+ ): number {
416
+ let minRemaining = 1.0;
417
+
418
+ // Extract the lowest remainingFraction
419
+ if ("groups" in summary && Array.isArray((summary as any).groups)) {
420
+ const groups = (summary as QuotaGroupParsed[] | QuotaGroupRaw[]).flatMap((g: any) =>
421
+ Array.isArray(g.buckets) ? g.buckets : [],
422
+ );
423
+ for (const b of groups) {
424
+ const f = clampFraction((b as any).remainingFraction ?? (b as any).remaining ?? 1);
425
+ if (f < minRemaining) minRemaining = f;
426
+ }
427
+ }
428
+
429
+ // When Parsed already carries globalMin
430
+ if ("globalMinRemaining" in summary) {
431
+ minRemaining = (summary as QuotaSummaryParsed).globalMinRemaining;
432
+ }
433
+
434
+ let ttl = baseTtlMs;
435
+
436
+ if (minRemaining <= QUOTA_SOFT_REMAINING) {
437
+ ttl = QUOTA_CACHE_TTL_MIN_MS; // 2min
438
+ } else if (minRemaining <= 0.2) {
439
+ ttl = 5 * 60 * 1000;
440
+ } else if (minRemaining <= 0.5) {
441
+ ttl = 10 * 60 * 1000;
442
+ } else {
443
+ ttl = baseTtlMs;
444
+ }
445
+
446
+ // Adjust when a resetTime is very close (e.g. < ttl)
447
+ let nearestResetMs: number | null = null;
448
+ if ("groups" in summary) {
449
+ for (const g of (summary as any).groups as QuotaGroupParsed[]) {
450
+ for (const b of g.buckets ?? []) {
451
+ const rt = (b as QuotaBucketParsed).resetTimeMs ?? parseIsoToMs((b as any).resetTime);
452
+ if (rt != null) {
453
+ if (nearestResetMs == null || rt < nearestResetMs) nearestResetMs = rt;
454
+ }
455
+ }
456
+ }
457
+ }
458
+
459
+ if (nearestResetMs != null) {
460
+ const msUntilReset = nearestResetMs - nowMs();
461
+ if (msUntilReset > 0 && msUntilReset < ttl) {
462
+ // If reset precedes the TTL, schedule refresh right after reset + 30s jitter
463
+ ttl = Math.max(QUOTA_CACHE_TTL_MIN_MS, msUntilReset + 30_000);
464
+ }
465
+ }
466
+
467
+ return Math.max(QUOTA_CACHE_TTL_MIN_MS, Math.min(ttl, QUOTA_CACHE_TTL_MAX_MS));
468
+ }
469
+
470
+ export function getQuotaRefreshIntervalMs(configMinutes?: number): number {
471
+ const envVal = process.env.QUOTA_REFRESH_INTERVAL_MINUTES ?? process.env.ANTIGRAVITY_QUOTA_REFRESH_MINUTES;
472
+ let minutes = configMinutes ?? QUOTA_REFRESH_INTERVAL_MINUTES_DEFAULT;
473
+
474
+ if (envVal != null) {
475
+ const parsed = Number.parseInt(envVal, 10);
476
+ if (!Number.isNaN(parsed) && parsed > 0 && parsed <= 120) {
477
+ minutes = parsed;
478
+ }
479
+ }
480
+
481
+ if (minutes <= 0) minutes = QUOTA_REFRESH_INTERVAL_MINUTES_DEFAULT;
482
+ if (minutes > 120) minutes = 120;
483
+
484
+ return minutes * 60 * 1000;
485
+ }
486
+
487
+ // =============================================================================
488
+ // MODEL -> GROUP RESOLUTION
489
+ // =============================================================================
490
+
491
+ /**
492
+ * Maps a model string onto the internal QuotaGroupId.
493
+ * Supports both antigravity-* models and native gemini-* models.
494
+ * models.ts owns ALL model identification; this map is quota-group routing
495
+ * derived from the models.ts catalog (v2.1.16 dedup note — values kept
496
+ * byte-identical, no behavior change).
497
+ */
498
+ const MODEL_TO_GROUP_EXACT: Record<string, QuotaGroupId> = {
499
+ "antigravity-gemini-3-pro": "gemini-3-pro",
500
+ "gemini-3-pro-preview": "gemini-3-pro",
501
+ "antigravity-gemini-3.1-pro": "gemini-3.1-pro",
502
+ "gemini-3.1-pro-preview": "gemini-3.1-pro",
503
+ "gemini-3.1-pro-preview-customtools": "gemini-3.1-pro",
504
+ "antigravity-gemini-3-flash": "gemini-3-flash",
505
+ "gemini-3-flash-preview": "gemini-3-flash",
506
+ "gemini-2.5-pro": "gemini-2.5-pro",
507
+ "gemini-2.5-flash": "gemini-2.5-flash",
508
+ "antigravity-claude-sonnet-4-6": "claude-sonnet",
509
+ "antigravity-claude-opus-4-6-thinking": "claude-opus",
510
+
511
+ // Generic API group aliases
512
+ gemini: "gemini",
513
+ claude: "claude-3p",
514
+ gpt: "claude-3p",
515
+ "3p": "claude-3p",
516
+ "claude-3p": "claude-3p",
517
+ };
518
+
519
+ export function resolveQuotaGroup(model: string): QuotaGroupId {
520
+ if (!model || typeof model !== "string") return "unknown";
521
+
522
+ const lower = model.trim().toLowerCase();
523
+
524
+ // Exact
525
+ if (MODEL_TO_GROUP_EXACT[lower]) return MODEL_TO_GROUP_EXACT[lower];
526
+ if (MODEL_TO_GROUP_EXACT[model]) return MODEL_TO_GROUP_EXACT[model];
527
+
528
+ // Heuristics
529
+ if (lower.includes("opus")) return "claude-opus";
530
+ if (lower.includes("sonnet") || lower.includes("claude") || lower.includes("3p") || lower.includes("gpt")) {
531
+ // distinguish opus (handled above)
532
+ if (lower.includes("sonnet")) return "claude-sonnet";
533
+ return "claude-3p";
534
+ }
535
+ if (lower.includes("3.1") && lower.includes("pro")) return "gemini-3.1-pro";
536
+ if (lower.includes("3") && lower.includes("pro")) return "gemini-3-pro";
537
+ if (lower.includes("2.5") && lower.includes("flash")) return "gemini-2.5-flash";
538
+ if (lower.includes("2.5") && lower.includes("pro")) return "gemini-2.5-pro";
539
+ if (lower.includes("flash")) return "gemini-3-flash";
540
+
541
+ // API display names
542
+ if (lower.includes("gemini models") || lower === "gemini") return "gemini";
543
+ if (lower.includes("claude and gpt") || lower.includes("claude")) return "claude-3p";
544
+
545
+ return "unknown";
546
+ }
547
+
548
+ /**
549
+ * Returns the group definition for a model or groupId.
550
+ * Returns null when the groupId is unknown.
551
+ */
552
+ export function getQuotaGroup(input: string): ResilientQuotaGroupDef | null {
553
+ if (!input) return null;
554
+ const gid = resolveQuotaGroup(input);
555
+ if (gid === "unknown") {
556
+ // try matching by displayName pattern
557
+ for (const def of QUOTA_GROUP_DEFS) {
558
+ if (def.displayNamePattern.test(input)) return def;
559
+ }
560
+ return null;
561
+ }
562
+ // generic groupId -> return the closest equivalent def
563
+ if (gid === "gemini") {
564
+ return QUOTA_GROUP_DEFS.find((d) => d.id === "gemini-3-pro") ?? null;
565
+ }
566
+ if (gid === "claude-3p") {
567
+ return QUOTA_GROUP_DEFS.find((d) => d.id === "claude-sonnet") ?? null;
568
+ }
569
+ return QUOTA_GROUP_DEFS.find((d) => d.id === gid) ?? null;
570
+ }
571
+
572
+ /**
573
+ * Lists every available group definition.
574
+ */
575
+ export function listQuotaGroups(): readonly ResilientQuotaGroupDef[] {
576
+ return QUOTA_GROUP_DEFS;
577
+ }
578
+
579
+ // =============================================================================
580
+ // BUILD HEADERS - GEMINI CLI BYPASS
581
+ // =============================================================================
582
+
583
+ function buildDynamicUserAgent(source: QuotaSource): string {
584
+ const platform = os.platform(); // darwin, linux, win32
585
+ const arch = os.arch(); // x64, arm64
586
+
587
+ // Keeps coherence with the local real platform, but allows env override
588
+ const envUA = process.env.GEMINI_CLI_USER_AGENT ?? process.env.ANTIGRAVITY_USER_AGENT;
589
+ if (envUA && envUA.length > 10) return envUA;
590
+
591
+ if (source === "antigravity") {
592
+ // Usa template antigravity para daily endpoint. Local variants of the
593
+ // GEMINI_CLI_USER_AGENT_ANTIGRAVITY family: the per-platform templates
594
+ // carry the models.js UA default model segment (v2.1.16 dedup).
595
+ if (platform === "darwin" && arch === "arm64") {
596
+ return `antigravity/1.12.8/${UA_DEFAULT_MODEL} (darwin; arm64; VSCode/1.90.0) google-api-nodejs-client/9.15.1`;
597
+ }
598
+ if (platform === "win32") {
599
+ return `antigravity/1.12.8/${UA_DEFAULT_MODEL} (win32; x64; VSCode/1.90.0) google-api-nodejs-client/9.15.1`;
600
+ }
601
+ return GEMINI_CLI_USER_AGENT_ANTIGRAVITY;
602
+ }
603
+
604
+ // gemini-cli source: varia por plataforma real. v2.1.16 dedup: the
605
+ // darwin/win32 templates derive from the constants.js GEMINI_CLI_USER_AGENT
606
+ // owner by platform-segment substitution (byte-identical results).
607
+ if (platform === "darwin" && arch === "arm64") {
608
+ return GEMINI_CLI_USER_AGENT.replace("(linux; x64; GitHub)", "(darwin; arm64; GitHub)");
609
+ }
610
+ if (platform === "darwin") {
611
+ return GEMINI_CLI_USER_AGENT.replace("(linux; x64; GitHub)", "(darwin; x64; GitHub)");
612
+ }
613
+ if (platform === "win32") {
614
+ return GEMINI_CLI_USER_AGENT.replace("(linux; x64; GitHub)", "(win32; x64; GitHub)");
615
+ }
616
+ return GEMINI_CLI_USER_AGENT;
617
+ }
618
+
619
+ export function buildQuotaHeaders(token: string, source: QuotaSource = "antigravity"): Record<string, string> {
620
+ const ua = buildDynamicUserAgent(source);
621
+ const xGoog = process.env.X_GOOG_API_CLIENT ?? X_GOOG_API_CLIENT;
622
+ const clientMeta = source === "antigravity" ? CLIENT_METADATA_ANTIGRAVITY : CLIENT_METADATA_STRING;
623
+
624
+ return {
625
+ Authorization: `Bearer ${token}`,
626
+ "Content-Type": "application/json",
627
+ "User-Agent": ua,
628
+ "X-Goog-Api-Client": xGoog,
629
+ "Client-Metadata": clientMeta,
630
+ Accept: "application/json",
631
+ "Accept-Encoding": "gzip, deflate, br",
632
+ };
633
+ }
634
+
635
+ function getEndpointForSource(source: QuotaSource): string {
636
+ return QUOTA_ENDPOINTS[source] ?? QUOTA_ENDPOINTS.antigravity;
637
+ }
638
+
639
+ // =============================================================================
640
+ // PARSE QUOTA RESPONSE - ROBUST
641
+ // =============================================================================
642
+
643
+ export function parseQuotaBucket(raw: QuotaBucketRaw, source: QuotaSource): QuotaBucketParsed {
644
+ const bucketId = String(raw.bucketId ?? raw.displayName ?? "unknown").toLowerCase();
645
+ const displayName = String(raw.displayName ?? raw.bucketId ?? "Unknown").trim() || "Unknown";
646
+ const window = (raw.window ??
647
+ (bucketId.includes("weekly") ? "weekly" : bucketId.includes("5h") ? "5h" : "unknown")) as QuotaWindow;
648
+ const resetIso = raw.resetTime ?? null;
649
+ const resetMs = parseIsoToMs(resetIso);
650
+
651
+ let remainingFraction: number;
652
+ if (typeof raw.remainingFraction === "number") {
653
+ remainingFraction = clampFraction(raw.remainingFraction);
654
+ } else if (typeof raw.remaining === "number" && typeof raw.limit === "number" && raw.limit > 0) {
655
+ remainingFraction = clampFraction(raw.remaining / raw.limit);
656
+ } else if (typeof raw.remaining === "number") {
657
+ // Already a fraction?
658
+ remainingFraction = raw.remaining > 1 ? clampFraction(raw.remaining / 100) : clampFraction(raw.remaining);
659
+ } else if (typeof raw.used === "number" && typeof raw.limit === "number" && raw.limit > 0) {
660
+ remainingFraction = clampFraction(1 - raw.used / raw.limit);
661
+ } else {
662
+ // Fallback: cloudcode-pa always returns 1.0 (known bug) -> treat as 1.0 but flag warning
663
+ remainingFraction = 1.0;
664
+ }
665
+
666
+ const remainingPercent = Math.round(remainingFraction * 10000) / 100; // 2 decimais
667
+ const isExhausted = remainingFraction <= 1 - QUOTA_HARD_THRESHOLD || remainingFraction <= 0.02;
668
+ const isSoftExhausted = remainingFraction <= QUOTA_SOFT_REMAINING;
669
+
670
+ return {
671
+ bucketId,
672
+ displayName,
673
+ window,
674
+ resetTime: resetIso,
675
+ resetTimeMs: resetMs,
676
+ remainingFraction,
677
+ remainingPercent,
678
+ isExhausted,
679
+ isSoftExhausted,
680
+ description: String(raw.description ?? "").trim(),
681
+ };
682
+ }
683
+
684
+ export function parseQuotaGroup(raw: QuotaGroupRaw, source: QuotaSource): QuotaGroupParsed {
685
+ const displayNameRaw = String(raw.displayName ?? raw.quotaGroup ?? "Unknown").trim();
686
+ // Resolve id heuristically
687
+ let id: QuotaGroupId = resolveQuotaGroup(displayNameRaw);
688
+
689
+ // displayName containing "Gemini Models" => generic gemini
690
+ if (/gemini models/i.test(displayNameRaw)) id = "gemini";
691
+ if (/claude.*gpt|3p/i.test(displayNameRaw)) id = "claude-3p";
692
+
693
+ const description = String(raw.description ?? "").trim();
694
+ const bucketsRaw = Array.isArray(raw.buckets) ? raw.buckets : [];
695
+
696
+ const buckets = bucketsRaw
697
+ .map((b) => parseQuotaBucket(b, source))
698
+ .sort((a, b) => {
699
+ // Ordena: 5h primeiro, depois weekly
700
+ if (a.window === "5h" && b.window !== "5h") return -1;
701
+ if (b.window === "5h" && a.window !== "5h") return 1;
702
+ return 0;
703
+ });
704
+
705
+ let weeklyRemaining: number | null = null;
706
+ let fiveHourRemaining: number | null = null;
707
+ let minRemaining = 1.0;
708
+ let minBucket: QuotaBucketParsed | null = null;
709
+
710
+ for (const bucket of buckets) {
711
+ if (bucket.window === "weekly" || bucket.bucketId.includes("weekly")) {
712
+ if (weeklyRemaining == null || bucket.remainingFraction < weeklyRemaining)
713
+ weeklyRemaining = bucket.remainingFraction;
714
+ }
715
+ if (bucket.window === "5h" || bucket.bucketId.includes("5h")) {
716
+ if (fiveHourRemaining == null || bucket.remainingFraction < fiveHourRemaining)
717
+ fiveHourRemaining = bucket.remainingFraction;
718
+ }
719
+ if (bucket.remainingFraction < minRemaining) {
720
+ minRemaining = bucket.remainingFraction;
721
+ minBucket = bucket;
722
+ }
723
+ }
724
+
725
+ if (buckets.length === 0) {
726
+ minRemaining = 1.0;
727
+ }
728
+
729
+ const isExhausted = buckets.some((b) => b.isExhausted) || minRemaining <= 0.02;
730
+ const isSoftExhausted = buckets.some((b) => b.isSoftExhausted) || minRemaining <= QUOTA_SOFT_REMAINING;
731
+
732
+ return {
733
+ id,
734
+ displayName: displayNameRaw,
735
+ description,
736
+ rawDisplayName: displayNameRaw,
737
+ source,
738
+ buckets,
739
+ weeklyRemaining,
740
+ fiveHourRemaining,
741
+ minRemainingFraction: minRemaining,
742
+ minBucket,
743
+ isExhausted,
744
+ isSoftExhausted,
745
+ };
746
+ }
747
+
748
+ /**
749
+ * Parse principal do payload v1internal:retrieveUserQuotaSummary
750
+ */
751
+ export function parseQuotaResponse(
752
+ raw: QuotaSummaryRaw,
753
+ opts?: { source?: QuotaSource; projectId?: string; fetchedAt?: number; baseTtlMs?: number },
754
+ ): QuotaSummaryParsed {
755
+ const source = opts?.source ?? "antigravity";
756
+ const projectId = opts?.projectId ?? PROJECT_FALLBACK;
757
+ const fetchedAt = opts?.fetchedAt ?? nowMs();
758
+ const baseTtl = opts?.baseTtlMs ?? QUOTA_CACHE_TTL_MS_DEFAULT;
759
+
760
+ // Normaliza grupos: pode vir em groups, quotaGroup, ou root buckets
761
+ let rawGroups: QuotaGroupRaw[] = [];
762
+ if (Array.isArray(raw.groups)) rawGroups = raw.groups;
763
+ else if (Array.isArray((raw as any).quotaGroup)) rawGroups = (raw as any).quotaGroup;
764
+ else if (Array.isArray(raw.buckets)) {
765
+ // Wrap isolated buckets into one generic Gemini group
766
+ rawGroups = [
767
+ {
768
+ displayName: "Gemini Models",
769
+ description: "Fallback grouped from root buckets",
770
+ buckets: raw.buckets,
771
+ },
772
+ ];
773
+ }
774
+
775
+ const groups = rawGroups
776
+ .map((g) => parseQuotaGroup(g, source))
777
+ .filter((g) => g.buckets.length > 0 || g.displayName !== "Unknown");
778
+
779
+ // When the API returned empty (cloudcode-pa bug), build a healthy fallback group so requests are not blocked
780
+ if (groups.length === 0) {
781
+ // silent, but builds a structure that keeps allowed=true
782
+ const fallbackGroup: QuotaGroupParsed = {
783
+ id: "gemini",
784
+ displayName: "Gemini Models",
785
+ description: "Fallback empty response - assuming full quota (cloudcode-pa returns 1.0)",
786
+ rawDisplayName: "Gemini Models",
787
+ source,
788
+ buckets: [
789
+ {
790
+ bucketId: "gemini-weekly",
791
+ displayName: "Weekly Limit",
792
+ window: "weekly",
793
+ resetTime: null,
794
+ resetTimeMs: null,
795
+ remainingFraction: 1.0,
796
+ remainingPercent: 100,
797
+ isExhausted: false,
798
+ isSoftExhausted: false,
799
+ description: "Fallback - API returned empty",
800
+ },
801
+ ],
802
+ weeklyRemaining: 1.0,
803
+ fiveHourRemaining: 1.0,
804
+ minRemainingFraction: 1.0,
805
+ minBucket: null,
806
+ isExhausted: false,
807
+ isSoftExhausted: false,
808
+ };
809
+ // Point minBucket at the single bucket
810
+ fallbackGroup.minBucket = fallbackGroup.buckets[0] ?? null;
811
+
812
+ const ttl = computeAdaptiveTtlMs({ groups: [fallbackGroup] } as any, baseTtl);
813
+
814
+ return {
815
+ groups: [fallbackGroup],
816
+ fetchedAt,
817
+ expiresAt: fetchedAt + ttl,
818
+ ttlMs: ttl,
819
+ source,
820
+ projectId,
821
+ globalMinRemaining: 1.0,
822
+ isAnyExhausted: false,
823
+ isAnySoftExhausted: false,
824
+ };
825
+ }
826
+
827
+ let globalMin = 1.0;
828
+ let anyExhausted = false;
829
+ let anySoft = false;
830
+
831
+ for (const g of groups) {
832
+ if (g.minRemainingFraction < globalMin) globalMin = g.minRemainingFraction;
833
+ if (g.isExhausted) anyExhausted = true;
834
+ if (g.isSoftExhausted) anySoft = true;
835
+ }
836
+
837
+ const ttl = computeAdaptiveTtlMs({ groups } as any, baseTtl);
838
+
839
+ return {
840
+ groups,
841
+ fetchedAt,
842
+ expiresAt: fetchedAt + ttl,
843
+ ttlMs: ttl,
844
+ source,
845
+ projectId,
846
+ globalMinRemaining: globalMin,
847
+ isAnyExhausted: anyExhausted,
848
+ isAnySoftExhausted: anySoft,
849
+ };
850
+ }
851
+
852
+ // =============================================================================
853
+ // FETCH QUOTA - retrieveUserQuotaSummary
854
+ // =============================================================================
855
+
856
+ export interface RetrieveQuotaOptions {
857
+ source?: QuotaSource; // default antigravity (daily endpoint = quota real)
858
+ timeoutMs?: number;
859
+ retries?: number;
860
+ baseTtlMs?: number;
861
+ signal?: AbortSignal;
862
+ }
863
+
864
+ export class QuotaFetchError extends Error {
865
+ public status?: number | undefined;
866
+ public body?: string | undefined;
867
+ public source: QuotaSource;
868
+ constructor(message: string, source: QuotaSource, status?: number, body?: string) {
869
+ super(message);
870
+ this.name = "QuotaFetchError";
871
+ this.status = status;
872
+ this.body = body;
873
+ this.source = source;
874
+ }
875
+ }
876
+
877
+ /**
878
+ * Direct call to the v1internal:retrieveUserQuotaSummary endpoint.
879
+ * POST {"project": "<projectId>"}
880
+ *
881
+ * @param token - OAuth Bearer access_token
882
+ * @param projectId - cloudaicompanionProject id, fallback rising-fact-p41fc
883
+ * @param opts - source antigravity|gemini-cli, timeout, retries
884
+ */
885
+ export async function retrieveUserQuotaSummary(
886
+ token: string,
887
+ projectId: string = PROJECT_FALLBACK,
888
+ opts?: RetrieveQuotaOptions,
889
+ ): Promise<QuotaSummaryParsed> {
890
+ if (!token || typeof token !== "string" || token.trim().length < 10) {
891
+ throw new TypeError("retrieveUserQuotaSummary: invalid token");
892
+ }
893
+
894
+ const source = opts?.source ?? "antigravity";
895
+ const timeoutMs = opts?.timeoutMs ?? QUOTA_REQUEST_TIMEOUT_MS;
896
+ const retries = opts?.retries ?? 1;
897
+ const baseTtlMs = opts?.baseTtlMs ?? QUOTA_CACHE_TTL_MS_DEFAULT;
898
+
899
+ const endpoint = getEndpointForSource(source);
900
+ const sanitizedProject = (projectId ?? PROJECT_FALLBACK).trim() || PROJECT_FALLBACK;
901
+
902
+ const headers = buildQuotaHeaders(token, source);
903
+ const body = JSON.stringify({ project: sanitizedProject });
904
+
905
+ let lastError: unknown = null;
906
+
907
+ for (let attempt = 0; attempt <= retries; attempt++) {
908
+ const controller = new AbortController();
909
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
910
+
911
+ // If the caller passed an external signal, propagate its abort
912
+ let externalAbortListener: (() => void) | null = null;
913
+ if (opts?.signal) {
914
+ if (opts.signal.aborted) {
915
+ clearTimeout(timer);
916
+ throw new QuotaFetchError("AbortError: external signal aborted", source, 0);
917
+ }
918
+ externalAbortListener = () => controller.abort();
919
+ opts.signal.addEventListener("abort", externalAbortListener, { once: true });
920
+ }
921
+
922
+ try {
923
+ const res = await fetch(endpoint, {
924
+ method: "POST",
925
+ headers,
926
+ body,
927
+ signal: controller.signal,
928
+ } as RequestInit);
929
+
930
+ clearTimeout(timer);
931
+ if (externalAbortListener && opts?.signal) {
932
+ opts.signal.removeEventListener("abort", externalAbortListener);
933
+ }
934
+
935
+ if (!res.ok) {
936
+ const text = await res.text().catch(() => "");
937
+ // A 403 on cloudcode-pa is expected for retrieveUserQuotaSummary under local-only OAuth.
938
+ // Try fallback? If antigravity fails, try gemini-cli? No - keep the error for the caller to decide.
939
+ throw new QuotaFetchError(
940
+ `Quota fetch failed ${res.status} ${res.statusText} on ${source}: ${text.slice(0, 500)}`,
941
+ source,
942
+ res.status,
943
+ text,
944
+ );
945
+ }
946
+
947
+ const json = (await res.json().catch(async () => {
948
+ const txt = await res.text().catch(() => "");
949
+ if (txt) {
950
+ try {
951
+ return JSON.parse(txt);
952
+ } catch {
953
+ // ignored
954
+ }
955
+ }
956
+ return {} as QuotaSummaryRaw;
957
+ })) as QuotaSummaryRaw;
958
+
959
+ const parsed = parseQuotaResponse(json, {
960
+ source,
961
+ projectId: sanitizedProject,
962
+ fetchedAt: nowMs(),
963
+ baseTtlMs,
964
+ });
965
+
966
+ return parsed;
967
+ } catch (err) {
968
+ clearTimeout(timer);
969
+ if (externalAbortListener && opts?.signal) {
970
+ opts.signal.removeEventListener("abort", externalAbortListener);
971
+ }
972
+
973
+ lastError = err;
974
+
975
+ // No retry on 401/403/400
976
+ if (err instanceof QuotaFetchError) {
977
+ if (err.status === 401 || err.status === 403 || err.status === 400) {
978
+ throw err;
979
+ }
980
+ }
981
+
982
+ // Timeout / network - retry while attempts remain
983
+ if (attempt < retries) {
984
+ const backoff = 200 * Math.pow(2, attempt) + Math.random() * 200;
985
+ await new Promise((r) => setTimeout(r, backoff));
986
+ continue;
987
+ }
988
+
989
+ // Attempts exhausted
990
+ if (err instanceof QuotaFetchError) throw err;
991
+ throw new QuotaFetchError(`Quota fetch network error on ${source}: ${(err as Error).message}`, source, 0);
992
+ }
993
+ }
994
+
995
+ throw lastError instanceof Error
996
+ ? lastError
997
+ : new QuotaFetchError(`Unknown quota fetch error on ${source}`, source, 0);
998
+ }
999
+
1000
+ /**
1001
+ * Dual fetch: tries antigravity (daily) first, then gemini-cli as fallback.
1002
+ * Returns the merged parsed summary with the primary source.
1003
+ */
1004
+ export async function retrieveDualQuota(
1005
+ token: string,
1006
+ projectId: string = PROJECT_FALLBACK,
1007
+ opts?: RetrieveQuotaOptions,
1008
+ ): Promise<{
1009
+ antigravity: QuotaSummaryParsed | null;
1010
+ geminiCli: QuotaSummaryParsed | null;
1011
+ primary: QuotaSummaryParsed;
1012
+ }> {
1013
+ // antigravity deve ser primary (quota real)
1014
+ let antigravity: QuotaSummaryParsed | null = null;
1015
+ let geminiCli: QuotaSummaryParsed | null = null;
1016
+
1017
+ // tenta ambos em paralelo, mas antigravity tem prioridade
1018
+ const results = await Promise.allSettled([
1019
+ retrieveUserQuotaSummary(token, projectId, { ...opts, source: "antigravity" }),
1020
+ retrieveUserQuotaSummary(token, projectId, { ...opts, source: "gemini-cli" }),
1021
+ ]);
1022
+
1023
+ if (results[0].status === "fulfilled") antigravity = results[0].value;
1024
+ if (results[1].status === "fulfilled") geminiCli = results[1].value;
1025
+
1026
+ const primary = antigravity ?? geminiCli;
1027
+
1028
+ if (!primary) {
1029
+ // both failed - rethrow the antigravity error when available
1030
+ const err0 = results[0].status === "rejected" ? results[0].reason : null;
1031
+ const err1 = results[1].status === "rejected" ? results[1].reason : null;
1032
+ throw err0 ?? err1 ?? new QuotaFetchError("Both quota sources failed", "antigravity", 0);
1033
+ }
1034
+
1035
+ return { antigravity, geminiCli, primary };
1036
+ }
1037
+
1038
+ // =============================================================================
1039
+ // CACHE MANAGER - ADAPTIVE TTL + FILE PERSISTENCE
1040
+ // =============================================================================
1041
+
1042
+ interface FileCachePayload {
1043
+ version: 1;
1044
+ entries: Record<string, QuotaCacheEntry>; // key = tokenHash+project+source
1045
+ savedAt: number;
1046
+ }
1047
+
1048
+ function makeCacheKey(token: string, projectId: string, source: QuotaSource): string {
1049
+ const h = hashToken(token);
1050
+ const proj = projectId || PROJECT_FALLBACK;
1051
+ return `${h}:${proj}:${source}`;
1052
+ }
1053
+
1054
+ function loadFileCacheSync(): Record<string, QuotaCacheEntry> {
1055
+ try {
1056
+ if (!fs.existsSync(QUOTA_CACHE_FILE)) return {};
1057
+ const raw = fs.readFileSync(QUOTA_CACHE_FILE, "utf8");
1058
+ const parsed = JSON.parse(raw) as FileCachePayload;
1059
+ if (parsed.version !== 1 || !parsed.entries) return {};
1060
+ // Drop entries expired long ago (> 24h)
1061
+ const now = nowMs();
1062
+ const out: Record<string, QuotaCacheEntry> = {};
1063
+ for (const [k, v] of Object.entries(parsed.entries)) {
1064
+ if (v.expiresAt && v.expiresAt > now - 24 * 60 * 60 * 1000) {
1065
+ out[k] = v;
1066
+ }
1067
+ }
1068
+ return out;
1069
+ } catch {
1070
+ return {};
1071
+ }
1072
+ }
1073
+
1074
+ function saveFileCacheSync(entries: Record<string, QuotaCacheEntry>): void {
1075
+ try {
1076
+ const dir = path.dirname(QUOTA_CACHE_FILE);
1077
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
1078
+ const payload: FileCachePayload = {
1079
+ version: 1,
1080
+ entries,
1081
+ savedAt: nowMs(),
1082
+ };
1083
+ const tmp = QUOTA_CACHE_FILE + ".tmp." + crypto.randomBytes(4).toString("hex");
1084
+ fs.writeFileSync(tmp, JSON.stringify(payload, null, 2), { encoding: "utf8", mode: 0o600 });
1085
+ try {
1086
+ fs.chmodSync(tmp, 0o600);
1087
+ } catch {
1088
+ /* the guarded best-effort operation falls through: the outer flow owns the failure */
1089
+ }
1090
+ fs.renameSync(tmp, QUOTA_CACHE_FILE);
1091
+ } catch {
1092
+ // silence cache errors
1093
+ }
1094
+ }
1095
+
1096
+ let _memoryFileCache: Record<string, QuotaCacheEntry> | null = null;
1097
+ let _inflightFetches: Map<string, Promise<QuotaSummaryParsed>> = new Map();
1098
+
1099
+ function getFileCache(): Record<string, QuotaCacheEntry> {
1100
+ if (_memoryFileCache) return _memoryFileCache;
1101
+ _memoryFileCache = loadFileCacheSync();
1102
+ return _memoryFileCache;
1103
+ }
1104
+
1105
+ export class QuotaCacheManager {
1106
+ private memory: Map<string, QuotaCacheEntry> = new Map();
1107
+ private requestCount: number = 0;
1108
+ private lastRefreshAt: number = 0;
1109
+ private refreshIntervalMs: number;
1110
+ public softThreshold: number; // remaining threshold
1111
+
1112
+ constructor(opts?: { refreshIntervalMinutes?: number; softThreshold?: number }) {
1113
+ this.refreshIntervalMs = getQuotaRefreshIntervalMs(opts?.refreshIntervalMinutes);
1114
+ this.softThreshold = opts?.softThreshold ?? QUOTA_SOFT_REMAINING; // 0.1 remaining = 90% usado
1115
+ // preload file cache into memory
1116
+ const file = getFileCache();
1117
+ for (const [k, v] of Object.entries(file)) {
1118
+ this.memory.set(k, v);
1119
+ }
1120
+ }
1121
+
1122
+ private getKey(token: string, projectId: string, source: QuotaSource): string {
1123
+ return makeCacheKey(token, projectId, source);
1124
+ }
1125
+
1126
+ getCached(token: string, projectId: string, source: QuotaSource = "antigravity"): QuotaSummaryParsed | null {
1127
+ const key = this.getKey(token, projectId, source);
1128
+ const entry = this.memory.get(key);
1129
+ if (!entry) return null;
1130
+
1131
+ if (nowMs() > entry.expiresAt) {
1132
+ // expired, but still serve stale within a grace window (50% ttl)
1133
+ const grace = entry.ttlMs * 0.5;
1134
+ if (nowMs() > entry.expiresAt + grace) {
1135
+ this.memory.delete(key);
1136
+ return null;
1137
+ }
1138
+ // stale-while-revalidate
1139
+ return entry.summary;
1140
+ }
1141
+
1142
+ return entry.summary;
1143
+ }
1144
+
1145
+ isCacheExpired(token: string, projectId: string, source: QuotaSource = "antigravity"): boolean {
1146
+ const key = this.getKey(token, projectId, source);
1147
+ const entry = this.memory.get(key);
1148
+ if (!entry) return true;
1149
+ return nowMs() > entry.expiresAt;
1150
+ }
1151
+
1152
+ setCached(
1153
+ token: string,
1154
+ projectId: string,
1155
+ source: QuotaSource,
1156
+ raw: QuotaSummaryRaw,
1157
+ parsed: QuotaSummaryParsed,
1158
+ ): void {
1159
+ const key = this.getKey(token, projectId, source);
1160
+ const ttl = parsed.ttlMs ?? computeAdaptiveTtlMs(parsed, QUOTA_CACHE_TTL_MS_DEFAULT);
1161
+
1162
+ const entry: QuotaCacheEntry = {
1163
+ summary: { ...parsed, ttlMs: ttl, expiresAt: nowMs() + ttl },
1164
+ raw,
1165
+ tokenHash: hashToken(token),
1166
+ cachedAt: nowMs(),
1167
+ expiresAt: nowMs() + ttl,
1168
+ ttlMs: ttl,
1169
+ };
1170
+
1171
+ this.memory.set(key, entry);
1172
+
1173
+ // persist
1174
+ const fileCache = getFileCache();
1175
+ fileCache[key] = entry;
1176
+ // cap size (max 50 entries)
1177
+ const keys = Object.keys(fileCache);
1178
+ if (keys.length > 50) {
1179
+ // drop oldest
1180
+ const sorted = keys.sort((a, b) => (fileCache[a]?.cachedAt ?? 0) - (fileCache[b]?.cachedAt ?? 0));
1181
+ for (let i = 0; i < sorted.length - 50; i++) delete fileCache[sorted[i]!];
1182
+ }
1183
+ saveFileCacheSync(fileCache);
1184
+ _memoryFileCache = fileCache;
1185
+ }
1186
+
1187
+ /**
1188
+ * Clears the cache for a token/project or everything.
1189
+ */
1190
+ clear(token?: string, projectId?: string, source?: QuotaSource): void {
1191
+ if (!token) {
1192
+ this.memory.clear();
1193
+ try {
1194
+ fs.unlinkSync(QUOTA_CACHE_FILE);
1195
+ } catch {
1196
+ /* the guarded best-effort operation falls through: the outer flow owns the failure */
1197
+ }
1198
+ _memoryFileCache = {};
1199
+ return;
1200
+ }
1201
+ const targetProject = projectId ?? PROJECT_FALLBACK;
1202
+ if (source) {
1203
+ const key = this.getKey(token, targetProject, source);
1204
+ this.memory.delete(key);
1205
+ const fc = getFileCache();
1206
+ delete fc[key];
1207
+ saveFileCacheSync(fc);
1208
+ } else {
1209
+ // clear both sources
1210
+ for (const src of ["antigravity", "gemini-cli"] as QuotaSource[]) {
1211
+ const key = this.getKey(token, targetProject, src);
1212
+ this.memory.delete(key);
1213
+ const fc = getFileCache();
1214
+ delete fc[key];
1215
+ saveFileCacheSync(fc);
1216
+ }
1217
+ }
1218
+ }
1219
+
1220
+ /**
1221
+ * Background refresh without blocking the caller.
1222
+ * Deduplicates in-flight requests per key.
1223
+ */
1224
+ refreshInBackground(token: string, projectId: string, source: QuotaSource = "antigravity"): void {
1225
+ const key = this.getKey(token, projectId, source);
1226
+
1227
+ if (_inflightFetches.has(key)) return; // already in progress
1228
+
1229
+ const p = retrieveUserQuotaSummary(token, projectId, { source })
1230
+ .then((parsed) => {
1231
+ // empty raw fallback because background has no original raw
1232
+ this.setCached(token, projectId, source, { groups: [] }, parsed);
1233
+ this.lastRefreshAt = nowMs();
1234
+ return parsed;
1235
+ })
1236
+ .catch(() => {
1237
+ // silencia erro background
1238
+ return null as any;
1239
+ })
1240
+ .finally(() => {
1241
+ _inflightFetches.delete(key);
1242
+ });
1243
+
1244
+ _inflightFetches.set(key, p);
1245
+
1246
+ // Non-blocking: fire-and-forget, but avoids unhandled rejections
1247
+ p.catch(() => {});
1248
+ }
1249
+
1250
+ /**
1251
+ * Called after each successful request to decide whether to schedule a refresh.
1252
+ */
1253
+ trackRequestAndMaybeRefresh(token: string, projectId: string, source: QuotaSource = "antigravity"): void {
1254
+ this.requestCount++;
1255
+ const now = nowMs();
1256
+ const sinceLast = now - this.lastRefreshAt;
1257
+
1258
+ // Refresh interval elapsed -> schedule background refresh
1259
+ if (sinceLast > this.refreshIntervalMs) {
1260
+ this.refreshInBackground(token, projectId, source);
1261
+ return;
1262
+ }
1263
+
1264
+ // Heuristic: low remaining requires more frequent refreshes
1265
+ const cached = this.getCached(token, projectId, source);
1266
+ if (cached && cached.globalMinRemaining < 0.3) {
1267
+ // Remaining <30% and 2min elapsed -> refresh
1268
+ if (sinceLast > QUOTA_CACHE_TTL_MIN_MS) {
1269
+ this.refreshInBackground(token, projectId, source);
1270
+ }
1271
+ }
1272
+ }
1273
+
1274
+ getStats(): { size: number; requestCount: number; lastRefreshAt: number; refreshIntervalMs: number } {
1275
+ return {
1276
+ size: this.memory.size,
1277
+ requestCount: this.requestCount,
1278
+ lastRefreshAt: this.lastRefreshAt,
1279
+ refreshIntervalMs: this.refreshIntervalMs,
1280
+ };
1281
+ }
1282
+ }
1283
+
1284
+ // Singleton default
1285
+ let _defaultCacheManager: QuotaCacheManager | null = null;
1286
+
1287
+ export function getQuotaCacheManager(opts?: { refreshIntervalMinutes?: number | undefined }): QuotaCacheManager {
1288
+ if (!_defaultCacheManager) {
1289
+ _defaultCacheManager = new QuotaCacheManager({
1290
+ refreshIntervalMinutes: opts?.refreshIntervalMinutes ?? getQuotaRefreshIntervalMs() / 60000,
1291
+ });
1292
+ }
1293
+ return _defaultCacheManager;
1294
+ }
1295
+
1296
+ // =============================================================================
1297
+ // CORE QUOTA CHECK APIs
1298
+ // =============================================================================
1299
+
1300
+ /**
1301
+ * Checks quota for a specific group.
1302
+ * Returns an allowed boolean plus metadata.
1303
+ *
1304
+ * @param token - access token
1305
+ * @param projectId - project id
1306
+ * @param group - group id ou model string (ex: "gemini-3-pro" ou "antigravity-gemini-3-pro")
1307
+ * @param opts - source, softThreshold override, useCache
1308
+ */
1309
+ export async function checkQuota(
1310
+ token: string,
1311
+ projectId: string = PROJECT_FALLBACK,
1312
+ group?: string | null,
1313
+ opts?: {
1314
+ source?: QuotaSource;
1315
+ softThreshold?: number; // remaining threshold (default 0.1)
1316
+ useCache?: boolean;
1317
+ refreshIntervalMinutes?: number | undefined;
1318
+ },
1319
+ ): Promise<CheckQuotaResult> {
1320
+ const source = opts?.source ?? "antigravity";
1321
+ const softRemaining = opts?.softThreshold ?? QUOTA_SOFT_REMAINING;
1322
+ const useCache = opts?.useCache ?? true;
1323
+
1324
+ const cacheMgr = getQuotaCacheManager({ refreshIntervalMinutes: opts?.refreshIntervalMinutes });
1325
+
1326
+ let summary: QuotaSummaryParsed | null = null;
1327
+
1328
+ if (useCache) {
1329
+ summary = cacheMgr.getCached(token, projectId, source);
1330
+ }
1331
+
1332
+ if (!summary) {
1333
+ try {
1334
+ summary = await retrieveUserQuotaSummary(token, projectId, { source });
1335
+ cacheMgr.setCached(token, projectId, source, { groups: [] }, summary);
1336
+ } catch (e) {
1337
+ // Se falha fetch mas tem cache expirado stale, usa stale como fallback permissivo
1338
+ const staleKey = makeCacheKey(token, projectId, source);
1339
+ const staleEntry = (getFileCache()[staleKey] ?? null) as QuotaCacheEntry | null;
1340
+ if (staleEntry?.summary) {
1341
+ summary = staleEntry.summary;
1342
+ } else {
1343
+ throw e;
1344
+ }
1345
+ }
1346
+ } else {
1347
+ // Expired cache -> trigger a non-blocking background refresh
1348
+ if (cacheMgr.isCacheExpired(token, projectId, source)) {
1349
+ cacheMgr.refreshInBackground(token, projectId, source);
1350
+ }
1351
+ // track request for a potential refresh
1352
+ cacheMgr.trackRequestAndMaybeRefresh(token, projectId, source);
1353
+ }
1354
+
1355
+ if (!summary) {
1356
+ // fallback final: allow
1357
+ return {
1358
+ allowed: true,
1359
+ groupId: null,
1360
+ bucketId: null,
1361
+ remainingFraction: 1,
1362
+ remainingPercent: 100,
1363
+ resetTime: null,
1364
+ resetTimeMs: null,
1365
+ reason: "no quota data, assuming allowed",
1366
+ isSoftThreshold: false,
1367
+ isHardExhausted: false,
1368
+ shouldSkipAccount: false,
1369
+ source,
1370
+ };
1371
+ }
1372
+
1373
+ // Resolve target group
1374
+ let targetGroups: QuotaGroupParsed[] = summary.groups;
1375
+
1376
+ if (group) {
1377
+ const gid = resolveQuotaGroup(group);
1378
+ if (gid !== "unknown") {
1379
+ // With a specific group, filter by exact id or generic mapping
1380
+ if (gid === "gemini") {
1381
+ // generic gemini => pick the Gemini Models group when present, else every gemini-*
1382
+ const geminiModelsGroup = summary.groups.find((g) => g.id === "gemini" || /gemini models/i.test(g.displayName));
1383
+ if (geminiModelsGroup) targetGroups = [geminiModelsGroup];
1384
+ else targetGroups = summary.groups.filter((g) => g.id.startsWith("gemini"));
1385
+ } else if (gid === "claude-3p") {
1386
+ const claudeGroup = summary.groups.find((g) => g.id === "claude-3p" || /claude.*gpt|3p/i.test(g.displayName));
1387
+ if (claudeGroup) targetGroups = [claudeGroup];
1388
+ else targetGroups = summary.groups.filter((g) => g.id.startsWith("claude"));
1389
+ } else {
1390
+ // try exact match
1391
+ const exact = summary.groups.filter((g) => g.id === gid);
1392
+ if (exact.length > 0) targetGroups = exact;
1393
+ else {
1394
+ // Without an exact API mapping (the API returns generic groups), use the global minRemaining but keep gid in the result
1395
+ // target stays as all groups since the API does not split sub-models
1396
+ targetGroups = summary.groups;
1397
+ }
1398
+ }
1399
+ } else {
1400
+ // unknown group -> use all groups
1401
+ targetGroups = summary.groups;
1402
+ }
1403
+ }
1404
+
1405
+ // Worst case across the target groups
1406
+ let minRemaining = 1.0;
1407
+ let minBucket: QuotaBucketParsed | null = null;
1408
+ let minGroup: QuotaGroupParsed | null = null;
1409
+
1410
+ for (const g of targetGroups) {
1411
+ if (g.minRemainingFraction < minRemaining) {
1412
+ minRemaining = g.minRemainingFraction;
1413
+ minBucket = g.minBucket ?? g.buckets[0] ?? null;
1414
+ minGroup = g;
1415
+ }
1416
+ }
1417
+
1418
+ if (targetGroups.length === 0) {
1419
+ // No target groups - fall back to allow
1420
+ return {
1421
+ allowed: true,
1422
+ groupId: group ? resolveQuotaGroup(group) : null,
1423
+ bucketId: null,
1424
+ remainingFraction: 1,
1425
+ remainingPercent: 100,
1426
+ resetTime: null,
1427
+ resetTimeMs: null,
1428
+ reason: "no matching groups, assuming allowed",
1429
+ isSoftThreshold: false,
1430
+ isHardExhausted: false,
1431
+ shouldSkipAccount: false,
1432
+ source,
1433
+ };
1434
+ }
1435
+
1436
+ const isHardExhausted = minRemaining <= 0.02 || minRemaining <= 1 - QUOTA_HARD_THRESHOLD;
1437
+ const isSoftThreshold = minRemaining <= softRemaining;
1438
+
1439
+ const shouldSkip = isHardExhausted || (isSoftThreshold && minRemaining <= softRemaining);
1440
+
1441
+ // allowed=false only on hard exhaustion; soft allows but flags shouldSkip for the load balancer
1442
+ const allowed = !isHardExhausted;
1443
+
1444
+ const gidResult = group ? resolveQuotaGroup(group) : (minGroup?.id ?? null);
1445
+
1446
+ return {
1447
+ allowed,
1448
+ groupId: gidResult,
1449
+ bucketId: minBucket?.bucketId ?? null,
1450
+ remainingFraction: minRemaining,
1451
+ remainingPercent: Math.round(minRemaining * 10000) / 100,
1452
+ resetTime: minBucket?.resetTime ?? null,
1453
+ resetTimeMs: minBucket?.resetTimeMs ?? null,
1454
+ reason: isHardExhausted
1455
+ ? `quota exhausted (${(minRemaining * 100).toFixed(2)}% remaining, bucket ${minBucket?.bucketId})`
1456
+ : isSoftThreshold
1457
+ ? `soft threshold breached (90% used, ${Math.round(minRemaining * 100)}% remaining)`
1458
+ : `quota ok (${Math.round(minRemaining * 100)}% remaining)`,
1459
+ isSoftThreshold,
1460
+ isHardExhausted,
1461
+ shouldSkipAccount: shouldSkip,
1462
+ source,
1463
+ group: minGroup ?? undefined,
1464
+ bucket: minBucket ?? undefined,
1465
+ };
1466
+ }
1467
+
1468
+ /**
1469
+ * Reports whether quota is exhausted (hard threshold).
1470
+ * Thin wrapper over checkQuota kept for compatibility.
1471
+ */
1472
+ export async function isQuotaExhausted(
1473
+ token: string,
1474
+ projectId: string = PROJECT_FALLBACK,
1475
+ group?: string | null,
1476
+ opts?: { source?: QuotaSource; useCache?: boolean },
1477
+ ): Promise<boolean> {
1478
+ const result = await checkQuota(token, projectId, group, opts);
1479
+ return result.isHardExhausted;
1480
+ }
1481
+
1482
+ /**
1483
+ * Decides whether the account should be skipped (soft 90% threshold).
1484
+ * Used during multi-account rotation.
1485
+ */
1486
+ export async function shouldSkipAccount(
1487
+ token: string,
1488
+ projectId: string = PROJECT_FALLBACK,
1489
+ group?: string | null,
1490
+ opts?: { source?: QuotaSource; softThreshold?: number; useCache?: boolean },
1491
+ ): Promise<boolean> {
1492
+ const result = await checkQuota(token, projectId, group, opts);
1493
+ return result.shouldSkipAccount;
1494
+ }
1495
+
1496
+ /**
1497
+ * Lists the current parsed groups (cache first, otherwise fetch).
1498
+ */
1499
+ export async function getQuotaGroups(
1500
+ token: string,
1501
+ projectId: string = PROJECT_FALLBACK,
1502
+ opts?: { source?: QuotaSource; useCache?: boolean },
1503
+ ): Promise<QuotaGroupParsed[]> {
1504
+ const source = opts?.source ?? "antigravity";
1505
+ const useCache = opts?.useCache ?? true;
1506
+
1507
+ const cacheMgr = getQuotaCacheManager();
1508
+
1509
+ if (useCache) {
1510
+ const cached = cacheMgr.getCached(token, projectId, source);
1511
+ if (cached) return cached.groups;
1512
+ }
1513
+
1514
+ const summary = await retrieveUserQuotaSummary(token, projectId, { source });
1515
+ cacheMgr.setCached(token, projectId, source, { groups: [] }, summary);
1516
+ return summary.groups;
1517
+ }
1518
+
1519
+ // =============================================================================
1520
+ // QUOTA MANAGER CLASS - DUAL QUOTA + BACKGROUND REFRESH
1521
+ // =============================================================================
1522
+
1523
+ export interface QuotaManagerOptions {
1524
+ refreshIntervalMinutes?: number;
1525
+ softThreshold?: number; // remaining fraction (default 0.1)
1526
+ hardThreshold?: number; // remaining fraction (default 0.02)
1527
+ projectId?: string;
1528
+ source?: QuotaSource; // default antigravity
1529
+ enableDual?: boolean; // se true, busca ambos antigravity+gemini-cli e mescla
1530
+ }
1531
+
1532
+ export class QuotaManager {
1533
+ public readonly projectId: string;
1534
+ public readonly source: QuotaSource;
1535
+ public readonly refreshIntervalMinutes: number;
1536
+ public readonly softThreshold: number; // remaining
1537
+ public readonly hardThreshold: number;
1538
+ public readonly enableDual: boolean;
1539
+
1540
+ private cacheMgr: QuotaCacheManager;
1541
+ private token: string | null = null;
1542
+ private lastGroups: QuotaGroupParsed[] = [];
1543
+
1544
+ constructor(opts?: QuotaManagerOptions) {
1545
+ this.projectId = opts?.projectId ?? PROJECT_FALLBACK;
1546
+ this.source = opts?.source ?? "antigravity";
1547
+ this.refreshIntervalMinutes = opts?.refreshIntervalMinutes ?? QUOTA_REFRESH_INTERVAL_MINUTES_DEFAULT;
1548
+ this.softThreshold = opts?.softThreshold ?? QUOTA_SOFT_REMAINING;
1549
+ this.hardThreshold = opts?.hardThreshold ?? 0.02;
1550
+ this.enableDual = opts?.enableDual ?? true;
1551
+
1552
+ this.cacheMgr = getQuotaCacheManager({ refreshIntervalMinutes: this.refreshIntervalMinutes });
1553
+ }
1554
+
1555
+ setToken(token: string): void {
1556
+ if (!token || token.length < 10) throw new TypeError("QuotaManager.setToken: invalid token");
1557
+ this.token = token;
1558
+ }
1559
+
1560
+ getToken(): string | null {
1561
+ return this.token;
1562
+ }
1563
+
1564
+ /**
1565
+ * Forces an immediate refresh (primary + secondary when dual).
1566
+ */
1567
+ async refresh(token?: string, projectId?: string): Promise<QuotaSummaryParsed> {
1568
+ const t = token ?? this.token;
1569
+ if (!t) throw new Error("QuotaManager.refresh: token not set. Call setToken() or pass a token.");
1570
+
1571
+ const pid = projectId ?? this.projectId;
1572
+
1573
+ if (this.enableDual) {
1574
+ const dual = await retrieveDualQuota(t, pid, {
1575
+ source: this.source,
1576
+ baseTtlMs: getQuotaRefreshIntervalMs(this.refreshIntervalMinutes),
1577
+ });
1578
+ // Cache both sources when available
1579
+ if (dual.antigravity) {
1580
+ this.cacheMgr.setCached(t, pid, "antigravity", { groups: [] }, dual.antigravity);
1581
+ }
1582
+ if (dual.geminiCli) {
1583
+ this.cacheMgr.setCached(t, pid, "gemini-cli", { groups: [] }, dual.geminiCli);
1584
+ }
1585
+ this.lastGroups = dual.primary.groups;
1586
+ return dual.primary;
1587
+ } else {
1588
+ const summary = await retrieveUserQuotaSummary(t, pid, {
1589
+ source: this.source,
1590
+ baseTtlMs: getQuotaRefreshIntervalMs(this.refreshIntervalMinutes),
1591
+ });
1592
+ this.cacheMgr.setCached(t, pid, this.source, { groups: [] }, summary);
1593
+ this.lastGroups = summary.groups;
1594
+ return summary;
1595
+ }
1596
+ }
1597
+
1598
+ /**
1599
+ * Non-blocking background refresh.
1600
+ */
1601
+ refreshInBackground(token?: string, projectId?: string, source?: QuotaSource): void {
1602
+ const t = token ?? this.token;
1603
+ if (!t) return;
1604
+ const pid = projectId ?? this.projectId;
1605
+ const src = source ?? this.source;
1606
+ this.cacheMgr.refreshInBackground(t, pid, src);
1607
+
1608
+ if (this.enableDual) {
1609
+ const other: QuotaSource = src === "antigravity" ? "gemini-cli" : "antigravity";
1610
+ this.cacheMgr.refreshInBackground(t, pid, other);
1611
+ }
1612
+ }
1613
+
1614
+ /**
1615
+ * Called after each successful generateContent request.
1616
+ */
1617
+ notifyRequestSuccess(token?: string, projectId?: string): void {
1618
+ const t = token ?? this.token;
1619
+ if (!t) return;
1620
+ this.cacheMgr.trackRequestAndMaybeRefresh(t, projectId ?? this.projectId, this.source);
1621
+ }
1622
+
1623
+ async checkQuota(group?: string | null, token?: string): Promise<CheckQuotaResult> {
1624
+ const t = token ?? this.token;
1625
+ if (!t) throw new Error("QuotaManager.checkQuota: token not set");
1626
+
1627
+ return checkQuota(t, this.projectId, group, {
1628
+ source: this.source,
1629
+ softThreshold: this.softThreshold,
1630
+ refreshIntervalMinutes: this.refreshIntervalMinutes,
1631
+ });
1632
+ }
1633
+
1634
+ async isQuotaExhausted(group?: string | null, token?: string): Promise<boolean> {
1635
+ const t = token ?? this.token;
1636
+ if (!t) throw new Error("QuotaManager.isQuotaExhausted: token not set");
1637
+ return isQuotaExhausted(t, this.projectId, group, { source: this.source });
1638
+ }
1639
+
1640
+ async shouldSkipAccount(group?: string | null, token?: string): Promise<boolean> {
1641
+ const t = token ?? this.token;
1642
+ if (!t) throw new Error("QuotaManager.shouldSkipAccount: token not set");
1643
+ return shouldSkipAccount(t, this.projectId, group, {
1644
+ source: this.source,
1645
+ softThreshold: this.softThreshold,
1646
+ });
1647
+ }
1648
+
1649
+ async getQuotaGroups(token?: string): Promise<QuotaGroupParsed[]> {
1650
+ const t = token ?? this.token;
1651
+ if (!t) throw new Error("QuotaManager.getQuotaGroups: token not set");
1652
+ return getQuotaGroups(t, this.projectId, { source: this.source });
1653
+ }
1654
+
1655
+ resolveQuotaGroup(model: string): QuotaGroupId {
1656
+ return resolveQuotaGroup(model);
1657
+ }
1658
+
1659
+ getQuotaGroup(input: string): ResilientQuotaGroupDef | null {
1660
+ return getQuotaGroup(input);
1661
+ }
1662
+
1663
+ getCacheStats() {
1664
+ return this.cacheMgr.getStats();
1665
+ }
1666
+
1667
+ clearCache(token?: string, source?: QuotaSource): void {
1668
+ if (token) {
1669
+ this.cacheMgr.clear(token, this.projectId, source);
1670
+ } else if (this.token) {
1671
+ this.cacheMgr.clear(this.token, this.projectId, source);
1672
+ } else {
1673
+ this.cacheMgr.clear();
1674
+ }
1675
+ }
1676
+ }
1677
+
1678
+ // =============================================================================
1679
+ // SINGLETON HELPERS + COMPATIBILITY EXPORTS
1680
+ // =============================================================================
1681
+
1682
+ let _defaultQuotaManager: QuotaManager | null = null;
1683
+
1684
+ export function getDefaultQuotaManager(opts?: QuotaManagerOptions): QuotaManager {
1685
+ if (!_defaultQuotaManager) {
1686
+ _defaultQuotaManager = new QuotaManager(opts);
1687
+ }
1688
+ return _defaultQuotaManager;
1689
+ }
1690
+
1691
+ /**
1692
+ * Helper for the opencode plugin: verifies quota before routing a request.
1693
+ * Returns true when the request may proceed.
1694
+ */
1695
+ export async function canProceedWithRequest(
1696
+ token: string,
1697
+ model: string,
1698
+ projectId: string = PROJECT_FALLBACK,
1699
+ opts?: { source?: QuotaSource | undefined; refreshIntervalMinutes?: number | undefined },
1700
+ ): Promise<{ allowed: boolean; check: CheckQuotaResult }> {
1701
+ const group = resolveQuotaGroup(model);
1702
+ const check = await checkQuota(token, projectId, group, {
1703
+ source: opts?.source ?? "antigravity",
1704
+ refreshIntervalMinutes: opts?.refreshIntervalMinutes,
1705
+ });
1706
+ return { allowed: check.allowed, check };
1707
+ }
1708
+
1709
+ // =============================================================================
1710
+ // CONFIG RESOLUTION - quota_refresh_interval_minutes
1711
+ // =============================================================================
1712
+
1713
+ export interface QuotaConfig {
1714
+ quota_refresh_interval_minutes: number;
1715
+ quota_soft_threshold_percent: number; // 90 default
1716
+ quota_hard_threshold_percent: number; // 98 default
1717
+ quota_source: QuotaSource;
1718
+ quota_dual_enabled: boolean;
1719
+ quota_cache_ttl_ms_default: number;
1720
+ project_fallback: string;
1721
+ }
1722
+
1723
+ export const DEFAULT_QUOTA_CONFIG: QuotaConfig = {
1724
+ quota_refresh_interval_minutes: QUOTA_REFRESH_INTERVAL_MINUTES_DEFAULT,
1725
+ quota_soft_threshold_percent: 90,
1726
+ quota_hard_threshold_percent: 98,
1727
+ quota_source: "antigravity",
1728
+ quota_dual_enabled: true,
1729
+ quota_cache_ttl_ms_default: QUOTA_CACHE_TTL_MS_DEFAULT,
1730
+ project_fallback: PROJECT_FALLBACK,
1731
+ };
1732
+
1733
+ export function resolveQuotaConfig(partial?: Partial<QuotaConfig>): QuotaConfig {
1734
+ const envMinutes = process.env.QUOTA_REFRESH_INTERVAL_MINUTES ?? process.env.ANTIGRAVITY_QUOTA_REFRESH_MINUTES;
1735
+ let minutes = partial?.quota_refresh_interval_minutes ?? DEFAULT_QUOTA_CONFIG.quota_refresh_interval_minutes;
1736
+ if (envMinutes) {
1737
+ const p = Number.parseInt(envMinutes, 10);
1738
+ if (!Number.isNaN(p) && p > 0) minutes = p;
1739
+ }
1740
+
1741
+ return {
1742
+ quota_refresh_interval_minutes: Math.max(1, Math.min(minutes, 120)),
1743
+ quota_soft_threshold_percent:
1744
+ partial?.quota_soft_threshold_percent ?? DEFAULT_QUOTA_CONFIG.quota_soft_threshold_percent,
1745
+ quota_hard_threshold_percent:
1746
+ partial?.quota_hard_threshold_percent ?? DEFAULT_QUOTA_CONFIG.quota_hard_threshold_percent,
1747
+ quota_source: partial?.quota_source ?? DEFAULT_QUOTA_CONFIG.quota_source,
1748
+ quota_dual_enabled: partial?.quota_dual_enabled ?? DEFAULT_QUOTA_CONFIG.quota_dual_enabled,
1749
+ quota_cache_ttl_ms_default: partial?.quota_cache_ttl_ms_default ?? DEFAULT_QUOTA_CONFIG.quota_cache_ttl_ms_default,
1750
+ project_fallback: partial?.project_fallback ?? DEFAULT_QUOTA_CONFIG.project_fallback,
1751
+ };
1752
+ }
1753
+
1754
+ // =============================================================================
1755
+ // PROVENANCE NOTES - BLINDED CONSTANTS (lineage-B history; the values
1756
+ // themselves are owned by constants.ts / auth.ts / models.ts and re-exported
1757
+ // at the top of this module)
1758
+ // =============================================================================
1759
+
1760
+ /**
1761
+ * Three public OAuth client IDs circulate in public binaries:
1762
+ * - 1071006060591-tmhssin2h21lcre235vtolojh4g403ep... (Antigravity primary, accounts.ts)
1763
+ * - 681255809395-oo8ft2oq6bda7b6t5a2prl1saaq1hhs... (Gemini CLI secondary)
1764
+ * - 681255809395-oo8f…b135j... (Gemini CLI official)
1765
+ * These are public client IDs (not confidential). The client secrets below are
1766
+ * likewise public in those binaries but flagged as blinded for log redaction.
1767
+ */
1768
+ // v2.1.15 Phase C: the REAL env-overridable antigravity-cli client pair is
1769
+ // owned by auth.js (CLIENT_ID/CLIENT_SECRET — same env chain incl. the
1770
+ // GOOGLE_CLIENT_ID fallback). constants.js keeps only the blinded decoy.
1771
+
1772
+ // v2.1.15 Phase C: ANTIGRAVITY_CLIENT_ID_FALLBACK_SECONDARY moved to constants.js.
1773
+
1774
+ // v2.1.15 Phase C: the local env-overridable gemini-cli pair (with the
1775
+ // stale typo'd secret tail Cu5cWa_kRl) was deleted in favor of the
1776
+ // constants.js GEMINI_CLI_OAUTH_CLIENT_ID/SECRET owner (live-validated
1777
+ // secret tail Cu5clXFsxl).
1778
+
1779
+ // ANTIGRAVITY_CLIENT_ID_IS_BLINDED / ANTIGRAVITY_CLIENT_SECRET_IS_BLINDED:
1780
+ // identical `true` flags — imported and re-exported above.
1781
+
1782
+ /** Version fallback (governance keeps 1.19.2 as last known to bypass ban) + floor. */
1783
+ // ANTIGRAVITY_VERSION_FALLBACK / ANTIGRAVITY_USER_AGENT_FALLBACK: identical
1784
+ // values — imported and re-exported above (from constants.ts).
1785
+ // ANTIGRAVITY_VERSION_MIN: identical "1.15.8" — imported and re-exported above (from models.ts).
1786
+
1787
+ // =============================================================================
1788
+ // v2.1.16 dead-code cleanup: the unused resilient lineage-B quota engine
1789
+ // (zero consumers) was deleted — resolveQuotaPoolGroup + its aliases
1790
+ // (resolveGroup, getQuotaGroupForModel, modelToGroup),
1791
+ // parseQuotaResponseResilient, retrieveUserQuotaSummaryResilient,
1792
+ // checkQuotaFromSummary, isQuotaExhaustedBySummary, shouldSkipAccountBySummary,
1793
+ // getAdaptiveTtlMs, resolveBaseTtlFromConfig, parseRateLimitHeaders +
1794
+ // RateLimitInfo, QuotaManagerResilient + getQuotaManager /
1795
+ // resetQuotaManager / fetchQuotaWithCache / refreshQuotaBackground,
1796
+ // resolveCachePath, QUOTA_INFO, the Quota facade, the resilient types
1797
+ // (QuotaPoolStats, QuotaSummary, CheckQuotaResultResilient,
1798
+ // QuotaFileCacheEntry, FileCacheData, QuotaManagerResilientOptions,
1799
+ // RetrieveQuotaOptionsResilient) and the private helpers used only by that
1800
+ // engine (normalizePlatform, normalizeArch, RESILIENT_CASCADE_HOSTS,
1801
+ // RESILIENT_QUOTA_CACHE_TTL_MIN/MAX_MS, FILE_MODE, DIR_MODE,
1802
+ // CACHE_FILE_NAME, ensureDir, ensureDirSync, atomicWriteFileAtomic,
1803
+ // atomicWriteFileSync, buildCacheKey, MODEL_GROUP_MAP, normalizeGroupName,
1804
+ // tryParseNumber, parseDateToMs, tryParseBool, extractPoolStats). Every symbol
1805
+ // was grep-verified to have zero consumers outside quota.ts before deletion.
1806
+ // The live lineage-A engine above is untouched.
1807
+ // =============================================================================
1808
+
1809
+ /**
1810
+ * Cloud Code Assist quota method path — the historical QUOTA_METHOD name
1811
+ * consumed by plugin.ts. v2.1.16 dedup: derived from the constants.js
1812
+ * QUOTA_ENDPOINT owner (itself derived from CODE_ASSIST_PATH_MAP
1813
+ * .RETRIEVE_USER_QUOTA_SUMMARY) instead of a duplicated path literal.
1814
+ */
1815
+ export const QUOTA_METHOD = QUOTA_ENDPOINT;
1816
+
1817
+ /**
1818
+ * SHA-256 token hash truncated to 32 hex chars (FNV-1a fallback) — the cache
1819
+ * key primitive of the live lineage-A file cache (makeCacheKey / setCached).
1820
+ */
1821
+ export function hashToken(token: string): string {
1822
+ try {
1823
+ return createHash("sha256").update(token).digest("hex").slice(0, 32);
1824
+ } catch {
1825
+ return String(fnv1a32(token)).padStart(8, "0");
1826
+ }
1827
+ }
1828
+
1829
+ // =============================================================================
1830
+ // EXPORT DEFAULT
1831
+ // =============================================================================
1832
+
1833
+ const quotaModule = {
1834
+ // canonical constants
1835
+ OAUTH_CLIENT_ID: GEMINI_CLI_OAUTH_CLIENT_ID,
1836
+ OAUTH_CLIENT_SECRET: GEMINI_CLI_OAUTH_CLIENT_SECRET,
1837
+ PROJECT_FALLBACK,
1838
+ ENDPOINTS: API_PATHS,
1839
+ ENDPOINTS_DAILY: API_PATHS_DAILY,
1840
+ QUOTA_ENDPOINTS,
1841
+ QUOTA_METHOD,
1842
+ MODELS_2026: MODELS_2026_LEGACY,
1843
+ QUOTA_GROUP_DEFS,
1844
+ QUOTA_REFRESH_INTERVAL_MINUTES_DEFAULT,
1845
+ QUOTA_SOFT_THRESHOLD,
1846
+ QUOTA_HARD_THRESHOLD,
1847
+
1848
+ // core functions
1849
+ retrieveUserQuotaSummary,
1850
+ retrieveDualQuota,
1851
+ parseQuotaResponse,
1852
+ parseQuotaBucket,
1853
+ parseQuotaGroup,
1854
+ computeAdaptiveTtlMs,
1855
+ getQuotaRefreshIntervalMs,
1856
+
1857
+ // resolution
1858
+ resolveQuotaGroup,
1859
+ getQuotaGroup,
1860
+ listQuotaGroups,
1861
+ isGeminiCLIOnlyModel,
1862
+
1863
+ // checks
1864
+ checkQuota,
1865
+ isQuotaExhausted,
1866
+ shouldSkipAccount,
1867
+ getQuotaGroups,
1868
+ canProceedWithRequest,
1869
+
1870
+ // cache + managers
1871
+ QuotaCacheManager,
1872
+ getQuotaCacheManager,
1873
+ QuotaManager,
1874
+ getDefaultQuotaManager,
1875
+
1876
+ // config
1877
+ DEFAULT_QUOTA_CONFIG,
1878
+ resolveQuotaConfig,
1879
+
1880
+ // headers
1881
+ buildQuotaHeaders,
1882
+
1883
+ // extras
1884
+ hashToken,
1885
+ };
1886
+
1887
+ export default quotaModule;