cartethyia 2.5.4 → 2.5.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (395) hide show
  1. package/.env.example +13 -4
  2. package/README.md +2 -2
  3. package/dist/dashboard/assets/{BannedPage-C2orBsI2.js → BannedPage-DE2607Tb.js} +1 -1
  4. package/dist/dashboard/assets/BannedPage-DE2607Tb.js.gz +0 -0
  5. package/dist/dashboard/assets/CliToolDetailPage-Duv6mfrl.js +1 -0
  6. package/dist/dashboard/assets/CliToolDetailPage-Duv6mfrl.js.gz +0 -0
  7. package/dist/dashboard/assets/{CliToolsPage-BV6pxVQo.js → CliToolsPage-C2PNYWzk.js} +1 -1
  8. package/dist/dashboard/assets/CliToolsPage-C2PNYWzk.js.gz +0 -0
  9. package/dist/dashboard/assets/CombosPage-TXP9OfAM.js +1 -0
  10. package/dist/dashboard/assets/CombosPage-TXP9OfAM.js.gz +0 -0
  11. package/dist/dashboard/assets/{ConsoleLogPage-CW-wPmJ6.js → ConsoleLogPage-B-9xnAQN.js} +1 -1
  12. package/dist/dashboard/assets/ConsoleLogPage-B-9xnAQN.js.gz +0 -0
  13. package/dist/dashboard/assets/{CustomizationPage-0p8uX7vB.js → CustomizationPage-xeax9nJt.js} +1 -1
  14. package/dist/dashboard/assets/CustomizationPage-xeax9nJt.js.gz +0 -0
  15. package/dist/dashboard/assets/{HealthEventsModal-BjqSMfko.js → HealthEventsModal-Df4jjaHJ.js} +1 -1
  16. package/dist/dashboard/assets/HealthEventsModal-Df4jjaHJ.js.gz +0 -0
  17. package/dist/dashboard/assets/{LoginPage-BvzCx8Kr.js → LoginPage-Dyojsm84.js} +1 -1
  18. package/dist/dashboard/assets/LoginPage-Dyojsm84.js.gz +0 -0
  19. package/dist/dashboard/assets/ModelPicker-C7pDb41R.js +1 -0
  20. package/dist/dashboard/assets/ModelPicker-C7pDb41R.js.gz +0 -0
  21. package/dist/dashboard/assets/OverviewPage-BJfg4ow6.js +1 -0
  22. package/dist/dashboard/assets/OverviewPage-BJfg4ow6.js.gz +0 -0
  23. package/dist/dashboard/assets/ProviderDetailPage-ChCX1gmT.js +3 -0
  24. package/dist/dashboard/assets/ProviderDetailPage-ChCX1gmT.js.gz +0 -0
  25. package/dist/dashboard/assets/{ProvidersPage-Js9jUPSb.js → ProvidersPage-DeWSOEUO.js} +1 -1
  26. package/dist/dashboard/assets/ProvidersPage-DeWSOEUO.js.gz +0 -0
  27. package/dist/dashboard/assets/ProxyPage-CkpWln2S.js +7 -0
  28. package/dist/dashboard/assets/ProxyPage-CkpWln2S.js.gz +0 -0
  29. package/dist/dashboard/assets/QuotaPage-B3VOgQmt.js +1 -0
  30. package/dist/dashboard/assets/QuotaPage-B3VOgQmt.js.gz +0 -0
  31. package/dist/dashboard/assets/SettingsPage-C_klIZom.js +1 -0
  32. package/dist/dashboard/assets/SettingsPage-C_klIZom.js.gz +0 -0
  33. package/dist/dashboard/assets/{SetupPage-DDbMQsoo.js → SetupPage-BYph-InK.js} +1 -1
  34. package/dist/dashboard/assets/SetupPage-BYph-InK.js.gz +0 -0
  35. package/dist/dashboard/assets/{SortableList-CB3Z7VTf.js → SortableList-CvyQCYWJ.js} +1 -1
  36. package/dist/dashboard/assets/SortableList-CvyQCYWJ.js.gz +0 -0
  37. package/dist/dashboard/assets/{StudioPage-D95UCPHI.js → StudioPage-Cm1JYilI.js} +3 -3
  38. package/dist/dashboard/assets/StudioPage-Cm1JYilI.js.gz +0 -0
  39. package/dist/dashboard/assets/UsagePage-DlFnc3_S.js +56 -0
  40. package/dist/dashboard/assets/UsagePage-DlFnc3_S.js.gz +0 -0
  41. package/dist/dashboard/assets/api-keys-DQYLQxpW.js +1 -0
  42. package/dist/dashboard/assets/api-keys-DQYLQxpW.js.gz +0 -0
  43. package/dist/dashboard/assets/app-CX_Z8cMs.css +2 -0
  44. package/dist/dashboard/assets/app-CX_Z8cMs.css.gz +0 -0
  45. package/dist/dashboard/assets/{assert-Chi7pCnt.js → assert-C9FD_JwJ.js} +1 -1
  46. package/dist/dashboard/assets/assert-C9FD_JwJ.js.gz +0 -0
  47. package/dist/dashboard/assets/{badge-D4pQapKj.js → badge-CR72dasT.js} +1 -1
  48. package/dist/dashboard/assets/badge-CR72dasT.js.gz +0 -0
  49. package/dist/dashboard/assets/{banner-DH6cg-_B.js → banner-CI9_eSfO.js} +1 -1
  50. package/dist/dashboard/assets/banner-CI9_eSfO.js.gz +0 -0
  51. package/dist/dashboard/assets/{card-BKqlUOe4.js → card-DS7CQEsO.js} +1 -1
  52. package/dist/dashboard/assets/card-DS7CQEsO.js.gz +0 -0
  53. package/dist/dashboard/assets/{cli-tools-fTgljTGA.js → cli-tools-CNqzrfdb.js} +1 -1
  54. package/dist/dashboard/assets/cli-tools-CNqzrfdb.js.gz +0 -0
  55. package/dist/dashboard/assets/{clipboard-button-D5Ys_sM0.js → clipboard-button-611pSsaX.js} +1 -1
  56. package/dist/dashboard/assets/clipboard-button-611pSsaX.js.gz +0 -0
  57. package/dist/dashboard/assets/{fingerprint-pattern-BWuHtbW4.js → concurrency-BDF0ok7y.js} +1 -1
  58. package/dist/dashboard/assets/concurrency-BDF0ok7y.js.gz +0 -0
  59. package/dist/dashboard/assets/dialog-k37exjNt.js +1 -0
  60. package/dist/dashboard/assets/dialog-k37exjNt.js.gz +0 -0
  61. package/dist/dashboard/assets/drawer-CX7N8ucR.js +1 -0
  62. package/dist/dashboard/assets/drawer-CX7N8ucR.js.gz +0 -0
  63. package/dist/dashboard/assets/{entry-CQBr6U9c.js → entry-B9Xvtnfj.js} +1 -1
  64. package/dist/dashboard/assets/entry-B9Xvtnfj.js.gz +0 -0
  65. package/dist/dashboard/assets/entry-Bog0p925.js +4 -0
  66. package/dist/dashboard/assets/entry-Bog0p925.js.gz +0 -0
  67. package/dist/dashboard/assets/entry-CTP8AyiS.js +1 -0
  68. package/dist/dashboard/assets/entry-CTP8AyiS.js.gz +0 -0
  69. package/dist/dashboard/assets/{github-badge-BlptL8Uf.js → github-badge-sltk1qUi.js} +1 -1
  70. package/dist/dashboard/assets/github-badge-sltk1qUi.js.gz +0 -0
  71. package/dist/dashboard/assets/{index-js6TJvdr.js → index-CZQIrdoX.js} +2 -2
  72. package/dist/dashboard/assets/index-CZQIrdoX.js.gz +0 -0
  73. package/dist/dashboard/assets/{inline-DUWDSmDV.js → inline-BhY2sSnO.js} +1 -1
  74. package/dist/dashboard/assets/inline-BhY2sSnO.js.gz +0 -0
  75. package/dist/dashboard/assets/{input-LVjgZq4f.js → input-V1umFiBk.js} +1 -1
  76. package/dist/dashboard/assets/input-V1umFiBk.js.gz +0 -0
  77. package/dist/dashboard/assets/layout-BBOYycyS.js +1 -0
  78. package/dist/dashboard/assets/layout-BBOYycyS.js.gz +0 -0
  79. package/dist/dashboard/assets/{list-row-DupvLfDx.js → list-row-1zzZYM0B.js} +1 -1
  80. package/dist/dashboard/assets/list-row-1zzZYM0B.js.gz +0 -0
  81. package/dist/dashboard/assets/{live-C4pEJUqI.js → live-BivGfuUs.js} +1 -1
  82. package/dist/dashboard/assets/live-BivGfuUs.js.gz +0 -0
  83. package/dist/dashboard/assets/{model-bans-BRiuufkB.js → model-bans-CmffjLuG.js} +1 -1
  84. package/dist/dashboard/assets/model-bans-CmffjLuG.js.gz +0 -0
  85. package/dist/dashboard/assets/{mutation-B4ROwk8L.js → mutation-46P0oWMe.js} +1 -1
  86. package/dist/dashboard/assets/mutation-46P0oWMe.js.gz +0 -0
  87. package/dist/dashboard/assets/numbers-DkfMR5Qs.js +2 -0
  88. package/dist/dashboard/assets/numbers-DkfMR5Qs.js.gz +0 -0
  89. package/dist/dashboard/assets/{query-keys-gmjzC3RS.js → query-keys-BFhGpwR1.js} +1 -1
  90. package/dist/dashboard/assets/query-keys-BFhGpwR1.js.gz +0 -0
  91. package/dist/dashboard/assets/{routing-C7tBT5o-.js → routing-CAb4F37R.js} +1 -1
  92. package/dist/dashboard/assets/routing-CAb4F37R.js.gz +0 -0
  93. package/dist/dashboard/assets/{select-WUCXorXw.js → select-BPpSsBgL.js} +1 -1
  94. package/dist/dashboard/assets/select-BPpSsBgL.js.gz +0 -0
  95. package/dist/dashboard/assets/{settings-C_zKl1GS.js → settings-DNX9sydi.js} +1 -1
  96. package/dist/dashboard/assets/settings-DNX9sydi.js.gz +0 -0
  97. package/dist/dashboard/assets/{stack-_3O0F8Xn.js → stack-BHTfXdz_.js} +1 -1
  98. package/dist/dashboard/assets/stack-BHTfXdz_.js.gz +0 -0
  99. package/dist/dashboard/assets/{state-BgTxHcJS.js → state-mPDRlQkl.js} +1 -1
  100. package/dist/dashboard/assets/state-mPDRlQkl.js.gz +0 -0
  101. package/dist/dashboard/assets/{switch-DEjTtOWf.js → switch-DOmJxg7H.js} +1 -1
  102. package/dist/dashboard/assets/switch-DOmJxg7H.js.gz +0 -0
  103. package/dist/dashboard/assets/tokens-COzario9.js +1 -0
  104. package/dist/dashboard/assets/tokens-COzario9.js.gz +0 -0
  105. package/dist/dashboard/assets/use-modal-focus-By7oJoGd.js +1 -0
  106. package/dist/dashboard/assets/use-modal-focus-By7oJoGd.js.gz +0 -0
  107. package/dist/dashboard/assets/{useMutation-BWSC0UAS.js → useMutation-Dem0lXQj.js} +1 -1
  108. package/dist/dashboard/assets/useMutation-Dem0lXQj.js.gz +0 -0
  109. package/dist/dashboard/index.html +1 -1
  110. package/dist/dashboard/index.html.gz +0 -0
  111. package/dist/dashboard/precompressed-assets.json +54 -54
  112. package/migrations/README.md +52 -52
  113. package/migrations/lite-sqlite3/0000_baseline.sql +2 -2
  114. package/package.json +95 -95
  115. package/scripts/cli/admin.ts +125 -0
  116. package/scripts/cli/{ui/background.ts → background.ts} +1 -1
  117. package/scripts/cli/{command/console-menu.ts → console-menu.ts} +275 -244
  118. package/scripts/cli/doctor.ts +37 -0
  119. package/scripts/cli/{support/env.ts → env.ts} +99 -99
  120. package/scripts/cli/{gateway/spawn.ts → gateway.ts} +112 -24
  121. package/scripts/cli/instance.ts +224 -0
  122. package/scripts/cli/main.ts +61 -18
  123. package/scripts/cli/{support/platform.ts → platform.ts} +64 -64
  124. package/scripts/cli/port.ts +100 -0
  125. package/scripts/cli/{support/services.ts → services.ts} +88 -88
  126. package/scripts/cli/{command/setup.ts → setup.ts} +35 -34
  127. package/scripts/cli/{command/start.ts → start.ts} +10 -5
  128. package/scripts/cli/{support/startup-signal.ts → startup-signal.ts} +23 -23
  129. package/scripts/cli/tray/index.ts +62 -12
  130. package/scripts/cli/{command/tray.ts → tray.ts} +208 -208
  131. package/scripts/cli/{command/update.ts → update.ts} +39 -39
  132. package/scripts/cli/version.ts +67 -0
  133. package/scripts/cli/{ui/wordmark.ts → wordmark.ts} +58 -58
  134. package/src/boot/app.ts +697 -693
  135. package/src/boot/dependencies.ts +647 -646
  136. package/src/boot/timeout.ts +2 -13
  137. package/src/config/cooldowns.ts +186 -138
  138. package/src/config/network.ts +8 -14
  139. package/src/config/providers.ts +7 -10
  140. package/src/config/runtime.ts +76 -76
  141. package/src/config/security.ts +15 -0
  142. package/src/config/spec.ts +20 -30
  143. package/src/config/telemetry.ts +0 -20
  144. package/src/config/timeouts.ts +0 -6
  145. package/src/console/admin/model-abuse/contracts.ts +28 -6
  146. package/src/console/admin/model-abuse/routes.ts +56 -3
  147. package/src/console/api-keys/contracts.ts +410 -414
  148. package/src/console/api-keys/routes.ts +881 -880
  149. package/src/console/auth/credentials.ts +3 -3
  150. package/src/console/auth/setup-service.ts +2 -2
  151. package/src/console/backup/export-endpoint.ts +6 -2
  152. package/src/console/backup/nine-router.ts +2 -5
  153. package/src/console/backup/service.ts +4 -0
  154. package/src/console/backup/store.ts +192 -29
  155. package/src/console/backup/validate.ts +3 -45
  156. package/src/console/cli-tools/fs-ops.ts +487 -487
  157. package/src/console/console-router-guards.ts +1 -1
  158. package/src/console/data-management/contracts.ts +13 -6
  159. package/src/console/data-management/operations.ts +23 -18
  160. package/src/console/data-management/routes.ts +0 -1
  161. package/src/console/domain-registration.ts +10 -5
  162. package/src/console/observability/live.ts +22 -0
  163. package/src/console/observability/store.ts +39 -16
  164. package/src/console/observability/usage-dimensions.ts +4 -4
  165. package/src/console/providers/catalog/catalog-projections.ts +6 -7
  166. package/src/console/providers/catalog/model-operations.ts +5 -2
  167. package/src/console/providers/catalog/provider-operations.ts +4 -4
  168. package/src/console/providers/catalog/public-model-store.ts +8 -11
  169. package/src/console/providers/catalog/store.ts +1553 -1553
  170. package/src/console/providers/oauth/routes.ts +8 -12
  171. package/src/console/quota/account-quota-tenant.ts +6 -6
  172. package/src/console/quota/quota-view.ts +1 -1
  173. package/src/console/remote-support/contracts.ts +61 -0
  174. package/src/console/remote-support/routes.ts +65 -0
  175. package/src/console/remote-support/service.ts +207 -0
  176. package/src/console/response-compression.ts +0 -3
  177. package/src/console/routing/pools/probe-result.ts +3 -2
  178. package/src/console/routing/pools/probe-verdict.ts +132 -132
  179. package/src/console/routing/pools/store.ts +8 -8
  180. package/src/console/settings/contracts.ts +58 -2
  181. package/src/console/settings/store.ts +30 -3
  182. package/src/console/shared/cursor.ts +8 -8
  183. package/src/console/shared/errors.ts +1 -3
  184. package/src/network/http2-bidi.ts +2 -4
  185. package/src/network/pool/agent.ts +478 -478
  186. package/src/network/pool/selector.ts +5 -4
  187. package/src/network/retry.ts +1 -2
  188. package/src/network/ssrf.ts +5 -4
  189. package/src/observability/event-compaction.ts +80 -78
  190. package/src/observability/event-day-index.ts +13 -15
  191. package/src/observability/event-log.ts +4 -4
  192. package/src/observability/logger.ts +6 -6
  193. package/src/observability/payload-capture.ts +151 -33
  194. package/src/observability/payload-store.ts +397 -440
  195. package/src/observability/redaction.ts +5 -6
  196. package/src/observability/request-notify.ts +43 -0
  197. package/src/observability/telemetry-status.ts +8 -9
  198. package/src/observability/token-speed.ts +5 -4
  199. package/src/persistence/db-mode.ts +7 -7
  200. package/src/persistence/readiness.ts +1 -1
  201. package/src/persistence/schema.ts +1406 -1402
  202. package/src/persistence/shared-cache.ts +1 -2
  203. package/src/persistence/stores/api-key-store.ts +18 -6
  204. package/src/persistence/stores/share-store.ts +4 -3
  205. package/src/persistence/stores/telemetry-store.ts +236 -237
  206. package/src/protocol/canonical-model.ts +11 -3
  207. package/src/protocol/codecs.ts +138 -4
  208. package/src/protocol/gateway-error.ts +6 -4
  209. package/src/protocol/request/chat.ts +21 -1
  210. package/src/protocol/request/codex.ts +10 -1
  211. package/src/protocol/request/messages.ts +4 -5
  212. package/src/protocol/request/responses.ts +9 -1
  213. package/src/protocol/response/chat.ts +34 -4
  214. package/src/protocol/response/codex.ts +7 -4
  215. package/src/protocol/response/dsml.ts +140 -28
  216. package/src/protocol/response/responses.ts +11 -5
  217. package/src/protocol/stream-error-frames.ts +4 -5
  218. package/src/providers/authentication/jwt-validator.ts +9 -10
  219. package/src/providers/authentication/oauth-refresh-service.ts +9 -11
  220. package/src/providers/configured-openai-providers.ts +134 -136
  221. package/src/providers/discovery/catalog/catalog-blob.ts +18 -18
  222. package/src/providers/discovery/catalog/catalog-format.ts +3 -6
  223. package/src/providers/discovery/models-dev-catalog.ts +354 -354
  224. package/src/providers/discovery/probe-phases.ts +4 -4
  225. package/src/providers/discovery/probe-wire.ts +6 -13
  226. package/src/providers/discovery/probing-service.ts +1117 -1045
  227. package/src/providers/integrations/alibabacp/alibabacp.ts +1 -1
  228. package/src/providers/integrations/antigravity/antigravity-quota.ts +5 -7
  229. package/src/providers/integrations/buddy/buddy-chat-shared.ts +23 -2
  230. package/src/providers/integrations/buddy/buddy-checkin.ts +1 -4
  231. package/src/providers/integrations/buddy/buddy-cli-headers.ts +23 -6
  232. package/src/providers/integrations/buddy/buddy-conversation-headers.ts +0 -1
  233. package/src/providers/integrations/buddy/buddy-identity.ts +52 -0
  234. package/src/providers/integrations/buddy/buddy-media.ts +2 -2
  235. package/src/providers/integrations/buddy/buddy-usage-store.ts +3 -4
  236. package/src/providers/integrations/buddy/codebuddy/quota.ts +2 -13
  237. package/src/providers/integrations/buddy/codebuddy/shared.ts +118 -115
  238. package/src/providers/integrations/buddy/workbuddy/quota.ts +0 -11
  239. package/src/providers/integrations/buddy/workbuddy/shared.ts +5 -2
  240. package/src/providers/integrations/claude/claude.ts +608 -608
  241. package/src/providers/integrations/cline/cline-quota.ts +132 -132
  242. package/src/providers/integrations/cline/cline.ts +537 -539
  243. package/src/providers/integrations/codex/codex-oauth.ts +1 -1
  244. package/src/providers/integrations/commandcode/commandcode.ts +674 -675
  245. package/src/providers/integrations/commandcode/discovery.ts +117 -116
  246. package/src/providers/integrations/cursor/cursor-auth.ts +418 -418
  247. package/src/providers/integrations/cursor/cursor-quota.ts +309 -310
  248. package/src/providers/integrations/cursor/cursor.ts +11 -163
  249. package/src/providers/integrations/factory/factory-models.ts +3 -5
  250. package/src/providers/integrations/grok/grok-install-id.ts +2 -3
  251. package/src/providers/integrations/grok/grok.ts +6 -7
  252. package/src/providers/integrations/hermes.ts +14 -18
  253. package/src/providers/integrations/kiro/kiro-discovery.ts +1 -1
  254. package/src/providers/integrations/kiro/kiro.ts +1 -1
  255. package/src/providers/integrations/mistral.ts +6 -6
  256. package/src/providers/integrations/opencode/fingerprint.ts +3 -3
  257. package/src/providers/integrations/opencode/provider.ts +8 -8
  258. package/src/providers/integrations/qoder/qoder-catalog.ts +450 -450
  259. package/src/providers/integrations/qoder/qoder-oauth.ts +388 -310
  260. package/src/providers/integrations/qoder/qoder.ts +884 -865
  261. package/src/providers/integrations/shared/connect.ts +6 -6
  262. package/src/providers/integrations/trae/trae-catalog.ts +1 -2
  263. package/src/providers/integrations/trae/trae.ts +2 -2
  264. package/src/providers/integrations/zcode/oauth.ts +6 -1
  265. package/src/providers/operations/account-health-service.ts +1528 -1523
  266. package/src/providers/operations/account-reset-service.ts +1 -1
  267. package/src/providers/operations/client-versions.ts +6 -6
  268. package/src/providers/operations/custom-cli-headers.ts +3 -4
  269. package/src/providers/operations/provider-catalog-seeder.ts +95 -95
  270. package/src/providers/operations/provider-catalog-service.ts +3 -4
  271. package/src/providers/provider-metadata.ts +4 -8
  272. package/src/providers/provider-registry.ts +6 -7
  273. package/src/providers/provider-usage.ts +1 -1
  274. package/src/security/abuse.ts +15 -10
  275. package/src/security/access-control.ts +144 -146
  276. package/src/security/admission/contracts.ts +11 -0
  277. package/src/security/admission/in-memory-store.ts +4 -3
  278. package/src/security/admission/lease-sweep.ts +6 -6
  279. package/src/security/admission/reasons.ts +5 -1
  280. package/src/security/admission/redis-store.ts +211 -210
  281. package/src/security/admission/service.ts +4 -0
  282. package/src/security/api-key-auth.ts +504 -504
  283. package/src/security/ip-boundary.ts +155 -15
  284. package/src/security/model-abuse.ts +366 -94
  285. package/src/security/outbound-headers.ts +3 -4
  286. package/src/shared/sleep.ts +4 -0
  287. package/src/transport/dispatch/attempt-finalize.ts +47 -5
  288. package/src/transport/dispatch/attempt-loop.ts +531 -530
  289. package/src/transport/dispatch/stream-bridge.ts +1 -3
  290. package/src/transport/dispatch/streaming/finalize.ts +1 -1
  291. package/src/transport/dispatch/streaming/stream.ts +5 -6
  292. package/src/transport/failure-policy.ts +162 -8
  293. package/src/transport/guards/auth-guard.ts +7 -4
  294. package/src/transport/guards/concurrency-guard.ts +0 -5
  295. package/src/transport/guards/error-lifecycle.ts +13 -6
  296. package/src/transport/guards/pipeline.ts +15 -3
  297. package/src/transport/guards/request-context.ts +8 -2
  298. package/src/transport/request/inflight.ts +5 -6
  299. package/src/transport/request/preparer.ts +2 -2
  300. package/src/transport/request/state.ts +12 -8
  301. package/src/transport/routing/admission.ts +4 -0
  302. package/src/transport/routing/route-catalog.ts +8 -1
  303. package/src/transport/routing/route-model.ts +46 -14
  304. package/src/transport/routing/router.ts +21 -14
  305. package/src/transport/surface/completion.ts +2 -3
  306. package/src/transport/surface/dialects.ts +5 -6
  307. package/src/transport/surface/responses/encode.ts +38 -0
  308. package/src/transport/surface/responses/parse.ts +4 -0
  309. package/src/transport/translation/capabilities.ts +2 -3
  310. package/src/transport/translation/quirks.ts +3 -3
  311. package/src/workers/buddy-growth-tasks.ts +5 -10
  312. package/src/workers/daily-checkin.ts +17 -23
  313. package/src/workers/quota-refresh-sweep.ts +26 -26
  314. package/dist/dashboard/assets/BannedPage-C2orBsI2.js.gz +0 -0
  315. package/dist/dashboard/assets/CliToolDetailPage-DUcZ6pMB.js +0 -1
  316. package/dist/dashboard/assets/CliToolDetailPage-DUcZ6pMB.js.gz +0 -0
  317. package/dist/dashboard/assets/CliToolsPage-BV6pxVQo.js.gz +0 -0
  318. package/dist/dashboard/assets/CombosPage-Bsan4679.js +0 -1
  319. package/dist/dashboard/assets/CombosPage-Bsan4679.js.gz +0 -0
  320. package/dist/dashboard/assets/ConsoleLogPage-CW-wPmJ6.js.gz +0 -0
  321. package/dist/dashboard/assets/CustomizationPage-0p8uX7vB.js.gz +0 -0
  322. package/dist/dashboard/assets/HealthEventsModal-BjqSMfko.js.gz +0 -0
  323. package/dist/dashboard/assets/LoginPage-BvzCx8Kr.js.gz +0 -0
  324. package/dist/dashboard/assets/ModelPicker-BY1aKEkP.js +0 -1
  325. package/dist/dashboard/assets/ModelPicker-BY1aKEkP.js.gz +0 -0
  326. package/dist/dashboard/assets/OverviewPage-DOQpy1y0.js +0 -1
  327. package/dist/dashboard/assets/OverviewPage-DOQpy1y0.js.gz +0 -0
  328. package/dist/dashboard/assets/ProviderDetailPage-ecxyUMzt.js +0 -3
  329. package/dist/dashboard/assets/ProviderDetailPage-ecxyUMzt.js.gz +0 -0
  330. package/dist/dashboard/assets/ProvidersPage-Js9jUPSb.js.gz +0 -0
  331. package/dist/dashboard/assets/ProxyPage-J3TBi3Kx.js +0 -7
  332. package/dist/dashboard/assets/ProxyPage-J3TBi3Kx.js.gz +0 -0
  333. package/dist/dashboard/assets/QuotaPage-C1gZS6xr.js +0 -1
  334. package/dist/dashboard/assets/QuotaPage-C1gZS6xr.js.gz +0 -0
  335. package/dist/dashboard/assets/SettingsPage-CS86ho5X.js +0 -1
  336. package/dist/dashboard/assets/SettingsPage-CS86ho5X.js.gz +0 -0
  337. package/dist/dashboard/assets/SetupPage-DDbMQsoo.js.gz +0 -0
  338. package/dist/dashboard/assets/SortableList-CB3Z7VTf.js.gz +0 -0
  339. package/dist/dashboard/assets/StudioPage-D95UCPHI.js.gz +0 -0
  340. package/dist/dashboard/assets/UsagePage-Cnae9SWA.js +0 -56
  341. package/dist/dashboard/assets/UsagePage-Cnae9SWA.js.gz +0 -0
  342. package/dist/dashboard/assets/api-keys-VY68Mhd1.js +0 -1
  343. package/dist/dashboard/assets/api-keys-VY68Mhd1.js.gz +0 -0
  344. package/dist/dashboard/assets/app-COlK95oi.css +0 -2
  345. package/dist/dashboard/assets/app-COlK95oi.css.gz +0 -0
  346. package/dist/dashboard/assets/assert-Chi7pCnt.js.gz +0 -0
  347. package/dist/dashboard/assets/badge-D4pQapKj.js.gz +0 -0
  348. package/dist/dashboard/assets/banner-DH6cg-_B.js.gz +0 -0
  349. package/dist/dashboard/assets/card-BKqlUOe4.js.gz +0 -0
  350. package/dist/dashboard/assets/cli-tools-fTgljTGA.js.gz +0 -0
  351. package/dist/dashboard/assets/clipboard-button-D5Ys_sM0.js.gz +0 -0
  352. package/dist/dashboard/assets/dialog-BYj-8WuK.js +0 -1
  353. package/dist/dashboard/assets/dialog-BYj-8WuK.js.gz +0 -0
  354. package/dist/dashboard/assets/drawer-CmXIZ6H1.js +0 -1
  355. package/dist/dashboard/assets/drawer-CmXIZ6H1.js.gz +0 -0
  356. package/dist/dashboard/assets/entry-CBg5zeu8.js +0 -1
  357. package/dist/dashboard/assets/entry-CBg5zeu8.js.gz +0 -0
  358. package/dist/dashboard/assets/entry-CQBr6U9c.js.gz +0 -0
  359. package/dist/dashboard/assets/entry-CgdSAzEV.js +0 -4
  360. package/dist/dashboard/assets/entry-CgdSAzEV.js.gz +0 -0
  361. package/dist/dashboard/assets/fingerprint-pattern-BWuHtbW4.js.gz +0 -0
  362. package/dist/dashboard/assets/github-badge-BlptL8Uf.js.gz +0 -0
  363. package/dist/dashboard/assets/index-js6TJvdr.js.gz +0 -0
  364. package/dist/dashboard/assets/inline-DUWDSmDV.js.gz +0 -0
  365. package/dist/dashboard/assets/input-LVjgZq4f.js.gz +0 -0
  366. package/dist/dashboard/assets/layout-CPfxpsfy.js +0 -1
  367. package/dist/dashboard/assets/layout-CPfxpsfy.js.gz +0 -0
  368. package/dist/dashboard/assets/list-row-DupvLfDx.js.gz +0 -0
  369. package/dist/dashboard/assets/live-C4pEJUqI.js.gz +0 -0
  370. package/dist/dashboard/assets/model-bans-BRiuufkB.js.gz +0 -0
  371. package/dist/dashboard/assets/mutation-B4ROwk8L.js.gz +0 -0
  372. package/dist/dashboard/assets/numbers-YNcHvRY7.js +0 -2
  373. package/dist/dashboard/assets/numbers-YNcHvRY7.js.gz +0 -0
  374. package/dist/dashboard/assets/query-keys-gmjzC3RS.js.gz +0 -0
  375. package/dist/dashboard/assets/routing-C7tBT5o-.js.gz +0 -0
  376. package/dist/dashboard/assets/select-WUCXorXw.js.gz +0 -0
  377. package/dist/dashboard/assets/settings-C_zKl1GS.js.gz +0 -0
  378. package/dist/dashboard/assets/stack-_3O0F8Xn.js.gz +0 -0
  379. package/dist/dashboard/assets/state-BgTxHcJS.js.gz +0 -0
  380. package/dist/dashboard/assets/switch-DEjTtOWf.js.gz +0 -0
  381. package/dist/dashboard/assets/tokens-DDxMbSRL.js +0 -1
  382. package/dist/dashboard/assets/tokens-DDxMbSRL.js.gz +0 -0
  383. package/dist/dashboard/assets/use-presence-BSZEBx7U.js +0 -1
  384. package/dist/dashboard/assets/use-presence-BSZEBx7U.js.gz +0 -0
  385. package/dist/dashboard/assets/useMutation-BWSC0UAS.js.gz +0 -0
  386. package/scripts/cli/command/admin.ts +0 -65
  387. package/scripts/cli/command/doctor.ts +0 -257
  388. package/scripts/cli/gateway/drain-token.ts +0 -78
  389. package/scripts/cli/gateway/instance-lock.ts +0 -105
  390. package/scripts/cli/gateway/stop.ts +0 -51
  391. package/scripts/cli/support/bun.ts +0 -27
  392. package/scripts/cli/support/version.ts +0 -37
  393. /package/dist/dashboard/assets/{app-DCQZRmHY.js → app-BIt446yE.js} +0 -0
  394. /package/dist/dashboard/assets/{app-DCQZRmHY.js.gz → app-BIt446yE.js.gz} +0 -0
  395. /package/scripts/cli/{ui/prompt.ts → prompt.ts} +0 -0
