cartethyia 2.5.3 → 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-hfCzi-Ux.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-DEDyKBlC.js → CliToolsPage-BV6pxVQo.js} +1 -1
- package/dist/dashboard/assets/CliToolsPage-BV6pxVQo.js.gz +0 -0
- package/dist/dashboard/assets/{CombosPage-CfuYiQ8Z.js → CombosPage-Bsan4679.js} +1 -1
- package/dist/dashboard/assets/CombosPage-Bsan4679.js.gz +0 -0
- package/dist/dashboard/assets/{ConsoleLogPage-bsLuZmYH.js → ConsoleLogPage-CW-wPmJ6.js} +1 -1
- package/dist/dashboard/assets/ConsoleLogPage-CW-wPmJ6.js.gz +0 -0
- package/dist/dashboard/assets/{CustomizationPage-_UWhwziJ.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-DDVIfZGW.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-D6pmQ86L.js → SetupPage-DDbMQsoo.js} +1 -1
- package/dist/dashboard/assets/SetupPage-DDbMQsoo.js.gz +0 -0
- package/dist/dashboard/assets/{SortableList-DWvKiuSl.js → SortableList-CB3Z7VTf.js} +1 -1
- package/dist/dashboard/assets/SortableList-CB3Z7VTf.js.gz +0 -0
- package/dist/dashboard/assets/{StudioPage-CmkUrgdj.js → StudioPage-D95UCPHI.js} +3 -3
- package/dist/dashboard/assets/StudioPage-D95UCPHI.js.gz +0 -0
- package/dist/dashboard/assets/{UsagePage-B47uOydr.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-BydyKaDz.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-BO6h8yNv.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 +4 -8
- 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 +55 -0
- 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/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/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 +8 -1
- package/src/observability/payload-store.ts +440 -376
- package/src/persistence/health-event-flush.ts +36 -0
- package/src/persistence/schema.ts +1402 -1360
- 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/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/cline/cline-quota.ts +132 -125
- 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 +12 -13
- package/src/providers/integrations/qoder/qoder-quota.ts +192 -0
- package/src/providers/integrations/trae/trae-quota.ts +384 -0
- package/src/providers/operations/account-health-service.ts +1523 -1480
- package/src/providers/provider-contracts.ts +7 -0
- package/src/providers/provider-metadata.ts +6 -0
- package/src/providers/quota/quota-support.ts +8 -0
- package/src/security/api-key-auth.ts +504 -494
- package/src/transport/dispatch/generation.ts +6 -0
- package/src/transport/dispatch/proxy-request.ts +24 -1
- package/src/transport/dispatch/ratelimit-capture.ts +37 -0
- package/src/transport/dispatch/streaming/stream.ts +14 -0
- 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 +58 -10
- package/dist/dashboard/assets/BannedPage-hfCzi-Ux.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-DLGqYEwI.js +0 -1
- package/dist/dashboard/assets/CliToolDetailPage-DLGqYEwI.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-DEDyKBlC.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-CfuYiQ8Z.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-bsLuZmYH.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-_UWhwziJ.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-Cjfc5WFS.js +0 -1
- package/dist/dashboard/assets/HealthEventsModal-Cjfc5WFS.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-DDVIfZGW.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-CWkTUtpX.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-B53YYMng.js +0 -1
- package/dist/dashboard/assets/OverviewPage-B53YYMng.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-B0KaAQf0.js +0 -3
- package/dist/dashboard/assets/ProviderDetailPage-B0KaAQf0.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-Dx31DgIO.js +0 -1
- package/dist/dashboard/assets/ProvidersPage-Dx31DgIO.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-BpnR4kXT.js +0 -7
- package/dist/dashboard/assets/ProxyPage-BpnR4kXT.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-BpOQCnun.js +0 -1
- package/dist/dashboard/assets/QuotaPage-BpOQCnun.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-DV1eo2J_.js +0 -1
- package/dist/dashboard/assets/SettingsPage-DV1eo2J_.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-D6pmQ86L.js.gz +0 -0
- package/dist/dashboard/assets/SortableList-DWvKiuSl.js.gz +0 -0
- package/dist/dashboard/assets/StudioPage-CmkUrgdj.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-B47uOydr.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-CkryrIea.js +0 -4
- package/dist/dashboard/assets/entry-CkryrIea.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-BydyKaDz.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-BO6h8yNv.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
|
@@ -1503,6 +1503,27 @@ export function createMessageTypes(): protobuf.Root {
|
|
|
1503
1503
|
{ id: 1, name: "request_context", type: "RequestContextPayload" },
|
|
1504
1504
|
])
|
|
1505
1505
|
|
|
1506
|
+
// Allowlist prechecks (exec #41/#42/#43): Cursor asks whether a shell
|
|
1507
|
+
// command / MCP tool / web fetch would be permitted before it emits the
|
|
1508
|
+
// real call. Without an args frame this is decodable, but its exec id is
|
|
1509
|
+
// then stranded — the server waits forever for the matching result.
|
|
1510
|
+
addType(root, "ShellAllowlistPrecheckArgs", [
|
|
1511
|
+
{ id: 1, name: "command", type: "string" },
|
|
1512
|
+
{ id: 2, name: "working_directory", type: "string" },
|
|
1513
|
+
{ id: 3, name: "parsing_result", type: "bytes" },
|
|
1514
|
+
{ id: 4, name: "classifier_result", type: "bytes" },
|
|
1515
|
+
{ id: 5, name: "tool_call_id", type: "string" },
|
|
1516
|
+
])
|
|
1517
|
+
addType(root, "McpAllowlistPrecheckArgs", [
|
|
1518
|
+
{ id: 1, name: "provider_identifier", type: "string" },
|
|
1519
|
+
{ id: 2, name: "tool_name", type: "string" },
|
|
1520
|
+
{ id: 3, name: "tool_call_id", type: "string" },
|
|
1521
|
+
])
|
|
1522
|
+
addType(root, "WebFetchAllowlistPrecheckArgs", [
|
|
1523
|
+
{ id: 1, name: "url", type: "string" },
|
|
1524
|
+
{ id: 2, name: "tool_call_id", type: "string" },
|
|
1525
|
+
])
|
|
1526
|
+
|
|
1506
1527
|
addType(root, "RequestContextResult", [
|
|
1507
1528
|
{ id: 1, name: "success", type: "RequestContextSuccess" },
|
|
1508
1529
|
])
|
|
@@ -1568,6 +1589,9 @@ export function createMessageTypes(): protobuf.Root {
|
|
|
1568
1589
|
{ id: 18, name: "read_mcp_resource_exec_args", type: "ReadMcpResourceExecArgs" },
|
|
1569
1590
|
{ id: 28, name: "subagent_args", type: "SubagentArgs" },
|
|
1570
1591
|
{ id: 36, name: "mcp_state_exec_args", type: "McpStateExecArgs" },
|
|
1592
|
+
{ id: 41, name: "shell_allowlist_precheck_args", type: "ShellAllowlistPrecheckArgs" },
|
|
1593
|
+
{ id: 42, name: "mcp_allowlist_precheck_args", type: "McpAllowlistPrecheckArgs" },
|
|
1594
|
+
{ id: 43, name: "web_fetch_allowlist_precheck_args", type: "WebFetchAllowlistPrecheckArgs" },
|
|
1571
1595
|
{ id: 44, name: "git_diff_request", type: "GetDiffRequest" },
|
|
1572
1596
|
{ id: 45, name: "pi_read_args", type: "PiReadToolArgs" },
|
|
1573
1597
|
{ id: 46, name: "pi_bash_args", type: "PiBashToolArgs" },
|
|
@@ -1582,6 +1606,7 @@ export function createMessageTypes(): protobuf.Root {
|
|
|
1582
1606
|
"shell_args", "write_args", "delete_args", "grep_args", "read_args", "ls_args",
|
|
1583
1607
|
"request_context_args", "mcp_args", "shell_stream_args", "background_shell_spawn_args",
|
|
1584
1608
|
"list_mcp_resources_exec_args", "read_mcp_resource_exec_args", "mcp_state_exec_args",
|
|
1609
|
+
"shell_allowlist_precheck_args", "mcp_allowlist_precheck_args", "web_fetch_allowlist_precheck_args",
|
|
1585
1610
|
"subagent_args", "git_diff_request",
|
|
1586
1611
|
"pi_read_args", "pi_bash_args", "pi_edit_args", "pi_write_args",
|
|
1587
1612
|
"pi_grep_args", "pi_find_args", "pi_ls_args",
|
|
@@ -496,7 +496,14 @@ class FactoryDroidAdapter implements ProviderAdapter {
|
|
|
496
496
|
response = await outboundFetch(url, {
|
|
497
497
|
method: "POST",
|
|
498
498
|
headers,
|
|
499
|
-
body: JSON.stringify(
|
|
499
|
+
body: JSON.stringify(
|
|
500
|
+
buildGeminiPayload(
|
|
501
|
+
request,
|
|
502
|
+
...(context.image_clamp_max_bytes === undefined
|
|
503
|
+
? []
|
|
504
|
+
: [{ maxBytesOverride: context.image_clamp_max_bytes }]),
|
|
505
|
+
),
|
|
506
|
+
),
|
|
500
507
|
signal: lifecycle.signal,
|
|
501
508
|
});
|
|
502
509
|
} catch (error: unknown) {
|
|
@@ -189,7 +189,12 @@ class GeminiAdapter implements ProviderAdapter {
|
|
|
189
189
|
const action = request.stream ? "streamGenerateContent?alt=sse" : "generateContent";
|
|
190
190
|
const url = geminiModelUrl(this.baseUrl, model, action);
|
|
191
191
|
|
|
192
|
-
const payload = buildGeminiPayload(
|
|
192
|
+
const payload = buildGeminiPayload(
|
|
193
|
+
request,
|
|
194
|
+
...(context.image_clamp_max_bytes === undefined
|
|
195
|
+
? []
|
|
196
|
+
: [{ maxBytesOverride: context.image_clamp_max_bytes }]),
|
|
197
|
+
);
|
|
193
198
|
if (
|
|
194
199
|
request.tools?.some(
|
|
195
200
|
(tool) =>
|
|
@@ -1,177 +1,279 @@
|
|
|
1
|
-
import type { FetchLike, ProviderQuotaResult, ProviderQuotaWindow } from "../../quota/quota-contracts";
|
|
2
|
-
import { isoDate, number, percentWindow, record, text } from "../../quota/quota-contracts";
|
|
3
|
-
import { buildGrokAuthUserAgent, getGrokVersion } from "../../operations/client-versions";
|
|
4
|
-
import { decodeGrokCreditsFrame } from "./grok-quota-frame";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
windows
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
)
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
1
|
+
import type { FetchLike, ProviderQuotaResult, ProviderQuotaWindow } from "../../quota/quota-contracts";
|
|
2
|
+
import { isoDate, number, percentWindow, record, text } from "../../quota/quota-contracts";
|
|
3
|
+
import { buildGrokAuthUserAgent, getGrokVersion } from "../../operations/client-versions";
|
|
4
|
+
import { decodeGrokCreditsFrame } from "./grok-quota-frame";
|
|
5
|
+
import {
|
|
6
|
+
grokAuthoritativeWindow,
|
|
7
|
+
grokHeaderRequestsWindow,
|
|
8
|
+
grokHeaderWindow,
|
|
9
|
+
GROK_FREE_USAGE_EXHAUSTED,
|
|
10
|
+
parseGrokFreeUsageVerdict,
|
|
11
|
+
readGrokRateLimitHeaders,
|
|
12
|
+
} from "./grok-ratelimit";
|
|
13
|
+
import type { GrokRateLimitProbe } from "./grok-ratelimit";
|
|
14
|
+
import { storedGrokRateLimit } from "./grok-ratelimit-store";
|
|
15
|
+
import type { QuotaCollectionContext } from "../../quota/quota-support";
|
|
16
|
+
|
|
17
|
+
const BILLING_URL = "https://cli-chat-proxy.grok.com/v1/billing?format=credits";
|
|
18
|
+
const BILLING_SOURCE = "cli-chat-proxy.grok.com/v1/billing";
|
|
19
|
+
const USER_URL = "https://cli-chat-proxy.grok.com/v1/user?include=subscription";
|
|
20
|
+
const GRPC_CREDITS_URL = "https://grok.com/grok_api_v2.GrokBuildBilling/GetGrokCreditsConfig";
|
|
21
|
+
const GRPC_EMPTY_FRAME = new Uint8Array([0, 0, 0, 0, 0]);
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The cheapest request that still returns the rate-limit headers.
|
|
25
|
+
*
|
|
26
|
+
* There is no dedicated quota endpoint — `grok.com/rest/*` does not expose one
|
|
27
|
+
* and `/v1/models` carries no rate-limit headers — so reading the bucket costs
|
|
28
|
+
* one real request. At ~360-480 tokens against a 1M allowance that is a few
|
|
29
|
+
* thousand readings per account, which is affordable; a larger cap would not
|
|
30
|
+
* be.
|
|
31
|
+
*/
|
|
32
|
+
const PROBE_URL = "https://cli-chat-proxy.grok.com/v1/responses";
|
|
33
|
+
const PROBE_BODY = {
|
|
34
|
+
model: "grok-4.6",
|
|
35
|
+
stream: false,
|
|
36
|
+
max_output_tokens: 1,
|
|
37
|
+
input: [{ type: "message", role: "user", content: [{ type: "input_text", text: "hi" }] }],
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Auth headers shared by every xAI billing/userinfo call: bearer token plus
|
|
42
|
+
* the CLI client identity the gateway gates on.
|
|
43
|
+
*/
|
|
44
|
+
function xaiHeaders(accessToken: string): Record<string, string> {
|
|
45
|
+
const version = getGrokVersion();
|
|
46
|
+
return {
|
|
47
|
+
authorization: `Bearer ${accessToken}`,
|
|
48
|
+
"x-xai-token-auth": "xai-grok-cli",
|
|
49
|
+
"x-grok-client-version": version,
|
|
50
|
+
"user-agent": buildGrokAuthUserAgent(version),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
interface XaiBillingPayload {
|
|
54
|
+
currentPeriod?: unknown;
|
|
55
|
+
creditUsagePercent?: unknown;
|
|
56
|
+
productUsage?: unknown;
|
|
57
|
+
onDemandCap?: unknown;
|
|
58
|
+
onDemandUsed?: unknown;
|
|
59
|
+
billingPeriodStart?: unknown;
|
|
60
|
+
billingPeriodEnd?: unknown;
|
|
61
|
+
monthlyLimit?: unknown;
|
|
62
|
+
used?: unknown;
|
|
63
|
+
subscriptionTier?: unknown;
|
|
64
|
+
user?: unknown;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function parsePayload(value: string): XaiBillingPayload {
|
|
68
|
+
const payload: unknown = JSON.parse(value);
|
|
69
|
+
// Live billing wraps usage in a `config` envelope (free-tier shape);
|
|
70
|
+
// paid shapes may carry the fields top-level — read the envelope first.
|
|
71
|
+
const parsed = record(payload);
|
|
72
|
+
return ((parsed && record(parsed.config)) ?? parsed ?? {}) as XaiBillingPayload;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function billingNumber(value: unknown): number | null {
|
|
76
|
+
const nested = record(value);
|
|
77
|
+
return number(nested?.val ?? value);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function period(raw: unknown): { start: string; end: string } | undefined {
|
|
81
|
+
const value = record(raw);
|
|
82
|
+
const type = text(value?.type);
|
|
83
|
+
if (type !== null && !type.toUpperCase().includes("WEEK")) return undefined;
|
|
84
|
+
const start = isoDate(value?.start);
|
|
85
|
+
const end = isoDate(value?.end);
|
|
86
|
+
return start && end && Date.parse(end) > Date.parse(start) ? { start, end } : undefined;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function buildWeeklyWindows(payload: XaiBillingPayload): ProviderQuotaWindow[] {
|
|
90
|
+
const current = period(payload.currentPeriod);
|
|
91
|
+
const creditPercent = billingNumber(payload.creditUsagePercent);
|
|
92
|
+
if (!current) return [];
|
|
93
|
+
if (creditPercent === null || creditPercent < 0 || creditPercent > 100) {
|
|
94
|
+
// Free-tier billing states the period but no usage percent: emit the
|
|
95
|
+
// window with the reset date and null percents (the dashboard renders
|
|
96
|
+
// the countdown without inventing a number) instead of empty windows.
|
|
97
|
+
return [percentWindow("1w", "SuperGrok Weekly Credits", null, current.end, null, null, "independent")];
|
|
98
|
+
}
|
|
99
|
+
const windows: ProviderQuotaWindow[] = [
|
|
100
|
+
percentWindow("1w", "SuperGrok Weekly Credits", creditPercent, current.end, creditPercent, 100, "independent"),
|
|
101
|
+
];
|
|
102
|
+
if (Array.isArray(payload.productUsage)) {
|
|
103
|
+
for (const entry of payload.productUsage) {
|
|
104
|
+
const value = record(entry);
|
|
105
|
+
const product = text(value?.product);
|
|
106
|
+
const percent = billingNumber(value?.usagePercent);
|
|
107
|
+
windows.push(percentWindow(`product:${product}:1w`, `${product} (Weekly)`, percent, current.end, percent, 100, "independent"));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return windows;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function buildMonthlyWindows(payload: XaiBillingPayload): ProviderQuotaWindow[] {
|
|
114
|
+
const start = isoDate(payload.billingPeriodStart);
|
|
115
|
+
const end = isoDate(payload.billingPeriodEnd);
|
|
116
|
+
const limit = billingNumber(payload.monthlyLimit);
|
|
117
|
+
const used = billingNumber(payload.used);
|
|
118
|
+
if (!start || !end || limit === null || limit <= 0 || used === null || used < 0) return [];
|
|
119
|
+
const usedPercent = Math.min(100, (used / limit) * 100);
|
|
120
|
+
return [percentWindow("1mo", "SuperGrok Monthly Included", usedPercent, end, used, limit, "independent")];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function addOnDemandWindow(windows: ProviderQuotaWindow[], payload: XaiBillingPayload): void {
|
|
124
|
+
const cap = billingNumber(payload.onDemandCap);
|
|
125
|
+
const used = billingNumber(payload.onDemandUsed);
|
|
126
|
+
if (cap === null || cap <= 0 || used === null || used < 0) return;
|
|
127
|
+
windows.push(percentWindow("on-demand", "On-demand", Math.min(100, (used / cap) * 100), null, used, cap, "independent"));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function fetchGrpcCredits(
|
|
131
|
+
accessToken: string,
|
|
132
|
+
fetcher: FetchLike,
|
|
133
|
+
): Promise<{ percentUsed: number; resetAt: string | null } | null> {
|
|
134
|
+
try {
|
|
135
|
+
const response = await fetcher(GRPC_CREDITS_URL, {
|
|
136
|
+
method: "POST",
|
|
137
|
+
headers: {
|
|
138
|
+
...xaiHeaders(accessToken),
|
|
139
|
+
"content-type": "application/grpc-web+proto",
|
|
140
|
+
"x-grpc-web": "1",
|
|
141
|
+
},
|
|
142
|
+
body: GRPC_EMPTY_FRAME,
|
|
143
|
+
signal: AbortSignal.timeout(8_000),
|
|
144
|
+
});
|
|
145
|
+
if (!response.ok) return null;
|
|
146
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
147
|
+
return decodeGrokCreditsFrame(new Uint8Array(arrayBuffer));
|
|
148
|
+
} catch {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function fetchUser(
|
|
154
|
+
accessToken: string,
|
|
155
|
+
fetcher: FetchLike,
|
|
156
|
+
): Promise<Record<string, unknown> | null> {
|
|
157
|
+
try {
|
|
158
|
+
const response = await fetcher(USER_URL, {
|
|
159
|
+
headers: { ...xaiHeaders(accessToken), accept: "application/json" },
|
|
160
|
+
signal: AbortSignal.timeout(10_000),
|
|
161
|
+
});
|
|
162
|
+
if (!response.ok) return null;
|
|
163
|
+
return record((await response.json()) as unknown);
|
|
164
|
+
} catch {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* One cheap request, read for its rate-limit headers — and, when the account is
|
|
171
|
+
* being refused, for the authoritative allowance in the 429 body.
|
|
172
|
+
*
|
|
173
|
+
* A refusal is not a failure of the probe: it is the only moment upstream
|
|
174
|
+
* states the real free allowance, so the body is parsed before anything is
|
|
175
|
+
* discarded. A transport failure is the opposite — it says nothing about the
|
|
176
|
+
* account, so it yields no reading rather than a zero.
|
|
177
|
+
*/
|
|
178
|
+
async function probeRateLimit(
|
|
179
|
+
accessToken: string,
|
|
180
|
+
fetcher: FetchLike,
|
|
181
|
+
): Promise<GrokRateLimitProbe> {
|
|
182
|
+
try {
|
|
183
|
+
const response = await fetcher(PROBE_URL, {
|
|
184
|
+
method: "POST",
|
|
185
|
+
headers: { ...xaiHeaders(accessToken), "content-type": "application/json" },
|
|
186
|
+
body: JSON.stringify(PROBE_BODY),
|
|
187
|
+
signal: AbortSignal.timeout(30_000),
|
|
188
|
+
});
|
|
189
|
+
const reading = readGrokRateLimitHeaders(response.headers);
|
|
190
|
+
let authoritative = null;
|
|
191
|
+
let note: string | null = null;
|
|
192
|
+
if (response.status === 429) {
|
|
193
|
+
const body = await response.text();
|
|
194
|
+
const verdict = parseGrokFreeUsageVerdict(parsePayload(body), body);
|
|
195
|
+
authoritative = verdict ? grokAuthoritativeWindow(verdict) : null;
|
|
196
|
+
note = GROK_FREE_USAGE_EXHAUSTED;
|
|
197
|
+
} else if (!response.ok) {
|
|
198
|
+
// Drain so the connection is reusable, then report nothing: an unrelated
|
|
199
|
+
// failure must not read as an exhausted allowance.
|
|
200
|
+
await response.text();
|
|
201
|
+
}
|
|
202
|
+
return {
|
|
203
|
+
header: grokHeaderWindow(reading),
|
|
204
|
+
requests: grokHeaderRequestsWindow(reading),
|
|
205
|
+
authoritative,
|
|
206
|
+
refusal: response.status === 429,
|
|
207
|
+
note,
|
|
208
|
+
};
|
|
209
|
+
} catch {
|
|
210
|
+
return { header: null, requests: null, authoritative: null, refusal: false, note: null };
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Fetches Grok Build subscription billing, including weekly and unified-monthly fallbacks. */
|
|
215
|
+
export async function fetchGrokQuota(
|
|
216
|
+
credential: string,
|
|
217
|
+
fetcher: FetchLike,
|
|
218
|
+
context?: QuotaCollectionContext,
|
|
219
|
+
): Promise<ProviderQuotaResult> {
|
|
220
|
+
const accessToken = credential.trim();
|
|
221
|
+
if (!accessToken) throw new Error("grok quota requires an OAuth access token");
|
|
222
|
+
const response = await fetcher(BILLING_URL, {
|
|
223
|
+
headers: { ...xaiHeaders(accessToken), accept: "application/json" },
|
|
224
|
+
signal: AbortSignal.timeout(15_000),
|
|
225
|
+
});
|
|
226
|
+
const raw = await response.text();
|
|
227
|
+
if (!response.ok) throw new Error(`Grok Build billing request failed (${response.status}): ${raw.slice(0, 500)}`);
|
|
228
|
+
const payload = parsePayload(raw);
|
|
229
|
+
const windows = buildWeeklyWindows(payload);
|
|
230
|
+
if (windows.length === 0 || windows.every((w) => w.usedPercent === null)) {
|
|
231
|
+
const grpc = await fetchGrpcCredits(accessToken, fetcher);
|
|
232
|
+
if (grpc) {
|
|
233
|
+
const idx = windows.findIndex((w) => w.label.includes("Weekly"));
|
|
234
|
+
const grpcWindow = percentWindow("1w", "SuperGrok Weekly Credits", grpc.percentUsed, grpc.resetAt, grpc.percentUsed, 100, "independent");
|
|
235
|
+
if (idx >= 0) windows[idx] = grpcWindow;
|
|
236
|
+
else windows.unshift(grpcWindow);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
if (windows.length === 0) windows.push(...buildMonthlyWindows(payload));
|
|
240
|
+
addOnDemandWindow(windows, payload);
|
|
241
|
+
const user = await fetchUser(accessToken, fetcher);
|
|
242
|
+
const userRecord = record(user?.user);
|
|
243
|
+
const plan =
|
|
244
|
+
text(user?.subscriptionTier) ??
|
|
245
|
+
text(payload.subscriptionTier) ??
|
|
246
|
+
text(userRecord?.subscriptionTier) ??
|
|
247
|
+
"SuperGrok";
|
|
248
|
+
// The billing windows are the subscription's own accounting; the two CLI
|
|
249
|
+
// readings say something different — what this account can still *serve* —
|
|
250
|
+
// so they are prepended and appended around it rather than merged in.
|
|
251
|
+
//
|
|
252
|
+
// Header first: it is what a caller sees every day and it never goes stale
|
|
253
|
+
// into "unknown". Authoritative last: it only exists once upstream has
|
|
254
|
+
// refused, and it is the number that decides the account, so it sits beneath
|
|
255
|
+
// the indicator it overrides.
|
|
256
|
+
// A reading captured from real traffic costs nothing, so it is preferred
|
|
257
|
+
// whenever one is fresh. The probe is the fallback for the first read after a
|
|
258
|
+
// start — never the routine path, or every console refresh would spend a
|
|
259
|
+
// request.
|
|
260
|
+
const stored = context?.accountId === undefined
|
|
261
|
+
? null
|
|
262
|
+
: storedGrokRateLimit(context.accountId);
|
|
263
|
+
const probe = stored ? null : await probeRateLimit(accessToken, fetcher);
|
|
264
|
+
const ordered: ProviderQuotaWindow[] = [];
|
|
265
|
+
if (stored) ordered.push(...stored);
|
|
266
|
+
else if (probe) {
|
|
267
|
+
if (probe.header) ordered.push(probe.header);
|
|
268
|
+
if (probe.requests) ordered.push(probe.requests);
|
|
269
|
+
}
|
|
270
|
+
ordered.push(...windows);
|
|
271
|
+
if (probe?.authoritative) ordered.push(probe.authoritative);
|
|
272
|
+
// Deliberately not `probe.note` in `error`: a refusal here is the probe's own
|
|
273
|
+
// doing and says the account is out of free usage, which is *account state*,
|
|
274
|
+
// not a read failure. Reporting it as an error would make a healthy read look
|
|
275
|
+
// broken; the authoritative window below is what carries the verdict.
|
|
276
|
+
return { source: "grok", plan, windows: ordered, error: null };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export { BILLING_SOURCE as GROK_BILLING_SOURCE };
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Where a Grok rate-limit reading lives between dispatch and display.
|
|
2
|
+
//
|
|
3
|
+
// The bucket headers come back on **every** upstream response, so the cheap
|
|
4
|
+
// place to learn them is the traffic the gateway is already sending — not a
|
|
5
|
+
// probe of its own. A dispatch that reads and stores them costs nothing extra;
|
|
6
|
+
// a probe costs a request every time quota is shown.
|
|
7
|
+
//
|
|
8
|
+
// So this store is fed by dispatch (manual test or automatic routing alike) and
|
|
9
|
+
// read by the quota collector. The collector only probes when there is nothing
|
|
10
|
+
// stored, which is the first read after a start and never after that.
|
|
11
|
+
//
|
|
12
|
+
// Keyed by account id: two Grok accounts carry different buckets, and a reading
|
|
13
|
+
// filed against the wrong one is worse than no reading at all.
|
|
14
|
+
|
|
15
|
+
import type { ProviderQuotaWindow } from "../../quota/quota-contracts";
|
|
16
|
+
import { grokHeaderRequestsWindow, grokHeaderWindow, type GrokRateLimitReading } from "./grok-ratelimit";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* How long a stored reading stays useful.
|
|
20
|
+
*
|
|
21
|
+
* The header is eventually consistent, so a fresh value is not meaningfully
|
|
22
|
+
* better than one from a few minutes ago — but a value from last week says
|
|
23
|
+
* nothing about now. Five minutes is short enough to follow real traffic and
|
|
24
|
+
* long enough that a burst of console refreshes costs no requests.
|
|
25
|
+
*/
|
|
26
|
+
const SNAPSHOT_TTL_MS = 5 * 60 * 1000;
|
|
27
|
+
|
|
28
|
+
interface Snapshot {
|
|
29
|
+
readonly windows: readonly ProviderQuotaWindow[];
|
|
30
|
+
readonly capturedAt: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const snapshots = new Map<string, Snapshot>();
|
|
34
|
+
|
|
35
|
+
/** Files a reading captured from a real dispatch against one account. */
|
|
36
|
+
export function rememberGrokRateLimit(
|
|
37
|
+
accountId: string,
|
|
38
|
+
reading: GrokRateLimitReading,
|
|
39
|
+
now = Date.now(),
|
|
40
|
+
): void {
|
|
41
|
+
const windows = [grokHeaderWindow(reading), grokHeaderRequestsWindow(reading)].filter(
|
|
42
|
+
(value): value is ProviderQuotaWindow => value !== null,
|
|
43
|
+
);
|
|
44
|
+
if (windows.length === 0) return;
|
|
45
|
+
snapshots.set(accountId, { windows, capturedAt: now });
|
|
46
|
+
if (snapshots.size > 4096) {
|
|
47
|
+
// Bounded: an unbounded map keyed by account is a leak dressed as a cache.
|
|
48
|
+
for (const [key, value] of snapshots) {
|
|
49
|
+
if (now - value.capturedAt > SNAPSHOT_TTL_MS) snapshots.delete(key);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The stored window for one account, or `null` when none is fresh enough. */
|
|
55
|
+
export function storedGrokRateLimit(accountId: string, now = Date.now()): readonly ProviderQuotaWindow[] | null {
|
|
56
|
+
const snapshot = snapshots.get(accountId);
|
|
57
|
+
if (!snapshot) return null;
|
|
58
|
+
if (now - snapshot.capturedAt > SNAPSHOT_TTL_MS) {
|
|
59
|
+
snapshots.delete(accountId);
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
return snapshot.windows;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Whether a fresh reading exists, so the collector can skip its probe. */
|
|
66
|
+
export function hasFreshGrokRateLimit(accountId: string, now = Date.now()): boolean {
|
|
67
|
+
return storedGrokRateLimit(accountId, now) !== null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Test seam. */
|
|
71
|
+
export function clearGrokRateLimitSnapshots(): void {
|
|
72
|
+
snapshots.clear();
|
|
73
|
+
}
|