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