@@ -1,210 +1,211 @@
1
- import type { RedisClient } from "../../persistence/redis";
2
- import { redisEvalNumber } from "../../persistence/redis";
3
- import type { AdmissionCounterStore, AdmissionReserveRequest } from "./contracts";
4
- import { assertResult, RECONCILE_SCRIPT, RELEASE_SCRIPT, RESERVE_SCRIPT } from "./lua";
5
- import {
6
- DAILY_COUNTER_TTL_SECONDS,
7
- LEASE_KEY_TTL_SECONDS,
8
- LEASE_TTL_MS,
9
- MONTHLY_COUNTER_TTL_SECONDS,
10
- dailyBucket,
11
- monthlyBucket,
12
- } from "./ttl";
13
-
14
- export class RedisAdmissionCounterStore implements AdmissionCounterStore {
15
- constructor(private readonly redis: RedisClient) {}
16
- async reserve(request: AdmissionReserveRequest): Promise<void> {
17
- const id = request.reservationId ?? `${request.apiKeyId}:${request.now}:${crypto.randomUUID()}`;
18
- const lifetimeKey = `admission:lifetime:${request.apiKeyId}`;
19
- const keys = [
20
- `admission:rpm:${request.apiKeyId}`,
21
- `admission:daily:${request.apiKeyId}:${dailyBucket(request.now)}`,
22
- `admission:monthly:${request.apiKeyId}:${monthlyBucket(request.now)}`,
23
- `admission:concurrent:${request.apiKeyId}`,
24
- lifetimeKey,
25
- `admission:lease:${id}`,
26
- `admission:tenant_concurrent:${request.tenantId}`,
27
- ];
28
- // The lifetime seed is the one write the Lua script cannot re-issue: once
29
- // the counter exists it never re-reads Postgres. If we are about to create
30
- // it, prefer a fresh persisted read over the ≤3s-stale snapshot value,
31
- // otherwise a snapshot that missed a recent commit freezes a low baseline
32
- // for the counter's whole 35-day TTL. `EXISTS` is a single extra command
33
- // and runs only for keys that actually have a lifetime budget.
34
- let lifetimeSeed = request.lifetimeConsumed;
35
- if (request.lifetimeBudget != null && request.freshLifetimeConsumed) {
36
- const exists = await this.redis.exists(lifetimeKey).catch(() => 1);
37
- if (exists === 0) {
38
- const fresh = await request.freshLifetimeConsumed().catch(() => undefined);
39
- if (typeof fresh === "number" && Number.isFinite(fresh) && fresh >= 0) {
40
- lifetimeSeed = Math.floor(fresh);
41
- }
42
- }
43
- }
44
- const result = await redisEvalNumber(
45
- this.redis,
46
- RESERVE_SCRIPT,
47
- keys.length,
48
- ...keys,
49
- String(request.now),
50
- String(request.estimatedTokens),
51
- request.rpmLimit == null ? "" : String(request.rpmLimit),
52
- request.dailyLimit == null ? "" : String(request.dailyLimit),
53
- request.monthlyLimit == null ? "" : String(request.monthlyLimit),
54
- request.lifetimeBudget == null ? "" : String(request.lifetimeBudget),
55
- request.concurrencyLimit == null ? "" : String(request.concurrencyLimit),
56
- id,
57
- String(lifetimeSeed),
58
- request.tenantConcurrencyLimit == null ? "" : String(request.tenantConcurrencyLimit),
59
- request.tenantId,
60
- request.apiKeyId,
61
- String(LEASE_TTL_MS),
62
- String(LEASE_KEY_TTL_SECONDS),
63
- );
64
- assertResult(result, "reserve");
65
- }
66
- async reconcile(
67
- apiKeyId: string,
68
- _reserved: number,
69
- actual: number,
70
- reservationId?: string,
71
- ): Promise<void> {
72
- if (!reservationId) throw new Error("reservation id required for atomic reconcile");
73
- const result = await redisEvalNumber(
74
- this.redis,
75
- RECONCILE_SCRIPT,
76
- 1,
77
- `admission:lease:${reservationId}`,
78
- String(actual),
79
- apiKeyId,
80
- );
81
- assertResult(result, "reconcile");
82
- }
83
- async release(
84
- apiKeyId: string,
85
- _reserved: number,
86
- reservationId?: string,
87
- ): Promise<void> {
88
- if (!reservationId) throw new Error("reservation id required for atomic release");
89
- const result = await redisEvalNumber(
90
- this.redis,
91
- RELEASE_SCRIPT,
92
- 1,
93
- `admission:lease:${reservationId}`,
94
- apiKeyId,
95
- );
96
- assertResult(result, "release");
97
- }
98
-
99
- /**
100
- * `SET NX`-seeds the current bucket's counters from already-recorded usage.
101
- * NX is what makes this safe: an existing counter is the live running total,
102
- * and overwriting it would erase spend. Best-effort by contract.
103
- */
104
- async seedBuckets(request: {
105
- readonly apiKeyId: string;
106
- readonly now: number;
107
- readonly daily?: number;
108
- readonly monthly?: number;
109
- }): Promise<void> {
110
- try {
111
- const operations: Promise<unknown>[] = [];
112
- if (request.daily !== undefined && request.daily > 0) {
113
- operations.push(
114
- this.redis.set(
115
- `admission:daily:${request.apiKeyId}:${dailyBucket(request.now)}`,
116
- Math.floor(request.daily),
117
- "EX",
118
- DAILY_COUNTER_TTL_SECONDS,
119
- "NX",
120
- ),
121
- );
122
- }
123
- if (request.monthly !== undefined && request.monthly > 0) {
124
- operations.push(
125
- this.redis.set(
126
- `admission:monthly:${request.apiKeyId}:${monthlyBucket(request.now)}`,
127
- Math.floor(request.monthly),
128
- "EX",
129
- MONTHLY_COUNTER_TTL_SECONDS,
130
- "NX",
131
- ),
132
- );
133
- }
134
- await Promise.all(operations);
135
- } catch {
136
- // Seeding is an optimisation, not a correctness gate.
137
- }
138
- }
139
-
140
- /**
141
- * Drops every counter for one key (revocation). Active in-flight leases
142
- * settle through the release script first so their concurrency slots do
143
- * not leak; remaining lease rows expire via TTL. Best-effort by contract.
144
- */
145
- async purge(apiKeyId: string): Promise<void> {
146
- try {
147
- let cursor = "0";
148
- do {
149
- const [nextCursor, keys] = await this.redis.scan(
150
- cursor,
151
- "MATCH",
152
- "admission:lease:*",
153
- "COUNT",
154
- 100,
155
- );
156
- cursor = String(nextCursor);
157
- const leaseKeys = keys as string[];
158
- if (leaseKeys.length === 0) continue;
159
- // One pipeline for the whole scan page instead of an HGET per lease:
160
- // the owner is a hash field, not part of the key, so the page has to be
161
- // read to find this key's leases — but it can be read in one round trip.
162
- // The key format stays `admission:lease:<reservationId>` (the id is not
163
- // always derived from the key id) so leases written before a deploy are
164
- // still found.
165
- const pipeline = this.redis.pipeline();
166
- for (const leaseKey of leaseKeys) pipeline.hget(leaseKey, "api_key_id");
167
- const owners = await pipeline.exec().catch(() => null);
168
- if (owners === null) continue;
169
- const owned = leaseKeys.filter((_, index) => {
170
- const entry = owners[index];
171
- return Array.isArray(entry) && entry[1] === apiKeyId;
172
- });
173
- for (const leaseKey of owned) {
174
- await redisEvalNumber(this.redis, RELEASE_SCRIPT, 1, leaseKey, apiKeyId).catch(
175
- () => -1,
176
- );
177
- }
178
- } while (cursor !== "0");
179
- let counterCursor = "0";
180
- const patterns = [`admission:*:${apiKeyId}`, `admission:*:${apiKeyId}:*`];
181
- for (const pattern of patterns) {
182
- do {
183
- const [nextCursor, keys] = await this.redis.scan(
184
- counterCursor,
185
- "MATCH",
186
- pattern,
187
- "COUNT",
188
- 100,
189
- );
190
- counterCursor = String(nextCursor);
191
- if ((keys as string[]).length > 0) await this.redis.del(...(keys as string[]));
192
- } while (counterCursor !== "0");
193
- }
194
- } catch {
195
- // Best-effort: revocation already succeeded; stale counters expire via TTL.
196
- }
197
- }
198
- }
199
-
200
- /**
201
- * Releases a lease identified only by its Redis key (used by the sweeper).
202
- * Reads the owning api key id from the lease hash and runs RELEASE_SCRIPT,
203
- * which also releases the per-tenant concurrency slot when present.
204
- */
205
- export async function releaseAdmissionLease(redis: RedisClient, leaseKey: string): Promise<void> {
206
- const apiKeyId = await redis.hget(leaseKey, "api_key_id");
207
- if (!apiKeyId) throw new Error("admission lease missing api_key_id");
208
- const result = await redisEvalNumber(redis, RELEASE_SCRIPT, 1, leaseKey, apiKeyId);
209
- assertResult(result, "release");
210
- }
1
+ import type { RedisClient } from "../../persistence/redis";
2
+ import { redisEvalNumber } from "../../persistence/redis";
3
+ import type { AdmissionCounterStore, AdmissionReserveRequest } from "./contracts";
4
+ import { assertResult, RECONCILE_SCRIPT, RELEASE_SCRIPT, RESERVE_SCRIPT } from "./lua";
5
+ import {
6
+ DAILY_COUNTER_TTL_SECONDS,
7
+ LEASE_KEY_TTL_SECONDS,
8
+ LEASE_TTL_MS,
9
+ MONTHLY_COUNTER_TTL_SECONDS,
10
+ dailyBucket,
11
+ monthlyBucket,
12
+ } from "./ttl";
13
+
14
+ export class RedisAdmissionCounterStore implements AdmissionCounterStore {
15
+ constructor(private readonly redis: RedisClient) {}
16
+ async reserve(request: AdmissionReserveRequest): Promise<void> {
17
+ const id = request.reservationId ?? `${request.apiKeyId}:${request.now}:${crypto.randomUUID()}`;
18
+ const lifetimeKey = `admission:lifetime:${request.apiKeyId}`;
19
+ const rpmConcId = request.rpmConcurrencyKeyId ?? request.apiKeyId;
20
+ const keys = [
21
+ `admission:rpm:${rpmConcId}`,
22
+ `admission:daily:${request.apiKeyId}:${dailyBucket(request.now)}`,
23
+ `admission:monthly:${request.apiKeyId}:${monthlyBucket(request.now)}`,
24
+ `admission:concurrent:${rpmConcId}`,
25
+ lifetimeKey,
26
+ `admission:lease:${id}`,
27
+ `admission:tenant_concurrent:${request.tenantId}`,
28
+ ];
29
+ // The lifetime seed is the one write the Lua script cannot re-issue: once
30
+ // the counter exists it never re-reads Postgres. If we are about to create
31
+ // it, prefer a fresh persisted read over the ≤3s-stale snapshot value,
32
+ // otherwise a snapshot that missed a recent commit freezes a low baseline
33
+ // for the counter's whole 35-day TTL. `EXISTS` is a single extra command
34
+ // and runs only for keys that actually have a lifetime budget.
35
+ let lifetimeSeed = request.lifetimeConsumed;
36
+ if (request.lifetimeBudget != null && request.freshLifetimeConsumed) {
37
+ const exists = await this.redis.exists(lifetimeKey).catch(() => 1);
38
+ if (exists === 0) {
39
+ const fresh = await request.freshLifetimeConsumed().catch(() => undefined);
40
+ if (typeof fresh === "number" && Number.isFinite(fresh) && fresh >= 0) {
41
+ lifetimeSeed = Math.floor(fresh);
42
+ }
43
+ }
44
+ }
45
+ const result = await redisEvalNumber(
46
+ this.redis,
47
+ RESERVE_SCRIPT,
48
+ keys.length,
49
+ ...keys,
50
+ String(request.now),
51
+ String(request.estimatedTokens),
52
+ request.rpmLimit == null ? "" : String(request.rpmLimit),
53
+ request.dailyLimit == null ? "" : String(request.dailyLimit),
54
+ request.monthlyLimit == null ? "" : String(request.monthlyLimit),
55
+ request.lifetimeBudget == null ? "" : String(request.lifetimeBudget),
56
+ request.concurrencyLimit == null ? "" : String(request.concurrencyLimit),
57
+ id,
58
+ String(lifetimeSeed),
59
+ request.tenantConcurrencyLimit == null ? "" : String(request.tenantConcurrencyLimit),
60
+ request.tenantId,
61
+ request.apiKeyId,
62
+ String(LEASE_TTL_MS),
63
+ String(LEASE_KEY_TTL_SECONDS),
64
+ );
65
+ assertResult(result, "reserve");
66
+ }
67
+ async reconcile(
68
+ apiKeyId: string,
69
+ _reserved: number,
70
+ actual: number,
71
+ reservationId?: string,
72
+ ): Promise<void> {
73
+ if (!reservationId) throw new Error("reservation id required for atomic reconcile");
74
+ const result = await redisEvalNumber(
75
+ this.redis,
76
+ RECONCILE_SCRIPT,
77
+ 1,
78
+ `admission:lease:${reservationId}`,
79
+ String(actual),
80
+ apiKeyId,
81
+ );
82
+ assertResult(result, "reconcile");
83
+ }
84
+ async release(
85
+ apiKeyId: string,
86
+ _reserved: number,
87
+ reservationId?: string,
88
+ ): Promise<void> {
89
+ if (!reservationId) throw new Error("reservation id required for atomic release");
90
+ const result = await redisEvalNumber(
91
+ this.redis,
92
+ RELEASE_SCRIPT,
93
+ 1,
94
+ `admission:lease:${reservationId}`,
95
+ apiKeyId,
96
+ );
97
+ assertResult(result, "release");
98
+ }
99
+
100
+ /**
101
+ * `SET NX`-seeds the current bucket's counters from already-recorded usage.
102
+ * NX is what makes this safe: an existing counter is the live running total,
103
+ * and overwriting it would erase spend. Best-effort by contract.
104
+ */
105
+ async seedBuckets(request: {
106
+ readonly apiKeyId: string;
107
+ readonly now: number;
108
+ readonly daily?: number;
109
+ readonly monthly?: number;
110
+ }): Promise<void> {
111
+ try {
112
+ const operations: Promise<unknown>[] = [];
113
+ if (request.daily !== undefined && request.daily > 0) {
114
+ operations.push(
115
+ this.redis.set(
116
+ `admission:daily:${request.apiKeyId}:${dailyBucket(request.now)}`,
117
+ Math.floor(request.daily),
118
+ "EX",
119
+ DAILY_COUNTER_TTL_SECONDS,
120
+ "NX",
121
+ ),
122
+ );
123
+ }
124
+ if (request.monthly !== undefined && request.monthly > 0) {
125
+ operations.push(
126
+ this.redis.set(
127
+ `admission:monthly:${request.apiKeyId}:${monthlyBucket(request.now)}`,
128
+ Math.floor(request.monthly),
129
+ "EX",
130
+ MONTHLY_COUNTER_TTL_SECONDS,
131
+ "NX",
132
+ ),
133
+ );
134
+ }
135
+ await Promise.all(operations);
136
+ } catch {
137
+ // Seeding is an optimisation, not a correctness gate.
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Drops every counter for one key (revocation). Active in-flight leases
143
+ * settle through the release script first so their concurrency slots do
144
+ * not leak; remaining lease rows expire via TTL. Best-effort by contract.
145
+ */
146
+ async purge(apiKeyId: string): Promise<void> {
147
+ try {
148
+ let cursor = "0";
149
+ do {
150
+ const [nextCursor, keys] = await this.redis.scan(
151
+ cursor,
152
+ "MATCH",
153
+ "admission:lease:*",
154
+ "COUNT",
155
+ 100,
156
+ );
157
+ cursor = String(nextCursor);
158
+ const leaseKeys = keys as string[];
159
+ if (leaseKeys.length === 0) continue;
160
+ // One pipeline for the whole scan page instead of an HGET per lease:
161
+ // the owner is a hash field, not part of the key, so the page has to be
162
+ // read to find this key's leases — but it can be read in one round trip.
163
+ // The key format stays `admission:lease:<reservationId>` (the id is not
164
+ // always derived from the key id) so leases written before a deploy are
165
+ // still found.
166
+ const pipeline = this.redis.pipeline();
167
+ for (const leaseKey of leaseKeys) pipeline.hget(leaseKey, "api_key_id");
168
+ const owners = await pipeline.exec().catch(() => null);
169
+ if (owners === null) continue;
170
+ const owned = leaseKeys.filter((_, index) => {
171
+ const entry = owners[index];
172
+ return Array.isArray(entry) && entry[1] === apiKeyId;
173
+ });
174
+ for (const leaseKey of owned) {
175
+ await redisEvalNumber(this.redis, RELEASE_SCRIPT, 1, leaseKey, apiKeyId).catch(
176
+ () => -1,
177
+ );
178
+ }
179
+ } while (cursor !== "0");
180
+ let counterCursor = "0";
181
+ const patterns = [`admission:*:${apiKeyId}`, `admission:*:${apiKeyId}:*`];
182
+ for (const pattern of patterns) {
183
+ do {
184
+ const [nextCursor, keys] = await this.redis.scan(
185
+ counterCursor,
186
+ "MATCH",
187
+ pattern,
188
+ "COUNT",
189
+ 100,
190
+ );
191
+ counterCursor = String(nextCursor);
192
+ if ((keys as string[]).length > 0) await this.redis.del(...(keys as string[]));
193
+ } while (counterCursor !== "0");
194
+ }
195
+ } catch {
196
+ // Best-effort: revocation already succeeded; stale counters expire via TTL.
197
+ }
198
+ }
199
+ }
200
+
201
+ /**
202
+ * Releases a lease identified only by its Redis key (used by the sweeper).
203
+ * Reads the owning api key id from the lease hash and runs RELEASE_SCRIPT,
204
+ * which also releases the per-tenant concurrency slot when present.
205
+ */
206
+ export async function releaseAdmissionLease(redis: RedisClient, leaseKey: string): Promise<void> {
207
+ const apiKeyId = await redis.hget(leaseKey, "api_key_id");
208
+ if (!apiKeyId) throw new Error("admission lease missing api_key_id");
209
+ const result = await redisEvalNumber(redis, RELEASE_SCRIPT, 1, leaseKey, apiKeyId);
210
+ assertResult(result, "release");
211
+ }
@@ -101,6 +101,10 @@ export class ApiKeyAdmissionService {
101
101
  await this.store.reserve({
102
102
  reservationId,
103
103
  apiKeyId,
104
+ // rpm and concurrency are tracked per child key so each share recipient
105
+ // has its own independent window and slot. Token budgets (daily/monthly/
106
+ // lifetime) remain on `apiKeyId` (the parent) — the shared pool.
107
+ ...(snapshot.api_key_id !== apiKeyId ? { rpmConcurrencyKeyId: snapshot.api_key_id } : {}),
104
108
  now,
105
109
  estimatedTokens,
106
110
  rpmLimit: snapshot.rpm ?? null,