cartethyia 2.5.2 → 2.5.4
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/dist/dashboard/assets/{BannedPage-MsQaeQTs.js → BannedPage-C2orBsI2.js} +1 -1
- package/dist/dashboard/assets/BannedPage-C2orBsI2.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-DUcZ6pMB.js +1 -0
- package/dist/dashboard/assets/CliToolDetailPage-DUcZ6pMB.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolsPage-DfWHTJ7v.js → CliToolsPage-BV6pxVQo.js} +1 -1
- package/dist/dashboard/assets/CliToolsPage-BV6pxVQo.js.gz +0 -0
- package/dist/dashboard/assets/{CombosPage-BrCBQDAc.js → CombosPage-Bsan4679.js} +1 -1
- package/dist/dashboard/assets/CombosPage-Bsan4679.js.gz +0 -0
- package/dist/dashboard/assets/{ConsoleLogPage-C6Uh33er.js → ConsoleLogPage-CW-wPmJ6.js} +1 -1
- package/dist/dashboard/assets/ConsoleLogPage-CW-wPmJ6.js.gz +0 -0
- package/dist/dashboard/assets/{CustomizationPage-8aS9hyJu.js → CustomizationPage-0p8uX7vB.js} +1 -1
- package/dist/dashboard/assets/CustomizationPage-0p8uX7vB.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-BjqSMfko.js +1 -0
- package/dist/dashboard/assets/HealthEventsModal-BjqSMfko.js.gz +0 -0
- package/dist/dashboard/assets/{LoginPage-C8hKyXBe.js → LoginPage-BvzCx8Kr.js} +1 -1
- package/dist/dashboard/assets/LoginPage-BvzCx8Kr.js.gz +0 -0
- package/dist/dashboard/assets/{ModelPicker-CWkTUtpX.js → ModelPicker-BY1aKEkP.js} +1 -1
- package/dist/dashboard/assets/ModelPicker-BY1aKEkP.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-DOQpy1y0.js +1 -0
- package/dist/dashboard/assets/OverviewPage-DOQpy1y0.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-ecxyUMzt.js +3 -0
- package/dist/dashboard/assets/ProviderDetailPage-ecxyUMzt.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-Js9jUPSb.js +1 -0
- package/dist/dashboard/assets/ProvidersPage-Js9jUPSb.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-J3TBi3Kx.js +7 -0
- package/dist/dashboard/assets/ProxyPage-J3TBi3Kx.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-C1gZS6xr.js +1 -0
- package/dist/dashboard/assets/QuotaPage-C1gZS6xr.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-CS86ho5X.js +1 -0
- package/dist/dashboard/assets/SettingsPage-CS86ho5X.js.gz +0 -0
- package/dist/dashboard/assets/{SetupPage-CXjUlfqr.js → SetupPage-DDbMQsoo.js} +1 -1
- package/dist/dashboard/assets/SetupPage-DDbMQsoo.js.gz +0 -0
- package/dist/dashboard/assets/{SortableList-C-_oD3F2.js → SortableList-CB3Z7VTf.js} +1 -1
- package/dist/dashboard/assets/SortableList-CB3Z7VTf.js.gz +0 -0
- package/dist/dashboard/assets/{StudioPage-u9IxmYOI.js → StudioPage-D95UCPHI.js} +3 -3
- package/dist/dashboard/assets/StudioPage-D95UCPHI.js.gz +0 -0
- package/dist/dashboard/assets/{UsagePage-BEVLVrwP.js → UsagePage-Cnae9SWA.js} +13 -13
- package/dist/dashboard/assets/UsagePage-Cnae9SWA.js.gz +0 -0
- package/dist/dashboard/assets/{api-keys-uNywYlfv.js → api-keys-VY68Mhd1.js} +1 -1
- package/dist/dashboard/assets/api-keys-VY68Mhd1.js.gz +0 -0
- package/dist/dashboard/assets/app-COlK95oi.css +2 -0
- package/dist/dashboard/assets/app-COlK95oi.css.gz +0 -0
- package/dist/dashboard/assets/{assert-NHwYq95t.js → assert-Chi7pCnt.js} +1 -1
- package/dist/dashboard/assets/assert-Chi7pCnt.js.gz +0 -0
- package/dist/dashboard/assets/{badge-6uzDDvqs.js → badge-D4pQapKj.js} +1 -1
- package/dist/dashboard/assets/badge-D4pQapKj.js.gz +0 -0
- package/dist/dashboard/assets/{banner-4_WNg2WZ.js → banner-DH6cg-_B.js} +1 -1
- package/dist/dashboard/assets/banner-DH6cg-_B.js.gz +0 -0
- package/dist/dashboard/assets/{card--AtFedAq.js → card-BKqlUOe4.js} +1 -1
- package/dist/dashboard/assets/card-BKqlUOe4.js.gz +0 -0
- package/dist/dashboard/assets/{cli-tools-B3e7dx3N.js → cli-tools-fTgljTGA.js} +1 -1
- package/dist/dashboard/assets/cli-tools-fTgljTGA.js.gz +0 -0
- package/dist/dashboard/assets/{clipboard-button-NreUx6c8.js → clipboard-button-D5Ys_sM0.js} +1 -1
- package/dist/dashboard/assets/clipboard-button-D5Ys_sM0.js.gz +0 -0
- package/dist/dashboard/assets/database-Dz9N3zGd.js +1 -0
- package/dist/dashboard/assets/database-Dz9N3zGd.js.gz +0 -0
- package/dist/dashboard/assets/{dialog-BtpsJU98.js → dialog-BYj-8WuK.js} +1 -1
- package/dist/dashboard/assets/dialog-BYj-8WuK.js.gz +0 -0
- package/dist/dashboard/assets/{drawer-WwwtzihF.js → drawer-CmXIZ6H1.js} +1 -1
- package/dist/dashboard/assets/drawer-CmXIZ6H1.js.gz +0 -0
- package/dist/dashboard/assets/entry-CBg5zeu8.js +1 -0
- package/dist/dashboard/assets/entry-CBg5zeu8.js.gz +0 -0
- package/dist/dashboard/assets/entry-CQBr6U9c.js +1 -0
- package/dist/dashboard/assets/entry-CQBr6U9c.js.gz +0 -0
- package/dist/dashboard/assets/entry-CgdSAzEV.js +4 -0
- package/dist/dashboard/assets/entry-CgdSAzEV.js.gz +0 -0
- package/dist/dashboard/assets/{github-badge-D_eM3IgG.js → github-badge-BlptL8Uf.js} +1 -1
- package/dist/dashboard/assets/github-badge-BlptL8Uf.js.gz +0 -0
- package/dist/dashboard/assets/{index-DCBtROoK.js → index-js6TJvdr.js} +2 -2
- package/dist/dashboard/assets/index-js6TJvdr.js.gz +0 -0
- package/dist/dashboard/assets/{inline-BcNGF8Gr.js → inline-DUWDSmDV.js} +1 -1
- package/dist/dashboard/assets/inline-DUWDSmDV.js.gz +0 -0
- package/dist/dashboard/assets/{input-BG41et9G.js → input-LVjgZq4f.js} +1 -1
- package/dist/dashboard/assets/input-LVjgZq4f.js.gz +0 -0
- package/dist/dashboard/assets/{layout-C8mer_Ak.js → layout-CPfxpsfy.js} +1 -1
- package/dist/dashboard/assets/layout-CPfxpsfy.js.gz +0 -0
- package/dist/dashboard/assets/{list-row-Gh_W4OR_.js → list-row-DupvLfDx.js} +1 -1
- package/dist/dashboard/assets/list-row-DupvLfDx.js.gz +0 -0
- package/dist/dashboard/assets/{live-BJGYRBnR.js → live-C4pEJUqI.js} +1 -1
- package/dist/dashboard/assets/live-C4pEJUqI.js.gz +0 -0
- package/dist/dashboard/assets/model-bans-BRiuufkB.js +1 -0
- package/dist/dashboard/assets/model-bans-BRiuufkB.js.gz +0 -0
- package/dist/dashboard/assets/{mutation-BkoLM5Ba.js → mutation-B4ROwk8L.js} +1 -1
- package/dist/dashboard/assets/mutation-B4ROwk8L.js.gz +0 -0
- package/dist/dashboard/assets/numbers-YNcHvRY7.js +2 -0
- package/dist/dashboard/assets/numbers-YNcHvRY7.js.gz +0 -0
- package/dist/dashboard/assets/{query-keys-D7wX2vM5.js → query-keys-gmjzC3RS.js} +1 -1
- package/dist/dashboard/assets/{query-keys-D7wX2vM5.js.gz → query-keys-gmjzC3RS.js.gz} +0 -0
- package/dist/dashboard/assets/{routing-CcusqXA7.js → routing-C7tBT5o-.js} +1 -1
- package/dist/dashboard/assets/routing-C7tBT5o-.js.gz +0 -0
- package/dist/dashboard/assets/{select-DsX5QT8K.js → select-WUCXorXw.js} +2 -2
- package/dist/dashboard/assets/select-WUCXorXw.js.gz +0 -0
- package/dist/dashboard/assets/{settings-mpmXTgUa.js → settings-C_zKl1GS.js} +1 -1
- package/dist/dashboard/assets/settings-C_zKl1GS.js.gz +0 -0
- package/dist/dashboard/assets/{stack-DwLmjUFK.js → stack-_3O0F8Xn.js} +1 -1
- package/dist/dashboard/assets/stack-_3O0F8Xn.js.gz +0 -0
- package/dist/dashboard/assets/{state-DYdQmdAl.js → state-BgTxHcJS.js} +1 -1
- package/dist/dashboard/assets/state-BgTxHcJS.js.gz +0 -0
- package/dist/dashboard/assets/switch-DEjTtOWf.js +1 -0
- package/dist/dashboard/assets/switch-DEjTtOWf.js.gz +0 -0
- package/dist/dashboard/assets/terminal-0_IjPWTk.js +1 -0
- package/dist/dashboard/assets/terminal-0_IjPWTk.js.gz +0 -0
- package/dist/dashboard/assets/{useMutation-w4u4CVue.js → useMutation-BWSC0UAS.js} +1 -1
- package/dist/dashboard/assets/useMutation-BWSC0UAS.js.gz +0 -0
- package/dist/dashboard/assets/zap-C4EeVW4q.js +1 -0
- package/dist/dashboard/assets/zap-C4EeVW4q.js.gz +0 -0
- package/dist/dashboard/donation/qris-risun-marke.png +0 -0
- package/dist/dashboard/index.html +1 -1
- package/dist/dashboard/index.html.gz +0 -0
- package/dist/dashboard/precompressed-assets.json +54 -54
- package/package.json +1 -1
- package/scripts/cli/autostart.ts +237 -0
- package/scripts/cli/command/console-menu.ts +244 -246
- package/scripts/cli/command/tray.ts +208 -204
- package/scripts/cli/command/update.ts +27 -10
- package/scripts/cli/main.ts +28 -1
- package/src/boot/app.ts +693 -693
- package/src/boot/dependencies.ts +646 -616
- package/src/config/spec.ts +81 -2
- package/src/config/telemetry.ts +46 -0
- package/src/console/api-keys/bucket-spend.ts +15 -4
- package/src/console/backup/contracts.ts +15 -2
- package/src/console/backup/routes.ts +52 -1
- package/src/console/backup/service.ts +60 -4
- package/src/console/console-router.ts +8 -0
- package/src/console/data-management/contracts.ts +93 -0
- package/src/console/data-management/operations.ts +229 -0
- package/src/console/data-management/routes.ts +104 -0
- package/src/console/domain-registration.ts +39 -0
- package/src/console/observability/contracts.ts +524 -485
- package/src/console/observability/store.ts +1115 -1018
- package/src/console/providers/catalog/contracts.ts +6 -0
- package/src/console/providers/catalog/provider-operations.ts +20 -0
- package/src/console/providers/catalog/routes.ts +7 -0
- package/src/console/providers/catalog/store.ts +22 -1
- package/src/console/quota/account-quota-shared.ts +52 -39
- package/src/console/quota/refresh.ts +441 -429
- package/src/console/routing/pools/import-parse.ts +206 -0
- package/src/console/routing/pools/operations.ts +543 -501
- package/src/console/routing/pools/routes.ts +181 -164
- package/src/console/settings/contracts.ts +37 -0
- package/src/console/settings/store.ts +51 -0
- package/src/console/share/share-router.ts +6 -9
- package/src/console/updates/routes.ts +16 -0
- package/src/main.ts +10 -0
- package/src/observability/event-compaction.ts +315 -0
- package/src/observability/event-day-index.ts +442 -0
- package/src/observability/event-log.ts +152 -3
- package/src/observability/payload-capture.ts +373 -344
- package/src/observability/payload-store.ts +440 -366
- package/src/observability/telemetry-status.ts +28 -16
- package/src/persistence/health-event-flush.ts +36 -0
- package/src/persistence/schema.ts +1402 -1360
- package/src/persistence/stores/api-key-store.ts +6 -0
- package/src/persistence/stores/share-store.ts +583 -556
- package/src/persistence/stores/telemetry-store.ts +237 -214
- package/src/protocol/codecs.ts +1 -2
- package/src/protocol/image-clamp.ts +19 -1
- package/src/protocol/registry.ts +8 -0
- package/src/protocol/request/gemini.ts +9 -2
- package/src/protocol/request/messages.ts +14 -2
- package/src/protocol/response/gemini.ts +18 -4
- package/src/providers/adapters/compatible-adapter.ts +3 -0
- package/src/providers/bundled-discovery-providers.ts +122 -119
- package/src/providers/configured-openai-providers.ts +136 -134
- package/src/providers/default-registry.ts +5 -4
- package/src/providers/discovery/openai-model-discovery.ts +5 -0
- package/src/providers/discovery/openrouter-free-roster.ts +15 -17
- package/src/providers/integrations/antigravity/antigravity-quota.ts +11 -3
- package/src/providers/integrations/buddy/buddy-catalog-shared.ts +4 -4
- package/src/providers/integrations/buddy/workbuddy/shared.ts +201 -200
- package/src/providers/integrations/cline/cline-quota.ts +132 -128
- package/src/providers/integrations/codex/codex-headers.ts +25 -39
- package/src/providers/integrations/codex/codex.ts +2 -29
- package/src/providers/integrations/cursor/cursor-quota.ts +310 -0
- package/src/providers/integrations/cursor/cursor-request.ts +616 -471
- package/src/providers/integrations/cursor/cursor-session-manager.ts +319 -281
- package/src/providers/integrations/cursor/cursor.ts +1485 -960
- package/src/providers/integrations/cursor/protocol/messages.ts +25 -0
- package/src/providers/integrations/factory/factory.ts +8 -1
- package/src/providers/integrations/gemini.ts +6 -1
- package/src/providers/integrations/grok/grok-quota.ts +279 -177
- package/src/providers/integrations/grok/grok-ratelimit-store.ts +73 -0
- package/src/providers/integrations/grok/grok-ratelimit.ts +218 -0
- package/src/providers/integrations/opencode/provider.ts +531 -532
- package/src/providers/integrations/qoder/qoder-quota.ts +192 -0
- package/src/providers/integrations/tokenharbor.ts +2 -2
- package/src/providers/integrations/trae/trae-catalog.ts +5 -3
- package/src/providers/integrations/trae/trae-quota.ts +384 -0
- package/src/providers/operations/account-health-service.ts +1523 -1457
- package/src/providers/operations/client-versions.ts +9 -2
- package/src/providers/provider-contracts.ts +7 -0
- package/src/providers/provider-metadata.ts +13 -1
- package/src/providers/quota/quota-support.ts +8 -0
- package/src/security/api-key-auth.ts +504 -494
- package/src/transport/dispatch/attempt-finalize.ts +46 -5
- package/src/transport/dispatch/generation.ts +6 -0
- package/src/transport/dispatch/proxy-request.ts +33 -1
- package/src/transport/dispatch/ratelimit-capture.ts +37 -0
- package/src/transport/dispatch/streaming/stream.ts +30 -2
- package/src/transport/dispatch/upstream.ts +9 -0
- package/src/transport/request/preparer.ts +7 -6
- package/src/transport/request/state.ts +9 -0
- package/src/transport/request/tenant-preferences.ts +60 -0
- package/src/transport/routing/route-catalog.ts +854 -799
- package/dist/dashboard/assets/BannedPage-MsQaeQTs.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-BO9nKg9w.js +0 -1
- package/dist/dashboard/assets/CliToolDetailPage-BO9nKg9w.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-DfWHTJ7v.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-BrCBQDAc.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-C6Uh33er.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-8aS9hyJu.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-qomXThxX.js +0 -1
- package/dist/dashboard/assets/HealthEventsModal-qomXThxX.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-C8hKyXBe.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-CWkTUtpX.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-C-ExCy1V.js +0 -1
- package/dist/dashboard/assets/OverviewPage-C-ExCy1V.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-tr811uaF.js +0 -3
- package/dist/dashboard/assets/ProviderDetailPage-tr811uaF.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-BS9IEpvv.js +0 -1
- package/dist/dashboard/assets/ProvidersPage-BS9IEpvv.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-Rn_qXScb.js +0 -7
- package/dist/dashboard/assets/ProxyPage-Rn_qXScb.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-BihkgQSq.js +0 -1
- package/dist/dashboard/assets/QuotaPage-BihkgQSq.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-DhiEjm6T.js +0 -1
- package/dist/dashboard/assets/SettingsPage-DhiEjm6T.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-CXjUlfqr.js.gz +0 -0
- package/dist/dashboard/assets/SortableList-C-_oD3F2.js.gz +0 -0
- package/dist/dashboard/assets/StudioPage-u9IxmYOI.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-BEVLVrwP.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-uNywYlfv.js.gz +0 -0
- package/dist/dashboard/assets/app-DxivpfYG.css +0 -2
- package/dist/dashboard/assets/app-DxivpfYG.css.gz +0 -0
- package/dist/dashboard/assets/assert-NHwYq95t.js.gz +0 -0
- package/dist/dashboard/assets/badge-6uzDDvqs.js.gz +0 -0
- package/dist/dashboard/assets/banner-4_WNg2WZ.js.gz +0 -0
- package/dist/dashboard/assets/card--AtFedAq.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-B3e7dx3N.js.gz +0 -0
- package/dist/dashboard/assets/clipboard-button-NreUx6c8.js.gz +0 -0
- package/dist/dashboard/assets/dialog-BtpsJU98.js.gz +0 -0
- package/dist/dashboard/assets/download-A0SW9OCW.js +0 -1
- package/dist/dashboard/assets/download-A0SW9OCW.js.gz +0 -0
- package/dist/dashboard/assets/drawer-WwwtzihF.js.gz +0 -0
- package/dist/dashboard/assets/entry-Dj9AR0gY.js +0 -4
- package/dist/dashboard/assets/entry-Dj9AR0gY.js.gz +0 -0
- package/dist/dashboard/assets/entry-TJIye_jT.js +0 -1
- package/dist/dashboard/assets/entry-TJIye_jT.js.gz +0 -0
- package/dist/dashboard/assets/entry-jy9_4-Ke.js +0 -1
- package/dist/dashboard/assets/entry-jy9_4-Ke.js.gz +0 -0
- package/dist/dashboard/assets/github-badge-D_eM3IgG.js.gz +0 -0
- package/dist/dashboard/assets/index-DCBtROoK.js.gz +0 -0
- package/dist/dashboard/assets/inline-BcNGF8Gr.js.gz +0 -0
- package/dist/dashboard/assets/input-BG41et9G.js.gz +0 -0
- package/dist/dashboard/assets/layout-C8mer_Ak.js.gz +0 -0
- package/dist/dashboard/assets/list-row-Gh_W4OR_.js.gz +0 -0
- package/dist/dashboard/assets/live-BJGYRBnR.js.gz +0 -0
- package/dist/dashboard/assets/model-bans-DeVLDA7I.js +0 -1
- package/dist/dashboard/assets/model-bans-DeVLDA7I.js.gz +0 -0
- package/dist/dashboard/assets/mutation-BkoLM5Ba.js.gz +0 -0
- package/dist/dashboard/assets/numbers-Bs4LOOuf.js +0 -2
- package/dist/dashboard/assets/numbers-Bs4LOOuf.js.gz +0 -0
- package/dist/dashboard/assets/routing-CcusqXA7.js.gz +0 -0
- package/dist/dashboard/assets/select-DsX5QT8K.js.gz +0 -0
- package/dist/dashboard/assets/settings-mpmXTgUa.js.gz +0 -0
- package/dist/dashboard/assets/sparkles-BRaRUxAU.js +0 -1
- package/dist/dashboard/assets/sparkles-BRaRUxAU.js.gz +0 -0
- package/dist/dashboard/assets/stack-DwLmjUFK.js.gz +0 -0
- package/dist/dashboard/assets/state-DYdQmdAl.js.gz +0 -0
- package/dist/dashboard/assets/switch-CidSfbG7.js +0 -1
- package/dist/dashboard/assets/switch-CidSfbG7.js.gz +0 -0
- package/dist/dashboard/assets/terminal-Bd4ee8TA.js +0 -1
- package/dist/dashboard/assets/terminal-Bd4ee8TA.js.gz +0 -0
- package/dist/dashboard/assets/useMutation-w4u4CVue.js.gz +0 -0
- /package/dist/dashboard/assets/{app-CafWtVXA.js → app-DCQZRmHY.js} +0 -0
- /package/dist/dashboard/assets/{app-CafWtVXA.js.gz → app-DCQZRmHY.js.gz} +0 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
// Qoder quota.
|
|
2
|
+
//
|
|
3
|
+
// `GET /api/v2/quota/usage` on the **openapi** host, with a plain bearer token
|
|
4
|
+
// — no COSY signature on this surface, which is the one place the business API
|
|
5
|
+
// differs from inference.
|
|
6
|
+
//
|
|
7
|
+
// The response carries two quota objects and they are not interchangeable:
|
|
8
|
+
//
|
|
9
|
+
// - `userQuota` is the plan allowance — what the account bought.
|
|
10
|
+
// - `addOnQuota` is granted credits (daily check-in, promotions) — what it was
|
|
11
|
+
// given on top.
|
|
12
|
+
//
|
|
13
|
+
// The donor sums them into one balance, and that is right for a credit balance:
|
|
14
|
+
// both are credits in the same unit and either can be spent. It would be wrong
|
|
15
|
+
// for two *rate limits*, which is why these windows are marked `pooled` here
|
|
16
|
+
// and the Trae request buckets are not.
|
|
17
|
+
//
|
|
18
|
+
// Teams add a third object (`orgResourcePackage`), a shared pool that sits
|
|
19
|
+
// outside the account's own allowance. It is only real when it is available,
|
|
20
|
+
// and an account whose plan is exhausted but whose shared pool still has
|
|
21
|
+
// credit is *not* exhausted.
|
|
22
|
+
|
|
23
|
+
import type {
|
|
24
|
+
FetchLike,
|
|
25
|
+
ProviderQuotaResult,
|
|
26
|
+
ProviderQuotaWindow,
|
|
27
|
+
} from "../../quota/quota-contracts";
|
|
28
|
+
import {
|
|
29
|
+
cleanError,
|
|
30
|
+
isoDate,
|
|
31
|
+
number,
|
|
32
|
+
percentWindow,
|
|
33
|
+
quotaRecord,
|
|
34
|
+
record,
|
|
35
|
+
} from "../../quota/quota-contracts";
|
|
36
|
+
import type { QuotaCollectionContext } from "../../quota/quota-support";
|
|
37
|
+
import { QODER_REALMS, type QoderRealm } from "./qoder-realm";
|
|
38
|
+
|
|
39
|
+
/** Paths on the realm's openapi host. */
|
|
40
|
+
const QUOTA_PATH = "/api/v2/quota/usage";
|
|
41
|
+
|
|
42
|
+
/** Shared-pool keys, in the order the donor tries them. */
|
|
43
|
+
const SHARED_KEYS: readonly string[] = [
|
|
44
|
+
"orgResourcePackage",
|
|
45
|
+
"sharedQuota",
|
|
46
|
+
"teamQuota",
|
|
47
|
+
"poolQuota",
|
|
48
|
+
"sharedCredits",
|
|
49
|
+
"sharedCredit",
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Whether a shared pool is usable.
|
|
54
|
+
*
|
|
55
|
+
* Absent means "not declared unavailable", which is usable. Only an explicit
|
|
56
|
+
* false/null/0 rules it out — reading absence as unavailable is what made a
|
|
57
|
+
* team account with a live pool look exhausted.
|
|
58
|
+
*/
|
|
59
|
+
function sharedAvailable(source: Record<string, unknown>): boolean {
|
|
60
|
+
const value = source["available"];
|
|
61
|
+
if (value === undefined || value === null) return true;
|
|
62
|
+
if (typeof value === "boolean") return value;
|
|
63
|
+
if (typeof value === "number") return value !== 0;
|
|
64
|
+
if (typeof value === "string") return value.trim().toLowerCase() !== "false";
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function creditWindow(
|
|
69
|
+
kind: string,
|
|
70
|
+
label: string,
|
|
71
|
+
remaining: number | null,
|
|
72
|
+
limit: number | null,
|
|
73
|
+
resetsAt: string | null,
|
|
74
|
+
): ProviderQuotaWindow | null {
|
|
75
|
+
if (remaining === null && limit === null) return null;
|
|
76
|
+
const used = remaining !== null && limit !== null ? limit - remaining : null;
|
|
77
|
+
return percentWindow(kind, label, null, resetsAt, used, limit, "pooled");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Reads one Qoder account's quota.
|
|
82
|
+
*
|
|
83
|
+
* A degenerate body — both quota objects missing, or `isQuotaExceeded` with a
|
|
84
|
+
* zero remaining *and* a zero total — is reported as an error rather than as a
|
|
85
|
+
* zero balance. "No quota" parks the account; "could not read the quota" does
|
|
86
|
+
* not, and those two are not the same claim.
|
|
87
|
+
*/
|
|
88
|
+
/** Records an unreadable body where an operator can act on it, not as an error. */
|
|
89
|
+
function logQoderQuotaMiss(detail: string): void {
|
|
90
|
+
console.warn(`[quota] qoder quota unreadable: ${detail}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export async function fetchQoderQuota(
|
|
94
|
+
credential: string,
|
|
95
|
+
fetcher: FetchLike,
|
|
96
|
+
context?: QuotaCollectionContext,
|
|
97
|
+
): Promise<ProviderQuotaResult> {
|
|
98
|
+
const raw = context?.auth_state ?? {};
|
|
99
|
+
const realm: QoderRealm = raw["realm"] === "cn" || raw["region"] === "CN" ? "cn" : "intl";
|
|
100
|
+
const config = QODER_REALMS[realm];
|
|
101
|
+
const url = `${config.openapi}${QUOTA_PATH}`;
|
|
102
|
+
|
|
103
|
+
let payload: unknown;
|
|
104
|
+
try {
|
|
105
|
+
const response = await fetcher(url, {
|
|
106
|
+
method: "GET",
|
|
107
|
+
headers: {
|
|
108
|
+
accept: "application/json",
|
|
109
|
+
authorization: `Bearer ${credential}`,
|
|
110
|
+
"user-agent": config.userAgent,
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
114
|
+
payload = await response.json();
|
|
115
|
+
} catch (error) {
|
|
116
|
+
// Unreachable is not a balance. See the note on the degenerate branch.
|
|
117
|
+
logQoderQuotaMiss(cleanError(error));
|
|
118
|
+
return { source: "qoder", plan: null, windows: [], error: null };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const root = quotaRecord(payload);
|
|
122
|
+
const userQuota = record(root["userQuota"]);
|
|
123
|
+
const addOnQuota = record(root["addOnQuota"]);
|
|
124
|
+
const owned = [userQuota, addOnQuota].filter(
|
|
125
|
+
(value): value is Record<string, unknown> => value !== null,
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
const bothMissing = owned.length === 0;
|
|
129
|
+
// A live account on the free tier really does answer `userQuota` all zeroes
|
|
130
|
+
// with `isQuotaExceeded: true` — verified against a real credential:
|
|
131
|
+
// `{"isQuotaExceeded":true,"userQuota":{"total":0,"used":0,"remaining":0},
|
|
132
|
+
// "totalUsagePercentage":0.0,...}`. That is a balance of zero, not an
|
|
133
|
+
// unreadable body. Only a response with *both* quota objects absent is
|
|
134
|
+
// unreadable, and it is logged rather than returned — never as an error,
|
|
135
|
+
// which would cool the account.
|
|
136
|
+
const degenerate = bothMissing;
|
|
137
|
+
if (degenerate) {
|
|
138
|
+
// A body we cannot trust is not a zero balance, and it is not a failed
|
|
139
|
+
// read either: `error` is what flags the account `quota_fetch_failed` and
|
|
140
|
+
// cools it, so a shape we simply do not recognise would park a working
|
|
141
|
+
// account. Logged instead of returned.
|
|
142
|
+
logQoderQuotaMiss(
|
|
143
|
+
bothMissing
|
|
144
|
+
? "Qoder quota response carried no quota objects."
|
|
145
|
+
: "Qoder reported quota exceeded with a zero total; not read as a balance.",
|
|
146
|
+
);
|
|
147
|
+
return { source: "qoder", plan: null, windows: [], error: null };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const resetsAt = isoDate(root["expiresAt"] ?? root["expireAt"] ?? root["expires_at"]);
|
|
151
|
+
const windows: ProviderQuotaWindow[] = [];
|
|
152
|
+
const plan = creditWindow("qoder:plan", "Plan credits", number(userQuota?.["remaining"]), number(userQuota?.["total"]), resetsAt);
|
|
153
|
+
if (plan) windows.push(plan);
|
|
154
|
+
const addOn = creditWindow("qoder:addon", "Granted credits", number(addOnQuota?.["remaining"]), number(addOnQuota?.["total"]), resetsAt);
|
|
155
|
+
if (addOn) windows.push(addOn);
|
|
156
|
+
|
|
157
|
+
for (const key of SHARED_KEYS) {
|
|
158
|
+
const shared = record(root[key]);
|
|
159
|
+
if (!shared) continue;
|
|
160
|
+
if (!sharedAvailable(shared)) continue;
|
|
161
|
+
const window = creditWindow(
|
|
162
|
+
"qoder:shared",
|
|
163
|
+
"Shared pool",
|
|
164
|
+
number(shared["remaining"]),
|
|
165
|
+
number(shared["cap"] ?? shared["total"]),
|
|
166
|
+
null,
|
|
167
|
+
);
|
|
168
|
+
if (window) windows.push(window);
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (windows.length === 0) {
|
|
173
|
+
logQoderQuotaMiss("Qoder quota response carried no readable balance.");
|
|
174
|
+
return { source: "qoder", plan: null, windows: [], error: null };
|
|
175
|
+
}
|
|
176
|
+
return { source: "qoder", plan: null, windows, error: null };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* The `qodercn` provider's quota.
|
|
181
|
+
*
|
|
182
|
+
* Same collector, realm forced to `cn`: a CN account is refused by
|
|
183
|
+
* `openapi.qoder.sh`, and the collector cannot see the provider id — the
|
|
184
|
+
* context carries auth state only.
|
|
185
|
+
*/
|
|
186
|
+
export function fetchQoderCnQuota(
|
|
187
|
+
credential: string,
|
|
188
|
+
fetcher: FetchLike,
|
|
189
|
+
context?: QuotaCollectionContext,
|
|
190
|
+
): Promise<ProviderQuotaResult> {
|
|
191
|
+
return fetchQoderQuota(credential, fetcher, { ...context, auth_state: { ...(context?.auth_state ?? {}), realm: "cn" } });
|
|
192
|
+
}
|
|
@@ -35,7 +35,7 @@ export const TOKENHARBOR_FALLBACK_MODELS: readonly ModelDefinition[] = [
|
|
|
35
35
|
defineModel({ id: "muse-spark-1.2", providerId: "tokenharbor", ctx: 1_048_576, reasoning: true, vision: true }),
|
|
36
36
|
defineModel({ id: "muse-spark-1-3", providerId: "tokenharbor", ctx: 1_048_576, reasoning: true, vision: true }),
|
|
37
37
|
defineModel({ id: "gpt-6-astra", providerId: "tokenharbor", ctx: 1_050_000, reasoning: true, vision: true }),
|
|
38
|
-
defineModel({ id: "deepseek-v4.1-flash", providerId: "tokenharbor", ctx: 1_048_576, reasoning: true }),
|
|
38
|
+
defineModel({ id: "deepseek-v4.1-flash", providerId: "tokenharbor", ctx: 1_048_576, reasoning: true, vision: true }),
|
|
39
39
|
defineModel({ id: "gemini-3.6-flash", providerId: "tokenharbor", ctx: 1_000_000, reasoning: true, vision: true }),
|
|
40
40
|
defineModel({ id: "glm-5.2", providerId: "tokenharbor", ctx: 1_000_000, reasoning: true }),
|
|
41
41
|
defineModel({ id: "gpt-5.6-luna", providerId: "tokenharbor", reasoning: true, vision: true }),
|
|
@@ -62,7 +62,7 @@ export const TOKENHARBOR_FALLBACK_MODELS: readonly ModelDefinition[] = [
|
|
|
62
62
|
defineModel({ id: "mimo-v2.6-pro", providerId: "tokenharbor", ctx: 1_048_576, reasoning: true, vision: true }),
|
|
63
63
|
defineModel({ id: "mimo-v2.5:free", providerId: "tokenharbor", ctx: 1_048_576, reasoning: true, vision: true, free: true }),
|
|
64
64
|
defineModel({ id: "deepseek-v4-flash:free", providerId: "tokenharbor", reasoning: true, free: true }),
|
|
65
|
-
defineModel({ id: "deepseek-v4.1-flash:free", providerId: "tokenharbor", ctx: 1_048_576, reasoning: true, free: true }),
|
|
65
|
+
defineModel({ id: "deepseek-v4.1-flash:free", providerId: "tokenharbor", ctx: 1_048_576, reasoning: true, vision: true, free: true }),
|
|
66
66
|
defineModel({ id: "qwen3.8-flash:free", providerId: "tokenharbor", reasoning: true, free: true }),
|
|
67
67
|
defineModel({ id: "mimo-v2.6-flash:free", providerId: "tokenharbor", ctx: 1_048_576, reasoning: true, vision: true, free: true }),
|
|
68
68
|
];
|
|
@@ -79,7 +79,9 @@ const TRAE_CN_ROWS: readonly TraeModelRow[] = [
|
|
|
79
79
|
* (DeepSeek-V4 rows: 200k active, 1M selectable), so reading `dev` makes a
|
|
80
80
|
* client under-request the window it is entitled to. The maximum is per model
|
|
81
81
|
* and is **not** uniform: the GPT rows cap at 272k and Kimi-K2.5 / MiniMax-M2.7
|
|
82
|
-
* offer no selectable upgrade at all, staying at 200k.
|
|
82
|
+
* offer no selectable upgrade at all, staying at 200k. `gpt-5.4` is the
|
|
83
|
+
* exception: upstream raised its selectable maximum to 1M, so it is filed at
|
|
84
|
+
* 1M while the rest of the GPT rows stay at 272k.
|
|
83
85
|
*
|
|
84
86
|
* `out` is what the same model is filed at in the CodeBuddy roster where that
|
|
85
87
|
* roster carries it (DeepSeek-V4 384k, MiniMax-M3 512k, Kimi-K2.5 262k,
|
|
@@ -98,13 +100,13 @@ const TRAE_CN_ROWS: readonly TraeModelRow[] = [
|
|
|
98
100
|
* one.
|
|
99
101
|
*/
|
|
100
102
|
const TRAE_INTL_ROWS: readonly TraeModelRow[] = [
|
|
101
|
-
{ id: "gpt-5.4", displayName: "GPT-5.4", ctx:
|
|
103
|
+
{ id: "gpt-5.4", displayName: "GPT-5.4", ctx: 1_000_000, out: 128_000, vision: true, reasoning: true, surface: "agent" },
|
|
102
104
|
{ id: "gpt-5.2", displayName: "GPT-5.2", ctx: 272000, out: 128_000, vision: true, reasoning: true, surface: "agent" },
|
|
103
105
|
{ id: "gpt-5.2-codex-paygo", displayName: "GPT-5.2-Codex", ctx: 272000, out: 128_000, vision: true, reasoning: true, surface: "agent" },
|
|
104
106
|
{ id: "gpt-5.1-paygo", displayName: "GPT-5.1", ctx: 272000, out: 128_000, vision: true, reasoning: true, surface: "agent" },
|
|
105
107
|
{ id: "gpt-5-paygo", displayName: "GPT-5", ctx: 272000, out: 128_000, vision: true, reasoning: true, surface: "agent" },
|
|
106
108
|
{ id: "Dola-Seed-2.0-Code", displayName: "Seed-2.1-Turbo", ctx: 1000000, out: 131_072, vision: true, reasoning: true, surface: "agent" },
|
|
107
|
-
{ id: "deepseek-v4.1-flash", displayName: "DeepSeek-V4.1-Flash", ctx: 1000000, out: 384_000, vision:
|
|
109
|
+
{ id: "deepseek-v4.1-flash", displayName: "DeepSeek-V4.1-Flash", ctx: 1000000, out: 384_000, vision: true, reasoning: true, surface: "agent" },
|
|
108
110
|
{ id: "deepseek-v4-flash-0731", displayName: "DeepSeek-V4-Flash", ctx: 1000000, out: 384_000, vision: false, reasoning: true, surface: "agent" },
|
|
109
111
|
{ id: "deepseek-v4-pro-0813", displayName: "DeepSeek-V4-Pro", ctx: 1000000, out: 384_000, vision: false, reasoning: true, surface: "agent" },
|
|
110
112
|
{ id: "kimi-k3", displayName: "Kimi-K3", ctx: 1000000, out: 131_072, vision: true, reasoning: true, surface: "agent" },
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
// Trae quota.
|
|
2
|
+
//
|
|
3
|
+
// Two surfaces, on two different hosts, and neither is optional if the account
|
|
4
|
+
// is to be read honestly:
|
|
5
|
+
//
|
|
6
|
+
// - `get_session_usage` on the **core** host is the running meter — what the
|
|
7
|
+
// account has left of its advanced/premium request buckets and its dollar
|
|
8
|
+
// balances.
|
|
9
|
+
// - `ide_user_ent_usage` on the **pay** host is the entitlement: which package
|
|
10
|
+
// the account holds and when it lapses.
|
|
11
|
+
//
|
|
12
|
+
// The donor reads both and lets either fail without the other, and that
|
|
13
|
+
// tolerance is load-bearing here too: entitlement is a *pay* host, which is the
|
|
14
|
+
// first thing a region-blocked or unpaid account loses. Letting a dead pay host
|
|
15
|
+
// fail the whole read would report "no quota" for an account whose session
|
|
16
|
+
// meter is perfectly readable — and "no quota" is what the router parks on.
|
|
17
|
+
//
|
|
18
|
+
// Headers come from `buildTraeHeaders` rather than being rebuilt here: the
|
|
19
|
+
// device fingerprint is derived from the uid, and an account that reports one
|
|
20
|
+
// device here and another at dispatch is exactly the correlation this gateway
|
|
21
|
+
// exists to avoid.
|
|
22
|
+
|
|
23
|
+
import type {
|
|
24
|
+
FetchLike,
|
|
25
|
+
ProviderQuotaResult,
|
|
26
|
+
ProviderQuotaWindow,
|
|
27
|
+
} from "../../quota/quota-contracts";
|
|
28
|
+
import {
|
|
29
|
+
cleanError,
|
|
30
|
+
isoDate,
|
|
31
|
+
number,
|
|
32
|
+
percentWindow,
|
|
33
|
+
record,
|
|
34
|
+
text,
|
|
35
|
+
} from "../../quota/quota-contracts";
|
|
36
|
+
import type { QuotaCollectionContext } from "../../quota/quota-support";
|
|
37
|
+
import { decodeJwtPayload } from "../../authentication/oauth-flow-store";
|
|
38
|
+
import { buildTraeHeaders } from "./trae-headers";
|
|
39
|
+
import {
|
|
40
|
+
TRAE_ENT_USAGE_PATH,
|
|
41
|
+
TRAE_REGION_CORE_HOSTS,
|
|
42
|
+
TRAE_REALMS,
|
|
43
|
+
TRAE_SESSION_USAGE_PATH,
|
|
44
|
+
type TraeRealm,
|
|
45
|
+
} from "./trae-realm";
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The buckets `remain_usage` carries, in the order the donor reads them.
|
|
49
|
+
*
|
|
50
|
+
* `advanced_model` and `premium_model_*` are request counts; `dollar_basic` and
|
|
51
|
+
* `dollar_bonus` are spend balances. They are separate budgets and must never
|
|
52
|
+
* be summed — a dollar balance is not a request count.
|
|
53
|
+
*/
|
|
54
|
+
const REQUEST_BUCKETS: readonly (readonly [string, string])[] = [
|
|
55
|
+
["advanced_model", "Advanced model requests"],
|
|
56
|
+
["premium_model_fast_request", "Premium fast requests"],
|
|
57
|
+
["premium_model_slow_request", "Premium slow requests"],
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
const DOLLAR_BUCKETS: readonly (readonly [string, string])[] = [
|
|
61
|
+
["dollar_basic", "Basic spend"],
|
|
62
|
+
["dollar_bonus", "Bonus spend"],
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
interface TraeQuotaAuth {
|
|
66
|
+
readonly realm: TraeRealm;
|
|
67
|
+
readonly uid: string;
|
|
68
|
+
/** Provisioned region (`sg`, `us`), which selects the core host. */
|
|
69
|
+
readonly region: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Reads the account's realm and uid for the quota call.
|
|
74
|
+
*
|
|
75
|
+
* Both come from `auth_state`, the same place dispatch reads them: a Trae
|
|
76
|
+
* token is realm-bound, so guessing `intl` for a CN account sends it to a host
|
|
77
|
+
* that will not authenticate it.
|
|
78
|
+
*/
|
|
79
|
+
function quotaAuth(context: QuotaCollectionContext | undefined): TraeQuotaAuth {
|
|
80
|
+
const raw = context?.auth_state ?? {};
|
|
81
|
+
// `realm` is not persisted by the OAuth client — it is a property of which
|
|
82
|
+
// provider id the account was registered under, so the CN wrapper forces it
|
|
83
|
+
// rather than trusting an absent field.
|
|
84
|
+
const realm = raw["realm"] === "cn" || raw["region"] === "CN" ? "cn" : "intl";
|
|
85
|
+
// The OAuth client stores the account id as `userId`; `uid` is the name the
|
|
86
|
+
// dispatch header builder uses. Reading only one of them left the device
|
|
87
|
+
// fingerprint empty for every account.
|
|
88
|
+
const uidRaw = raw["uid"] ?? raw["userId"] ?? raw["user_id"];
|
|
89
|
+
const uid = typeof uidRaw === "string" && uidRaw.trim() ? uidRaw.trim() : "";
|
|
90
|
+
// `region` is provisioned, not chosen: an intl account is created into one
|
|
91
|
+
// and its token only authenticates against that region's core host. `us` and
|
|
92
|
+
// `cn` are served by the realm's default host, but `sg` is a different host
|
|
93
|
+
// entirely — sending it to the US core answers 401 code 1001.
|
|
94
|
+
const region = typeof raw["region"] === "string" ? raw["region"].trim().toLowerCase() : "";
|
|
95
|
+
return { realm, uid, region };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The account id out of the access token itself.
|
|
100
|
+
*
|
|
101
|
+
* The device fingerprint is derived from the uid, so an empty one presents no
|
|
102
|
+
* device at all — and this surface correlates the credential against its
|
|
103
|
+
* device, answering `1001` for a token that is perfectly valid. The JWT carries
|
|
104
|
+
* the id, so it is read there when the auth state does not have it.
|
|
105
|
+
*/
|
|
106
|
+
function uidFromToken(credential: string): string | undefined {
|
|
107
|
+
const payload = decodeJwtPayload(credential);
|
|
108
|
+
if (!payload) return undefined;
|
|
109
|
+
// The id is nested: this token's payload is `{data:{id,type,tenant_id},exp,
|
|
110
|
+
// iat}` — there is no `sub` and no top-level `userId`, so a flat read found
|
|
111
|
+
// nothing and the request went out with no device identity at all.
|
|
112
|
+
for (const source of [payload, record(payload["data"])]) {
|
|
113
|
+
if (!source) continue;
|
|
114
|
+
for (const key of ["userId", "user_id", "uid", "sub", "id"] as const) {
|
|
115
|
+
const value = source[key];
|
|
116
|
+
if (typeof value === "string" && value.trim()) return value.trim();
|
|
117
|
+
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** One bucket of `remain_usage`, paired with its cap from the sibling object. */
|
|
124
|
+
function bucketWindow(
|
|
125
|
+
label: string,
|
|
126
|
+
remaining: number | null,
|
|
127
|
+
limit: number | null,
|
|
128
|
+
resetsAt: string | null,
|
|
129
|
+
): ProviderQuotaWindow | null {
|
|
130
|
+
if (remaining === null && limit === null) return null;
|
|
131
|
+
const used = remaining !== null && limit !== null ? limit - remaining : null;
|
|
132
|
+
return percentWindow(
|
|
133
|
+
`trae:${label}`,
|
|
134
|
+
label,
|
|
135
|
+
null,
|
|
136
|
+
resetsAt,
|
|
137
|
+
used,
|
|
138
|
+
limit,
|
|
139
|
+
// Independent: each bucket is its own budget. Summing a request count into
|
|
140
|
+
// a dollar balance would produce a number that means nothing.
|
|
141
|
+
"independent",
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Pulls `remaining`/`limit` for one key out of either usage object.
|
|
147
|
+
*
|
|
148
|
+
* `remain_usage` carries a **bare number** per bucket (`advanced_model: 40`),
|
|
149
|
+
* not a nested object — the caps live in the sibling `usage` object. Reading a
|
|
150
|
+
* nested `remaining` here is what dropped every balance and left a limit with
|
|
151
|
+
* no remaining, i.e. a window that cannot say how much is left.
|
|
152
|
+
*/
|
|
153
|
+
function bucketValues(
|
|
154
|
+
source: Record<string, unknown>,
|
|
155
|
+
key: string,
|
|
156
|
+
): { readonly remaining: number | null; readonly limit: number | null } {
|
|
157
|
+
const direct = record(source[key]);
|
|
158
|
+
if (direct) {
|
|
159
|
+
return {
|
|
160
|
+
remaining: number(direct["remaining"] ?? direct["remain"] ?? direct["remain_usage"]),
|
|
161
|
+
limit: number(direct["limit"] ?? direct["total"] ?? direct["cap"]),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
remaining: number(source[key] ?? source[`${key}_remaining`]),
|
|
166
|
+
limit: number(source[`${key}_limit`] ?? source[`${key}_total`]),
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* The session-usage meter: `remain_usage` holds the remaining balance per
|
|
172
|
+
* bucket, with the caps alongside it on the same object.
|
|
173
|
+
*/
|
|
174
|
+
function parseSessionUsage(payload: unknown): {
|
|
175
|
+
readonly windows: readonly ProviderQuotaWindow[];
|
|
176
|
+
readonly plan: string | null;
|
|
177
|
+
} {
|
|
178
|
+
// Deliberately *not* `quotaRecord`: it unwraps a `usage` envelope, and on
|
|
179
|
+
// this surface `usage` is the sibling object holding the caps — unwrapping it
|
|
180
|
+
// discards `remain_usage` entirely and leaves a limit with no remaining.
|
|
181
|
+
const root = record(payload) ?? {};
|
|
182
|
+
const windows: ProviderQuotaWindow[] = [];
|
|
183
|
+
// The meter is nested under `data` on some realms and at the root on others;
|
|
184
|
+
// both shapes are live, so look in each rather than picking one.
|
|
185
|
+
const candidates = [root, record(root["data"]), record(root["quota"])].filter(
|
|
186
|
+
(value): value is Record<string, unknown> => value !== null,
|
|
187
|
+
);
|
|
188
|
+
const resetsAt = candidates
|
|
189
|
+
.map((value) => isoDate(value["reset_time"] ?? value["resetTime"] ?? value["next_reset_time"]))
|
|
190
|
+
.find((value) => value !== null) ?? null;
|
|
191
|
+
|
|
192
|
+
for (const source of candidates) {
|
|
193
|
+
const remain = record(source["remain_usage"]) ?? source;
|
|
194
|
+
const usage = record(source["usage"]) ?? {};
|
|
195
|
+
for (const [key, label] of [...REQUEST_BUCKETS, ...DOLLAR_BUCKETS]) {
|
|
196
|
+
// Remaining is the bare number under `remain_usage`; the cap is the
|
|
197
|
+
// sibling object under `usage`. Two objects, one per side of the same
|
|
198
|
+
// budget, so both are read before a window is built.
|
|
199
|
+
const remaining = bucketValues(remain, key).remaining ?? number(usage[key]);
|
|
200
|
+
const limit = bucketValues(usage, key).limit ?? bucketValues(remain, key).limit;
|
|
201
|
+
const window = bucketWindow(label, remaining, limit, resetsAt);
|
|
202
|
+
if (window) windows.push(window);
|
|
203
|
+
}
|
|
204
|
+
if (windows.length > 0) break;
|
|
205
|
+
}
|
|
206
|
+
const plan = candidates
|
|
207
|
+
.map((value) => text(value["plan_name"] ?? value["planName"] ?? value["user_type"]))
|
|
208
|
+
.find((value) => value !== null) ?? null;
|
|
209
|
+
return { windows, plan };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** The entitlement package name and lapse, from the pay host. */
|
|
213
|
+
function parseEntitlement(payload: unknown): {
|
|
214
|
+
readonly windows: readonly ProviderQuotaWindow[];
|
|
215
|
+
readonly plan: string | null;
|
|
216
|
+
} {
|
|
217
|
+
const root = record(payload) ?? {};
|
|
218
|
+
const data = record(root["data"]) ?? root;
|
|
219
|
+
const items = Array.isArray(data["entitlement_list"])
|
|
220
|
+
? data["entitlement_list"]
|
|
221
|
+
: Array.isArray(data["list"])
|
|
222
|
+
? data["list"]
|
|
223
|
+
: [];
|
|
224
|
+
const windows: ProviderQuotaWindow[] = [];
|
|
225
|
+
let plan: string | null = null;
|
|
226
|
+
for (const entry of items) {
|
|
227
|
+
const item = record(entry);
|
|
228
|
+
if (!item) continue;
|
|
229
|
+
const name = text(item["ent_title"] ?? item["name"] ?? item["package_name"]);
|
|
230
|
+
if (!plan && name) plan = name;
|
|
231
|
+
const total = number(item["total"] ?? item["quota"] ?? item["advanced_model_request_limit"]);
|
|
232
|
+
const remaining = number(item["remain"] ?? item["remaining"] ?? item["remain_usage"]);
|
|
233
|
+
const resetsAt = isoDate(item["end_time"] ?? item["endTime"] ?? item["expires_at"]);
|
|
234
|
+
const window = bucketWindow(name ?? "Entitlement", remaining, total, resetsAt);
|
|
235
|
+
if (window) windows.push(window);
|
|
236
|
+
}
|
|
237
|
+
return { windows, plan };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Records a quiet surface where an operator can act on it.
|
|
242
|
+
*
|
|
243
|
+
* Deliberately not the returned `error`: that field is what flags the account
|
|
244
|
+
* `quota_fetch_failed` and cools it. A 403 from the wrong realm, or a pay host
|
|
245
|
+
* that is unreachable, says nothing about whether the account can serve.
|
|
246
|
+
*/
|
|
247
|
+
function logTraeQuotaMiss(errors: readonly string[]): void {
|
|
248
|
+
if (errors.length === 0) return;
|
|
249
|
+
console.warn(`[quota] trae surfaces unavailable: ${errors.join("; ")}`);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Reads one Trae account's quota.
|
|
254
|
+
*
|
|
255
|
+
* The session meter and the entitlement are fetched independently: whichever
|
|
256
|
+
* answers contributes, and a failure is only fatal if *both* fail. An account
|
|
257
|
+
* whose pay host is unreachable still has a readable meter, and reporting
|
|
258
|
+
* "no quota" for it would park an account that can still serve.
|
|
259
|
+
*/
|
|
260
|
+
/**
|
|
261
|
+
* The core host for this account.
|
|
262
|
+
*
|
|
263
|
+
* A provisioned region overrides the realm default: an `sg` account sent to the
|
|
264
|
+
* US core is answered 401 code 1001, which looks exactly like a dead token.
|
|
265
|
+
*/
|
|
266
|
+
function coreHost(region: string, config: (typeof TRAE_REALMS)[TraeRealm]): string {
|
|
267
|
+
return (region !== "" ? TRAE_REGION_CORE_HOSTS[region] : undefined) ?? config.core;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export async function fetchTraeQuota(
|
|
271
|
+
credential: string,
|
|
272
|
+
fetcher: FetchLike,
|
|
273
|
+
context?: QuotaCollectionContext,
|
|
274
|
+
): Promise<ProviderQuotaResult> {
|
|
275
|
+
const { realm, uid, region } = quotaAuth(context);
|
|
276
|
+
const resolvedUid = uid || uidFromToken(credential) || "";
|
|
277
|
+
const errors: string[] = [];
|
|
278
|
+
|
|
279
|
+
const call = async (
|
|
280
|
+
realm: TraeRealm,
|
|
281
|
+
url: string,
|
|
282
|
+
surface: string,
|
|
283
|
+
): Promise<unknown> => {
|
|
284
|
+
const response = await fetcher(url, {
|
|
285
|
+
// POST with an empty JSON body, not GET: this is the commercial surface
|
|
286
|
+
// the desktop client calls, and it answers a GET with an error rather
|
|
287
|
+
// than a meter — which read as "no quota" for every account.
|
|
288
|
+
method: "POST",
|
|
289
|
+
headers: {
|
|
290
|
+
// The commercial surface authenticates with `Cloud-IDE-JWT` on both
|
|
291
|
+
// realms.
|
|
292
|
+
...buildTraeHeaders({ realm, token: credential, uid: resolvedUid }),
|
|
293
|
+
// Applied after the builder, which switches intl to `Bearer` for raw
|
|
294
|
+
// chat: a Bearer here is answered 401 code 1001, so the account shows
|
|
295
|
+
// no windows while its token is perfectly valid.
|
|
296
|
+
authorization: `Cloud-IDE-JWT ${credential}`,
|
|
297
|
+
"content-type": "application/json",
|
|
298
|
+
// The builder pins `us` for every intl account. An `sg` account sent
|
|
299
|
+
// with a US country code is refused with 401 code 1001 — the country
|
|
300
|
+
// code is part of the identity this surface checks, not a hint.
|
|
301
|
+
"store-country-code": region || "us",
|
|
302
|
+
},
|
|
303
|
+
body: "{}",
|
|
304
|
+
});
|
|
305
|
+
if (!response.ok) {
|
|
306
|
+
// 403 here is a verdict about *how we asked*, not about the balance: the
|
|
307
|
+
// commercial surface correlates the credential against its realm and its
|
|
308
|
+
// device fingerprint, and a CN token sent to an intl host — or one sent
|
|
309
|
+
// with no device id — is refused outright. Reporting that as a failed
|
|
310
|
+
// read parks the account, so it is reported as unreadable-for-now and
|
|
311
|
+
// left alone by the cooldown path.
|
|
312
|
+
const body = await response.text().catch(() => "");
|
|
313
|
+
throw new Error(
|
|
314
|
+
response.status === 403
|
|
315
|
+
? `${surface}: HTTP 403 (realm or device mismatch, not a balance)`
|
|
316
|
+
: `${surface}: HTTP ${response.status}${body ? ` ${body.slice(0, 80)}` : ""}`,
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
return await response.json();
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Tries the realm we believe, then the other one.
|
|
324
|
+
*
|
|
325
|
+
* A Trae token is realm-bound and the auth state does not record which — the
|
|
326
|
+
* provider id does, and a collector cannot see it. The wrong host answers 401
|
|
327
|
+
* code 1001, which is indistinguishable from a dead token unless the other
|
|
328
|
+
* realm is tried: one of them accepts, and that is the answer.
|
|
329
|
+
*/
|
|
330
|
+
const tryRealms = async (
|
|
331
|
+
path: (config: (typeof TRAE_REALMS)[TraeRealm]) => string,
|
|
332
|
+
surface: string,
|
|
333
|
+
parse: (payload: unknown) => { readonly windows: readonly ProviderQuotaWindow[]; readonly plan: string | null },
|
|
334
|
+
): Promise<{ readonly windows: readonly ProviderQuotaWindow[]; readonly plan: string | null } | null> => {
|
|
335
|
+
for (const candidate of realm === "cn" ? (["cn", "intl"] as const) : (["intl", "cn"] as const)) {
|
|
336
|
+
try {
|
|
337
|
+
const payload = await call(candidate, path(TRAE_REALMS[candidate]), surface);
|
|
338
|
+
return parse(payload);
|
|
339
|
+
} catch (error) {
|
|
340
|
+
errors.push(`${candidate} ${surface}: ${cleanError(error)}`);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return null;
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
const session = await tryRealms((config) => coreHost(region, config) + TRAE_SESSION_USAGE_PATH, "session usage", parseSessionUsage);
|
|
347
|
+
const entitlement = await tryRealms((config) => config.pay + TRAE_ENT_USAGE_PATH, "entitlement", parseEntitlement);
|
|
348
|
+
|
|
349
|
+
const windows = [...(session?.windows ?? []), ...(entitlement?.windows ?? [])];
|
|
350
|
+
if (!session && !entitlement) {
|
|
351
|
+
// No window and *no error*: a quota read that could not reach upstream is
|
|
352
|
+
// not evidence about the account, and `error` here is what flags the
|
|
353
|
+
// account `quota_fetch_failed` and cools it down. An unreachable or
|
|
354
|
+
// refusing surface must leave the account alone — otherwise an account is
|
|
355
|
+
// parked by its own monitor, for a reason that has nothing to do with it.
|
|
356
|
+
// The detail is kept for the log instead, where it cannot be read as a
|
|
357
|
+
// balance.
|
|
358
|
+
logTraeQuotaMiss(errors);
|
|
359
|
+
return { source: "trae", plan: null, windows: [], error: null };
|
|
360
|
+
}
|
|
361
|
+
return {
|
|
362
|
+
source: "trae",
|
|
363
|
+
plan: session?.plan ?? entitlement?.plan ?? null,
|
|
364
|
+
windows,
|
|
365
|
+
// A partial read is still a read, and the quiet surface is a diagnostic —
|
|
366
|
+
// never a reason to park the account.
|
|
367
|
+
error: null,
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* The `traecn` provider's quota.
|
|
373
|
+
*
|
|
374
|
+
* Same collector, realm forced to `cn`: a CN token is refused by the intl
|
|
375
|
+
* hosts, and the provider id is not something the collector can see — the
|
|
376
|
+
* context carries no provider, only auth state.
|
|
377
|
+
*/
|
|
378
|
+
export function fetchTraeCnQuota(
|
|
379
|
+
credential: string,
|
|
380
|
+
fetcher: FetchLike,
|
|
381
|
+
context?: QuotaCollectionContext,
|
|
382
|
+
): Promise<ProviderQuotaResult> {
|
|
383
|
+
return fetchTraeQuota(credential, fetcher, { ...context, auth_state: { ...(context?.auth_state ?? {}), realm: "cn" } });
|
|
384
|
+
}
|