@wenathlan/devthink 2.0.14 → 2.0.19

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 +60 -0
  2. package/{dist/bridge.d.ts → bridge.d.ts} +1 -1
  3. package/bridge.d.ts.map +1 -0
  4. package/bridge.js +1877 -0
  5. package/bun.d.ts.map +1 -0
  6. package/bun.js +52329 -0
  7. package/caps/background.json +712 -0
  8. package/caps/cli.json +360 -0
  9. package/caps/library.json +3611 -0
  10. package/caps/mcp.json +126 -0
  11. package/caps/pagebridge.json +377 -0
  12. package/caps/popup.json +51 -0
  13. package/caps/sidepanel.json +522 -0
  14. package/checksums.txt +142 -0
  15. package/cli.d.ts.map +1 -0
  16. package/{dist/cli.js → cli.js} +2 -2
  17. package/companion.d.ts.map +1 -0
  18. package/companion.js +389 -0
  19. package/crossbrowser.d.ts.map +1 -0
  20. package/crossbrowser.js +1191 -0
  21. package/dashdone.d.ts.map +1 -0
  22. package/dashdone.js +368 -0
  23. package/deno.d.ts.map +1 -0
  24. package/deno.js +52325 -0
  25. package/devthink.umd.js +52346 -0
  26. package/fixtures/bun.ts +7 -0
  27. package/fixtures/cjs.js +6 -0
  28. package/fixtures/deno.ts +6 -0
  29. package/fixtures/example-org-pagestate.json +22 -0
  30. package/fixtures/importers/automa-workflow.json +18 -0
  31. package/fixtures/importers/selenium-side.json +20 -0
  32. package/fixtures/importers/tabular-plan.csv +7 -0
  33. package/fixtures/importers/uivision-macro.json +10 -0
  34. package/fixtures/importers/v1-plan.json +13 -0
  35. package/fixtures/mcp-client.mjs +14 -0
  36. package/fixtures/pages/checkout.html +192 -0
  37. package/fixtures/pages/dashboard.html +110 -0
  38. package/fixtures/pages/feed.html +113 -0
  39. package/fixtures/pages/product-grid.html +150 -0
  40. package/fixtures/plans/form-inventory-plan.json +11 -0
  41. package/fixtures/plans/release-notes-plan.json +11 -0
  42. package/fixtures/recipes/agentscompete.json +11 -0
  43. package/fixtures/recipes/agentsforms.json +11 -0
  44. package/fixtures/recipes/agentsmonitor.json +11 -0
  45. package/fixtures/recipes/agentsreview.json +29 -0
  46. package/fixtures/recipes/agentswarm.json +11 -0
  47. package/fixtures/recipes/fillcalendar.json +12 -0
  48. package/fixtures/recipes/fillcheckout.json +13 -0
  49. package/fixtures/recipes/filldependentdropdowns.json +12 -0
  50. package/fixtures/recipes/fillfromcsv.json +13 -0
  51. package/fixtures/recipes/fillloginform.json +12 -0
  52. package/fixtures/recipes/fillmultistepwizard.json +13 -0
  53. package/fixtures/recipes/fillregistration.json +12 -0
  54. package/fixtures/recipes/fillsearchfilters.json +12 -0
  55. package/fixtures/recipes/filltypeahead.json +11 -0
  56. package/fixtures/recipes/monitoralerts.json +12 -0
  57. package/fixtures/recipes/monitoravailability.json +11 -0
  58. package/fixtures/recipes/monitornetworkquiet.json +12 -0
  59. package/fixtures/recipes/monitorpagechanges.json +11 -0
  60. package/fixtures/recipes/monitorpricechange.json +11 -0
  61. package/fixtures/recipes/monitorschedule.json +12 -0
  62. package/fixtures/recipes/scrapedynamicprices.json +13 -0
  63. package/fixtures/recipes/scrapeimagegallery.json +12 -0
  64. package/fixtures/recipes/scrapeinfinitefeed.json +13 -0
  65. package/fixtures/recipes/scrapenestedlists.json +12 -0
  66. package/fixtures/recipes/scrapeproductgrid.json +12 -0
  67. package/fixtures/recipes/scrapereviews.json +12 -0
  68. package/fixtures/recipes/scrapesearchresults.json +12 -0
  69. package/fixtures/recipes/scrapetablepagination.json +12 -0
  70. package/fixtures/recipes/testaccessibility.json +34 -0
  71. package/fixtures/recipes/testconsoleerrors.json +12 -0
  72. package/fixtures/recipes/testdeeplinks.json +12 -0
  73. package/fixtures/recipes/testformvalidation.json +13 -0
  74. package/fixtures/recipes/testlayoutshift.json +12 -0
  75. package/fixtures/recipes/testlinkcheck.json +12 -0
  76. package/fixtures/recipes/testloadtiming.json +12 -0
  77. package/fixtures/recipes/testvisualdiff.json +12 -0
  78. package/fixtures/workflows/changelog-digest-workflow.json +13 -0
  79. package/fixtures/workflows/form-survey-workflow.json +13 -0
  80. package/gallery.json +706 -0
  81. package/hardening.d.ts.map +1 -0
  82. package/hardening.js +188 -0
  83. package/headless.d.ts.map +1 -0
  84. package/headless.js +11522 -0
  85. package/http.d.ts.map +1 -0
  86. package/http.js +2165 -0
  87. package/index.cjs +54682 -0
  88. package/index.d.ts.map +1 -0
  89. package/index.js +52310 -0
  90. package/index.neutral.js +46343 -0
  91. package/{web/manifest.json → manifest.json} +1 -1
  92. package/mcp.d.ts.map +1 -0
  93. package/mcp.js +20515 -0
  94. package/memory.d.ts.map +1 -0
  95. package/{memory.ts → memory.js} +2310 -3834
  96. package/nativehost.template.json +10 -0
  97. package/node.cjs +54686 -0
  98. package/node.d.ts.map +1 -0
  99. package/pack.d.ts.map +1 -0
  100. package/pack.js +805 -0
  101. package/package.json +83 -176
  102. package/policy.d.ts.map +1 -0
  103. package/{policy.ts → policy.js} +4679 -5073
  104. package/progress.d.ts.map +1 -0
  105. package/progress.js +645 -0
  106. package/protocol.d.ts.map +1 -0
  107. package/protocol.js +8334 -0
  108. package/schemas/audit.schema.json +226 -0
  109. package/schemas/capability.schema.json +113 -0
  110. package/schemas/envelope.schema.json +162 -0
  111. package/schemas/memory.schema.json +51 -0
  112. package/schemas/observation.schema.json +113 -0
  113. package/schemas/plan.schema.json +208 -0
  114. package/schemas/progress.schema.json +126 -0
  115. package/schemas/proposal.schema.json +68 -0
  116. package/schemas/session.schema.json +82 -0
  117. package/schemas/tool.schema.json +94 -0
  118. package/server.d.ts.map +1 -0
  119. package/server.js +28877 -0
  120. package/umd-example.html +18 -0
  121. package/umd.d.ts.map +1 -0
  122. package/accounts.ts +0 -2087
  123. package/agent.ts +0 -2331
  124. package/antigravity.ts +0 -2139
  125. package/apifreeze.ts +0 -4146
  126. package/auth.ts +0 -642
  127. package/background.ts +0 -16837
  128. package/bridge.ts +0 -2080
  129. package/build.ts +0 -54
  130. package/bun.ts +0 -26
  131. package/capture.ts +0 -1614
  132. package/checksecrets.ts +0 -72
  133. package/cjs.ts +0 -9
  134. package/cli.ts +0 -3500
  135. package/commands.ts +0 -1101
  136. package/companion.ts +0 -450
  137. package/compatibility.ts +0 -37
  138. package/config.ts +0 -3137
  139. package/constants.ts +0 -943
  140. package/core.ts +0 -1473
  141. package/crossbrowser.ts +0 -1359
  142. package/dashdone.ts +0 -605
  143. package/data.ts +0 -943
  144. package/database.ts +0 -133
  145. package/debug.ts +0 -850
  146. package/deno.ts +0 -27
  147. package/devthink.ts +0 -707
  148. package/dist/accounts.d.ts +0 -409
  149. package/dist/agent.d.ts +0 -837
  150. package/dist/antigravity.d.ts +0 -380
  151. package/dist/apifreeze.d.ts +0 -113
  152. package/dist/auth.d.ts +0 -224
  153. package/dist/capture.d.ts +0 -595
  154. package/dist/cjs.d.ts +0 -6
  155. package/dist/commands.d.ts +0 -310
  156. package/dist/compatibility.d.ts +0 -17
  157. package/dist/config.d.ts +0 -1853
  158. package/dist/constants.d.ts +0 -637
  159. package/dist/core.d.ts +0 -361
  160. package/dist/data.d.ts +0 -330
  161. package/dist/database.d.ts +0 -24
  162. package/dist/debug.d.ts +0 -270
  163. package/dist/engine.d.ts +0 -74
  164. package/dist/environments.d.ts +0 -244
  165. package/dist/evidence.d.ts +0 -277
  166. package/dist/export.d.ts +0 -311
  167. package/dist/fingerprint.d.ts +0 -594
  168. package/dist/flow.d.ts +0 -300
  169. package/dist/gates.d.ts +0 -103
  170. package/dist/identity.d.ts +0 -51
  171. package/dist/ids.d.ts +0 -7
  172. package/dist/llm.d.ts +0 -652
  173. package/dist/models.d.ts +0 -1807
  174. package/dist/net.d.ts +0 -325
  175. package/dist/neutral.d.ts +0 -41
  176. package/dist/oauth.d.ts +0 -703
  177. package/dist/page.d.ts +0 -1166
  178. package/dist/perf.d.ts +0 -481
  179. package/dist/plan.d.ts +0 -134
  180. package/dist/project.d.ts +0 -395
  181. package/dist/providers.d.ts +0 -37
  182. package/dist/quota.d.ts +0 -453
  183. package/dist/recovery.d.ts +0 -621
  184. package/dist/request.d.ts +0 -1492
  185. package/dist/run.d.ts +0 -779
  186. package/dist/runtime.d.ts +0 -212
  187. package/dist/search.d.ts +0 -375
  188. package/dist/security.d.ts +0 -534
  189. package/dist/serve.d.ts +0 -136
  190. package/dist/session.d.ts +0 -296
  191. package/dist/storage.d.ts +0 -53
  192. package/dist/streaming.d.ts +0 -1119
  193. package/dist/swarm.d.ts +0 -328
  194. package/dist/tools.d.ts +0 -170
  195. package/dist/types.d.ts +0 -8357
  196. package/dist/utils.d.ts +0 -82
  197. package/dist/version.d.ts +0 -7
  198. package/dist/versionregistry.d.ts +0 -406
  199. package/dist/views.d.ts +0 -579
  200. package/dist/workbenchsession.d.ts +0 -69
  201. package/dist/workflow.d.ts +0 -649
  202. package/docs/readme.md +0 -92
  203. package/docs/schemas/antigravity.json +0 -192
  204. package/engine.ts +0 -1524
  205. package/environments.ts +0 -1005
  206. package/evidence.ts +0 -643
  207. package/export.ts +0 -889
  208. package/fingerprint.ts +0 -1458
  209. package/flow.ts +0 -1190
  210. package/gates.ts +0 -307
  211. package/gateway.ts +0 -34
  212. package/hardening.ts +0 -191
  213. package/headless.ts +0 -369
  214. package/http.ts +0 -2110
  215. package/identity.ts +0 -246
  216. package/ids.ts +0 -26
  217. package/index.ts +0 -369
  218. package/llm.ts +0 -2682
  219. package/mcp.ts +0 -1835
  220. package/models.ts +0 -3805
  221. package/modes.ts +0 -43
  222. package/net.ts +0 -1214
  223. package/neutral.ts +0 -362
  224. package/node.ts +0 -26
  225. package/oauth.ts +0 -2216
  226. package/offscreen.ts +0 -170
  227. package/pack.ts +0 -956
  228. package/page.ts +0 -7824
  229. package/pagebridge.ts +0 -1116
  230. package/perf.ts +0 -957
  231. package/plan.ts +0 -672
  232. package/plugin.ts +0 -76
  233. package/prisma.config.ts +0 -27
  234. package/progress.ts +0 -1308
  235. package/project.ts +0 -1291
  236. package/protocol.ts +0 -4187
  237. package/providers.ts +0 -246
  238. package/quota.ts +0 -1887
  239. package/recovery.ts +0 -2433
  240. package/request.ts +0 -4482
  241. package/run.ts +0 -2135
  242. package/runtime.ts +0 -526
  243. package/search.ts +0 -1760
  244. package/security.ts +0 -1673
  245. package/serve.ts +0 -419
  246. package/server.ts +0 -5538
  247. package/session.ts +0 -1162
  248. package/storage.ts +0 -182
  249. package/streaming.ts +0 -4101
  250. package/swarm.ts +0 -884
  251. package/sync.ts +0 -56
  252. package/tests/agentcert.test.ts +0 -128
  253. package/tests/agentfleet.test.ts +0 -214
  254. package/tests/agentreview.test.ts +0 -241
  255. package/tests/agentstream.test.ts +0 -335
  256. package/tests/agentwork.test.ts +0 -356
  257. package/tests/apifreeze.test.ts +0 -290
  258. package/tests/apimap.test.ts +0 -112
  259. package/tests/approvalgate.test.ts +0 -175
  260. package/tests/artifactmanifest.test.ts +0 -204
  261. package/tests/attentionfeed.test.ts +0 -163
  262. package/tests/backgroundruns.test.ts +0 -100
  263. package/tests/batchscheduling.test.ts +0 -131
  264. package/tests/blackboard.test.ts +0 -238
  265. package/tests/bridge.test.ts +0 -1088
  266. package/tests/browsercoverage.test.ts +0 -134
  267. package/tests/browserpolyfills.test.ts +0 -124
  268. package/tests/build.mjs +0 -1260
  269. package/tests/clientauth.test.ts +0 -223
  270. package/tests/clitools.test.ts +0 -578
  271. package/tests/confirmgates.test.ts +0 -190
  272. package/tests/containerpack.test.ts +0 -143
  273. package/tests/controlflow.test.ts +0 -2120
  274. package/tests/coordination.test.ts +0 -345
  275. package/tests/copyscreen.test.ts +0 -139
  276. package/tests/core.test.ts +0 -417
  277. package/tests/costcert.test.ts +0 -95
  278. package/tests/crossbrowser.test.ts +0 -115
  279. package/tests/cspaudit.test.ts +0 -79
  280. package/tests/dashdone.test.ts +0 -615
  281. package/tests/datagrid.test.ts +0 -137
  282. package/tests/doccheck.test.ts +0 -107
  283. package/tests/dombatch.test.ts +0 -109
  284. package/tests/emulation.test.ts +0 -282
  285. package/tests/environments.test.ts +0 -259
  286. package/tests/evidenceviews.test.ts +0 -88
  287. package/tests/exporttools.test.ts +0 -185
  288. package/tests/firefoxprep.test.ts +0 -134
  289. package/tests/flowlibrary.test.ts +0 -242
  290. package/tests/flowrun.test.ts +0 -303
  291. package/tests/forensics.test.ts +0 -439
  292. package/tests/gateway.test.ts +0 -939
  293. package/tests/headlesslib.test.ts +0 -171
  294. package/tests/httpstream.test.ts +0 -277
  295. package/tests/immutablelog.test.ts +0 -157
  296. package/tests/inboundguard.test.ts +0 -177
  297. package/tests/lazyload.test.ts +0 -72
  298. package/tests/librarymodes.test.ts +0 -623
  299. package/tests/llm.test.ts +0 -696
  300. package/tests/manifest.test.ts +0 -226
  301. package/tests/maskinputs.test.ts +0 -150
  302. package/tests/matrixverify.test.ts +0 -111
  303. package/tests/mavenpack.test.ts +0 -75
  304. package/tests/mcpserver.test.ts +0 -704
  305. package/tests/memory.test.ts +0 -4155
  306. package/tests/memorycare.test.ts +0 -230
  307. package/tests/migrateplan.test.ts +0 -349
  308. package/tests/minimization.test.ts +0 -414
  309. package/tests/modelroute.test.ts +0 -112
  310. package/tests/multiagent.test.ts +0 -440
  311. package/tests/nativebridge.test.ts +0 -807
  312. package/tests/navigation.test.ts +0 -284
  313. package/tests/nugetpack.test.ts +0 -83
  314. package/tests/orchestration.test.ts +0 -449
  315. package/tests/originpolicy.test.ts +0 -405
  316. package/tests/outputcompare.test.ts +0 -97
  317. package/tests/package.test.ts +0 -91
  318. package/tests/pagebridge.test.ts +0 -5909
  319. package/tests/pentest.test.ts +0 -75
  320. package/tests/perfrecords.test.ts +0 -257
  321. package/tests/permdiff.test.ts +0 -85
  322. package/tests/phishguard.test.ts +0 -128
  323. package/tests/pickerviews.test.ts +0 -155
  324. package/tests/pipelines.test.ts +0 -381
  325. package/tests/planlint.test.ts +0 -240
  326. package/tests/planreview.test.ts +0 -383
  327. package/tests/platformtargets.test.ts +0 -56
  328. package/tests/policy.test.ts +0 -8284
  329. package/tests/portability.test.ts +0 -96
  330. package/tests/progress.test.ts +0 -1036
  331. package/tests/promptlibrary.test.ts +0 -116
  332. package/tests/protocol.test.ts +0 -4793
  333. package/tests/publishingpipeline.test.ts +0 -314
  334. package/tests/quickactions.test.ts +0 -164
  335. package/tests/readiness.test.ts +0 -78
  336. package/tests/recipes.test.ts +0 -163
  337. package/tests/redactshots.test.ts +0 -216
  338. package/tests/registernvidiakeys.ts +0 -51
  339. package/tests/relayserve.test.ts +0 -304
  340. package/tests/releasenotes.test.ts +0 -276
  341. package/tests/resilience.test.ts +0 -467
  342. package/tests/resourceaware.test.ts +0 -187
  343. package/tests/runbudget.test.ts +0 -125
  344. package/tests/runhistory.test.ts +0 -299
  345. package/tests/runreplay.test.ts +0 -113
  346. package/tests/runresume.test.ts +0 -184
  347. package/tests/runstate.test.ts +0 -288
  348. package/tests/runtimeadapters.test.ts +0 -251
  349. package/tests/safariskeleton.test.ts +0 -115
  350. package/tests/sandboxframe.test.ts +0 -196
  351. package/tests/sbom.test.ts +0 -105
  352. package/tests/secretvault.test.ts +0 -188
  353. package/tests/selcache.test.ts +0 -111
  354. package/tests/sessioninterface.test.ts +0 -791
  355. package/tests/sessions.test.ts +0 -591
  356. package/tests/siteprefs.test.ts +0 -228
  357. package/tests/snapshotdelta.test.ts +0 -165
  358. package/tests/statusviews.test.ts +0 -237
  359. package/tests/stepmeter.test.ts +0 -200
  360. package/tests/streamparse.test.ts +0 -163
  361. package/tests/stylepruning.test.ts +0 -90
  362. package/tests/surfaces.test.ts +0 -282
  363. package/tests/sweep.test.ts +0 -78
  364. package/tests/syncbridge.test.ts +0 -164
  365. package/tests/taskqueue.test.ts +0 -206
  366. package/tests/toolcalls.test.ts +0 -336
  367. package/tests/toolcatalog.test.ts +0 -201
  368. package/tests/torture-agent.test.ts +0 -706
  369. package/tests/torture-auth.test.ts +0 -461
  370. package/tests/torture-commands.test.ts +0 -895
  371. package/tests/torture-data.test.ts +0 -923
  372. package/tests/torture-environments.test.ts +0 -285
  373. package/tests/torture-export.test.ts +0 -263
  374. package/tests/torture-http.test.ts +0 -1379
  375. package/tests/torture-llm.test.ts +0 -808
  376. package/tests/torture-memory.test.ts +0 -1084
  377. package/tests/torture-plan.test.ts +0 -829
  378. package/tests/torture-run.test.ts +0 -626
  379. package/tests/torture-session.test.ts +0 -928
  380. package/tests/torture-swarm.test.ts +0 -1245
  381. package/tests/torture-tools.test.ts +0 -964
  382. package/tests/torture-workflow.test.ts +0 -2354
  383. package/tests/tourviews.test.ts +0 -52
  384. package/tests/transparency.test.ts +0 -147
  385. package/tests/trigger.test.ts +0 -941
  386. package/tests/v1sunset.test.ts +0 -428
  387. package/tests/virtlist.test.ts +0 -94
  388. package/tests/vision.test.ts +0 -540
  389. package/tests/vsixpack.test.ts +0 -145
  390. package/tests/webapi.test.ts +0 -523
  391. package/tests/workflow.test.ts +0 -1785
  392. package/tests/workfloweditor.test.ts +0 -759
  393. package/tests/xpipack.test.ts +0 -128
  394. package/tools.ts +0 -734
  395. package/types.ts +0 -9908
  396. package/umd.ts +0 -9
  397. package/utils.ts +0 -249
  398. package/version.ts +0 -8
  399. package/versionregistry.ts +0 -1310
  400. package/views.ts +0 -2153
  401. package/vitest.config.ts +0 -12
  402. package/web/App.tsx +0 -78
  403. package/web/README.md +0 -47
  404. package/web/capacitor.config.ts +0 -46
  405. package/web/const.ts +0 -19
  406. package/web/control.shell.tsx +0 -66
  407. package/web/dashboardpage.ts +0 -554
  408. package/web/db.ts +0 -493
  409. package/web/design.html +0 -541
  410. package/web/gatewayview/Gateway.tsx +0 -345
  411. package/web/gatewayview/config.ts +0 -1042
  412. package/web/gatewayview/definition.ts +0 -174
  413. package/web/icons.ts +0 -15
  414. package/web/optionspage.ts +0 -691
  415. package/web/popup.ts +0 -1651
  416. package/web/schema.prisma +0 -176
  417. package/web/sidepanel.ts +0 -7941
  418. package/web/sitemanifest.json +0 -13
  419. package/web/terminal-ui.tsx +0 -622
  420. package/web/transparencypage.ts +0 -541
  421. package/web/vite.config.ts +0 -39
  422. package/web/workspace.ts +0 -23
  423. package/workbenchmemory.ts +0 -104
  424. package/workbenchsession.ts +0 -304
  425. package/workflow.ts +0 -4495
  426. /package/{dist/bun.d.ts → bun.d.ts} +0 -0
  427. /package/{dist/cli.d.ts → cli.d.ts} +0 -0
  428. /package/{dist/companion.d.ts → companion.d.ts} +0 -0
  429. /package/{dist/crossbrowser.d.ts → crossbrowser.d.ts} +0 -0
  430. /package/{dist/dashdone.d.ts → dashdone.d.ts} +0 -0
  431. /package/{dist/deno.d.ts → deno.d.ts} +0 -0
  432. /package/{dist/hardening.d.ts → hardening.d.ts} +0 -0
  433. /package/{dist/headless.d.ts → headless.d.ts} +0 -0
  434. /package/{dist/http.d.ts → http.d.ts} +0 -0
  435. /package/{dist/index.d.ts → index.d.ts} +0 -0
  436. /package/{dist/mcp.d.ts → mcp.d.ts} +0 -0
  437. /package/{dist/memory.d.ts → memory.d.ts} +0 -0
  438. /package/{dist/node.d.ts → node.d.ts} +0 -0
  439. /package/{dist/pack.d.ts → pack.d.ts} +0 -0
  440. /package/{dist/policy.d.ts → policy.d.ts} +0 -0
  441. /package/{dist/progress.d.ts → progress.d.ts} +0 -0
  442. /package/{dist/protocol.d.ts → protocol.d.ts} +0 -0
  443. /package/{dist/server.d.ts → server.d.ts} +0 -0
  444. /package/{dist/umd.d.ts → umd.d.ts} +0 -0
package/quota.ts DELETED
@@ -1,1887 +0,0 @@
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;