cartethyia 2.7.3 → 2.7.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (352) hide show
  1. package/dist/dashboard/assets/{BannedPage-2TJD37Hb.js → BannedPage-ChG5svFb.js} +1 -1
  2. package/dist/dashboard/assets/BannedPage-ChG5svFb.js.gz +0 -0
  3. package/dist/dashboard/assets/{CliToolDetailPage-xefJNRmJ.js → CliToolDetailPage-BIcWQun7.js} +1 -1
  4. package/dist/dashboard/assets/CliToolDetailPage-BIcWQun7.js.gz +0 -0
  5. package/dist/dashboard/assets/{CliToolsPage-Rj9iHQ3F.js → CliToolsPage-Bf2VelDe.js} +1 -1
  6. package/dist/dashboard/assets/CliToolsPage-Bf2VelDe.js.gz +0 -0
  7. package/dist/dashboard/assets/{CombosPage-JEbYJFxw.js → CombosPage-DCgm5g6g.js} +1 -1
  8. package/dist/dashboard/assets/CombosPage-DCgm5g6g.js.gz +0 -0
  9. package/dist/dashboard/assets/ConsoleLogPage-a8mD0QF1.js +1 -0
  10. package/dist/dashboard/assets/ConsoleLogPage-a8mD0QF1.js.gz +0 -0
  11. package/dist/dashboard/assets/CustomizationPage-ggeMSjte.js +1 -0
  12. package/dist/dashboard/assets/CustomizationPage-ggeMSjte.js.gz +0 -0
  13. package/dist/dashboard/assets/HealthEventsModal-DzSWzE_j.js +1 -0
  14. package/dist/dashboard/assets/HealthEventsModal-DzSWzE_j.js.gz +0 -0
  15. package/dist/dashboard/assets/{LoginPage-C4w7vea-.js → LoginPage-U6AKaJcF.js} +1 -1
  16. package/dist/dashboard/assets/LoginPage-U6AKaJcF.js.gz +0 -0
  17. package/dist/dashboard/assets/{ModelPicker-BtPcAfot.js → ModelPicker-BMS2t8BA.js} +1 -1
  18. package/dist/dashboard/assets/ModelPicker-BMS2t8BA.js.gz +0 -0
  19. package/dist/dashboard/assets/OverviewPage-D2D4IiHl.js +1 -0
  20. package/dist/dashboard/assets/OverviewPage-D2D4IiHl.js.gz +0 -0
  21. package/dist/dashboard/assets/ProviderDetailPage-CrNf7j_X.js +5 -0
  22. package/dist/dashboard/assets/ProviderDetailPage-CrNf7j_X.js.gz +0 -0
  23. package/dist/dashboard/assets/ProvidersPage-BSDbquc1.js +1 -0
  24. package/dist/dashboard/assets/ProvidersPage-BSDbquc1.js.gz +0 -0
  25. package/dist/dashboard/assets/ProxyPage-lsrx7NYg.js +9 -0
  26. package/dist/dashboard/assets/ProxyPage-lsrx7NYg.js.gz +0 -0
  27. package/dist/dashboard/assets/QuotaPage-L0skzML8.js +1 -0
  28. package/dist/dashboard/assets/QuotaPage-L0skzML8.js.gz +0 -0
  29. package/dist/dashboard/assets/SettingsPage-CmAbjRHW.js +1 -0
  30. package/dist/dashboard/assets/SettingsPage-CmAbjRHW.js.gz +0 -0
  31. package/dist/dashboard/assets/{SetupPage-BQS0Rx_u.js → SetupPage-f0DE1h0-.js} +1 -1
  32. package/dist/dashboard/assets/SetupPage-f0DE1h0-.js.gz +0 -0
  33. package/dist/dashboard/assets/{SortableList-BAL5npBC.js → SortableList-ClzpwyET.js} +1 -1
  34. package/dist/dashboard/assets/SortableList-ClzpwyET.js.gz +0 -0
  35. package/dist/dashboard/assets/{StudioPage-TP7voAQ0.js → StudioPage-Cq5tW7hW.js} +3 -3
  36. package/dist/dashboard/assets/StudioPage-Cq5tW7hW.js.gz +0 -0
  37. package/dist/dashboard/assets/UsagePage-Dfc9rWQn.js +56 -0
  38. package/dist/dashboard/assets/UsagePage-Dfc9rWQn.js.gz +0 -0
  39. package/dist/dashboard/assets/api-keys-C38iXoO4.js +1 -0
  40. package/dist/dashboard/assets/api-keys-C38iXoO4.js.gz +0 -0
  41. package/dist/dashboard/assets/app-a159k0ub.css +2 -0
  42. package/dist/dashboard/assets/app-a159k0ub.css.gz +0 -0
  43. package/dist/dashboard/assets/assert-DbRA8EG2.js +15 -0
  44. package/dist/dashboard/assets/assert-DbRA8EG2.js.gz +0 -0
  45. package/dist/dashboard/assets/{badge-B_DMpMJN.js → badge-D9CS6dZk.js} +1 -1
  46. package/dist/dashboard/assets/badge-D9CS6dZk.js.gz +0 -0
  47. package/dist/dashboard/assets/{banner-DMrMPmth.js → banner-DympBNq4.js} +1 -1
  48. package/dist/dashboard/assets/banner-DympBNq4.js.gz +0 -0
  49. package/dist/dashboard/assets/{card-Be5Uzamx.js → card-ibRWU_uT.js} +1 -1
  50. package/dist/dashboard/assets/card-ibRWU_uT.js.gz +0 -0
  51. package/dist/dashboard/assets/cli-tools-63qBGaRw.js +1 -0
  52. package/dist/dashboard/assets/cli-tools-63qBGaRw.js.gz +0 -0
  53. package/dist/dashboard/assets/{clipboard-button-DOfrVmsi.js → clipboard-button-DJm-9pz7.js} +1 -1
  54. package/dist/dashboard/assets/clipboard-button-DJm-9pz7.js.gz +0 -0
  55. package/dist/dashboard/assets/{dialog-C9omdjQt.js → dialog-8fiE6tec.js} +1 -1
  56. package/dist/dashboard/assets/dialog-8fiE6tec.js.gz +0 -0
  57. package/dist/dashboard/assets/{drawer-Bm8WKaGY.js → drawer-CqGYeOrd.js} +1 -1
  58. package/dist/dashboard/assets/drawer-CqGYeOrd.js.gz +0 -0
  59. package/dist/dashboard/assets/{entry-q5HwZ8nN.js → entry-BSAsYsmV.js} +1 -1
  60. package/dist/dashboard/assets/entry-BSAsYsmV.js.gz +0 -0
  61. package/dist/dashboard/assets/entry-CiIQULZD.js +4 -0
  62. package/dist/dashboard/assets/entry-CiIQULZD.js.gz +0 -0
  63. package/dist/dashboard/assets/entry-Daz0lJ-W.js +1 -0
  64. package/dist/dashboard/assets/entry-Daz0lJ-W.js.gz +0 -0
  65. package/dist/dashboard/assets/{formatters-BTScrQpe.js → formatters-D-MTJxMr.js} +1 -1
  66. package/dist/dashboard/assets/formatters-D-MTJxMr.js.gz +0 -0
  67. package/dist/dashboard/assets/{github-badge-Orjuzemr.js → github-badge-CPK47xQx.js} +1 -1
  68. package/dist/dashboard/assets/github-badge-CPK47xQx.js.gz +0 -0
  69. package/dist/dashboard/assets/{index-cPrX5vn6.js → index-DBNPOfhV.js} +2 -2
  70. package/dist/dashboard/assets/index-DBNPOfhV.js.gz +0 -0
  71. package/dist/dashboard/assets/{inline-Dvux8w-0.js → inline-BT0zntEr.js} +1 -1
  72. package/dist/dashboard/assets/inline-BT0zntEr.js.gz +0 -0
  73. package/dist/dashboard/assets/{input-CU1Rgf3p.js → input-Da2ZViWg.js} +1 -1
  74. package/dist/dashboard/assets/input-Da2ZViWg.js.gz +0 -0
  75. package/dist/dashboard/assets/{layout-C_7A6H55.js → layout-BXNeLw6M.js} +1 -1
  76. package/dist/dashboard/assets/layout-BXNeLw6M.js.gz +0 -0
  77. package/dist/dashboard/assets/{list-row-CzFvogbj.js → list-row-BycOrrIV.js} +1 -1
  78. package/dist/dashboard/assets/list-row-BycOrrIV.js.gz +0 -0
  79. package/dist/dashboard/assets/live-DCQNZpF7.js +1 -0
  80. package/dist/dashboard/assets/live-DCQNZpF7.js.gz +0 -0
  81. package/dist/dashboard/assets/minimize-2-BTmMTK4o.js +1 -0
  82. package/dist/dashboard/assets/minimize-2-BTmMTK4o.js.gz +0 -0
  83. package/dist/dashboard/assets/{mutation-JJGQm8TD.js → mutation-Rr0TIvcj.js} +1 -1
  84. package/dist/dashboard/assets/mutation-Rr0TIvcj.js.gz +0 -0
  85. package/dist/dashboard/assets/network-9Yj2ZyLo.js +1 -0
  86. package/dist/dashboard/assets/network-9Yj2ZyLo.js.gz +0 -0
  87. package/dist/dashboard/assets/{numbers-BNDZnsUE.js → numbers-CEagj6ZZ.js} +2 -2
  88. package/dist/dashboard/assets/numbers-CEagj6ZZ.js.gz +0 -0
  89. package/dist/dashboard/assets/{query-keys-CLaNz8Xz.js → query-keys-BloKtaXJ.js} +1 -1
  90. package/dist/dashboard/assets/query-keys-BloKtaXJ.js.gz +0 -0
  91. package/dist/dashboard/assets/quota-Caw7wqci.js +1 -0
  92. package/dist/dashboard/assets/quota-Caw7wqci.js.gz +0 -0
  93. package/dist/dashboard/assets/routing-CO9zCUus.js +1 -0
  94. package/dist/dashboard/assets/routing-CO9zCUus.js.gz +0 -0
  95. package/dist/dashboard/assets/{select-B3iTetHN.js → select-BIYIBkMU.js} +2 -2
  96. package/dist/dashboard/assets/select-BIYIBkMU.js.gz +0 -0
  97. package/dist/dashboard/assets/settings-jS3-PNSA.js +1 -0
  98. package/dist/dashboard/assets/settings-jS3-PNSA.js.gz +0 -0
  99. package/dist/dashboard/assets/{stack-KrOrm7JZ.js → stack-DfgnFDWY.js} +1 -1
  100. package/dist/dashboard/assets/stack-DfgnFDWY.js.gz +0 -0
  101. package/dist/dashboard/assets/{state-DLS7IDMy.js → state-CkfdYWel.js} +1 -1
  102. package/dist/dashboard/assets/state-CkfdYWel.js.gz +0 -0
  103. package/dist/dashboard/assets/{switch-42ItvbAs.js → switch-SYm237HG.js} +1 -1
  104. package/dist/dashboard/assets/switch-SYm237HG.js.gz +0 -0
  105. package/dist/dashboard/assets/{useMutation-Dmnfh1Ue.js → useMutation-BQqCRNIi.js} +1 -1
  106. package/dist/dashboard/assets/useMutation-BQqCRNIi.js.gz +0 -0
  107. package/dist/dashboard/index.html +2 -2
  108. package/dist/dashboard/index.html.gz +0 -0
  109. package/dist/dashboard/precompressed-assets.json +55 -54
  110. package/migrations/full-postgres/0027_qoder_identity_genuine_seed.sql +9 -10
  111. package/migrations/full-postgres/0032_add_provider_pool_accounts.sql +53 -0
  112. package/migrations/full-postgres/0033_add_api_key_ban.sql +9 -0
  113. package/migrations/full-postgres/0034_add_api_key_ban_until.sql +7 -0
  114. package/migrations/full-postgres/0035_align_pool_account_status_enum.sql +21 -0
  115. package/migrations/lite-sqlite3/0027_qoder_identity_genuine_seed.sql +9 -10
  116. package/migrations/lite-sqlite3/0032_add_provider_pool_accounts.sql +44 -0
  117. package/migrations/lite-sqlite3/0033_add_api_key_ban.sql +8 -0
  118. package/migrations/lite-sqlite3/0034_add_api_key_ban_until.sql +5 -0
  119. package/migrations/lite-sqlite3/0035_align_pool_account_status_enum.sql +6 -0
  120. package/package.json +2 -1
  121. package/src/boot/app.ts +9 -8
  122. package/src/config/network.ts +7 -3
  123. package/src/console/api-keys/contracts.ts +52 -0
  124. package/src/console/api-keys/routes.ts +93 -4
  125. package/src/console/audit/routes.ts +3 -3
  126. package/src/console/auth/account-reset.ts +4 -4
  127. package/src/console/auth/session.ts +3 -3
  128. package/src/console/backup/contracts.ts +2 -0
  129. package/src/console/backup/export-endpoint.ts +6 -6
  130. package/src/console/backup/routes.ts +3 -3
  131. package/src/console/cli-tools/routes.ts +3 -3
  132. package/src/console/console-router-guards.ts +2 -2
  133. package/src/console/console-router.ts +4 -4
  134. package/src/console/data-management/routes.ts +3 -3
  135. package/src/console/domain-registration.ts +2 -2
  136. package/src/console/observability/contracts.ts +4 -0
  137. package/src/console/observability/live.ts +3 -3
  138. package/src/console/observability/logs.ts +3 -3
  139. package/src/console/observability/performance.ts +3 -3
  140. package/src/console/observability/routes.ts +3 -3
  141. package/src/console/observability/store.ts +16 -16
  142. package/src/console/providers/catalog/contracts.ts +43 -22
  143. package/src/console/providers/catalog/provider-operations.ts +17 -2
  144. package/src/console/providers/catalog/routes.ts +3 -3
  145. package/src/console/providers/catalog/store-helpers.ts +349 -326
  146. package/src/console/providers/catalog/store.ts +1523 -1292
  147. package/src/console/providers/detail/routes.ts +3 -3
  148. package/src/console/providers/oauth/routes.ts +3 -3
  149. package/src/console/quota/account-quota-tenant.ts +26 -1
  150. package/src/console/quota/account-test-egress.ts +4 -4
  151. package/src/console/routing/model/routes.ts +3 -3
  152. package/src/console/routing/pools/contracts.ts +11 -8
  153. package/src/console/routing/pools/operations.ts +14 -7
  154. package/src/console/routing/pools/probe-verdict.ts +8 -3
  155. package/src/console/routing/pools/relay-deploy.ts +9 -9
  156. package/src/console/routing/pools/routes.ts +3 -3
  157. package/src/console/routing/pools/store.ts +21 -1
  158. package/src/console/settings/routes.ts +3 -3
  159. package/src/console/share/share-router.ts +12 -12
  160. package/src/console/share/share-stats.ts +105 -35
  161. package/src/console/shared/read-through-cache.ts +38 -2
  162. package/src/console/updates/routes.ts +7 -4
  163. package/src/network/outbound-fetch.ts +75 -4
  164. package/src/network/pool/resolver.ts +39 -39
  165. package/src/network/pool-cooldown-store.ts +71 -16
  166. package/src/network/pool-health-machine.ts +21 -1
  167. package/src/network/rate-limit-verdict.ts +68 -0
  168. package/src/network/ssrf.ts +4 -4
  169. package/src/observability/event-query.ts +320 -16
  170. package/src/observability/telemetry-status.ts +1 -1
  171. package/src/persistence/db-sqlite.ts +7 -2
  172. package/src/persistence/readiness.ts +18 -6
  173. package/src/persistence/schema.ts +66 -7
  174. package/src/persistence/stores/api-key-store.ts +99 -3
  175. package/src/persistence/stores/telemetry-store.ts +1 -1
  176. package/src/protocol/codecs.ts +6 -6
  177. package/src/protocol/gateway-error.ts +6 -3
  178. package/src/protocol/image-generation.ts +2 -2
  179. package/src/protocol/response/chat.ts +2 -2
  180. package/src/protocol/response/codex.ts +1 -1
  181. package/src/protocol/response/gemini.ts +1 -1
  182. package/src/protocol/response/messages.ts +5 -5
  183. package/src/protocol/response/responses.ts +2 -2
  184. package/src/protocol/sse-decoder.ts +14 -3
  185. package/src/protocol/translation/capabilities.ts +2 -2
  186. package/src/protocol/upstream/send-claude-messages.ts +3 -3
  187. package/src/providers/adapters/base-adapter.ts +1 -1
  188. package/src/providers/adapters/compatible-adapter.ts +2 -2
  189. package/src/providers/authentication/oauth-refresh-service.ts +2 -2
  190. package/src/providers/configured-provider.ts +1 -1
  191. package/src/providers/discovery/probe-phases.ts +2 -2
  192. package/src/providers/discovery/probing-service.ts +12 -12
  193. package/src/providers/integrations/agentrouter/agentrouter.ts +2 -3
  194. package/src/providers/integrations/alibabacp/alibabacp.ts +2 -2
  195. package/src/providers/integrations/anthropic.ts +4 -11
  196. package/src/providers/integrations/antigravity/antigravity.ts +13 -24
  197. package/src/providers/integrations/buddy/buddy-catalog-shared.ts +1 -1
  198. package/src/providers/integrations/buddy/buddy-discovery-shared.ts +37 -6
  199. package/src/providers/integrations/buddy/buddy-identity.ts +22 -1
  200. package/src/providers/integrations/buddy/buddy-media.ts +16 -16
  201. package/src/providers/integrations/buddy/codebuddy/provider.ts +1 -1
  202. package/src/providers/integrations/buddy/workbuddy/provider.ts +1 -1
  203. package/src/providers/integrations/claude/claude-betas.ts +1 -1
  204. package/src/providers/integrations/claude/claude-credentials.ts +1 -1
  205. package/src/providers/integrations/claude/claude-slow-lane-retry.ts +1 -1
  206. package/src/providers/integrations/claude/claude.ts +3 -10
  207. package/src/providers/integrations/cline/cline.ts +1 -1
  208. package/src/providers/integrations/cloudflareai/cloudflareai.ts +4 -7
  209. package/src/providers/integrations/codex/codex-errors.ts +1 -1
  210. package/src/providers/integrations/codex/codex.ts +14 -15
  211. package/src/providers/integrations/commandcode/commandcode.ts +4 -4
  212. package/src/providers/integrations/commandcode/quota.ts +5 -1
  213. package/src/providers/integrations/cursor/cursor-http2.ts +11 -11
  214. package/src/providers/integrations/cursor/cursor.ts +15 -15
  215. package/src/providers/integrations/factory/factory.ts +4 -4
  216. package/src/providers/integrations/gemini.ts +11 -11
  217. package/src/providers/integrations/github/github.ts +6 -6
  218. package/src/providers/integrations/grok/grok.ts +2 -6
  219. package/src/providers/integrations/inferhub.ts +4 -11
  220. package/src/providers/integrations/kimi/kimi.ts +4 -11
  221. package/src/providers/integrations/kiro/kiro.ts +10 -10
  222. package/src/providers/integrations/muse/muse.ts +6 -9
  223. package/src/providers/integrations/perplexity.ts +2 -2
  224. package/src/providers/integrations/qoder/qoder-catalog.ts +47 -9
  225. package/src/providers/integrations/qoder/qoder-checkin.ts +26 -8
  226. package/src/providers/integrations/qoder/qoder-native-identity-store.ts +9 -8
  227. package/src/providers/integrations/qoder/qoder-realm.ts +15 -0
  228. package/src/providers/integrations/qoder/qoder-umid.ts +12 -13
  229. package/src/providers/integrations/qoder/qoder-virtual.ts +2 -2
  230. package/src/providers/integrations/qoder/qoder.ts +188 -22
  231. package/src/providers/integrations/tokenharbor.ts +2 -2
  232. package/src/providers/integrations/trae/trae.ts +6 -9
  233. package/src/providers/integrations/zcode/oauth.ts +1 -1
  234. package/src/providers/operations/provider-catalog-service.ts +4 -4
  235. package/src/providers/operations/provider-credential-service.ts +16 -4
  236. package/src/providers/operations/upstream-deadline.ts +3 -3
  237. package/src/providers/provider-contracts.ts +47 -3
  238. package/src/providers/provider-credential-resolver.ts +1 -1
  239. package/src/providers/provider-registry.ts +4 -4
  240. package/src/providers/quota/quota-contracts.ts +24 -10
  241. package/src/providers/quota/refresh.ts +2 -2
  242. package/src/providers/search/search-provider.ts +5 -5
  243. package/src/security/abuse.ts +4 -4
  244. package/src/security/admission/reasons.ts +17 -17
  245. package/src/security/api-key-auth.ts +4 -4
  246. package/src/shared/coerce.ts +19 -0
  247. package/src/transport/dispatch/attempt-finalize.ts +4 -3
  248. package/src/transport/dispatch/attempt-loop.ts +10 -7
  249. package/src/transport/dispatch/empty-stream.ts +6 -6
  250. package/src/transport/dispatch/generation.ts +9 -9
  251. package/src/transport/dispatch/leases.ts +8 -8
  252. package/src/transport/dispatch/proxy-request.ts +7 -7
  253. package/src/transport/dispatch/responses-compact.ts +11 -11
  254. package/src/transport/dispatch/retry-policy.ts +4 -67
  255. package/src/transport/dispatch/stream-bridge.ts +1 -1
  256. package/src/transport/dispatch/streaming/finalize.ts +9 -8
  257. package/src/transport/dispatch/systemone.ts +8 -8
  258. package/src/transport/dispatch/token-count.ts +9 -9
  259. package/src/transport/dispatch/websearch.ts +6 -6
  260. package/src/transport/drain-endpoint.ts +2 -2
  261. package/src/transport/failure-policy.ts +13 -6
  262. package/src/transport/guards/auth-guard.ts +6 -5
  263. package/src/transport/guards/body-policy.ts +4 -4
  264. package/src/transport/guards/error-lifecycle.ts +18 -3
  265. package/src/transport/guards/guard-errors.ts +7 -3
  266. package/src/transport/guards/headroom-guard.ts +1 -1
  267. package/src/transport/guards/pipeline.ts +6 -6
  268. package/src/transport/guards/readiness-guard.ts +16 -2
  269. package/src/transport/guards/request-context.ts +3 -3
  270. package/src/transport/request/preparer.ts +20 -20
  271. package/src/transport/request/state.ts +4 -7
  272. package/src/transport/routing/route-catalog.ts +97 -2
  273. package/src/transport/routing/route-model.ts +7 -7
  274. package/src/transport/routing/router.ts +2 -2
  275. package/src/transport/shutdown-notice.ts +19 -7
  276. package/src/transport/surface/completion.ts +1 -1
  277. package/src/transport/surface/messages/encode.ts +1 -1
  278. package/dist/dashboard/assets/BannedPage-2TJD37Hb.js.gz +0 -0
  279. package/dist/dashboard/assets/CliToolDetailPage-xefJNRmJ.js.gz +0 -0
  280. package/dist/dashboard/assets/CliToolsPage-Rj9iHQ3F.js.gz +0 -0
  281. package/dist/dashboard/assets/CombosPage-JEbYJFxw.js.gz +0 -0
  282. package/dist/dashboard/assets/ConsoleLogPage-C3xx_inM.js +0 -1
  283. package/dist/dashboard/assets/ConsoleLogPage-C3xx_inM.js.gz +0 -0
  284. package/dist/dashboard/assets/CustomizationPage-Dw306gJU.js +0 -1
  285. package/dist/dashboard/assets/CustomizationPage-Dw306gJU.js.gz +0 -0
  286. package/dist/dashboard/assets/HealthEventsModal-jaBo1TWV.js +0 -1
  287. package/dist/dashboard/assets/HealthEventsModal-jaBo1TWV.js.gz +0 -0
  288. package/dist/dashboard/assets/LoginPage-C4w7vea-.js.gz +0 -0
  289. package/dist/dashboard/assets/ModelPicker-BtPcAfot.js.gz +0 -0
  290. package/dist/dashboard/assets/OverviewPage-CGC5gNs1.js +0 -1
  291. package/dist/dashboard/assets/OverviewPage-CGC5gNs1.js.gz +0 -0
  292. package/dist/dashboard/assets/ProviderDetailPage-DQcthNGt.js +0 -4
  293. package/dist/dashboard/assets/ProviderDetailPage-DQcthNGt.js.gz +0 -0
  294. package/dist/dashboard/assets/ProvidersPage-0OsXRGXe.js +0 -1
  295. package/dist/dashboard/assets/ProvidersPage-0OsXRGXe.js.gz +0 -0
  296. package/dist/dashboard/assets/ProxyPage-DdPc8slb.js +0 -9
  297. package/dist/dashboard/assets/ProxyPage-DdPc8slb.js.gz +0 -0
  298. package/dist/dashboard/assets/QuotaPage-Ci5O4BnH.js +0 -1
  299. package/dist/dashboard/assets/QuotaPage-Ci5O4BnH.js.gz +0 -0
  300. package/dist/dashboard/assets/SettingsPage-CDZjTt87.js +0 -1
  301. package/dist/dashboard/assets/SettingsPage-CDZjTt87.js.gz +0 -0
  302. package/dist/dashboard/assets/SetupPage-BQS0Rx_u.js.gz +0 -0
  303. package/dist/dashboard/assets/SortableList-BAL5npBC.js.gz +0 -0
  304. package/dist/dashboard/assets/StudioPage-TP7voAQ0.js.gz +0 -0
  305. package/dist/dashboard/assets/UsagePage-Cj_fEzhg.js +0 -56
  306. package/dist/dashboard/assets/UsagePage-Cj_fEzhg.js.gz +0 -0
  307. package/dist/dashboard/assets/api-keys-MbI7e7rt.js +0 -1
  308. package/dist/dashboard/assets/api-keys-MbI7e7rt.js.gz +0 -0
  309. package/dist/dashboard/assets/app-B2vyPoII.css +0 -2
  310. package/dist/dashboard/assets/app-B2vyPoII.css.gz +0 -0
  311. package/dist/dashboard/assets/assert-C0YYuZJ4.js +0 -1
  312. package/dist/dashboard/assets/assert-C0YYuZJ4.js.gz +0 -0
  313. package/dist/dashboard/assets/badge-B_DMpMJN.js.gz +0 -0
  314. package/dist/dashboard/assets/banner-DMrMPmth.js.gz +0 -0
  315. package/dist/dashboard/assets/card-Be5Uzamx.js.gz +0 -0
  316. package/dist/dashboard/assets/cli-tools-muFRphDu.js +0 -1
  317. package/dist/dashboard/assets/cli-tools-muFRphDu.js.gz +0 -0
  318. package/dist/dashboard/assets/clipboard-button-DOfrVmsi.js.gz +0 -0
  319. package/dist/dashboard/assets/dialog-C9omdjQt.js.gz +0 -0
  320. package/dist/dashboard/assets/drawer-Bm8WKaGY.js.gz +0 -0
  321. package/dist/dashboard/assets/entry-DS90gUks.js +0 -1
  322. package/dist/dashboard/assets/entry-DS90gUks.js.gz +0 -0
  323. package/dist/dashboard/assets/entry-j40LJotO.js +0 -4
  324. package/dist/dashboard/assets/entry-j40LJotO.js.gz +0 -0
  325. package/dist/dashboard/assets/entry-q5HwZ8nN.js.gz +0 -0
  326. package/dist/dashboard/assets/formatters-BTScrQpe.js.gz +0 -0
  327. package/dist/dashboard/assets/github-badge-Orjuzemr.js.gz +0 -0
  328. package/dist/dashboard/assets/index-cPrX5vn6.js.gz +0 -0
  329. package/dist/dashboard/assets/inline-Dvux8w-0.js.gz +0 -0
  330. package/dist/dashboard/assets/input-CU1Rgf3p.js.gz +0 -0
  331. package/dist/dashboard/assets/layout-C_7A6H55.js.gz +0 -0
  332. package/dist/dashboard/assets/list-row-CzFvogbj.js.gz +0 -0
  333. package/dist/dashboard/assets/live-KaG15HLQ.js +0 -1
  334. package/dist/dashboard/assets/live-KaG15HLQ.js.gz +0 -0
  335. package/dist/dashboard/assets/minimize-2-D-MN8W8a.js +0 -1
  336. package/dist/dashboard/assets/minimize-2-D-MN8W8a.js.gz +0 -0
  337. package/dist/dashboard/assets/mutation-JJGQm8TD.js.gz +0 -0
  338. package/dist/dashboard/assets/numbers-BNDZnsUE.js.gz +0 -0
  339. package/dist/dashboard/assets/query-keys-CLaNz8Xz.js.gz +0 -0
  340. package/dist/dashboard/assets/quota-DZGyp0N5.js +0 -1
  341. package/dist/dashboard/assets/quota-DZGyp0N5.js.gz +0 -0
  342. package/dist/dashboard/assets/routing-BEH32QOC.js +0 -1
  343. package/dist/dashboard/assets/routing-BEH32QOC.js.gz +0 -0
  344. package/dist/dashboard/assets/select-B3iTetHN.js.gz +0 -0
  345. package/dist/dashboard/assets/settings-Dt3Eqbnq.js +0 -1
  346. package/dist/dashboard/assets/settings-Dt3Eqbnq.js.gz +0 -0
  347. package/dist/dashboard/assets/stack-KrOrm7JZ.js.gz +0 -0
  348. package/dist/dashboard/assets/state-DLS7IDMy.js.gz +0 -0
  349. package/dist/dashboard/assets/switch-42ItvbAs.js.gz +0 -0
  350. package/dist/dashboard/assets/useMutation-Dmnfh1Ue.js.gz +0 -0
  351. /package/dist/dashboard/assets/{app-wvwCi4Eh.js → app-BayTTy01.js} +0 -0
  352. /package/dist/dashboard/assets/{app-wvwCi4Eh.js.gz → app-BayTTy01.js.gz} +0 -0
