cartethyia 2.5.3 → 2.5.6

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 (456) hide show
  1. package/.env.example +13 -4
  2. package/README.md +2 -2
  3. package/dist/dashboard/assets/{BannedPage-hfCzi-Ux.js → BannedPage-CADDq4IU.js} +1 -1
  4. package/dist/dashboard/assets/BannedPage-CADDq4IU.js.gz +0 -0
  5. package/dist/dashboard/assets/CliToolDetailPage-_z1hID7E.js +1 -0
  6. package/dist/dashboard/assets/CliToolDetailPage-_z1hID7E.js.gz +0 -0
  7. package/dist/dashboard/assets/{CliToolsPage-DEDyKBlC.js → CliToolsPage-DuO1Z7dq.js} +1 -1
  8. package/dist/dashboard/assets/CliToolsPage-DuO1Z7dq.js.gz +0 -0
  9. package/dist/dashboard/assets/CombosPage-DNEPi3xU.js +1 -0
  10. package/dist/dashboard/assets/CombosPage-DNEPi3xU.js.gz +0 -0
  11. package/dist/dashboard/assets/{ConsoleLogPage-bsLuZmYH.js → ConsoleLogPage-BVdpfiz1.js} +1 -1
  12. package/dist/dashboard/assets/ConsoleLogPage-BVdpfiz1.js.gz +0 -0
  13. package/dist/dashboard/assets/{CustomizationPage-_UWhwziJ.js → CustomizationPage-BR5HHvbi.js} +1 -1
  14. package/dist/dashboard/assets/CustomizationPage-BR5HHvbi.js.gz +0 -0
  15. package/dist/dashboard/assets/HealthEventsModal-mUfcknqm.js +1 -0
  16. package/dist/dashboard/assets/HealthEventsModal-mUfcknqm.js.gz +0 -0
  17. package/dist/dashboard/assets/{LoginPage-DDVIfZGW.js → LoginPage-V5faV9V2.js} +1 -1
  18. package/dist/dashboard/assets/LoginPage-V5faV9V2.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-BDu20Jdx.js +1 -0
  22. package/dist/dashboard/assets/OverviewPage-BDu20Jdx.js.gz +0 -0
  23. package/dist/dashboard/assets/ProviderDetailPage-Dllr_aFO.js +3 -0
  24. package/dist/dashboard/assets/ProviderDetailPage-Dllr_aFO.js.gz +0 -0
  25. package/dist/dashboard/assets/ProvidersPage-uPiGVTTK.js +1 -0
  26. package/dist/dashboard/assets/ProvidersPage-uPiGVTTK.js.gz +0 -0
  27. package/dist/dashboard/assets/ProxyPage-BaUkO-4O.js +7 -0
  28. package/dist/dashboard/assets/ProxyPage-BaUkO-4O.js.gz +0 -0
  29. package/dist/dashboard/assets/QuotaPage-o0IFAsFb.js +1 -0
  30. package/dist/dashboard/assets/QuotaPage-o0IFAsFb.js.gz +0 -0
  31. package/dist/dashboard/assets/SettingsPage-CYWqbXjp.js +1 -0
  32. package/dist/dashboard/assets/SettingsPage-CYWqbXjp.js.gz +0 -0
  33. package/dist/dashboard/assets/{SetupPage-D6pmQ86L.js → SetupPage-UB690SE2.js} +1 -1
  34. package/dist/dashboard/assets/SetupPage-UB690SE2.js.gz +0 -0
  35. package/dist/dashboard/assets/{SortableList-DWvKiuSl.js → SortableList-BLujdX7N.js} +1 -1
  36. package/dist/dashboard/assets/SortableList-BLujdX7N.js.gz +0 -0
  37. package/dist/dashboard/assets/{StudioPage-CmkUrgdj.js → StudioPage-D5C2raAc.js} +3 -3
  38. package/dist/dashboard/assets/StudioPage-D5C2raAc.js.gz +0 -0
  39. package/dist/dashboard/assets/UsagePage-8uIcxa1W.js +56 -0
  40. package/dist/dashboard/assets/UsagePage-8uIcxa1W.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-NHwYq95t.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-6uzDDvqs.js → badge-CR72dasT.js} +1 -1
  48. package/dist/dashboard/assets/badge-CR72dasT.js.gz +0 -0
  49. package/dist/dashboard/assets/{banner-4_WNg2WZ.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--AtFedAq.js → card-DS7CQEsO.js} +1 -1
  52. package/dist/dashboard/assets/card-DS7CQEsO.js.gz +0 -0
  53. package/dist/dashboard/assets/{cli-tools-B3e7dx3N.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-NreUx6c8.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/database-Dz9N3zGd.js +1 -0
  60. package/dist/dashboard/assets/database-Dz9N3zGd.js.gz +0 -0
  61. package/dist/dashboard/assets/dialog-k37exjNt.js +1 -0
  62. package/dist/dashboard/assets/dialog-k37exjNt.js.gz +0 -0
  63. package/dist/dashboard/assets/drawer-CX7N8ucR.js +1 -0
  64. package/dist/dashboard/assets/drawer-CX7N8ucR.js.gz +0 -0
  65. package/dist/dashboard/assets/entry-B9Xvtnfj.js +1 -0
  66. package/dist/dashboard/assets/entry-B9Xvtnfj.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/entry-DxBHnauy.js +4 -0
  70. package/dist/dashboard/assets/entry-DxBHnauy.js.gz +0 -0
  71. package/dist/dashboard/assets/{github-badge-D_eM3IgG.js → github-badge-sltk1qUi.js} +1 -1
  72. package/dist/dashboard/assets/github-badge-sltk1qUi.js.gz +0 -0
  73. package/dist/dashboard/assets/{index-BydyKaDz.js → index-CmxZr14F.js} +2 -2
  74. package/dist/dashboard/assets/index-CmxZr14F.js.gz +0 -0
  75. package/dist/dashboard/assets/{inline-BcNGF8Gr.js → inline-BhY2sSnO.js} +1 -1
  76. package/dist/dashboard/assets/inline-BhY2sSnO.js.gz +0 -0
  77. package/dist/dashboard/assets/{input-BG41et9G.js → input-V1umFiBk.js} +1 -1
  78. package/dist/dashboard/assets/input-V1umFiBk.js.gz +0 -0
  79. package/dist/dashboard/assets/layout-BBOYycyS.js +1 -0
  80. package/dist/dashboard/assets/layout-BBOYycyS.js.gz +0 -0
  81. package/dist/dashboard/assets/{list-row-Gh_W4OR_.js → list-row-1zzZYM0B.js} +1 -1
  82. package/dist/dashboard/assets/list-row-1zzZYM0B.js.gz +0 -0
  83. package/dist/dashboard/assets/{live-BJGYRBnR.js → live-BivGfuUs.js} +1 -1
  84. package/dist/dashboard/assets/live-BivGfuUs.js.gz +0 -0
  85. package/dist/dashboard/assets/model-bans-CmffjLuG.js +1 -0
  86. package/dist/dashboard/assets/model-bans-CmffjLuG.js.gz +0 -0
  87. package/dist/dashboard/assets/{mutation-BkoLM5Ba.js → mutation-46P0oWMe.js} +1 -1
  88. package/dist/dashboard/assets/mutation-46P0oWMe.js.gz +0 -0
  89. package/dist/dashboard/assets/numbers-DkfMR5Qs.js +2 -0
  90. package/dist/dashboard/assets/numbers-DkfMR5Qs.js.gz +0 -0
  91. package/dist/dashboard/assets/{query-keys-D7wX2vM5.js → query-keys-BFhGpwR1.js} +1 -1
  92. package/dist/dashboard/assets/query-keys-BFhGpwR1.js.gz +0 -0
  93. package/dist/dashboard/assets/{routing-CcusqXA7.js → routing-CAb4F37R.js} +1 -1
  94. package/dist/dashboard/assets/routing-CAb4F37R.js.gz +0 -0
  95. package/dist/dashboard/assets/{select-BO6h8yNv.js → select-BYR_5K2c.js} +2 -2
  96. package/dist/dashboard/assets/select-BYR_5K2c.js.gz +0 -0
  97. package/dist/dashboard/assets/{settings-mpmXTgUa.js → settings-DNX9sydi.js} +1 -1
  98. package/dist/dashboard/assets/settings-DNX9sydi.js.gz +0 -0
  99. package/dist/dashboard/assets/{stack-DwLmjUFK.js → stack-BHTfXdz_.js} +1 -1
  100. package/dist/dashboard/assets/stack-BHTfXdz_.js.gz +0 -0
  101. package/dist/dashboard/assets/{state-DYdQmdAl.js → state-mPDRlQkl.js} +1 -1
  102. package/dist/dashboard/assets/state-mPDRlQkl.js.gz +0 -0
  103. package/dist/dashboard/assets/switch-DOmJxg7H.js +1 -0
  104. package/dist/dashboard/assets/switch-DOmJxg7H.js.gz +0 -0
  105. package/dist/dashboard/assets/terminal-0_IjPWTk.js +1 -0
  106. package/dist/dashboard/assets/terminal-0_IjPWTk.js.gz +0 -0
  107. package/dist/dashboard/assets/tokens-COzario9.js +1 -0
  108. package/dist/dashboard/assets/tokens-COzario9.js.gz +0 -0
  109. package/dist/dashboard/assets/use-modal-focus-By7oJoGd.js +1 -0
  110. package/dist/dashboard/assets/use-modal-focus-By7oJoGd.js.gz +0 -0
  111. package/dist/dashboard/assets/{useMutation-w4u4CVue.js → useMutation-Dem0lXQj.js} +1 -1
  112. package/dist/dashboard/assets/useMutation-Dem0lXQj.js.gz +0 -0
  113. package/dist/dashboard/assets/zap-C4EeVW4q.js +1 -0
  114. package/dist/dashboard/assets/zap-C4EeVW4q.js.gz +0 -0
  115. package/dist/dashboard/donation/qris-risun-marke.png +0 -0
  116. package/dist/dashboard/index.html +1 -1
  117. package/dist/dashboard/index.html.gz +0 -0
  118. package/dist/dashboard/precompressed-assets.json +57 -57
  119. package/migrations/README.md +52 -52
  120. package/migrations/lite-sqlite3/0000_baseline.sql +2 -2
  121. package/package.json +96 -95
  122. package/scripts/cli/admin.ts +125 -0
  123. package/scripts/cli/autostart.ts +237 -0
  124. package/scripts/cli/{ui/background.ts → background.ts} +1 -1
  125. package/scripts/cli/{command/console-menu.ts → console-menu.ts} +275 -248
  126. package/scripts/cli/doctor.ts +37 -0
  127. package/scripts/cli/{support/env.ts → env.ts} +99 -99
  128. package/scripts/cli/{gateway/spawn.ts → gateway.ts} +112 -24
  129. package/scripts/cli/instance.ts +224 -0
  130. package/scripts/cli/main.ts +88 -18
  131. package/scripts/cli/{support/platform.ts → platform.ts} +64 -64
  132. package/scripts/cli/port.ts +100 -0
  133. package/scripts/cli/{support/services.ts → services.ts} +88 -88
  134. package/scripts/cli/{command/setup.ts → setup.ts} +35 -34
  135. package/scripts/cli/{command/start.ts → start.ts} +10 -5
  136. package/scripts/cli/{support/startup-signal.ts → startup-signal.ts} +23 -23
  137. package/scripts/cli/tray/index.ts +62 -12
  138. package/scripts/cli/{command/tray.ts → tray.ts} +208 -208
  139. package/scripts/cli/update.ts +40 -0
  140. package/scripts/cli/version.ts +67 -0
  141. package/scripts/cli/{ui/wordmark.ts → wordmark.ts} +58 -58
  142. package/src/boot/app.ts +11 -7
  143. package/src/boot/dependencies.ts +50 -19
  144. package/src/boot/timeout.ts +2 -13
  145. package/src/config/cooldowns.ts +186 -138
  146. package/src/config/network.ts +8 -14
  147. package/src/config/providers.ts +7 -10
  148. package/src/config/runtime.ts +76 -76
  149. package/src/config/security.ts +15 -0
  150. package/src/config/spec.ts +52 -7
  151. package/src/config/telemetry.ts +26 -0
  152. package/src/config/timeouts.ts +0 -6
  153. package/src/console/admin/model-abuse/contracts.ts +28 -6
  154. package/src/console/admin/model-abuse/routes.ts +56 -3
  155. package/src/console/api-keys/bucket-spend.ts +15 -4
  156. package/src/console/api-keys/contracts.ts +410 -414
  157. package/src/console/api-keys/routes.ts +881 -880
  158. package/src/console/auth/credentials.ts +3 -3
  159. package/src/console/auth/setup-service.ts +2 -2
  160. package/src/console/backup/contracts.ts +15 -2
  161. package/src/console/backup/export-endpoint.ts +6 -2
  162. package/src/console/backup/nine-router.ts +2 -5
  163. package/src/console/backup/service.ts +4 -0
  164. package/src/console/backup/store.ts +192 -29
  165. package/src/console/backup/validate.ts +3 -45
  166. package/src/console/cli-tools/fs-ops.ts +487 -487
  167. package/src/console/console-router-guards.ts +1 -1
  168. package/src/console/console-router.ts +8 -0
  169. package/src/console/data-management/contracts.ts +100 -0
  170. package/src/console/data-management/operations.ts +234 -0
  171. package/src/console/data-management/routes.ts +103 -0
  172. package/src/console/domain-registration.ts +45 -1
  173. package/src/console/observability/contracts.ts +524 -485
  174. package/src/console/observability/live.ts +22 -0
  175. package/src/console/observability/store.ts +1138 -1018
  176. package/src/console/observability/usage-dimensions.ts +4 -4
  177. package/src/console/providers/catalog/catalog-projections.ts +6 -7
  178. package/src/console/providers/catalog/contracts.ts +6 -0
  179. package/src/console/providers/catalog/model-operations.ts +5 -2
  180. package/src/console/providers/catalog/provider-operations.ts +24 -4
  181. package/src/console/providers/catalog/public-model-store.ts +8 -11
  182. package/src/console/providers/catalog/routes.ts +7 -0
  183. package/src/console/providers/catalog/store.ts +1553 -1532
  184. package/src/console/providers/oauth/routes.ts +8 -12
  185. package/src/console/quota/account-quota-shared.ts +52 -39
  186. package/src/console/quota/account-quota-tenant.ts +6 -6
  187. package/src/console/quota/quota-view.ts +1 -1
  188. package/src/console/quota/refresh.ts +441 -429
  189. package/src/console/remote-support/contracts.ts +61 -0
  190. package/src/console/remote-support/routes.ts +65 -0
  191. package/src/console/remote-support/service.ts +207 -0
  192. package/src/console/response-compression.ts +0 -3
  193. package/src/console/routing/pools/import-parse.ts +206 -0
  194. package/src/console/routing/pools/operations.ts +543 -501
  195. package/src/console/routing/pools/probe-result.ts +3 -2
  196. package/src/console/routing/pools/probe-verdict.ts +132 -132
  197. package/src/console/routing/pools/routes.ts +181 -164
  198. package/src/console/routing/pools/store.ts +8 -8
  199. package/src/console/settings/contracts.ts +95 -2
  200. package/src/console/settings/store.ts +81 -3
  201. package/src/console/shared/cursor.ts +8 -8
  202. package/src/console/shared/errors.ts +1 -3
  203. package/src/console/updates/routes.ts +16 -0
  204. package/src/main.ts +10 -0
  205. package/src/network/http2-bidi.ts +2 -4
  206. package/src/network/pool/agent.ts +478 -478
  207. package/src/network/pool/selector.ts +5 -4
  208. package/src/network/retry.ts +1 -2
  209. package/src/network/ssrf.ts +5 -4
  210. package/src/observability/event-compaction.ts +317 -0
  211. package/src/observability/event-day-index.ts +440 -0
  212. package/src/observability/event-log.ts +155 -6
  213. package/src/observability/logger.ts +6 -6
  214. package/src/observability/payload-capture.ts +158 -33
  215. package/src/observability/payload-store.ts +82 -61
  216. package/src/observability/redaction.ts +5 -6
  217. package/src/observability/request-notify.ts +43 -0
  218. package/src/observability/telemetry-status.ts +8 -9
  219. package/src/observability/token-speed.ts +5 -4
  220. package/src/persistence/db-mode.ts +7 -7
  221. package/src/persistence/health-event-flush.ts +36 -0
  222. package/src/persistence/readiness.ts +1 -1
  223. package/src/persistence/schema.ts +75 -29
  224. package/src/persistence/shared-cache.ts +1 -2
  225. package/src/persistence/stores/api-key-store.ts +18 -6
  226. package/src/persistence/stores/share-store.ts +4 -3
  227. package/src/persistence/stores/telemetry-store.ts +33 -11
  228. package/src/protocol/canonical-model.ts +11 -3
  229. package/src/protocol/codecs.ts +139 -6
  230. package/src/protocol/gateway-error.ts +6 -4
  231. package/src/protocol/image-clamp.ts +19 -1
  232. package/src/protocol/registry.ts +8 -0
  233. package/src/protocol/request/chat.ts +21 -1
  234. package/src/protocol/request/codex.ts +10 -1
  235. package/src/protocol/request/gemini.ts +9 -2
  236. package/src/protocol/request/messages.ts +18 -7
  237. package/src/protocol/request/responses.ts +9 -1
  238. package/src/protocol/response/chat.ts +34 -4
  239. package/src/protocol/response/codex.ts +7 -4
  240. package/src/protocol/response/dsml.ts +140 -28
  241. package/src/protocol/response/responses.ts +11 -5
  242. package/src/protocol/stream-error-frames.ts +4 -5
  243. package/src/providers/adapters/compatible-adapter.ts +3 -0
  244. package/src/providers/authentication/jwt-validator.ts +9 -10
  245. package/src/providers/authentication/oauth-refresh-service.ts +9 -11
  246. package/src/providers/bundled-discovery-providers.ts +122 -119
  247. package/src/providers/configured-openai-providers.ts +4 -4
  248. package/src/providers/default-registry.ts +5 -4
  249. package/src/providers/discovery/catalog/catalog-blob.ts +18 -18
  250. package/src/providers/discovery/catalog/catalog-format.ts +3 -6
  251. package/src/providers/discovery/models-dev-catalog.ts +354 -354
  252. package/src/providers/discovery/openai-model-discovery.ts +5 -0
  253. package/src/providers/discovery/openrouter-free-roster.ts +15 -17
  254. package/src/providers/discovery/probe-phases.ts +4 -4
  255. package/src/providers/discovery/probe-wire.ts +6 -13
  256. package/src/providers/discovery/probing-service.ts +1117 -1045
  257. package/src/providers/integrations/alibabacp/alibabacp.ts +1 -1
  258. package/src/providers/integrations/antigravity/antigravity-quota.ts +5 -7
  259. package/src/providers/integrations/buddy/buddy-chat-shared.ts +23 -2
  260. package/src/providers/integrations/buddy/buddy-checkin.ts +1 -4
  261. package/src/providers/integrations/buddy/buddy-cli-headers.ts +23 -6
  262. package/src/providers/integrations/buddy/buddy-conversation-headers.ts +0 -1
  263. package/src/providers/integrations/buddy/buddy-identity.ts +52 -0
  264. package/src/providers/integrations/buddy/buddy-media.ts +2 -2
  265. package/src/providers/integrations/buddy/buddy-usage-store.ts +3 -4
  266. package/src/providers/integrations/buddy/codebuddy/quota.ts +2 -13
  267. package/src/providers/integrations/buddy/codebuddy/shared.ts +118 -115
  268. package/src/providers/integrations/buddy/workbuddy/quota.ts +0 -11
  269. package/src/providers/integrations/buddy/workbuddy/shared.ts +5 -2
  270. package/src/providers/integrations/claude/claude.ts +608 -608
  271. package/src/providers/integrations/cline/cline-quota.ts +27 -20
  272. package/src/providers/integrations/cline/cline.ts +537 -539
  273. package/src/providers/integrations/codex/codex-headers.ts +25 -39
  274. package/src/providers/integrations/codex/codex-oauth.ts +1 -1
  275. package/src/providers/integrations/codex/codex.ts +2 -29
  276. package/src/providers/integrations/commandcode/commandcode.ts +674 -675
  277. package/src/providers/integrations/commandcode/discovery.ts +117 -116
  278. package/src/providers/integrations/cursor/cursor-auth.ts +418 -418
  279. package/src/providers/integrations/cursor/cursor-quota.ts +309 -0
  280. package/src/providers/integrations/cursor/cursor-request.ts +616 -471
  281. package/src/providers/integrations/cursor/cursor-session-manager.ts +319 -281
  282. package/src/providers/integrations/cursor/cursor.ts +1333 -960
  283. package/src/providers/integrations/cursor/protocol/messages.ts +25 -0
  284. package/src/providers/integrations/factory/factory-models.ts +3 -5
  285. package/src/providers/integrations/factory/factory.ts +8 -1
  286. package/src/providers/integrations/gemini.ts +6 -1
  287. package/src/providers/integrations/grok/grok-install-id.ts +2 -3
  288. package/src/providers/integrations/grok/grok-quota.ts +279 -177
  289. package/src/providers/integrations/grok/grok-ratelimit-store.ts +73 -0
  290. package/src/providers/integrations/grok/grok-ratelimit.ts +218 -0
  291. package/src/providers/integrations/grok/grok.ts +6 -7
  292. package/src/providers/integrations/hermes.ts +14 -18
  293. package/src/providers/integrations/kiro/kiro-discovery.ts +1 -1
  294. package/src/providers/integrations/kiro/kiro.ts +1 -1
  295. package/src/providers/integrations/mistral.ts +6 -6
  296. package/src/providers/integrations/opencode/fingerprint.ts +3 -3
  297. package/src/providers/integrations/opencode/provider.ts +19 -20
  298. package/src/providers/integrations/qoder/qoder-catalog.ts +450 -450
  299. package/src/providers/integrations/qoder/qoder-oauth.ts +388 -310
  300. package/src/providers/integrations/qoder/qoder-quota.ts +192 -0
  301. package/src/providers/integrations/qoder/qoder.ts +884 -865
  302. package/src/providers/integrations/shared/connect.ts +6 -6
  303. package/src/providers/integrations/trae/trae-catalog.ts +1 -2
  304. package/src/providers/integrations/trae/trae-quota.ts +384 -0
  305. package/src/providers/integrations/trae/trae.ts +2 -2
  306. package/src/providers/integrations/zcode/oauth.ts +6 -1
  307. package/src/providers/operations/account-health-service.ts +75 -27
  308. package/src/providers/operations/account-reset-service.ts +1 -1
  309. package/src/providers/operations/client-versions.ts +6 -6
  310. package/src/providers/operations/custom-cli-headers.ts +3 -4
  311. package/src/providers/operations/provider-catalog-seeder.ts +95 -95
  312. package/src/providers/operations/provider-catalog-service.ts +3 -4
  313. package/src/providers/provider-contracts.ts +7 -0
  314. package/src/providers/provider-metadata.ts +10 -8
  315. package/src/providers/provider-registry.ts +6 -7
  316. package/src/providers/provider-usage.ts +1 -1
  317. package/src/providers/quota/quota-support.ts +8 -0
  318. package/src/security/abuse.ts +15 -10
  319. package/src/security/access-control.ts +144 -146
  320. package/src/security/admission/contracts.ts +11 -0
  321. package/src/security/admission/in-memory-store.ts +4 -3
  322. package/src/security/admission/lease-sweep.ts +6 -6
  323. package/src/security/admission/reasons.ts +5 -1
  324. package/src/security/admission/redis-store.ts +211 -210
  325. package/src/security/admission/service.ts +4 -0
  326. package/src/security/api-key-auth.ts +27 -17
  327. package/src/security/ip-boundary.ts +155 -15
  328. package/src/security/model-abuse.ts +366 -94
  329. package/src/security/outbound-headers.ts +3 -4
  330. package/src/shared/sleep.ts +4 -0
  331. package/src/transport/dispatch/attempt-finalize.ts +47 -5
  332. package/src/transport/dispatch/attempt-loop.ts +531 -530
  333. package/src/transport/dispatch/generation.ts +6 -0
  334. package/src/transport/dispatch/proxy-request.ts +24 -1
  335. package/src/transport/dispatch/ratelimit-capture.ts +37 -0
  336. package/src/transport/dispatch/stream-bridge.ts +1 -3
  337. package/src/transport/dispatch/streaming/finalize.ts +1 -1
  338. package/src/transport/dispatch/streaming/stream.ts +19 -6
  339. package/src/transport/dispatch/upstream.ts +9 -0
  340. package/src/transport/failure-policy.ts +162 -8
  341. package/src/transport/guards/auth-guard.ts +7 -4
  342. package/src/transport/guards/concurrency-guard.ts +0 -5
  343. package/src/transport/guards/error-lifecycle.ts +13 -6
  344. package/src/transport/guards/pipeline.ts +15 -3
  345. package/src/transport/guards/request-context.ts +8 -2
  346. package/src/transport/request/inflight.ts +5 -6
  347. package/src/transport/request/preparer.ts +9 -8
  348. package/src/transport/request/state.ts +21 -8
  349. package/src/transport/request/tenant-preferences.ts +60 -0
  350. package/src/transport/routing/admission.ts +4 -0
  351. package/src/transport/routing/route-catalog.ts +65 -10
  352. package/src/transport/routing/route-model.ts +46 -14
  353. package/src/transport/routing/router.ts +21 -14
  354. package/src/transport/surface/completion.ts +2 -3
  355. package/src/transport/surface/dialects.ts +5 -6
  356. package/src/transport/surface/responses/encode.ts +38 -0
  357. package/src/transport/surface/responses/parse.ts +4 -0
  358. package/src/transport/translation/capabilities.ts +2 -3
  359. package/src/transport/translation/quirks.ts +3 -3
  360. package/src/workers/buddy-growth-tasks.ts +5 -10
  361. package/src/workers/daily-checkin.ts +17 -23
  362. package/src/workers/quota-refresh-sweep.ts +26 -26
  363. package/dist/dashboard/assets/BannedPage-hfCzi-Ux.js.gz +0 -0
  364. package/dist/dashboard/assets/CliToolDetailPage-DLGqYEwI.js +0 -1
  365. package/dist/dashboard/assets/CliToolDetailPage-DLGqYEwI.js.gz +0 -0
  366. package/dist/dashboard/assets/CliToolsPage-DEDyKBlC.js.gz +0 -0
  367. package/dist/dashboard/assets/CombosPage-CfuYiQ8Z.js +0 -1
  368. package/dist/dashboard/assets/CombosPage-CfuYiQ8Z.js.gz +0 -0
  369. package/dist/dashboard/assets/ConsoleLogPage-bsLuZmYH.js.gz +0 -0
  370. package/dist/dashboard/assets/CustomizationPage-_UWhwziJ.js.gz +0 -0
  371. package/dist/dashboard/assets/HealthEventsModal-Cjfc5WFS.js +0 -1
  372. package/dist/dashboard/assets/HealthEventsModal-Cjfc5WFS.js.gz +0 -0
  373. package/dist/dashboard/assets/LoginPage-DDVIfZGW.js.gz +0 -0
  374. package/dist/dashboard/assets/ModelPicker-CWkTUtpX.js +0 -1
  375. package/dist/dashboard/assets/ModelPicker-CWkTUtpX.js.gz +0 -0
  376. package/dist/dashboard/assets/OverviewPage-B53YYMng.js +0 -1
  377. package/dist/dashboard/assets/OverviewPage-B53YYMng.js.gz +0 -0
  378. package/dist/dashboard/assets/ProviderDetailPage-B0KaAQf0.js +0 -3
  379. package/dist/dashboard/assets/ProviderDetailPage-B0KaAQf0.js.gz +0 -0
  380. package/dist/dashboard/assets/ProvidersPage-Dx31DgIO.js +0 -1
  381. package/dist/dashboard/assets/ProvidersPage-Dx31DgIO.js.gz +0 -0
  382. package/dist/dashboard/assets/ProxyPage-BpnR4kXT.js +0 -7
  383. package/dist/dashboard/assets/ProxyPage-BpnR4kXT.js.gz +0 -0
  384. package/dist/dashboard/assets/QuotaPage-BpOQCnun.js +0 -1
  385. package/dist/dashboard/assets/QuotaPage-BpOQCnun.js.gz +0 -0
  386. package/dist/dashboard/assets/SettingsPage-DV1eo2J_.js +0 -1
  387. package/dist/dashboard/assets/SettingsPage-DV1eo2J_.js.gz +0 -0
  388. package/dist/dashboard/assets/SetupPage-D6pmQ86L.js.gz +0 -0
  389. package/dist/dashboard/assets/SortableList-DWvKiuSl.js.gz +0 -0
  390. package/dist/dashboard/assets/StudioPage-CmkUrgdj.js.gz +0 -0
  391. package/dist/dashboard/assets/UsagePage-B47uOydr.js +0 -56
  392. package/dist/dashboard/assets/UsagePage-B47uOydr.js.gz +0 -0
  393. package/dist/dashboard/assets/api-keys-uNywYlfv.js +0 -1
  394. package/dist/dashboard/assets/api-keys-uNywYlfv.js.gz +0 -0
  395. package/dist/dashboard/assets/app-DxivpfYG.css +0 -2
  396. package/dist/dashboard/assets/app-DxivpfYG.css.gz +0 -0
  397. package/dist/dashboard/assets/assert-NHwYq95t.js.gz +0 -0
  398. package/dist/dashboard/assets/badge-6uzDDvqs.js.gz +0 -0
  399. package/dist/dashboard/assets/banner-4_WNg2WZ.js.gz +0 -0
  400. package/dist/dashboard/assets/card--AtFedAq.js.gz +0 -0
  401. package/dist/dashboard/assets/cli-tools-B3e7dx3N.js.gz +0 -0
  402. package/dist/dashboard/assets/clipboard-button-NreUx6c8.js.gz +0 -0
  403. package/dist/dashboard/assets/dialog-BtpsJU98.js +0 -1
  404. package/dist/dashboard/assets/dialog-BtpsJU98.js.gz +0 -0
  405. package/dist/dashboard/assets/download-A0SW9OCW.js +0 -1
  406. package/dist/dashboard/assets/download-A0SW9OCW.js.gz +0 -0
  407. package/dist/dashboard/assets/drawer-WwwtzihF.js +0 -1
  408. package/dist/dashboard/assets/drawer-WwwtzihF.js.gz +0 -0
  409. package/dist/dashboard/assets/entry-CkryrIea.js +0 -4
  410. package/dist/dashboard/assets/entry-CkryrIea.js.gz +0 -0
  411. package/dist/dashboard/assets/entry-TJIye_jT.js +0 -1
  412. package/dist/dashboard/assets/entry-TJIye_jT.js.gz +0 -0
  413. package/dist/dashboard/assets/entry-jy9_4-Ke.js +0 -1
  414. package/dist/dashboard/assets/entry-jy9_4-Ke.js.gz +0 -0
  415. package/dist/dashboard/assets/fingerprint-pattern-BWuHtbW4.js.gz +0 -0
  416. package/dist/dashboard/assets/github-badge-D_eM3IgG.js.gz +0 -0
  417. package/dist/dashboard/assets/index-BydyKaDz.js.gz +0 -0
  418. package/dist/dashboard/assets/inline-BcNGF8Gr.js.gz +0 -0
  419. package/dist/dashboard/assets/input-BG41et9G.js.gz +0 -0
  420. package/dist/dashboard/assets/layout-C8mer_Ak.js +0 -1
  421. package/dist/dashboard/assets/layout-C8mer_Ak.js.gz +0 -0
  422. package/dist/dashboard/assets/list-row-Gh_W4OR_.js.gz +0 -0
  423. package/dist/dashboard/assets/live-BJGYRBnR.js.gz +0 -0
  424. package/dist/dashboard/assets/model-bans-DeVLDA7I.js +0 -1
  425. package/dist/dashboard/assets/model-bans-DeVLDA7I.js.gz +0 -0
  426. package/dist/dashboard/assets/mutation-BkoLM5Ba.js.gz +0 -0
  427. package/dist/dashboard/assets/numbers-Bs4LOOuf.js +0 -2
  428. package/dist/dashboard/assets/numbers-Bs4LOOuf.js.gz +0 -0
  429. package/dist/dashboard/assets/query-keys-D7wX2vM5.js.gz +0 -0
  430. package/dist/dashboard/assets/routing-CcusqXA7.js.gz +0 -0
  431. package/dist/dashboard/assets/select-BO6h8yNv.js.gz +0 -0
  432. package/dist/dashboard/assets/settings-mpmXTgUa.js.gz +0 -0
  433. package/dist/dashboard/assets/sparkles-BRaRUxAU.js +0 -1
  434. package/dist/dashboard/assets/sparkles-BRaRUxAU.js.gz +0 -0
  435. package/dist/dashboard/assets/stack-DwLmjUFK.js.gz +0 -0
  436. package/dist/dashboard/assets/state-DYdQmdAl.js.gz +0 -0
  437. package/dist/dashboard/assets/switch-CidSfbG7.js +0 -1
  438. package/dist/dashboard/assets/switch-CidSfbG7.js.gz +0 -0
  439. package/dist/dashboard/assets/terminal-Bd4ee8TA.js +0 -1
  440. package/dist/dashboard/assets/terminal-Bd4ee8TA.js.gz +0 -0
  441. package/dist/dashboard/assets/tokens-DDxMbSRL.js +0 -1
  442. package/dist/dashboard/assets/tokens-DDxMbSRL.js.gz +0 -0
  443. package/dist/dashboard/assets/use-presence-BSZEBx7U.js +0 -1
  444. package/dist/dashboard/assets/use-presence-BSZEBx7U.js.gz +0 -0
  445. package/dist/dashboard/assets/useMutation-w4u4CVue.js.gz +0 -0
  446. package/scripts/cli/command/admin.ts +0 -65
  447. package/scripts/cli/command/doctor.ts +0 -257
  448. package/scripts/cli/command/update.ts +0 -23
  449. package/scripts/cli/gateway/drain-token.ts +0 -78
  450. package/scripts/cli/gateway/instance-lock.ts +0 -105
  451. package/scripts/cli/gateway/stop.ts +0 -51
  452. package/scripts/cli/support/bun.ts +0 -27
  453. package/scripts/cli/support/version.ts +0 -37
  454. /package/dist/dashboard/assets/{app-CafWtVXA.js → app-BIt446yE.js} +0 -0
  455. /package/dist/dashboard/assets/{app-CafWtVXA.js.gz → app-BIt446yE.js.gz} +0 -0
  456. /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,
@@ -69,7 +69,7 @@ export class ApiKeyStoreUnavailableError extends Error {
69
69
  * Injected rather than imported: this module is on the auth path and must not
70
70
  * depend on the persistence/write layer it would otherwise pull in. Set once at
71
71
  * boot (`boot/dependencies.ts`); until then it reads as zero, which is the
72
- * value the code has always used.
72
+ * value the code uses when no source is installed.
73
73
  */
74
74
  let pendingUsage: (apiKeyId: string) => number = () => 0;
75
75
 
@@ -85,11 +85,11 @@ export function setPendingUsageSource(source: (apiKeyId: string) => number): voi
85
85
  pendingUsage = source;
86
86
  }
87
87
  /**
88
- * Negative-result fast path. Unknown/revoked tokens previously hit the DB on
89
- * every request; now a repeat miss within 1.5 s costs one HMAC + one map
90
- * lookup and no query. Keyed by the full HMAC: a cheap pre-hash would let a
91
- * crafted collision reject someone else's valid key, while the HMAC key can
92
- * only match its own token. Bound below beside the positive cache.
88
+ * Negative-result fast path. A repeat miss for an unknown/revoked token within
89
+ * 1.5 s costs one HMAC + one map lookup and no query. Keyed by the full HMAC: a
90
+ * cheap pre-hash would let a crafted collision reject someone else's valid key,
91
+ * while the HMAC key can only match its own token. Bound below beside the
92
+ * positive cache.
93
93
  */
94
94
 
95
95
  /**
@@ -136,8 +136,8 @@ function evictAuthNegativeOldest(): void {
136
136
  * mutation that changes a key's scopes, limits, or revoked state.
137
137
  */
138
138
  export function invalidateApiKeyCache(keyId?: string): void {
139
- // A mutation may mint the previously-unknown token, so negative entries
140
- // must go with the positive ones.
139
+ // A mutation may mint a token the negative cache has already rejected, so
140
+ // negative entries must go with the positive ones.
141
141
  authNegativeCache.clear();
142
142
  if (keyId === undefined) {
143
143
  authCache.clear();
@@ -314,13 +314,23 @@ export interface ResolvedApiKey {
314
314
  * surface it hit. Unknown, revoked and expired stay deliberately
315
315
  * indistinguishable — distinguishing them would let a caller confirm that a
316
316
  * guessed token once existed — so there is no variant carrying a reason.
317
- *
318
- * The rotate instruction is unconditional for the same reason. The key that
319
- * fails is usually the one seeded at first boot, which cannot be deleted and
320
- * only rotates, and an operator hitting this on a personal key loses nothing by
321
- * being told the same thing.
322
317
  */
318
+ const INVALID_KEY_MESSAGE = "Invalid API key. Check the key you are sending, or create a new one in API Keys and send it as: Authorization: Bearer <key>.";
319
+
323
320
  export function unknownApiKeyError(): GatewayError {
321
+ return new GatewayError("authentication_failed", 401, INVALID_KEY_MESSAGE);
322
+ }
323
+
324
+ /**
325
+ * The 401 for the public model surface specifically.
326
+ *
327
+ * Model Studio is where the seeded default key is normally used, and that key
328
+ * cannot be deleted — it only rotates. Naming rotation there is actionable. The
329
+ * same sentence on a proxy request was misleading instead: a revoked or mistyped
330
+ * personal key was answered with an instruction that could not apply to it,
331
+ * sending the operator to rotate a key they were never using.
332
+ */
333
+ export function modelsListApiKeyError(): GatewayError {
324
334
  return new GatewayError(
325
335
  "authentication_failed",
326
336
  401,
@@ -430,9 +440,9 @@ export async function resolveApiKeyAuthorization(
430
440
  row.parentKeyId !== null ? await store.findActiveById(row.parentKeyId) : undefined;
431
441
  // Share children authenticate as themselves but inherit the template's live
432
442
  if (row.parentKeyId !== null && parent === undefined) return undefined;
433
- // policy. Copy-on-issue alone went stale the moment an operator tightened a
434
- // limit or blocked a client router on the parent — existing recipients kept
435
- // the old row values forever. Reading the parent here makes denylist /
443
+ // policy. Copy-on-issue alone goes stale the moment an operator tightens a
444
+ // limit or blocks a client router on the parent — existing recipients would
445
+ // keep the issued row values forever. Reading the parent here makes denylist /
436
446
  // one-time / recurring edits apply on the next cache miss.
437
447
  const policy = parent ?? row;
438
448
  // Only a `share` parent can have children, and children are the only thing
@@ -488,7 +498,7 @@ export async function resolveApiKeyAuthorization(
488
498
  // That figure moves constantly, and the module already documents the snapshot
489
499
  // as advisory on that axis (spending is enforced by the admission counter),
490
500
  // so a peer serving it stale for up to the TTL is the same tradeoff the
491
- // in-process cache has always made.
501
+ // in-process cache makes.
492
502
  void writeShared("auth", hash, JSON.stringify(value), AUTH_CACHE_TTL_MS);
493
503
  return value;
494
504
  }