cartethyia 2.5.9 → 2.6.0
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/.env.example +15 -42
- package/README.md +11 -10
- package/dist/dashboard/assets/{BannedPage-Brvuj_Eb.js → BannedPage-l6XMnfr9.js} +1 -1
- package/dist/dashboard/assets/BannedPage-l6XMnfr9.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolDetailPage-CqfXtI3y.js → CliToolDetailPage-DwOzAW45.js} +1 -1
- package/dist/dashboard/assets/CliToolDetailPage-DwOzAW45.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolsPage-GQeS91bg.js → CliToolsPage-BT3Up3QA.js} +1 -1
- package/dist/dashboard/assets/CliToolsPage-BT3Up3QA.js.gz +0 -0
- package/dist/dashboard/assets/{CombosPage-BDRsTXBB.js → CombosPage-6kYIUpzR.js} +1 -1
- package/dist/dashboard/assets/CombosPage-6kYIUpzR.js.gz +0 -0
- package/dist/dashboard/assets/{ConsoleLogPage-zP8AFHX0.js → ConsoleLogPage-zRnj1mlR.js} +1 -1
- package/dist/dashboard/assets/ConsoleLogPage-zRnj1mlR.js.gz +0 -0
- package/dist/dashboard/assets/{CustomizationPage-BmfrNZ14.js → CustomizationPage-C7m-H1Rz.js} +1 -1
- package/dist/dashboard/assets/CustomizationPage-C7m-H1Rz.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-C8MR_BdB.js +1 -0
- package/dist/dashboard/assets/HealthEventsModal-C8MR_BdB.js.gz +0 -0
- package/dist/dashboard/assets/{LoginPage-DoreIgm8.js → LoginPage-CbphLvjg.js} +1 -1
- package/dist/dashboard/assets/LoginPage-CbphLvjg.js.gz +0 -0
- package/dist/dashboard/assets/{ModelPicker-C7pDb41R.js → ModelPicker-DVny4S5_.js} +1 -1
- package/dist/dashboard/assets/ModelPicker-DVny4S5_.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-DABu2Wgt.js +1 -0
- package/dist/dashboard/assets/OverviewPage-DABu2Wgt.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-C15x9q13.js +3 -0
- package/dist/dashboard/assets/ProviderDetailPage-C15x9q13.js.gz +0 -0
- package/dist/dashboard/assets/{ProvidersPage-BnoPgA7w.js → ProvidersPage-Dfb2896L.js} +1 -1
- package/dist/dashboard/assets/ProvidersPage-Dfb2896L.js.gz +0 -0
- package/dist/dashboard/assets/{ProxyPage-Bq_TGgdb.js → ProxyPage-BaCIdAUt.js} +1 -1
- package/dist/dashboard/assets/ProxyPage-BaCIdAUt.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-BsdoCIw1.js +1 -0
- package/dist/dashboard/assets/QuotaPage-BsdoCIw1.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-KVJFh3Ip.js +1 -0
- package/dist/dashboard/assets/SettingsPage-KVJFh3Ip.js.gz +0 -0
- package/dist/dashboard/assets/{SetupPage-Day3mCOn.js → SetupPage-D4g6vOGc.js} +1 -1
- package/dist/dashboard/assets/SetupPage-D4g6vOGc.js.gz +0 -0
- package/dist/dashboard/assets/{SortableList-DQxllnB5.js → SortableList-C93i14BD.js} +1 -1
- package/dist/dashboard/assets/SortableList-C93i14BD.js.gz +0 -0
- package/dist/dashboard/assets/{StudioPage-uMExoWu1.js → StudioPage-SgQpymjm.js} +1 -1
- package/dist/dashboard/assets/{StudioPage-uMExoWu1.js.gz → StudioPage-SgQpymjm.js.gz} +0 -0
- package/dist/dashboard/assets/{UsagePage-DXa1oR-4.js → UsagePage-BFME-eba.js} +2 -2
- package/dist/dashboard/assets/UsagePage-BFME-eba.js.gz +0 -0
- package/dist/dashboard/assets/{api-keys-DQYLQxpW.js → api-keys-C23S9sgp.js} +1 -1
- package/dist/dashboard/assets/api-keys-C23S9sgp.js.gz +0 -0
- package/dist/dashboard/assets/{app-BIt446yE.js → app-2KhtHCv9.js} +1 -1
- package/dist/dashboard/assets/{app-BIt446yE.js.gz → app-2KhtHCv9.js.gz} +0 -0
- package/dist/dashboard/assets/app-CEeaMHOy.css +2 -0
- package/dist/dashboard/assets/app-CEeaMHOy.css.gz +0 -0
- package/dist/dashboard/assets/{assert-C9FD_JwJ.js → assert-DU6qrMA_.js} +1 -1
- package/dist/dashboard/assets/assert-DU6qrMA_.js.gz +0 -0
- package/dist/dashboard/assets/{badge-CR72dasT.js → badge-CKsF123d.js} +1 -1
- package/dist/dashboard/assets/badge-CKsF123d.js.gz +0 -0
- package/dist/dashboard/assets/{banner-CI9_eSfO.js → banner-8j7uChA9.js} +1 -1
- package/dist/dashboard/assets/banner-8j7uChA9.js.gz +0 -0
- package/dist/dashboard/assets/{card-DS7CQEsO.js → card-CdsSlWA3.js} +1 -1
- package/dist/dashboard/assets/card-CdsSlWA3.js.gz +0 -0
- package/dist/dashboard/assets/{cli-tools-CNqzrfdb.js → cli-tools-BaFUuFFF.js} +1 -1
- package/dist/dashboard/assets/cli-tools-BaFUuFFF.js.gz +0 -0
- package/dist/dashboard/assets/{clipboard-button-611pSsaX.js → clipboard-button-uL5aYLfW.js} +1 -1
- package/dist/dashboard/assets/clipboard-button-uL5aYLfW.js.gz +0 -0
- package/dist/dashboard/assets/{dialog-k37exjNt.js → dialog-C07hr5_G.js} +1 -1
- package/dist/dashboard/assets/dialog-C07hr5_G.js.gz +0 -0
- package/dist/dashboard/assets/{drawer-CX7N8ucR.js → drawer-BPO1TcKH.js} +1 -1
- package/dist/dashboard/assets/drawer-BPO1TcKH.js.gz +0 -0
- package/dist/dashboard/assets/{entry-CTP8AyiS.js → entry-Bz_CtDfC.js} +1 -1
- package/dist/dashboard/assets/entry-Bz_CtDfC.js.gz +0 -0
- package/dist/dashboard/assets/entry-CwPRKk0d.js +4 -0
- package/dist/dashboard/assets/entry-CwPRKk0d.js.gz +0 -0
- package/dist/dashboard/assets/entry-GJRbmrpA.js +1 -0
- package/dist/dashboard/assets/entry-GJRbmrpA.js.gz +0 -0
- package/dist/dashboard/assets/{github-badge-sltk1qUi.js → github-badge-DmQreSDd.js} +1 -1
- package/dist/dashboard/assets/github-badge-DmQreSDd.js.gz +0 -0
- package/dist/dashboard/assets/{index-z3zDOSMM.js → index-Bblu6erR.js} +2 -2
- package/dist/dashboard/assets/index-Bblu6erR.js.gz +0 -0
- package/dist/dashboard/assets/{inline-BhY2sSnO.js → inline-C9dCYaBZ.js} +1 -1
- package/dist/dashboard/assets/inline-C9dCYaBZ.js.gz +0 -0
- package/dist/dashboard/assets/{input-V1umFiBk.js → input-CjCnXweJ.js} +1 -1
- package/dist/dashboard/assets/input-CjCnXweJ.js.gz +0 -0
- package/dist/dashboard/assets/{layout-BBOYycyS.js → layout-V6y8DAkl.js} +1 -1
- package/dist/dashboard/assets/layout-V6y8DAkl.js.gz +0 -0
- package/dist/dashboard/assets/{list-row-1zzZYM0B.js → list-row-CM1AaG_y.js} +1 -1
- package/dist/dashboard/assets/list-row-CM1AaG_y.js.gz +0 -0
- package/dist/dashboard/assets/{live-BivGfuUs.js → live-ta6GMb-8.js} +1 -1
- package/dist/dashboard/assets/live-ta6GMb-8.js.gz +0 -0
- package/dist/dashboard/assets/{model-bans-CmffjLuG.js → model-bans-BmYTmbAk.js} +1 -1
- package/dist/dashboard/assets/model-bans-BmYTmbAk.js.gz +0 -0
- package/dist/dashboard/assets/{mutation-46P0oWMe.js → mutation-g_fduiNw.js} +1 -1
- package/dist/dashboard/assets/mutation-g_fduiNw.js.gz +0 -0
- package/dist/dashboard/assets/{numbers-DkfMR5Qs.js → numbers-BOevJzdz.js} +1 -1
- package/dist/dashboard/assets/numbers-BOevJzdz.js.gz +0 -0
- package/dist/dashboard/assets/{query-keys-BFhGpwR1.js → query-keys-UETqfytp.js} +1 -1
- package/dist/dashboard/assets/query-keys-UETqfytp.js.gz +0 -0
- package/dist/dashboard/assets/{routing-CAb4F37R.js → routing-BPUo-W8d.js} +1 -1
- package/dist/dashboard/assets/routing-BPUo-W8d.js.gz +0 -0
- package/dist/dashboard/assets/{select-C6MMiaio.js → select-C980c4cz.js} +1 -1
- package/dist/dashboard/assets/select-C980c4cz.js.gz +0 -0
- package/dist/dashboard/assets/{settings-DNX9sydi.js → settings--3hxSyry.js} +1 -1
- package/dist/dashboard/assets/settings--3hxSyry.js.gz +0 -0
- package/dist/dashboard/assets/{stack-BHTfXdz_.js → stack-_gfTdLVB.js} +1 -1
- package/dist/dashboard/assets/stack-_gfTdLVB.js.gz +0 -0
- package/dist/dashboard/assets/{state-mPDRlQkl.js → state-2c5V4zo7.js} +1 -1
- package/dist/dashboard/assets/state-2c5V4zo7.js.gz +0 -0
- package/dist/dashboard/assets/{switch-DOmJxg7H.js → switch-Dkke7Naf.js} +1 -1
- package/dist/dashboard/assets/switch-Dkke7Naf.js.gz +0 -0
- package/dist/dashboard/assets/{useMutation-Dem0lXQj.js → useMutation-EDFRkn1i.js} +1 -1
- package/dist/dashboard/assets/useMutation-EDFRkn1i.js.gz +0 -0
- package/dist/dashboard/index.html +1 -1
- package/dist/dashboard/index.html.gz +0 -0
- package/dist/dashboard/precompressed-assets.json +51 -51
- package/migrations/README.md +52 -52
- package/migrations/full-postgres/0020_add_telemetry_event_analytics.sql +77 -0
- package/migrations/full-postgres/0021_add_api_key_concurrency_tolerance.sql +13 -0
- package/migrations/full-postgres/0022_add_inferhub_config_key.sql +17 -0
- package/migrations/full-postgres/0023_drop_api_key_concurrency_tolerance.sql +13 -0
- package/migrations/lite-sqlite3/0020_add_telemetry_event_analytics.sql +69 -0
- package/migrations/lite-sqlite3/0021_add_api_key_concurrency_tolerance.sql +13 -0
- package/migrations/lite-sqlite3/0022_add_inferhub_config_key.sql +17 -0
- package/migrations/lite-sqlite3/0023_drop_api_key_concurrency_tolerance.sql +13 -0
- package/package.json +96 -97
- package/scripts/cli/admin.ts +126 -125
- package/scripts/cli/autostart.ts +237 -237
- package/scripts/cli/background.ts +120 -110
- package/scripts/cli/bin/carte.cjs +8 -8
- package/scripts/cli/bin/cartethyia.cjs +167 -167
- package/scripts/cli/console-menu.ts +308 -275
- package/scripts/cli/controller.ts +145 -0
- package/scripts/cli/doctor.ts +1 -1
- package/scripts/cli/env.ts +99 -99
- package/scripts/cli/gateway.ts +264 -261
- package/scripts/{health.ts → cli/health.ts} +4 -4
- package/scripts/cli/instance.ts +234 -224
- package/scripts/cli/main.ts +188 -164
- package/scripts/cli/platform.ts +64 -64
- package/scripts/cli/prompt.ts +275 -107
- package/scripts/cli/services.ts +88 -88
- package/scripts/cli/setup.ts +445 -445
- package/scripts/cli/start.ts +85 -82
- package/scripts/cli/startup-signal.ts +23 -23
- package/scripts/cli/telemetry.ts +70 -0
- package/scripts/cli/tray/index.ts +263 -263
- package/scripts/cli/tray/windows.ps1 +107 -107
- package/scripts/cli/tray.ts +192 -208
- package/scripts/cli/update.ts +39 -39
- package/scripts/cli/version.ts +67 -67
- package/scripts/cli/wordmark.ts +58 -58
- package/src/boot/app.ts +16 -1
- package/src/boot/dependencies.ts +128 -25
- package/src/config/package-paths.ts +24 -0
- package/src/config/resources.ts +137 -0
- package/src/config/runtime.ts +133 -76
- package/src/config/spec.ts +30 -29
- package/src/config/telemetry.ts +0 -25
- package/src/console/api-keys/bucket-spend.ts +14 -17
- package/src/console/api-keys/contracts.ts +410 -410
- package/src/console/api-keys/routes.ts +12 -0
- package/src/console/audit/store.ts +2 -3
- package/src/console/auth/setup-service.ts +8 -1
- package/src/console/backup/routes.ts +7 -0
- package/src/console/backup/service.ts +463 -463
- package/src/console/backup/store.ts +16 -11
- package/src/console/backup/validate.ts +1 -4
- package/src/console/cli-tools/fs-ops.ts +18 -2
- package/src/console/data-management/contracts.ts +1 -8
- package/src/console/data-management/operations.ts +61 -48
- package/src/console/data-management/routes.ts +4 -2
- package/src/console/domain-registration.ts +25 -3
- package/src/console/inferhub/client.ts +139 -0
- package/src/console/inferhub/key-store.ts +129 -0
- package/src/console/inferhub/normalize.ts +347 -0
- package/src/console/inferhub/routes.ts +411 -0
- package/src/console/inferhub/store.ts +109 -0
- package/src/console/inferhub/types.ts +183 -0
- package/src/console/observability/contracts.ts +2 -41
- package/src/console/observability/store.ts +176 -371
- package/src/console/providers/catalog/public-model-store.ts +0 -0
- package/src/console/providers/catalog/store.ts +23 -36
- package/src/console/quota/account-quota-shared.ts +52 -52
- package/src/console/quota/account-quota-tenant.ts +16 -24
- package/src/console/quota/refresh.ts +441 -441
- package/src/console/remote-support/service.ts +270 -114
- package/src/console/routing/pools/import-parse.ts +206 -206
- package/src/console/routing/pools/operations.ts +543 -543
- package/src/console/routing/pools/probe-verdict.ts +132 -132
- package/src/console/routing/pools/routes.ts +181 -181
- package/src/console/routing/pools/store.ts +1 -1
- package/src/console/settings/contracts.ts +42 -40
- package/src/console/settings/store.ts +25 -25
- package/src/console/share/share-stats.ts +22 -4
- package/src/console/share/share-usage.ts +30 -4
- package/src/console/shared/cursor.ts +15 -0
- package/src/console/updates/repository-updates.ts +40 -39
- package/src/console/updates/routes.ts +12 -0
- package/src/main.ts +13 -5
- package/src/network/pool/resolver.ts +1104 -1104
- package/src/observability/client-name.ts +22 -0
- package/src/observability/event-log.ts +5 -29
- package/src/observability/event-query.ts +690 -0
- package/src/observability/log-ring.ts +26 -2
- package/src/observability/log-sanitize.ts +118 -0
- package/src/observability/logger.ts +17 -13
- package/src/observability/metrics.ts +35 -0
- package/src/observability/payload-capture.ts +114 -41
- package/src/observability/payload-store.ts +249 -24
- package/src/observability/raw-event-writer.ts +118 -0
- package/src/observability/runtime-metrics.ts +71 -1
- package/src/observability/telemetry-buffer.ts +100 -17
- package/src/observability/telemetry-reconciliation.ts +417 -0
- package/src/persistence/boot.ts +20 -9
- package/src/persistence/db-pg.ts +13 -0
- package/src/persistence/db-sqlite.ts +569 -497
- package/src/persistence/dialect.ts +3 -2
- package/src/persistence/migrate.ts +181 -181
- package/src/persistence/schema.ts +1496 -1406
- package/src/persistence/shared-cache.ts +5 -1
- package/src/persistence/stores/api-key-store.ts +581 -581
- package/src/persistence/stores/telemetry-store.ts +124 -63
- package/src/protocol/reasoning-text.ts +30 -0
- package/src/protocol/registry.ts +2 -0
- package/src/protocol/response/chat.ts +33 -7
- package/src/protocol/response/codex.ts +14 -2
- package/src/protocol/response/messages.ts +5 -2
- package/src/providers/authentication/oauth-flow-store.ts +3 -10
- package/src/providers/bundled-discovery-providers.ts +122 -122
- package/src/providers/default-registry.ts +14 -0
- package/src/providers/discovery/catalog/catalog-blob.ts +18 -18
- package/src/providers/discovery/models-dev-catalog.ts +354 -354
- package/src/providers/discovery/probe-phases.ts +43 -7
- package/src/providers/discovery/probing-service.ts +70 -32
- package/src/providers/integrations/antigravity/antigravity.ts +9 -2
- package/src/providers/integrations/buddy/buddy-catalog-shared.ts +85 -17
- package/src/providers/integrations/buddy/buddy-chat-shared.ts +934 -934
- package/src/providers/integrations/buddy/buddy-checkin.ts +5 -9
- package/src/providers/integrations/buddy/buddy-discovery-shared.ts +3 -7
- package/src/providers/integrations/buddy/buddy-quota-shared.ts +3 -1
- package/src/providers/integrations/buddy/capabilities.ts +4 -0
- package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +57 -49
- package/src/providers/integrations/buddy/codebuddy/provider.ts +180 -180
- package/src/providers/integrations/buddy/workbuddy/provider.ts +206 -206
- package/src/providers/integrations/buddy/workbuddy/quota.ts +161 -161
- package/src/providers/integrations/claude/claude-quota.ts +2 -2
- package/src/providers/integrations/claude/claude-user-profiles.ts +3 -2
- package/src/providers/integrations/claude/claude.ts +3 -4
- package/src/providers/integrations/cline/cline.ts +94 -8
- package/src/providers/integrations/codex/codex.ts +61 -37
- package/src/providers/integrations/cursor/cursor-catalog.ts +368 -368
- package/src/providers/integrations/cursor/cursor-http2.ts +300 -300
- package/src/providers/integrations/cursor/cursor-identity.ts +2 -4
- package/src/providers/integrations/cursor/cursor-request.ts +11 -0
- package/src/providers/integrations/cursor/cursor.ts +29 -17
- package/src/providers/integrations/cursor/protocol/framing.ts +129 -129
- package/src/providers/integrations/cursor/protocol/messages.ts +2380 -2380
- package/src/providers/integrations/cursor/protocol/struct.ts +305 -305
- package/src/providers/integrations/cursor/protocol/tool-canonical.ts +275 -0
- package/src/providers/integrations/github/github.ts +3 -1
- package/src/providers/integrations/grok/grok-quota.ts +279 -279
- package/src/providers/integrations/grok/grok-ratelimit-store.ts +73 -73
- package/src/providers/integrations/grok/grok-ratelimit.ts +218 -218
- package/src/providers/integrations/hermes.ts +7 -1
- package/src/providers/integrations/inferhub.ts +201 -73
- package/src/providers/integrations/kilo/kilo.ts +152 -152
- package/src/providers/integrations/kiro/kiro-machine-id.ts +2 -4
- package/src/providers/integrations/opencode/provider.ts +2 -0
- package/src/providers/integrations/openrouter/provider.ts +129 -129
- package/src/providers/integrations/qoder/qoder-quota.ts +193 -192
- package/src/providers/integrations/qoder/qoder-realm.ts +106 -106
- package/src/providers/integrations/trae/trae-headers.ts +17 -48
- package/src/providers/integrations/trae/trae-oauth.ts +4 -26
- package/src/providers/integrations/trae/trae-quota.ts +389 -384
- package/src/providers/integrations/trae/trae-realm.ts +15 -2
- package/src/providers/integrations/trae/trae.ts +22 -24
- package/src/providers/integrations/xiaomi-mimo/think-stream.ts +100 -24
- package/src/providers/integrations/zcode/oauth.ts +1 -1
- package/src/providers/operations/account-health-service.ts +605 -279
- package/src/providers/provider-metadata.ts +12 -1
- package/src/providers/provider-model-definition.ts +373 -373
- package/src/providers/provider-probe.ts +3 -0
- package/src/providers/quota/quota-contracts.ts +3 -7
- package/src/providers/search/search-provider.ts +2 -6
- package/src/security/admission/contracts.ts +10 -0
- package/src/security/admission/in-memory-store.ts +89 -14
- package/src/security/admission/lua.ts +191 -182
- package/src/security/admission/redis-store.ts +30 -0
- package/src/security/admission/service.ts +18 -0
- package/src/security/api-key-auth-cache.ts +96 -0
- package/src/security/api-key-auth.ts +11 -100
- package/src/security/client-router-fingerprint.ts +3 -7
- package/src/security/ip-boundary.ts +7 -3
- package/src/shared/coerce.ts +67 -0
- package/src/shared/hash.ts +6 -0
- package/src/shared/time.ts +7 -0
- package/src/transport/dispatch/attempt-finalize.ts +26 -138
- package/src/transport/dispatch/attempt-loop.ts +103 -81
- package/src/transport/dispatch/capture-policy.ts +78 -0
- package/src/transport/dispatch/proxy-request.ts +4 -2
- package/src/transport/dispatch/streaming/encoder.ts +111 -5
- package/src/transport/dispatch/streaming/stream.ts +19 -2
- package/src/transport/dispatch/streaming/thinking-loop.ts +300 -0
- package/src/transport/failure-policy.ts +27 -0
- package/src/transport/guards/abuse-guard.ts +2 -2
- package/src/transport/guards/auth-guard.ts +17 -0
- package/src/transport/guards/body-policy.ts +3 -2
- package/src/transport/guards/concurrency-guard.ts +89 -13
- package/src/transport/guards/early-rejection-capture.ts +43 -3
- package/src/transport/guards/error-lifecycle.ts +10 -2
- package/src/transport/guards/guard-errors.ts +23 -0
- package/src/transport/guards/headroom-guard.ts +41 -7
- package/src/transport/guards/pipeline.ts +24 -0
- package/src/transport/guards/request-context.ts +2 -1
- package/src/transport/routing/admission.ts +51 -9
- package/src/transport/routing/bounded-lru.ts +49 -0
- package/src/transport/routing/candidate-index.ts +88 -0
- package/src/transport/routing/reservations.ts +121 -7
- package/src/transport/routing/round-robin.ts +10 -22
- package/src/transport/routing/route-catalog.ts +26 -13
- package/src/transport/routing/route-model.ts +22 -2
- package/src/transport/routing/router.ts +53 -31
- package/src/transport/surface/chat/encode.ts +2 -3
- package/src/transport/surface/chat/parse.ts +2 -1
- package/src/transport/surface/dialects.ts +11 -3
- package/src/transport/surface/messages/encode.ts +2 -1
- package/src/transport/surface/messages/parse.ts +3 -1
- package/src/transport/surface/messages/stream.ts +2 -6
- package/src/workers/maintenance-lease.ts +200 -0
- package/src/workers/scheduler.ts +101 -1
- package/dist/dashboard/assets/BannedPage-Brvuj_Eb.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-CqfXtI3y.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-GQeS91bg.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-BDRsTXBB.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-zP8AFHX0.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-BmfrNZ14.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-Dn3M6WZc.js +0 -1
- package/dist/dashboard/assets/HealthEventsModal-Dn3M6WZc.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-DoreIgm8.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-C7pDb41R.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-CvvD1Ugk.js +0 -1
- package/dist/dashboard/assets/OverviewPage-CvvD1Ugk.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-Clbn1gsW.js +0 -3
- package/dist/dashboard/assets/ProviderDetailPage-Clbn1gsW.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-BnoPgA7w.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-Bq_TGgdb.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-D9DHXvom.js +0 -1
- package/dist/dashboard/assets/QuotaPage-D9DHXvom.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-7xV8WkWL.js +0 -1
- package/dist/dashboard/assets/SettingsPage-7xV8WkWL.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-Day3mCOn.js.gz +0 -0
- package/dist/dashboard/assets/SortableList-DQxllnB5.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-DXa1oR-4.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-DQYLQxpW.js.gz +0 -0
- package/dist/dashboard/assets/app-CX_Z8cMs.css +0 -2
- package/dist/dashboard/assets/app-CX_Z8cMs.css.gz +0 -0
- package/dist/dashboard/assets/assert-C9FD_JwJ.js.gz +0 -0
- package/dist/dashboard/assets/badge-CR72dasT.js.gz +0 -0
- package/dist/dashboard/assets/banner-CI9_eSfO.js.gz +0 -0
- package/dist/dashboard/assets/card-DS7CQEsO.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-CNqzrfdb.js.gz +0 -0
- package/dist/dashboard/assets/clipboard-button-611pSsaX.js.gz +0 -0
- package/dist/dashboard/assets/dialog-k37exjNt.js.gz +0 -0
- package/dist/dashboard/assets/drawer-CX7N8ucR.js.gz +0 -0
- package/dist/dashboard/assets/entry-B9Xvtnfj.js +0 -1
- package/dist/dashboard/assets/entry-B9Xvtnfj.js.gz +0 -0
- package/dist/dashboard/assets/entry-ByMGkR3C.js +0 -4
- package/dist/dashboard/assets/entry-ByMGkR3C.js.gz +0 -0
- package/dist/dashboard/assets/entry-CTP8AyiS.js.gz +0 -0
- package/dist/dashboard/assets/github-badge-sltk1qUi.js.gz +0 -0
- package/dist/dashboard/assets/index-z3zDOSMM.js.gz +0 -0
- package/dist/dashboard/assets/inline-BhY2sSnO.js.gz +0 -0
- package/dist/dashboard/assets/input-V1umFiBk.js.gz +0 -0
- package/dist/dashboard/assets/layout-BBOYycyS.js.gz +0 -0
- package/dist/dashboard/assets/list-row-1zzZYM0B.js.gz +0 -0
- package/dist/dashboard/assets/live-BivGfuUs.js.gz +0 -0
- package/dist/dashboard/assets/model-bans-CmffjLuG.js.gz +0 -0
- package/dist/dashboard/assets/mutation-46P0oWMe.js.gz +0 -0
- package/dist/dashboard/assets/numbers-DkfMR5Qs.js.gz +0 -0
- package/dist/dashboard/assets/query-keys-BFhGpwR1.js.gz +0 -0
- package/dist/dashboard/assets/routing-CAb4F37R.js.gz +0 -0
- package/dist/dashboard/assets/select-C6MMiaio.js.gz +0 -0
- package/dist/dashboard/assets/settings-DNX9sydi.js.gz +0 -0
- package/dist/dashboard/assets/stack-BHTfXdz_.js.gz +0 -0
- package/dist/dashboard/assets/state-mPDRlQkl.js.gz +0 -0
- package/dist/dashboard/assets/switch-DOmJxg7H.js.gz +0 -0
- package/dist/dashboard/assets/useMutation-Dem0lXQj.js.gz +0 -0
- package/scripts/build/aot.ts +0 -143
- package/scripts/build/build.ts +0 -24
- package/scripts/build/compile.ts +0 -88
- package/scripts/build/restart.ts +0 -120
- package/scripts/build/start.ts +0 -25
- package/src/observability/event-compaction.ts +0 -317
- package/src/observability/event-day-index.ts +0 -440
|
@@ -1,73 +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
|
-
}
|
|
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
|
+
}
|
|
@@ -1,218 +1,218 @@
|
|
|
1
|
-
// Grok's two quota numbers, and why they disagree.
|
|
2
|
-
//
|
|
3
|
-
// The CLI plane answers every response with a token bucket:
|
|
4
|
-
//
|
|
5
|
-
// x-ratelimit-limit-tokens: 1000000
|
|
6
|
-
// x-ratelimit-remaining-tokens: 996169
|
|
7
|
-
// x-ratelimit-remaining-requests: 20
|
|
8
|
-
//
|
|
9
|
-
// and that bucket is *eventually consistent*. Right after a request it can
|
|
10
|
-
// still read a full `1000000` and only drop later. Worse, the figure it
|
|
11
|
-
// advertises is not the figure that binds: the real free allowance for a model
|
|
12
|
-
// like `grok-4.6` is 600k tokens per rolling 24 hours, and the only place
|
|
13
|
-
// upstream states it is the body of a 429:
|
|
14
|
-
//
|
|
15
|
-
// {"code":"subscription:free-usage-exhausted",
|
|
16
|
-
// "error":"... tokens (actual/limit): 603024/600000 ... Usage resets over a
|
|
17
|
-
// rolling 24-hour window"}
|
|
18
|
-
//
|
|
19
|
-
// So there are two readings with different authority. The header is available
|
|
20
|
-
// on every call and is the coarse indicator; the 429 body is only available
|
|
21
|
-
// once the account is actually being refused, and is the authoritative one.
|
|
22
|
-
//
|
|
23
|
-
// They are therefore kept as separate windows rather than merged: a merged
|
|
24
|
-
// number would look precise and be neither. The header window is listed first
|
|
25
|
-
// because it is what a caller sees day to day, and the authoritative one is
|
|
26
|
-
// appended beneath it.
|
|
27
|
-
|
|
28
|
-
import type { ProviderQuotaWindow } from "../../quota/quota-contracts";
|
|
29
|
-
import { isoDate, number, percentWindow, record, text } from "../../quota/quota-contracts";
|
|
30
|
-
|
|
31
|
-
/** Header names the CLI plane uses for its token bucket. */
|
|
32
|
-
const HEADER_LIMIT_TOKENS = "x-ratelimit-limit-tokens";
|
|
33
|
-
const HEADER_REMAINING_TOKENS = "x-ratelimit-remaining-tokens";
|
|
34
|
-
const HEADER_REMAINING_REQUESTS = "x-ratelimit-remaining-requests";
|
|
35
|
-
const HEADER_RESET_TOKENS = "x-ratelimit-reset-tokens";
|
|
36
|
-
|
|
37
|
-
/** The upstream code that carries the authoritative free-usage allowance. */
|
|
38
|
-
export const GROK_FREE_USAGE_EXHAUSTED = "subscription:free-usage-exhausted";
|
|
39
|
-
|
|
40
|
-
/** Kinds, so the view can tell an indicator from a verdict. */
|
|
41
|
-
export const GROK_HEADER_WINDOW_KIND = "grok:cli-header";
|
|
42
|
-
export const GROK_HEADER_REQUESTS_WINDOW_KIND = "grok:cli-header-requests";
|
|
43
|
-
export const GROK_AUTHORITATIVE_WINDOW_KIND = "grok:free-usage-authoritative";
|
|
44
|
-
|
|
45
|
-
/** The per-minute request cap the CLI plane applies. */
|
|
46
|
-
const REQUEST_WINDOW_LIMIT = 21;
|
|
47
|
-
|
|
48
|
-
/** One reading of the CLI token bucket. */
|
|
49
|
-
export interface GrokRateLimitReading {
|
|
50
|
-
readonly limitTokens: number | null;
|
|
51
|
-
readonly remainingTokens: number | null;
|
|
52
|
-
readonly remainingRequests: number | null;
|
|
53
|
-
readonly resetsAt: string | null;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/** A `Headers`-like reader, narrowed at the boundary rather than asserted. */
|
|
57
|
-
function headerReader(
|
|
58
|
-
headers: { get(name: string): string | null } | Record<string, string>,
|
|
59
|
-
): (name: string) => string | null {
|
|
60
|
-
if (typeof headers.get === "function") {
|
|
61
|
-
const reader = headers as { get(name: string): string | null };
|
|
62
|
-
return (name) => reader.get(name);
|
|
63
|
-
}
|
|
64
|
-
const literal = headers as Record<string, string>;
|
|
65
|
-
return (name) => literal[name] ?? null;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Reads the CLI token bucket off a response.
|
|
70
|
-
*
|
|
71
|
-
* `Headers` and a plain object both reach here — the quota probe gets the real
|
|
72
|
-
* thing, a test gets a literal — so both shapes are accepted rather than
|
|
73
|
-
* forcing one caller to adapt.
|
|
74
|
-
*/
|
|
75
|
-
export function readGrokRateLimitHeaders(
|
|
76
|
-
headers: { get(name: string): string | null } | Record<string, string>,
|
|
77
|
-
): GrokRateLimitReading {
|
|
78
|
-
const get = headerReader(headers);
|
|
79
|
-
const resetSeconds = number(get(HEADER_RESET_TOKENS));
|
|
80
|
-
return {
|
|
81
|
-
limitTokens: number(get(HEADER_LIMIT_TOKENS)),
|
|
82
|
-
remainingTokens: number(get(HEADER_REMAINING_TOKENS)),
|
|
83
|
-
remainingRequests: number(get(HEADER_REMAINING_REQUESTS)),
|
|
84
|
-
resetsAt:
|
|
85
|
-
isoDate(get(HEADER_RESET_TOKENS)) ??
|
|
86
|
-
(resetSeconds !== null && resetSeconds > 0
|
|
87
|
-
? new Date(Date.now() + resetSeconds * 1000).toISOString()
|
|
88
|
-
: null),
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* The header bucket as a quota window.
|
|
94
|
-
*
|
|
95
|
-
* Marked independent: this is the CLI request bucket, a different budget from
|
|
96
|
-
* the free-usage allowance, so it must never be summed into a credit total.
|
|
97
|
-
*/
|
|
98
|
-
export function grokHeaderWindow(reading: GrokRateLimitReading): ProviderQuotaWindow | null {
|
|
99
|
-
if (reading.remainingTokens === null && reading.limitTokens === null) return null;
|
|
100
|
-
const used =
|
|
101
|
-
reading.remainingTokens !== null && reading.limitTokens !== null
|
|
102
|
-
? reading.limitTokens - reading.remainingTokens
|
|
103
|
-
: null;
|
|
104
|
-
// The third argument is the *percentage*, not the count. Passing null left
|
|
105
|
-
// `usedPercent` null, and the bar renders a null percentage as grey — so a
|
|
106
|
-
// window that knew its exact used and limit still drew as unknown.
|
|
107
|
-
const usedPercent =
|
|
108
|
-
reading.limitTokens !== null && reading.limitTokens > 0 && used !== null
|
|
109
|
-
? (used / reading.limitTokens) * 100
|
|
110
|
-
: null;
|
|
111
|
-
return percentWindow(
|
|
112
|
-
GROK_HEADER_WINDOW_KIND,
|
|
113
|
-
"Rate limit token (header)",
|
|
114
|
-
usedPercent,
|
|
115
|
-
reading.resetsAt,
|
|
116
|
-
used,
|
|
117
|
-
reading.limitTokens,
|
|
118
|
-
"independent",
|
|
119
|
-
);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* The request bucket as its own window.
|
|
124
|
-
*
|
|
125
|
-
* Separate from the token bucket on purpose: `x-ratelimit-remaining-requests`
|
|
126
|
-
* is a per-minute cap (21) with no token equivalent, and folding it into the
|
|
127
|
-
* token window produced one number that described neither budget.
|
|
128
|
-
*/
|
|
129
|
-
export function grokHeaderRequestsWindow(
|
|
130
|
-
reading: GrokRateLimitReading,
|
|
131
|
-
): ProviderQuotaWindow | null {
|
|
132
|
-
if (reading.remainingRequests === null) return null;
|
|
133
|
-
const used = REQUEST_WINDOW_LIMIT - reading.remainingRequests;
|
|
134
|
-
const usedPercent = (used / REQUEST_WINDOW_LIMIT) * 100;
|
|
135
|
-
return percentWindow(
|
|
136
|
-
GROK_HEADER_REQUESTS_WINDOW_KIND,
|
|
137
|
-
"Rate limit request (header)",
|
|
138
|
-
usedPercent,
|
|
139
|
-
reading.resetsAt,
|
|
140
|
-
used,
|
|
141
|
-
REQUEST_WINDOW_LIMIT,
|
|
142
|
-
"independent",
|
|
143
|
-
);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/** The authoritative allowance a 429 states, parsed out of its message. */
|
|
147
|
-
export interface GrokFreeUsageVerdict {
|
|
148
|
-
readonly actual: number | null;
|
|
149
|
-
readonly limit: number | null;
|
|
150
|
-
readonly resetsAt: string | null;
|
|
151
|
-
readonly rolling: boolean;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Parses `tokens (actual/limit): 603024/600000` out of a refusal.
|
|
156
|
-
*
|
|
157
|
-
* The numbers are in prose, not in a field, so this is a text match — but the
|
|
158
|
-
* shape is the only place upstream publishes the real allowance, and a refusal
|
|
159
|
-
* that does not carry it yields `null` rather than a guess.
|
|
160
|
-
*/
|
|
161
|
-
export function parseGrokFreeUsageVerdict(body: unknown, message?: string): GrokFreeUsageVerdict | null {
|
|
162
|
-
const root = record(body) ?? {};
|
|
163
|
-
const error = text(root["error"] ?? root["message"]) ?? message ?? "";
|
|
164
|
-
const haystack = `${error} ${text(root["code"]) ?? ""}`;
|
|
165
|
-
const match = /tokens\s*\(actual\/limit\)\s*:?\s*([\d_,]+)\s*\/\s*([\d_,]+)/i.exec(haystack);
|
|
166
|
-
const actual = match?.[1] ? number(match[1].replaceAll(/[_,]/g, "")) : null;
|
|
167
|
-
const limit = match?.[2] ? number(match[2].replaceAll(/[_,]/g, "")) : null;
|
|
168
|
-
if (actual === null && limit === null) {
|
|
169
|
-
// A refusal without the figures still tells us the code; it just cannot
|
|
170
|
-
// tell us the allowance.
|
|
171
|
-
if (!haystack.toLowerCase().includes("free-usage") && !haystack.toLowerCase().includes("rolling 24")) return null;
|
|
172
|
-
}
|
|
173
|
-
const rolling = /rolling\s*24[\s-]*hour/i.test(haystack);
|
|
174
|
-
const resetMatch = /resets?\s+(?:over|in|at)\s+([^.;]{0,60})/i.exec(haystack);
|
|
175
|
-
return {
|
|
176
|
-
actual,
|
|
177
|
-
limit,
|
|
178
|
-
resetsAt: resetMatch ? isoDate(resetMatch[1]) ?? null : null,
|
|
179
|
-
rolling,
|
|
180
|
-
};
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* The authoritative allowance as a quota window.
|
|
185
|
-
*
|
|
186
|
-
* `remaining` is what decides whether the account can serve, so it is derived
|
|
187
|
-
* rather than left null: an account that has spent 603024 of 600000 has none
|
|
188
|
-
* left, and reporting "unknown" there would leave a refused account looking
|
|
189
|
-
* merely unmeasured.
|
|
190
|
-
*/
|
|
191
|
-
export function grokAuthoritativeWindow(verdict: GrokFreeUsageVerdict): ProviderQuotaWindow | null {
|
|
192
|
-
if (verdict.limit === null && verdict.actual === null) return null;
|
|
193
|
-
return percentWindow(
|
|
194
|
-
GROK_AUTHORITATIVE_WINDOW_KIND,
|
|
195
|
-
"Free usage allowance (authoritative)",
|
|
196
|
-
null,
|
|
197
|
-
verdict.resetsAt,
|
|
198
|
-
verdict.actual,
|
|
199
|
-
verdict.limit,
|
|
200
|
-
// Independent: the free allowance is per model per rolling day, not part of
|
|
201
|
-
// a pooled credit balance.
|
|
202
|
-
"independent",
|
|
203
|
-
);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/** One probe's two readings: the coarse header and the authoritative refusal. */
|
|
207
|
-
export interface GrokRateLimitProbe {
|
|
208
|
-
/** The CLI token bucket from the response headers. */
|
|
209
|
-
readonly header: ProviderQuotaWindow | null;
|
|
210
|
-
/** The per-minute request bucket, which has no token equivalent. */
|
|
211
|
-
readonly requests: ProviderQuotaWindow | null;
|
|
212
|
-
/** The free-usage allowance from a 429 body, or `null` when not refused. */
|
|
213
|
-
readonly authoritative: ProviderQuotaWindow | null;
|
|
214
|
-
/** Whether upstream refused the probe at all. */
|
|
215
|
-
readonly refusal: boolean;
|
|
216
|
-
/** The refusal code, for the console note. */
|
|
217
|
-
readonly note: string | null;
|
|
218
|
-
}
|
|
1
|
+
// Grok's two quota numbers, and why they disagree.
|
|
2
|
+
//
|
|
3
|
+
// The CLI plane answers every response with a token bucket:
|
|
4
|
+
//
|
|
5
|
+
// x-ratelimit-limit-tokens: 1000000
|
|
6
|
+
// x-ratelimit-remaining-tokens: 996169
|
|
7
|
+
// x-ratelimit-remaining-requests: 20
|
|
8
|
+
//
|
|
9
|
+
// and that bucket is *eventually consistent*. Right after a request it can
|
|
10
|
+
// still read a full `1000000` and only drop later. Worse, the figure it
|
|
11
|
+
// advertises is not the figure that binds: the real free allowance for a model
|
|
12
|
+
// like `grok-4.6` is 600k tokens per rolling 24 hours, and the only place
|
|
13
|
+
// upstream states it is the body of a 429:
|
|
14
|
+
//
|
|
15
|
+
// {"code":"subscription:free-usage-exhausted",
|
|
16
|
+
// "error":"... tokens (actual/limit): 603024/600000 ... Usage resets over a
|
|
17
|
+
// rolling 24-hour window"}
|
|
18
|
+
//
|
|
19
|
+
// So there are two readings with different authority. The header is available
|
|
20
|
+
// on every call and is the coarse indicator; the 429 body is only available
|
|
21
|
+
// once the account is actually being refused, and is the authoritative one.
|
|
22
|
+
//
|
|
23
|
+
// They are therefore kept as separate windows rather than merged: a merged
|
|
24
|
+
// number would look precise and be neither. The header window is listed first
|
|
25
|
+
// because it is what a caller sees day to day, and the authoritative one is
|
|
26
|
+
// appended beneath it.
|
|
27
|
+
|
|
28
|
+
import type { ProviderQuotaWindow } from "../../quota/quota-contracts";
|
|
29
|
+
import { isoDate, number, percentWindow, record, text } from "../../quota/quota-contracts";
|
|
30
|
+
|
|
31
|
+
/** Header names the CLI plane uses for its token bucket. */
|
|
32
|
+
const HEADER_LIMIT_TOKENS = "x-ratelimit-limit-tokens";
|
|
33
|
+
const HEADER_REMAINING_TOKENS = "x-ratelimit-remaining-tokens";
|
|
34
|
+
const HEADER_REMAINING_REQUESTS = "x-ratelimit-remaining-requests";
|
|
35
|
+
const HEADER_RESET_TOKENS = "x-ratelimit-reset-tokens";
|
|
36
|
+
|
|
37
|
+
/** The upstream code that carries the authoritative free-usage allowance. */
|
|
38
|
+
export const GROK_FREE_USAGE_EXHAUSTED = "subscription:free-usage-exhausted";
|
|
39
|
+
|
|
40
|
+
/** Kinds, so the view can tell an indicator from a verdict. */
|
|
41
|
+
export const GROK_HEADER_WINDOW_KIND = "grok:cli-header";
|
|
42
|
+
export const GROK_HEADER_REQUESTS_WINDOW_KIND = "grok:cli-header-requests";
|
|
43
|
+
export const GROK_AUTHORITATIVE_WINDOW_KIND = "grok:free-usage-authoritative";
|
|
44
|
+
|
|
45
|
+
/** The per-minute request cap the CLI plane applies. */
|
|
46
|
+
const REQUEST_WINDOW_LIMIT = 21;
|
|
47
|
+
|
|
48
|
+
/** One reading of the CLI token bucket. */
|
|
49
|
+
export interface GrokRateLimitReading {
|
|
50
|
+
readonly limitTokens: number | null;
|
|
51
|
+
readonly remainingTokens: number | null;
|
|
52
|
+
readonly remainingRequests: number | null;
|
|
53
|
+
readonly resetsAt: string | null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** A `Headers`-like reader, narrowed at the boundary rather than asserted. */
|
|
57
|
+
function headerReader(
|
|
58
|
+
headers: { get(name: string): string | null } | Record<string, string>,
|
|
59
|
+
): (name: string) => string | null {
|
|
60
|
+
if (typeof headers.get === "function") {
|
|
61
|
+
const reader = headers as { get(name: string): string | null };
|
|
62
|
+
return (name) => reader.get(name);
|
|
63
|
+
}
|
|
64
|
+
const literal = headers as Record<string, string>;
|
|
65
|
+
return (name) => literal[name] ?? null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Reads the CLI token bucket off a response.
|
|
70
|
+
*
|
|
71
|
+
* `Headers` and a plain object both reach here — the quota probe gets the real
|
|
72
|
+
* thing, a test gets a literal — so both shapes are accepted rather than
|
|
73
|
+
* forcing one caller to adapt.
|
|
74
|
+
*/
|
|
75
|
+
export function readGrokRateLimitHeaders(
|
|
76
|
+
headers: { get(name: string): string | null } | Record<string, string>,
|
|
77
|
+
): GrokRateLimitReading {
|
|
78
|
+
const get = headerReader(headers);
|
|
79
|
+
const resetSeconds = number(get(HEADER_RESET_TOKENS));
|
|
80
|
+
return {
|
|
81
|
+
limitTokens: number(get(HEADER_LIMIT_TOKENS)),
|
|
82
|
+
remainingTokens: number(get(HEADER_REMAINING_TOKENS)),
|
|
83
|
+
remainingRequests: number(get(HEADER_REMAINING_REQUESTS)),
|
|
84
|
+
resetsAt:
|
|
85
|
+
isoDate(get(HEADER_RESET_TOKENS)) ??
|
|
86
|
+
(resetSeconds !== null && resetSeconds > 0
|
|
87
|
+
? new Date(Date.now() + resetSeconds * 1000).toISOString()
|
|
88
|
+
: null),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* The header bucket as a quota window.
|
|
94
|
+
*
|
|
95
|
+
* Marked independent: this is the CLI request bucket, a different budget from
|
|
96
|
+
* the free-usage allowance, so it must never be summed into a credit total.
|
|
97
|
+
*/
|
|
98
|
+
export function grokHeaderWindow(reading: GrokRateLimitReading): ProviderQuotaWindow | null {
|
|
99
|
+
if (reading.remainingTokens === null && reading.limitTokens === null) return null;
|
|
100
|
+
const used =
|
|
101
|
+
reading.remainingTokens !== null && reading.limitTokens !== null
|
|
102
|
+
? reading.limitTokens - reading.remainingTokens
|
|
103
|
+
: null;
|
|
104
|
+
// The third argument is the *percentage*, not the count. Passing null left
|
|
105
|
+
// `usedPercent` null, and the bar renders a null percentage as grey — so a
|
|
106
|
+
// window that knew its exact used and limit still drew as unknown.
|
|
107
|
+
const usedPercent =
|
|
108
|
+
reading.limitTokens !== null && reading.limitTokens > 0 && used !== null
|
|
109
|
+
? (used / reading.limitTokens) * 100
|
|
110
|
+
: null;
|
|
111
|
+
return percentWindow(
|
|
112
|
+
GROK_HEADER_WINDOW_KIND,
|
|
113
|
+
"Rate limit token (header)",
|
|
114
|
+
usedPercent,
|
|
115
|
+
reading.resetsAt,
|
|
116
|
+
used,
|
|
117
|
+
reading.limitTokens,
|
|
118
|
+
"independent",
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* The request bucket as its own window.
|
|
124
|
+
*
|
|
125
|
+
* Separate from the token bucket on purpose: `x-ratelimit-remaining-requests`
|
|
126
|
+
* is a per-minute cap (21) with no token equivalent, and folding it into the
|
|
127
|
+
* token window produced one number that described neither budget.
|
|
128
|
+
*/
|
|
129
|
+
export function grokHeaderRequestsWindow(
|
|
130
|
+
reading: GrokRateLimitReading,
|
|
131
|
+
): ProviderQuotaWindow | null {
|
|
132
|
+
if (reading.remainingRequests === null) return null;
|
|
133
|
+
const used = REQUEST_WINDOW_LIMIT - reading.remainingRequests;
|
|
134
|
+
const usedPercent = (used / REQUEST_WINDOW_LIMIT) * 100;
|
|
135
|
+
return percentWindow(
|
|
136
|
+
GROK_HEADER_REQUESTS_WINDOW_KIND,
|
|
137
|
+
"Rate limit request (header)",
|
|
138
|
+
usedPercent,
|
|
139
|
+
reading.resetsAt,
|
|
140
|
+
used,
|
|
141
|
+
REQUEST_WINDOW_LIMIT,
|
|
142
|
+
"independent",
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** The authoritative allowance a 429 states, parsed out of its message. */
|
|
147
|
+
export interface GrokFreeUsageVerdict {
|
|
148
|
+
readonly actual: number | null;
|
|
149
|
+
readonly limit: number | null;
|
|
150
|
+
readonly resetsAt: string | null;
|
|
151
|
+
readonly rolling: boolean;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Parses `tokens (actual/limit): 603024/600000` out of a refusal.
|
|
156
|
+
*
|
|
157
|
+
* The numbers are in prose, not in a field, so this is a text match — but the
|
|
158
|
+
* shape is the only place upstream publishes the real allowance, and a refusal
|
|
159
|
+
* that does not carry it yields `null` rather than a guess.
|
|
160
|
+
*/
|
|
161
|
+
export function parseGrokFreeUsageVerdict(body: unknown, message?: string): GrokFreeUsageVerdict | null {
|
|
162
|
+
const root = record(body) ?? {};
|
|
163
|
+
const error = text(root["error"] ?? root["message"]) ?? message ?? "";
|
|
164
|
+
const haystack = `${error} ${text(root["code"]) ?? ""}`;
|
|
165
|
+
const match = /tokens\s*\(actual\/limit\)\s*:?\s*([\d_,]+)\s*\/\s*([\d_,]+)/i.exec(haystack);
|
|
166
|
+
const actual = match?.[1] ? number(match[1].replaceAll(/[_,]/g, "")) : null;
|
|
167
|
+
const limit = match?.[2] ? number(match[2].replaceAll(/[_,]/g, "")) : null;
|
|
168
|
+
if (actual === null && limit === null) {
|
|
169
|
+
// A refusal without the figures still tells us the code; it just cannot
|
|
170
|
+
// tell us the allowance.
|
|
171
|
+
if (!haystack.toLowerCase().includes("free-usage") && !haystack.toLowerCase().includes("rolling 24")) return null;
|
|
172
|
+
}
|
|
173
|
+
const rolling = /rolling\s*24[\s-]*hour/i.test(haystack);
|
|
174
|
+
const resetMatch = /resets?\s+(?:over|in|at)\s+([^.;]{0,60})/i.exec(haystack);
|
|
175
|
+
return {
|
|
176
|
+
actual,
|
|
177
|
+
limit,
|
|
178
|
+
resetsAt: resetMatch ? isoDate(resetMatch[1]) ?? null : null,
|
|
179
|
+
rolling,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* The authoritative allowance as a quota window.
|
|
185
|
+
*
|
|
186
|
+
* `remaining` is what decides whether the account can serve, so it is derived
|
|
187
|
+
* rather than left null: an account that has spent 603024 of 600000 has none
|
|
188
|
+
* left, and reporting "unknown" there would leave a refused account looking
|
|
189
|
+
* merely unmeasured.
|
|
190
|
+
*/
|
|
191
|
+
export function grokAuthoritativeWindow(verdict: GrokFreeUsageVerdict): ProviderQuotaWindow | null {
|
|
192
|
+
if (verdict.limit === null && verdict.actual === null) return null;
|
|
193
|
+
return percentWindow(
|
|
194
|
+
GROK_AUTHORITATIVE_WINDOW_KIND,
|
|
195
|
+
"Free usage allowance (authoritative)",
|
|
196
|
+
null,
|
|
197
|
+
verdict.resetsAt,
|
|
198
|
+
verdict.actual,
|
|
199
|
+
verdict.limit,
|
|
200
|
+
// Independent: the free allowance is per model per rolling day, not part of
|
|
201
|
+
// a pooled credit balance.
|
|
202
|
+
"independent",
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** One probe's two readings: the coarse header and the authoritative refusal. */
|
|
207
|
+
export interface GrokRateLimitProbe {
|
|
208
|
+
/** The CLI token bucket from the response headers. */
|
|
209
|
+
readonly header: ProviderQuotaWindow | null;
|
|
210
|
+
/** The per-minute request bucket, which has no token equivalent. */
|
|
211
|
+
readonly requests: ProviderQuotaWindow | null;
|
|
212
|
+
/** The free-usage allowance from a 429 body, or `null` when not refused. */
|
|
213
|
+
readonly authoritative: ProviderQuotaWindow | null;
|
|
214
|
+
/** Whether upstream refused the probe at all. */
|
|
215
|
+
readonly refusal: boolean;
|
|
216
|
+
/** The refusal code, for the console note. */
|
|
217
|
+
readonly note: string | null;
|
|
218
|
+
}
|
|
@@ -92,7 +92,13 @@ export const HERMES_MODELS: readonly ModelDefinition[] = [
|
|
|
92
92
|
defineModel({ id: "stepfun/step-3.7-flash:free", ctx: 262144, out: 256000, vision: true, reasoning: true, free: true, freeTier: true }),
|
|
93
93
|
defineModel({ id: "upstage/solar-mini4:free", ctx: 524288, out: 131072, reasoning: true, free: true, freeTier: true }),
|
|
94
94
|
defineModel({ id: "reflection/beam-1.0-501b-a23b:free", ctx: 262144, out: 131072, reasoning: true, free: true, freeTier: true }),
|
|
95
|
-
|
|
95
|
+
// Vision-corrected against the live catalog: `architecture.input_modalities`
|
|
96
|
+
// lists `image` for this row, so it was published without vision it has.
|
|
97
|
+
defineModel({ id: "meituan/longcat-2.5-preview:free", ctx: 1048576, out: 131072, vision: true, reasoning: true, free: true, freeTier: true }),
|
|
98
|
+
// Added after a live read: the catalog carries it, it is free, and a dispatch
|
|
99
|
+
// answers 200 — it was simply absent from this list, so a client asking for
|
|
100
|
+
// it resolved to nothing. 262144 out is the catalog's own cap.
|
|
101
|
+
defineModel({ id: "ornith/ornith-1.5-122b:free", ctx: 262144, out: 262144, vision: true, reasoning: true, free: true, freeTier: true }),
|
|
96
102
|
];
|
|
97
103
|
|
|
98
104
|
|