@@ -19,7 +19,7 @@
19
19
  */
20
20
  import { and, desc, eq, gte, inArray, isNotNull, or, sql, type SQL } from "drizzle-orm";
21
21
  import type { CartethyiaDatabase } from "../persistence/db-handle";
22
- import { apiKeys, telemetryEvents } from "../persistence/schema";
22
+ import { apiKeys, telemetryEvents, telemetryUsageTotals } from "../persistence/schema";
23
23
  import type { TelemetryEventRecord } from "./event-log";
24
24
  import { gatewayErrorSql } from "./telemetry-status";
25
25
 
@@ -31,7 +31,7 @@ import { gatewayErrorSql } from "./telemetry-status";
31
31
  * totals, and the durable usage rollup cannot drift apart — an unknown status
32
32
  * must be classified identically in all three.
33
33
  */
34
- const IS_GATEWAY_ERROR_SQL = gatewayErrorSql(telemetryEvents.status, telemetryEvents.httpStatus);
34
+ export const IS_GATEWAY_ERROR_SQL = gatewayErrorSql(telemetryEvents.status, telemetryEvents.httpStatus);
35
35
 
36
36
  /** One bounded page of events, newest first. */
37
37
  export interface EventPage {
@@ -516,23 +516,39 @@ async function latencyPercentiles(
516
516
  }
517
517
 
518
518
  // SQLite: nearest-rank percentile = the value at ceil(p * n) rows into the
519
- // ordered latency column, via ORDER BY + OFFSET/LIMIT in SQL.
520
- const rankAt = async (fraction: number): Promise<number | null> => {
521
- const offset = Math.max(0, Math.ceil(fraction * sampled) - 1);
522
- const [row] = await db
523
- .select({ value: telemetryEvents.latencyMs })
524
- .from(telemetryEvents)
525
- .where(and(...base))
526
- .orderBy(telemetryEvents.latencyMs)
527
- .limit(1)
528
- .offset(offset);
529
- return row?.value ?? null;
519
+ // ordered latency column. One query computes all three ranks via a window
520
+ // function, instead of three separate OFFSET/LIMIT passes over the same
521
+ // ordered column (one per percentile).
522
+ const n = Math.max(1, sampled);
523
+ const ranks = [
524
+ Math.ceil(0.5 * n),
525
+ Math.ceil(0.95 * n),
526
+ Math.ceil(0.99 * n),
527
+ ];
528
+ const [row] = await db
529
+ .select({
530
+ p50: sql<number | null>`max(case when rn = ${ranks[0]} then value end)`,
531
+ p95: sql<number | null>`max(case when rn = ${ranks[1]} then value end)`,
532
+ p99: sql<number | null>`max(case when rn = ${ranks[2]} then value end)`,
533
+ })
534
+ .from(
535
+ sql`(
536
+ select ${telemetryEvents.latencyMs} as value,
537
+ row_number() over (order by ${telemetryEvents.latencyMs}) as rn
538
+ from ${telemetryEvents}
539
+ where ${and(...base)}
540
+ )`,
541
+ );
542
+ return {
543
+ p50: numericNullable(row?.p50 ?? null),
544
+ p95: numericNullable(row?.p95 ?? null),
545
+ p99: numericNullable(row?.p99 ?? null),
530
546
  };
531
- const [p50, p95, p99] = await Promise.all([rankAt(0.5), rankAt(0.95), rankAt(0.99)]);
532
- return { p50, p95, p99 };
533
547
  }
534
548
 
535
- /** The SQL column one breakdown dimension groups by. */
549
+ /**
550
+ * The SQL column one breakdown dimension groups by.
551
+ */
536
552
  function dimensionColumn(dimension: string) {
537
553
  if (dimension === "model") return telemetryEvents.requestedModel;
538
554
  if (dimension === "provider") return telemetryEvents.providerId;
@@ -767,6 +783,196 @@ export async function sumTelemetryTokens(
767
783
  return numeric(row?.total ?? 0);
768
784
  }
769
785
 
786
+ /** One hour, in milliseconds — the share page's hourly bucket width. */
787
+ const HOUR_MS = 3_600_000;
788
+
789
+ /**
790
+ * All-time family counts from the durable rollup, not from a scan of the
791
+ * retained telemetry window.
792
+ *
793
+ * The share KPI cards need lifetime numbers. `telemetry_usage_totals` is
794
+ * charged at write time and accumulates per identity, so a family's lifetime
795
+ * requests/errors/tokens are a SUM over its rows (keyed by
796
+ * `family_template_key_id`) — sub-millisecond on a small table, versus a
797
+ * full-window aggregate over `telemetry_events` that reads every retained
798
+ * event on every tick. The rollup is the same definition as the read-side
799
+ * counts, so the two cannot drift.
800
+ */
801
+ export interface FamilyKpiTotals {
802
+ readonly requests: number;
803
+ readonly errors: number;
804
+ readonly tokens: number;
805
+ }
806
+
807
+ /** Sums one share family's durable lifetime totals. */
808
+ export async function familyKpiTotals(
809
+ db: CartethyiaDatabase,
810
+ tenantId: string,
811
+ familyKeyId: string,
812
+ ): Promise<FamilyKpiTotals> {
813
+ const [row] = await db
814
+ .select({
815
+ requests: sql<number>`coalesce(sum(${telemetryUsageTotals.requests}), 0)`,
816
+ errors: sql<number>`coalesce(sum(${telemetryUsageTotals.errors}), 0)`,
817
+ tokens: sql<number>`coalesce(sum(coalesce(${telemetryUsageTotals.inputTokens}, 0) + coalesce(${telemetryUsageTotals.outputTokens}, 0)), 0)`,
818
+ })
819
+ .from(telemetryUsageTotals)
820
+ .where(
821
+ and(
822
+ eq(telemetryUsageTotals.tenantId, tenantId),
823
+ eq(telemetryUsageTotals.familyTemplateKeyId, familyKeyId),
824
+ ),
825
+ );
826
+ return {
827
+ requests: numeric(row?.requests ?? 0),
828
+ errors: numeric(row?.errors ?? 0),
829
+ tokens: numeric(row?.tokens ?? 0),
830
+ };
831
+ }
832
+
833
+ /**
834
+ * All-time family counts: the numbers a share page's KPI card shows.
835
+ *
836
+ * These must be computed in SQL, not derived from the bounded page read — the
837
+ * page caps at {@link MAX_LIMIT}, so a busy family would freeze at 500
838
+ * requests/errors/last-hour regardless of real traffic.
839
+ */
840
+ export interface FamilyUsageTotals {
841
+ readonly requests: number;
842
+ readonly errors: number;
843
+ readonly lastHourRequests: number;
844
+ }
845
+
846
+ /** Counts one share family's requests, errors, and last-hour traffic in SQL. */
847
+ export async function familyUsageTotals(
848
+ db: CartethyiaDatabase,
849
+ tenantId: string,
850
+ familyKeyId: string,
851
+ now: Date,
852
+ ): Promise<FamilyUsageTotals> {
853
+ const lastHourStart = new Date(now.getTime() - HOUR_MS);
854
+ const conditions = [eq(telemetryEvents.tenantId, tenantId), familyKeyCondition(familyKeyId)];
855
+ const [row] = await db
856
+ .select({
857
+ requests: sql<number>`count(*)`,
858
+ errors: sql<number>`sum(case when ${IS_GATEWAY_ERROR_SQL} then 1 else 0 end)`,
859
+ lastHourRequests: sql<number>`sum(case when ${telemetryEvents.createdAt} >= ${lastHourStart} then 1 else 0 end)`,
860
+ })
861
+ .from(telemetryEvents)
862
+ .where(and(...conditions));
863
+ return {
864
+ requests: numeric(row?.requests ?? 0),
865
+ errors: numeric(row?.errors ?? 0),
866
+ lastHourRequests: numeric(row?.lastHourRequests ?? 0),
867
+ };
868
+ }
869
+
870
+ /** Counts one share family's requests in a bounded window — a range scan, not a full-window aggregate. */
871
+ export async function familyWindowRequests(
872
+ db: CartethyiaDatabase,
873
+ tenantId: string,
874
+ familyKeyId: string,
875
+ since: Date,
876
+ ): Promise<number> {
877
+ const [row] = await db
878
+ .select({
879
+ requests: sql<number>`count(*)`,
880
+ })
881
+ .from(telemetryEvents)
882
+ .where(
883
+ and(
884
+ eq(telemetryEvents.tenantId, tenantId),
885
+ familyKeyCondition(familyKeyId),
886
+ gte(telemetryEvents.createdAt, since),
887
+ ),
888
+ );
889
+ return numeric(row?.requests ?? 0);
890
+ }
891
+
892
+ /**
893
+ * One share family's client-IP breakdown, computed in SQL.
894
+ *
895
+ * Unlike the 24-hour page read, this covers a bounded history so an address
896
+ * that stopped before the last day still appears — marked inactive. The
897
+ * `active` flag is decided in the caller from `lastSeenAt` vs a cutoff; here
898
+ * we only aggregate. The window is bounded (`since`, default the whole
899
+ * retained window) so an unbounded scan is never the default for callers
900
+ * that only need recent addresses.
901
+ */
902
+ export async function familyIpBreakdown(
903
+ db: CartethyiaDatabase,
904
+ tenantId: string,
905
+ familyKeyId: string,
906
+ limit = 50,
907
+ since?: Date,
908
+ ): Promise<{ ip: string; requests: number; tokens: number; lastSeenAt: Date | null }[]> {
909
+ const conditions = [
910
+ eq(telemetryEvents.tenantId, tenantId),
911
+ familyKeyCondition(familyKeyId),
912
+ ...(since === undefined ? [] : [gte(telemetryEvents.createdAt, since)]),
913
+ ];
914
+ const rows = await db
915
+ .select({
916
+ ip: telemetryEvents.clientIp,
917
+ requests: sql<number>`count(*)`,
918
+ tokens: sql<number>`coalesce(sum(coalesce(${telemetryEvents.inputTokens}, 0) + coalesce(${telemetryEvents.outputTokens}, 0)), 0)`,
919
+ lastSeenAt: sql<Date>`max(${telemetryEvents.createdAt})`,
920
+ })
921
+ .from(telemetryEvents)
922
+ .where(and(...conditions))
923
+ .groupBy(telemetryEvents.clientIp)
924
+ // Highest volume first: the table answers "who has used the link most",
925
+ // not just who is active right now.
926
+ .orderBy(desc(sql`count(*)`))
927
+ .limit(limit);
928
+ return rows.flatMap((row) =>
929
+ row.ip === null ? [] : [{ ip: row.ip, requests: numeric(row.requests), tokens: numeric(row.tokens), lastSeenAt: row.lastSeenAt }],
930
+ );
931
+ }
932
+
933
+ /**
934
+ * One share family's time-series buckets, computed in SQL with the family
935
+ * membership filter.
936
+ *
937
+ * Mirrors {@link chartTelemetry} but scoped to a share family. The share page's
938
+ * 24-hour strip must not be truncated to the bounded page (500 events), or a
939
+ * busy family's chart undercounts every hour bucket.
940
+ */
941
+ export async function familyHourlyTelemetry(
942
+ db: CartethyiaDatabase,
943
+ tenantId: string,
944
+ familyKeyId: string,
945
+ since: Date,
946
+ bucketSeconds: number,
947
+ ): Promise<ChartBucket[]> {
948
+ const width = Math.max(1, Math.trunc(bucketSeconds));
949
+ // Inlined (not parameterised) so Postgres sees the SELECT and GROUP BY
950
+ // expressions as identical — see chartTelemetry for the failure this avoids.
951
+ const w = sql.raw(String(width));
952
+ const bucketExpression = isPostgres(db)
953
+ ? sql<number>`(cast(extract(epoch from ${telemetryEvents.createdAt}) as bigint) / ${w}) * ${w}`
954
+ : sql<number>`(cast(strftime('%s', ${telemetryEvents.createdAt}) as integer) / ${w}) * ${w}`;
955
+ const conditions = [scoped(tenantId, since), familyKeyCondition(familyKeyId)];
956
+ const rows = await db
957
+ .select({
958
+ at: bucketExpression,
959
+ requests: sql<number>`count(*)`,
960
+ errors: sql<number>`sum(case when ${IS_GATEWAY_ERROR_SQL} then 1 else 0 end)`,
961
+ inputTokens: sql<number>`coalesce(sum(${telemetryEvents.inputTokens}), 0)`,
962
+ outputTokens: sql<number>`coalesce(sum(${telemetryEvents.outputTokens}), 0)`,
963
+ })
964
+ .from(telemetryEvents)
965
+ .where(and(...conditions))
966
+ .groupBy(bucketExpression);
967
+ return rows.map((row) => ({
968
+ at: numeric(row.at) * 1000,
969
+ requests: numeric(row.requests),
970
+ errors: numeric(row.errors),
971
+ inputTokens: numeric(row.inputTokens),
972
+ outputTokens: numeric(row.outputTokens),
973
+ }));
974
+ }
975
+
770
976
  /**
771
977
  * The health card's latency and throughput figures for one window.
772
978
  *
@@ -861,3 +1067,101 @@ export async function summarizeUsagePerformanceSql(
861
1067
  avgTokensPerSec: numeric(row?.avgTokensPerSec ?? 0),
862
1068
  };
863
1069
  }
1070
+
1071
+ /**
1072
+ * Combined usage totals + performance in ONE window scan.
1073
+ *
1074
+ * `summarizeTelemetry` and `summarizeUsagePerformanceSql` both scan the same
1075
+ * tenant window and each additionally run the percentile path, so the Usage
1076
+ * summary endpoint paid two scans plus two percentile passes for one response.
1077
+ * This runs the combined SELECT once and one percentile pass, returning both
1078
+ * shapes so the caller needs no second query.
1079
+ */
1080
+ export async function summarizeUsageWithPerformance(
1081
+ db: CartethyiaDatabase,
1082
+ tenantId: string,
1083
+ since: Date,
1084
+ ): Promise<{ summary: UsageSummary; performance: UsagePerformanceTotals }> {
1085
+ const condition = scoped(tenantId, since);
1086
+ const [row] = await db
1087
+ .select({
1088
+ requests: sql<number>`count(*)`,
1089
+ errors: sql<number>`sum(case when ${IS_GATEWAY_ERROR_SQL} then 1 else 0 end)`,
1090
+ cancelled: sql<number>`sum(case when ${telemetryEvents.status} = 'cancelled' then 1 else 0 end)`,
1091
+ truncated: sql<number>`sum(case when ${telemetryEvents.status} = 'truncated' then 1 else 0 end)`,
1092
+ inputTokens: sql<number>`coalesce(sum(${telemetryEvents.inputTokens}), 0)`,
1093
+ outputTokens: sql<number>`coalesce(sum(${telemetryEvents.outputTokens}), 0)`,
1094
+ cachedInputTokens: sql<number>`coalesce(sum(${telemetryEvents.cachedInputTokens}), 0)`,
1095
+ reasoningTokens: sql<number>`coalesce(sum(${telemetryEvents.reasoningTokens}), 0)`,
1096
+ estimatedCostUsd: sql<number>`coalesce(sum(${telemetryEvents.estimatedCostUsd}), 0)`,
1097
+ sampled: sql<number>`count(${telemetryEvents.latencyMs})`,
1098
+ latencyAvg: sql<number>`avg(${telemetryEvents.latencyMs})`,
1099
+ unpriced: sql<number>`sum(case when ${telemetryEvents.estimatedCostUsd} is null then 1 else 0 end)`,
1100
+ resolveSum: sql<number>`coalesce(sum(${telemetryEvents.resolveMs}), 0)`,
1101
+ resolveCount: sql<number>`count(${telemetryEvents.resolveMs})`,
1102
+ avgTokensPerSec: sql<number>`coalesce(avg(${telemetryEvents.tokensPerSec}), 0)`,
1103
+ })
1104
+ .from(telemetryEvents)
1105
+ .where(condition);
1106
+
1107
+ if (!row) {
1108
+ const empty: UsageSummary = {
1109
+ requests: 0,
1110
+ errors: 0,
1111
+ cancelled: 0,
1112
+ truncated: 0,
1113
+ inputTokens: 0,
1114
+ outputTokens: 0,
1115
+ cachedInputTokens: 0,
1116
+ reasoningTokens: 0,
1117
+ estimatedCostUsd: 0,
1118
+ sampled: 0,
1119
+ latencyMs: { avg: null, p50: null, p95: null, p99: null },
1120
+ unpriced: 0,
1121
+ };
1122
+ const emptyPerformance: UsagePerformanceTotals = {
1123
+ avgResolveMs: 0,
1124
+ avgDurationMs: 0,
1125
+ latencyP50Ms: 0,
1126
+ latencyP95Ms: 0,
1127
+ latencyP99Ms: 0,
1128
+ latencySampleCount: 0,
1129
+ resolveSampleCount: 0,
1130
+ avgTokensPerSec: 0,
1131
+ };
1132
+ return { summary: empty, performance: emptyPerformance };
1133
+ }
1134
+
1135
+ const sampled = numeric(row.sampled);
1136
+ const percentiles = await latencyPercentiles(db, tenantId, since, sampled);
1137
+ const resolveCount = numeric(row.resolveCount);
1138
+ return {
1139
+ summary: {
1140
+ requests: numeric(row.requests),
1141
+ errors: numeric(row.errors),
1142
+ cancelled: numeric(row.cancelled),
1143
+ truncated: numeric(row.truncated),
1144
+ inputTokens: numeric(row.inputTokens),
1145
+ outputTokens: numeric(row.outputTokens),
1146
+ cachedInputTokens: numeric(row.cachedInputTokens),
1147
+ reasoningTokens: numeric(row.reasoningTokens),
1148
+ estimatedCostUsd: numeric(row.estimatedCostUsd),
1149
+ sampled,
1150
+ latencyMs: {
1151
+ avg: numericNullable(row.latencyAvg),
1152
+ ...percentiles,
1153
+ },
1154
+ unpriced: numeric(row.unpriced),
1155
+ },
1156
+ performance: {
1157
+ avgResolveMs: resolveCount > 0 ? numeric(row.resolveSum) / resolveCount : 0,
1158
+ avgDurationMs: numericNullable(row.latencyAvg) ?? 0,
1159
+ latencyP50Ms: percentiles.p50 ?? 0,
1160
+ latencyP95Ms: percentiles.p95 ?? 0,
1161
+ latencyP99Ms: percentiles.p99 ?? 0,
1162
+ latencySampleCount: sampled,
1163
+ resolveSampleCount: resolveCount,
1164
+ avgTokensPerSec: numeric(row.avgTokensPerSec),
1165
+ },
1166
+ };
1167
+ }
@@ -30,7 +30,7 @@
30
30
  * rule as well, so it needs no special case.
31
31
  * - **`503`** is capacity or availability: the gateway *correctly* refusing work
32
32
  * it cannot do right now (`admission_unavailable`, `accounts_unavailable`,
33
- * `shutting_down`, `restart_for_update`, `platform_unavailable`). Retryable,
33
+ * `shutting_down` (incl. update restart), `platform_unavailable`). Retryable,
34
34
  * self-inflicted, and an operator-capacity signal rather than a defect.
35
35
  * - **`504`** is a timeout, not a malfunction. It represents a deadline that
36
36
  * expired: the request deadline, the stream-stall
@@ -327,7 +327,8 @@ const AUTH_KEY_COLUMNS = [
327
327
  "issued_client_ip_key", "label", "scopes", "requests_per_minute", "daily_token_limit",
328
328
  "monthly_token_limit", "lifetime_token_budget", "lifetime_tokens_consumed",
329
329
  "max_concurrent_requests", "model_access_mode", "model_list", "client_router_denylist",
330
- "model_prefix", "created_at", "enabled", "revoked_at", "key_prefix", "key_encrypted",
330
+ "model_prefix", "created_at", "enabled", "revoked_at", "banned_at", "ban_reason", "banned_until",
331
+ "key_prefix", "key_encrypted",
331
332
  "notes_title", "notes_subtitle", "notes_body", "is_default", "sort_index",
332
333
  ].join(", ");
333
334
 
@@ -341,7 +342,11 @@ export interface SqliteAuthStatements {
341
342
  }
342
343
 
343
344
  export function prepareAuthStatements(sqlite: Database): SqliteAuthStatements {
344
- const active = `enabled = 1 AND revoked_at IS NULL`;
345
+ // A key is active while not revoked and not under an active ban: a ban is
346
+ // active when `banned_at` is set and `banned_until` is null (permanent) or
347
+ // still in the future. SQLite stores timestamps as ISO text, so the
348
+ // comparison is lexical (ISO sorts chronologically).
349
+ const active = `enabled = 1 AND revoked_at IS NULL AND (banned_at IS NULL OR (banned_until IS NOT NULL AND banned_until <= datetime('now')))`;
345
350
  // `query`, not `prepare`: bun's `prepare` compiles a statement per call and
346
351
  // never finalizes one, while `query` caches the compiled statement behind an
347
352
  // LRU of its own. All three are constant text run once per authenticated
@@ -89,12 +89,15 @@ async function runReadinessChecks(
89
89
 
90
90
  // 1. Check database connectivity via simple query
91
91
  let dbOk = false;
92
+ let dbTimedOut = false;
92
93
  try {
93
94
  const result = await withTimeoutCheck(
94
95
  () => db.execute(sql`SELECT 1`),
95
96
  "Database connectivity check",
96
97
  );
97
- if (result !== "timeout") {
98
+ if (result === "timeout") {
99
+ dbTimedOut = true;
100
+ } else {
98
101
  dbOk = true;
99
102
  }
100
103
  } catch {
@@ -107,7 +110,10 @@ async function runReadinessChecks(
107
110
  db: "disconnected",
108
111
  migrations: "pending",
109
112
  redis: "disconnected",
110
- reason: "database not connected",
113
+ // A timed-out probe under load is not evidence the database is down —
114
+ // it is the probe itself failing to get a backend in time. The guard
115
+ // reads this to call it overload rather than a dependency failure.
116
+ reason: dbTimedOut ? "database probe timed out" : "database not connected",
111
117
  };
112
118
  }
113
119
 
@@ -115,13 +121,16 @@ async function runReadinessChecks(
115
121
  // Readiness requires every discovered migration to have a ledger row; a
116
122
  // reachable ledger alone is not proof the upgrade finished.
117
123
  let migrationsApplied = false;
124
+ let migrationsTimedOut = false;
118
125
  try {
119
126
  const result = await withTimeoutCheck(
120
127
  () => readMigrationLedgerStatus(db),
121
128
  "Migrations check",
122
129
  );
123
130
 
124
- if (result !== "timeout") {
131
+ if (result === "timeout") {
132
+ migrationsTimedOut = true;
133
+ } else {
125
134
  migrationsApplied = result.applied;
126
135
  }
127
136
  } catch {
@@ -134,18 +143,21 @@ async function runReadinessChecks(
134
143
  db: "connected",
135
144
  migrations: "pending",
136
145
  redis: "disconnected",
137
- reason: "migrations not yet applied",
146
+ reason: migrationsTimedOut ? "migrations probe timed out" : "migrations not yet applied",
138
147
  };
139
148
  }
140
149
 
141
150
  // 3. Check Redis. The memory backend is healthy by construction — there is
142
151
  // no connection to probe — so it passes without a round trip.
143
152
  let redisOk = redisBackend === "memory";
153
+ let redisTimedOut = false;
144
154
 
145
155
  if (redisBackend === "redis" && redis) {
146
156
  try {
147
157
  const result = await withTimeoutCheck(() => redis.ping(), "Redis connectivity check");
148
- if (result !== "timeout" && result === "PONG") {
158
+ if (result === "timeout") {
159
+ redisTimedOut = true;
160
+ } else if (result === "PONG") {
149
161
  redisOk = true;
150
162
  }
151
163
  } catch {
@@ -159,7 +171,7 @@ async function runReadinessChecks(
159
171
  db: "connected",
160
172
  migrations: "applied",
161
173
  redis: "disconnected",
162
- reason: "redis not connected",
174
+ reason: redisTimedOut ? "redis probe timed out" : "redis not connected",
163
175
  };
164
176
  }
165
177
 
@@ -2,7 +2,7 @@
2
2
  import { desc, sql } from "drizzle-orm";
3
3
  import {
4
4
  bigint, boolean, check, customType, index, integer, jsonb, numeric, pgEnum, pgTable,
5
- primaryKey, text, timestamp, uniqueIndex, uuid, type AnyPgColumn,
5
+ primaryKey, text, timestamp, unique, uniqueIndex, uuid, type AnyPgColumn,
6
6
  } from "drizzle-orm/pg-core";
7
7
 
8
8
  export const TENANT_STATUSES = ["active", "suspended", "deleted"] as const;
@@ -188,15 +188,16 @@ export const providers = pgTable(
188
188
  *
189
189
  * The device identity is a deterministic function of a seed file
190
190
  * (`~/.config/.locale_cfg`): the `runtime-info` bridge reads that seed and
191
- * derives `machineToken`/`machineType`/`machineCode`. The server gates the
192
- * daily 100-credit campaign on a *genuine, non-VM* device identity, so each
193
- * account gets its own minted seed (a distinct "device").
191
+ * derives `machineToken`/`machineType`/`machineCode`. The daily 100-credit
192
+ * campaign does not gate grants on this identity. A persisted seed allows the
193
+ * vendor bridge to replay the same native identity where another Qoder surface
194
+ * requires it.
194
195
  *
195
196
  * `seed` is the exact seed-file content the identity was minted from, stored
196
197
  * so the same device identity can be replayed (restore seed → bridge → same
197
- * token). It is not a credential, but it is the per-account device material
198
- * and must never be shared between accounts. `machineToken` is the unique
199
- * device key: two accounts sharing a token are the same device to the server.
198
+ * token). It is not a credential, but it is per-account device material and
199
+ * should not be shared between accounts. Campaign grant eligibility is
200
+ * independent of `machineToken`.
200
201
  */
201
202
  export interface NativeIdentityRecord {
202
203
  readonly machineToken: string;
@@ -569,6 +570,49 @@ export const networkPools = pgTable("network_pools", {
569
570
  ],
570
571
  );
571
572
 
573
+ /**
574
+ * A pool's account-equivalent routing entity for ip-scoped, credential-less
575
+ * providers (OpenCode Free today).
576
+ *
577
+ * These providers own no `provider_accounts` rows — the rate limit belongs to
578
+ * the egress address, not to a credential. Materializing one row per (pool,
579
+ * ip-scoped provider) lets the account health machine, routing eligibility, and
580
+ * the provider-detail accounts UI treat each egress like an account: the row
581
+ * carries the same health state machine and cooldown columns, so a spent egress
582
+ * is excluded from the plan the same way a cooling account is, and the operator
583
+ * sees it in the provider's Accounts card with a "Pool" badge.
584
+ *
585
+ * The row is an *account-equivalent*, not an account: it resolves no credential
586
+ * (`credential_kind` is fixed to `none`-style egress), is skipped by the quota
587
+ * refresh and identity sweeps (they filter on real credential kinds), and is
588
+ * owned by its pool — deleting the pool cascades the row.
589
+ */
590
+ export const providerPoolAccounts = pgTable(
591
+ "provider_pool_accounts",
592
+ {
593
+ id: uuid("id").primaryKey().defaultRandom(),
594
+ tenantId: tenantRefRequired(),
595
+ networkPoolId: uuid("network_pool_id")
596
+ .notNull()
597
+ .references(() => networkPools.id, { onDelete: "cascade" }),
598
+ providerId: text("provider_id").notNull(),
599
+ label: text("label").notNull(),
600
+ ...healthColumns(),
601
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
602
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
603
+ },
604
+ (table) => [
605
+ unique("provider_pool_accounts_pool_provider_uidx").on(
606
+ table.networkPoolId,
607
+ table.providerId,
608
+ table.tenantId,
609
+ ),
610
+ index("provider_pool_accounts_tenant_status_idx").on(table.tenantId, table.status),
611
+ index("provider_pool_accounts_provider_status_idx").on(table.providerId, table.status),
612
+ ],
613
+ );
614
+ export type ProviderPoolAccount = typeof providerPoolAccounts.$inferSelect;
615
+
572
616
 
573
617
  // Account and network-pool health events share one table. The entity kind
574
618
  // selects exactly one owner FK, and the CHECK constraint below preserves
@@ -845,6 +889,21 @@ export const apiKeys = pgTable(
845
889
  createdAt: createdAtColumn(),
846
890
  enabled: boolean("enabled").notNull().default(true),
847
891
  revokedAt: timestamp("revoked_at", { withTimezone: true }),
892
+ /**
893
+ * Ban state. A ban is distinct from a revoke: it records the operator's
894
+ * reason (`banReason`) so the console can show it, and it is fully
895
+ * reversible — unban clears both columns, leaving no metadata behind.
896
+ * Banned keys are rejected by the auth path exactly like revoked keys.
897
+ */
898
+ bannedAt: timestamp("banned_at", { withTimezone: true }),
899
+ banReason: text("ban_reason"),
900
+ /**
901
+ * When the ban lifts on its own, if it is not permanent. `null` means the
902
+ * ban does not lapse (until an operator unbans it). The auth path treats a
903
+ * key as banned only while `banned_at` is set and `banned_until` is either
904
+ * null or in the future — so a timed ban expires without SQL.
905
+ */
906
+ bannedUntil: timestamp("banned_until", { withTimezone: true }),
848
907
  /**
849
908
  * The tenant's mandatory gateway key. Seeded at first boot whether or not
850
909
  * `CARTETHYIA_API_KEY` was configured, and the one key the console refuses