cartethyia 2.7.8 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (519) hide show
  1. package/.env.example +3 -3
  2. package/README.md +29 -59
  3. package/dist/dashboard/assets/BannedPage-Cc0vKpMG.js +1 -0
  4. package/dist/dashboard/assets/BannedPage-Cc0vKpMG.js.gz +0 -0
  5. package/dist/dashboard/assets/{CliToolDetailPage-BRWKPQPf.js → CliToolDetailPage-Bqj2aa1T.js} +1 -1
  6. package/dist/dashboard/assets/CliToolDetailPage-Bqj2aa1T.js.gz +0 -0
  7. package/dist/dashboard/assets/{CliToolsPage-BP-Y5iHN.js → CliToolsPage-CXsaOAO2.js} +1 -1
  8. package/dist/dashboard/assets/CliToolsPage-CXsaOAO2.js.gz +0 -0
  9. package/dist/dashboard/assets/CombosPage-BeLpJ4ma.js +1 -0
  10. package/dist/dashboard/assets/CombosPage-BeLpJ4ma.js.gz +0 -0
  11. package/dist/dashboard/assets/ConsoleLogPage-DnV0uZ3m.js +1 -0
  12. package/dist/dashboard/assets/ConsoleLogPage-DnV0uZ3m.js.gz +0 -0
  13. package/dist/dashboard/assets/CustomizationPage-CBxAjo63.js +1 -0
  14. package/dist/dashboard/assets/CustomizationPage-CBxAjo63.js.gz +0 -0
  15. package/dist/dashboard/assets/HealthEventsModal-B5yEE0k6.js +1 -0
  16. package/dist/dashboard/assets/HealthEventsModal-B5yEE0k6.js.gz +0 -0
  17. package/dist/dashboard/assets/{InjectSystemPromptPanel-Cv-2Ol_K.js → InjectSystemPromptPanel-BWFLECmz.js} +2 -2
  18. package/dist/dashboard/assets/InjectSystemPromptPanel-BWFLECmz.js.gz +0 -0
  19. package/dist/dashboard/assets/{LoginPage-bKfz_b1K.js → LoginPage-CgoPc5B9.js} +1 -1
  20. package/dist/dashboard/assets/LoginPage-CgoPc5B9.js.gz +0 -0
  21. package/dist/dashboard/assets/{ModelPicker-B-WQxx_H.js → ModelPicker-D9bFQern.js} +1 -1
  22. package/dist/dashboard/assets/ModelPicker-D9bFQern.js.gz +0 -0
  23. package/dist/dashboard/assets/OverviewPage-B_p4tL8k.js +1 -0
  24. package/dist/dashboard/assets/OverviewPage-B_p4tL8k.js.gz +0 -0
  25. package/dist/dashboard/assets/ProviderDetailPage-rs5E2rxv.js +8 -0
  26. package/dist/dashboard/assets/ProviderDetailPage-rs5E2rxv.js.gz +0 -0
  27. package/dist/dashboard/assets/ProviderIcon-D_0PiZtg.js +1 -0
  28. package/dist/dashboard/assets/ProviderIcon-D_0PiZtg.js.gz +0 -0
  29. package/dist/dashboard/assets/ProvidersPage-CLI2bJTK.js +1 -0
  30. package/dist/dashboard/assets/ProvidersPage-CLI2bJTK.js.gz +0 -0
  31. package/dist/dashboard/assets/{ProxyPage-emlTuxz6.js → ProxyPage-BwNC_-Cx.js} +1 -1
  32. package/dist/dashboard/assets/ProxyPage-BwNC_-Cx.js.gz +0 -0
  33. package/dist/dashboard/assets/QuotaPage-DA-a1iSN.js +1 -0
  34. package/dist/dashboard/assets/QuotaPage-DA-a1iSN.js.gz +0 -0
  35. package/dist/dashboard/assets/SettingsPage-CctTga2C.js +1 -0
  36. package/dist/dashboard/assets/SettingsPage-CctTga2C.js.gz +0 -0
  37. package/dist/dashboard/assets/{SetupPage-CUZrKs9r.js → SetupPage-febLU4r1.js} +1 -1
  38. package/dist/dashboard/assets/SetupPage-febLU4r1.js.gz +0 -0
  39. package/dist/dashboard/assets/{StudioPage-jWmUdDpy.js → StudioPage-M6fvR5Wz.js} +1 -1
  40. package/dist/dashboard/assets/StudioPage-M6fvR5Wz.js.gz +0 -0
  41. package/dist/dashboard/assets/UsagePage-nyeuRqz9.js +56 -0
  42. package/dist/dashboard/assets/UsagePage-nyeuRqz9.js.gz +0 -0
  43. package/dist/dashboard/assets/api-keys-BFOlvrJC.js +1 -0
  44. package/dist/dashboard/assets/api-keys-BFOlvrJC.js.gz +0 -0
  45. package/dist/dashboard/assets/app-Dx2QgsNj.css +2 -0
  46. package/dist/dashboard/assets/app-Dx2QgsNj.css.gz +0 -0
  47. package/dist/dashboard/assets/{assert-CidQVoNo.js → assert-CqTLw5e7.js} +1 -1
  48. package/dist/dashboard/assets/assert-CqTLw5e7.js.gz +0 -0
  49. package/dist/dashboard/assets/{badge-DyI_mJfi.js → badge-C83EqLB0.js} +1 -1
  50. package/dist/dashboard/assets/badge-C83EqLB0.js.gz +0 -0
  51. package/dist/dashboard/assets/ban-durations-B58FyRxC.js +1 -0
  52. package/dist/dashboard/assets/ban-durations-B58FyRxC.js.gz +0 -0
  53. package/dist/dashboard/assets/{banner-B-yXtCqb.js → banner-D1xn7pIe.js} +1 -1
  54. package/dist/dashboard/assets/banner-D1xn7pIe.js.gz +0 -0
  55. package/dist/dashboard/assets/brand/bansos_page.webp +0 -0
  56. package/dist/dashboard/assets/brand/denia_bansos.webp +0 -0
  57. package/dist/dashboard/assets/brand/favicon.webp +0 -0
  58. package/dist/dashboard/assets/brand/login_register.webp +0 -0
  59. package/dist/dashboard/assets/brand/login_register_initial.webp +0 -0
  60. package/dist/dashboard/assets/brand/og_bansos.webp +0 -0
  61. package/dist/dashboard/assets/brand/og_image.webp +0 -0
  62. package/dist/dashboard/assets/brand/sidebar_ico.webp +0 -0
  63. package/dist/dashboard/assets/{card-_HwAEqFF.js → card-DU3Lq5Lm.js} +1 -1
  64. package/dist/dashboard/assets/card-DU3Lq5Lm.js.gz +0 -0
  65. package/dist/dashboard/assets/cli-tools-Dw1U_drK.js +1 -0
  66. package/dist/dashboard/assets/cli-tools-Dw1U_drK.js.gz +0 -0
  67. package/dist/dashboard/assets/{clipboard-button-ByjCk0o9.js → clipboard-button-CEL5Sg_J.js} +1 -1
  68. package/dist/dashboard/assets/clipboard-button-CEL5Sg_J.js.gz +0 -0
  69. package/dist/dashboard/assets/{dialog-BZhOlfz-.js → dialog-BjFR5UHy.js} +1 -1
  70. package/dist/dashboard/assets/dialog-BjFR5UHy.js.gz +0 -0
  71. package/dist/dashboard/assets/{drawer-BADqIRYP.js → drawer-HvBW0AjI.js} +1 -1
  72. package/dist/dashboard/assets/drawer-HvBW0AjI.js.gz +0 -0
  73. package/dist/dashboard/assets/entry-BSu3gFlt.js +4 -0
  74. package/dist/dashboard/assets/entry-BSu3gFlt.js.gz +0 -0
  75. package/dist/dashboard/assets/{entry-COaC-irN.js → entry-CECVELyy.js} +1 -1
  76. package/dist/dashboard/assets/entry-CECVELyy.js.gz +0 -0
  77. package/dist/dashboard/assets/entry-DYYofQ7W.js +1 -0
  78. package/dist/dashboard/assets/entry-DYYofQ7W.js.gz +0 -0
  79. package/dist/dashboard/assets/{formatters-DcHJyRiu.js → formatters-DLW4i8zK.js} +1 -1
  80. package/dist/dashboard/assets/formatters-DLW4i8zK.js.gz +0 -0
  81. package/dist/dashboard/assets/{index-CgKmFngv.js → index-1-fCqe64.js} +2 -2
  82. package/dist/dashboard/assets/index-1-fCqe64.js.gz +0 -0
  83. package/dist/dashboard/assets/{inline-B2BirKry.js → inline-DGvU2oLK.js} +1 -1
  84. package/dist/dashboard/assets/inline-DGvU2oLK.js.gz +0 -0
  85. package/dist/dashboard/assets/{input-D-qV9dKY.js → input-iNnO1X2K.js} +1 -1
  86. package/dist/dashboard/assets/input-iNnO1X2K.js.gz +0 -0
  87. package/dist/dashboard/assets/{layout-B7tVWhtV.js → layout-0q8UGypb.js} +1 -1
  88. package/dist/dashboard/assets/layout-0q8UGypb.js.gz +0 -0
  89. package/dist/dashboard/assets/{list-row-CtkGE3bF.js → list-row-C0w2bQjJ.js} +1 -1
  90. package/dist/dashboard/assets/list-row-C0w2bQjJ.js.gz +0 -0
  91. package/dist/dashboard/assets/{live-DGizF06-.js → live-M5aJ_xCa.js} +1 -1
  92. package/dist/dashboard/assets/live-M5aJ_xCa.js.gz +0 -0
  93. package/dist/dashboard/assets/{mutation-GU7XdEjz.js → mutation-BQ0ccH_A.js} +1 -1
  94. package/dist/dashboard/assets/mutation-BQ0ccH_A.js.gz +0 -0
  95. package/dist/dashboard/assets/{network-C9UE-bOr.js → network-BKeQGT3K.js} +1 -1
  96. package/dist/dashboard/assets/network-BKeQGT3K.js.gz +0 -0
  97. package/dist/dashboard/assets/{numbers-BnAd22a_.js → numbers-CivAk3da.js} +1 -1
  98. package/dist/dashboard/assets/numbers-CivAk3da.js.gz +0 -0
  99. package/dist/dashboard/assets/{pagination-BAywyMzS.js → pagination-CpzvI5ep.js} +1 -1
  100. package/dist/dashboard/assets/pagination-CpzvI5ep.js.gz +0 -0
  101. package/dist/dashboard/assets/providers/devin.svg +9 -0
  102. package/dist/dashboard/assets/providers/devin.svg.gz +0 -0
  103. package/dist/dashboard/assets/providers/windsurf.svg +10 -0
  104. package/dist/dashboard/assets/providers/windsurf.svg.gz +0 -0
  105. package/dist/dashboard/assets/providers/zro.svg +3 -0
  106. package/dist/dashboard/assets/providers/zro.svg.gz +0 -0
  107. package/dist/dashboard/assets/{query-keys-YSoQOUh6.js → query-keys-IKLh0YFd.js} +1 -1
  108. package/dist/dashboard/assets/query-keys-IKLh0YFd.js.gz +0 -0
  109. package/dist/dashboard/assets/quota-DiPD6UKH.js +1 -0
  110. package/dist/dashboard/assets/quota-DiPD6UKH.js.gz +0 -0
  111. package/dist/dashboard/assets/routing-BFQX2VBc.js +1 -0
  112. package/dist/dashboard/assets/routing-BFQX2VBc.js.gz +0 -0
  113. package/dist/dashboard/assets/{select-BCOcv8WK.js → select-CXbck0jG.js} +2 -2
  114. package/dist/dashboard/assets/select-CXbck0jG.js.gz +0 -0
  115. package/dist/dashboard/assets/settings-C_XvMHh9.js +1 -0
  116. package/dist/dashboard/assets/settings-C_XvMHh9.js.gz +0 -0
  117. package/dist/dashboard/assets/{stack-ByKt3Q1g.js → stack-DggRiCxE.js} +1 -1
  118. package/dist/dashboard/assets/stack-DggRiCxE.js.gz +0 -0
  119. package/dist/dashboard/assets/{state-DXbGTx7N.js → state-ClFzfbYp.js} +1 -1
  120. package/dist/dashboard/assets/state-ClFzfbYp.js.gz +0 -0
  121. package/dist/dashboard/assets/story/25817331.webp +0 -0
  122. package/dist/dashboard/assets/story/Shorekeeper.webp +0 -0
  123. package/dist/dashboard/assets/story/cartethyia-god.webp +0 -0
  124. package/dist/dashboard/assets/story/fleurdelys_plus.webp +0 -0
  125. package/dist/dashboard/assets/story/jinhsi-blossom.webp +0 -0
  126. package/dist/dashboard/assets/story/phrolova-a.webp +0 -0
  127. package/dist/dashboard/assets/story/requestdeniawokkjpg.webp +0 -0
  128. package/dist/dashboard/assets/story/wuthering-waves-hiyuki-aemeath-hiyuki-rover.webp +0 -0
  129. package/dist/dashboard/assets/{switch-ZeTsW623.js → switch-CTIFxKqS.js} +1 -1
  130. package/dist/dashboard/assets/switch-CTIFxKqS.js.gz +0 -0
  131. package/dist/dashboard/assets/{useMutation-CbCP4w1o.js → useMutation-BClG3yWj.js} +1 -1
  132. package/dist/dashboard/assets/useMutation-BClG3yWj.js.gz +0 -0
  133. package/dist/dashboard/console-backgrounds.webp +0 -0
  134. package/dist/dashboard/console-backgrounds_initial.webp +0 -0
  135. package/dist/dashboard/donation/qris-risun-marke.webp +0 -0
  136. package/dist/dashboard/index.html +3 -3
  137. package/dist/dashboard/index.html.gz +0 -0
  138. package/dist/dashboard/precompressed-assets.json +59 -56
  139. package/migrations/full-postgres/0044_add_models_warning_metadata.sql +10 -0
  140. package/migrations/full-postgres/0045_migrate_workbuddy_to_codebuddy.sql +156 -0
  141. package/migrations/full-postgres/0046_add_accounts_quota_plan_fetched_at.sql +7 -0
  142. package/migrations/lite-sqlite3/0044_add_models_warning_metadata.sql +11 -0
  143. package/migrations/lite-sqlite3/0045_migrate_workbuddy_to_codebuddy.sql +156 -0
  144. package/migrations/lite-sqlite3/0046_add_accounts_quota_plan_fetched_at.sql +4 -0
  145. package/package.json +1 -1
  146. package/src/boot/app.ts +10 -13
  147. package/src/boot/dependencies.ts +12 -4
  148. package/src/boot/flush.ts +20 -0
  149. package/src/boot/lifecycle.ts +7 -5
  150. package/src/config/cooldowns.ts +1 -1
  151. package/src/config/routing.ts +26 -0
  152. package/src/config/spec-metadata.ts +72 -0
  153. package/src/config/spec.ts +20 -6
  154. package/src/console/api-keys/contracts.ts +12 -1
  155. package/src/console/api-keys/routes.ts +50 -31
  156. package/src/console/auth/account-reset.ts +7 -9
  157. package/src/console/backup/contracts.ts +4 -0
  158. package/src/console/backup/export-endpoint.ts +7 -5
  159. package/src/console/cli-tools/service.ts +6 -12
  160. package/src/console/console-router-guards.ts +23 -16
  161. package/src/console/console-router.ts +7 -0
  162. package/src/console/dashboard-assets.ts +5 -5
  163. package/src/console/domain-registration.ts +27 -2
  164. package/src/console/inferhub/normalize.ts +24 -28
  165. package/src/console/inferhub/types.ts +3 -0
  166. package/src/console/ip-bans/routes.ts +93 -0
  167. package/src/console/observability/store.ts +0 -53
  168. package/src/console/providers/catalog/catalog-projections.ts +5 -2
  169. package/src/console/providers/catalog/contracts.ts +123 -0
  170. package/src/console/providers/catalog/discover-constants.ts +6 -0
  171. package/src/console/providers/catalog/model-operations.ts +122 -10
  172. package/src/console/providers/catalog/provider-operations.ts +116 -8
  173. package/src/console/providers/catalog/public-model-store.ts +0 -0
  174. package/src/console/providers/catalog/routes.ts +65 -4
  175. package/src/console/providers/catalog/store-helpers.ts +30 -1
  176. package/src/console/providers/catalog/store.ts +249 -4
  177. package/src/console/quota/account-quota-global.ts +97 -19
  178. package/src/console/quota/account-quota-shared.ts +49 -0
  179. package/src/console/quota/account-quota-tenant.ts +6 -18
  180. package/src/console/quota/native-identity.ts +2 -0
  181. package/src/console/quota/quota-view.ts +19 -3
  182. package/src/console/routing/pools/operations.ts +31 -46
  183. package/src/console/settings/contracts.ts +18 -0
  184. package/src/console/settings/store.ts +32 -0
  185. package/src/console/shared/errors.ts +33 -6
  186. package/src/console/system-prompt/contracts.ts +0 -1
  187. package/src/main.ts +3 -3
  188. package/src/network/pool/byte-accounting.ts +3 -2
  189. package/src/network/pool/selector.ts +182 -39
  190. package/src/network/pool-cooldown-store.ts +89 -14
  191. package/src/network/pool-health.ts +49 -61
  192. package/src/network/rate-limit-verdict.ts +1 -1
  193. package/src/network/ssrf.ts +6 -6
  194. package/src/observability/event-log.ts +7 -361
  195. package/src/observability/event-query.ts +18 -4
  196. package/src/observability/performance-metrics.ts +9 -0
  197. package/src/observability/runtime-metrics.ts +14 -9
  198. package/src/observability/telemetry-buffer.ts +15 -25
  199. package/src/observability/telemetry-reconciliation.ts +9 -37
  200. package/src/observability/telemetry-row.ts +125 -0
  201. package/src/persistence/db-mode.ts +4 -1
  202. package/src/persistence/dialect.ts +30 -0
  203. package/src/persistence/migrate.ts +6 -3
  204. package/src/persistence/schema.ts +28 -2
  205. package/src/persistence/ttl-cache.ts +20 -2
  206. package/src/protocol/codecs.ts +1 -1
  207. package/src/protocol/dsml.ts +35 -87
  208. package/src/protocol/gateway-error.ts +1 -0
  209. package/src/protocol/markup-stream-parser.ts +195 -0
  210. package/src/protocol/reasoning-text.ts +19 -0
  211. package/src/protocol/reasoning.ts +1 -1
  212. package/src/protocol/request/chat.ts +14 -14
  213. package/src/protocol/request/gemini.ts +8 -2
  214. package/src/protocol/request/messages.ts +4 -5
  215. package/src/protocol/response/chat.ts +12 -20
  216. package/src/protocol/response/gemini.ts +6 -1
  217. package/src/protocol/response/omp-tool-call-markup.ts +43 -86
  218. package/src/protocol/response/responses.ts +18 -2
  219. package/src/protocol/sse-decoder.ts +25 -8
  220. package/src/protocol/stop-reason.ts +23 -1
  221. package/src/protocol/stream-error-frames.ts +13 -3
  222. package/src/protocol/think-split.ts +112 -0
  223. package/src/protocol/translation/dialect/qwen.ts +26 -58
  224. package/src/protocol/translation/quirks.ts +1 -1
  225. package/src/protocol/translation/tool-repair.ts +2 -2
  226. package/src/protocol/upstream/send-claude-messages.ts +15 -4
  227. package/src/protocol/upstream-parsing.ts +1 -1
  228. package/src/providers/adapters/compatible-adapter.ts +78 -17
  229. package/src/providers/authentication/oauth-client.ts +1 -1
  230. package/src/providers/authentication/oauth-device-flow.ts +98 -8
  231. package/src/providers/authentication/oauth-refresh-service.ts +153 -59
  232. package/src/providers/configured-provider.ts +148 -0
  233. package/src/providers/default-registry.ts +117 -27
  234. package/src/providers/discovery/discovery-types.ts +12 -0
  235. package/src/providers/discovery/probe-phases.ts +74 -33
  236. package/src/providers/discovery/probing-service.ts +293 -165
  237. package/src/providers/integrations/alibabacp/alibabacp-models.ts +1 -2
  238. package/src/providers/integrations/{dashscope-models.ts → alibabacp/dashscope-models.ts} +2 -2
  239. package/src/providers/integrations/antigravity/antigravity-protocol.ts +121 -2
  240. package/src/providers/integrations/antigravity/antigravity.ts +13 -2
  241. package/src/providers/integrations/buddy/buddy-catalog-shared.ts +72 -34
  242. package/src/providers/integrations/buddy/buddy-chat-shared.ts +191 -35
  243. package/src/providers/integrations/buddy/buddy-checkin.ts +17 -39
  244. package/src/providers/integrations/buddy/buddy-conversation-headers.ts +2 -2
  245. package/src/providers/integrations/buddy/buddy-discovery-shared.ts +5 -5
  246. package/src/providers/integrations/buddy/buddy-identity.ts +89 -6
  247. package/src/providers/integrations/buddy/buddy-media.ts +4 -5
  248. package/src/providers/integrations/buddy/buddy-oauth-shared.ts +2 -2
  249. package/src/providers/integrations/buddy/buddy-quota-shared.ts +1 -1
  250. package/src/providers/integrations/buddy/buddy-usage-shared.ts +1 -3
  251. package/src/providers/integrations/buddy/capabilities.ts +6 -43
  252. package/src/providers/integrations/buddy/codebuddy/cn-catalog.ts +2 -1
  253. package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +23 -1
  254. package/src/providers/integrations/buddy/codebuddy/provider.ts +6 -2
  255. package/src/providers/integrations/buddy/codebuddy/shared.ts +38 -27
  256. package/src/providers/integrations/claude/claude-quota.ts +14 -17
  257. package/src/providers/integrations/cline/cline.ts +2 -2
  258. package/src/providers/integrations/commandcode/quota.ts +22 -28
  259. package/src/providers/integrations/cursor/cursor-auth.ts +0 -8
  260. package/src/providers/integrations/cursor/cursor-catalog.ts +2 -0
  261. package/src/providers/integrations/cursor/cursor-exec.ts +745 -0
  262. package/src/providers/integrations/cursor/cursor-http2.ts +2 -0
  263. package/src/providers/integrations/cursor/cursor-request.ts +35 -20
  264. package/src/providers/integrations/cursor/cursor-shared.ts +26 -5
  265. package/src/providers/integrations/cursor/cursor.ts +139 -70
  266. package/src/providers/integrations/cursor/protocol/messages.ts +22 -29
  267. package/src/providers/integrations/cursor/protocol/protobuf-schema.ts +37 -0
  268. package/src/providers/integrations/cursor/protocol/tool-canonical.ts +26 -100
  269. package/src/providers/integrations/devin/devin-auth.ts +353 -0
  270. package/src/providers/integrations/devin/devin-catalog.ts +207 -0
  271. package/src/providers/integrations/devin/devin-http.ts +91 -0
  272. package/src/providers/integrations/devin/devin-quota.ts +214 -0
  273. package/src/providers/integrations/devin/devin-shared.ts +111 -0
  274. package/src/providers/integrations/devin/devin.ts +539 -0
  275. package/src/providers/integrations/devin/protocol/devin-proto.ts +558 -0
  276. package/src/providers/integrations/devin/protocol/framing.ts +124 -0
  277. package/src/providers/integrations/factory/factory-oauth.ts +17 -44
  278. package/src/providers/integrations/factory/factory.ts +2 -2
  279. package/src/providers/integrations/freebuff/assets/official-system-manager.txt +88 -0
  280. package/src/providers/integrations/freebuff/assets/official-system-worker.txt +57 -0
  281. package/src/providers/integrations/freebuff/assets/official-tools.json +1070 -0
  282. package/src/providers/integrations/freebuff/freebuff-catalog.ts +120 -0
  283. package/src/providers/integrations/freebuff/freebuff-constants.ts +1233 -0
  284. package/src/providers/integrations/freebuff/freebuff-discovery.ts +108 -0
  285. package/src/providers/integrations/freebuff/freebuff-oauth.ts +240 -0
  286. package/src/providers/integrations/freebuff/freebuff-quota.ts +260 -0
  287. package/src/providers/integrations/freebuff/freebuff-virtual.ts +89 -0
  288. package/src/providers/integrations/freebuff/freebuff.ts +682 -0
  289. package/src/providers/integrations/freepi/freepi-catalog.ts +60 -0
  290. package/src/providers/integrations/freepi/freepi-discovery.ts +82 -0
  291. package/src/providers/integrations/freepi/freepi-headers.ts +21 -0
  292. package/src/providers/integrations/freepi/freepi-oauth.ts +142 -0
  293. package/src/providers/integrations/freepi/freepi-protocol.ts +242 -0
  294. package/src/providers/integrations/freepi/freepi-quota.ts +126 -0
  295. package/src/providers/integrations/freepi/freepi-realm.ts +35 -0
  296. package/src/providers/integrations/freepi/freepi.ts +292 -0
  297. package/src/providers/integrations/gemini.ts +2 -2
  298. package/src/providers/integrations/grok/grok-oauth.ts +48 -45
  299. package/src/providers/integrations/grok/grok-ratelimit-store.ts +10 -17
  300. package/src/providers/integrations/grok/grok-turn-index.ts +11 -16
  301. package/src/providers/integrations/hermes/hermes-oauth.ts +94 -0
  302. package/src/providers/integrations/{hermes.ts → hermes/hermes.ts} +13 -21
  303. package/src/providers/integrations/hermes/index.ts +2 -0
  304. package/src/providers/integrations/kiro/kiro-catalog.ts +17 -1
  305. package/src/providers/integrations/kiro/kiro-discovery.ts +10 -12
  306. package/src/providers/integrations/kiro/kiro-quota.ts +8 -4
  307. package/src/providers/integrations/kiro/kiro.ts +15 -19
  308. package/src/providers/integrations/ollamacloud/index.ts +2 -0
  309. package/src/providers/integrations/{ollamacloud-models.ts → ollamacloud/models.ts} +2 -2
  310. package/src/providers/integrations/{ollamacloud-quota.ts → ollamacloud/quota.ts} +3 -3
  311. package/src/providers/integrations/opencode/fingerprint.ts +1 -1
  312. package/src/providers/integrations/opencode/opencode-go-quota.ts +2 -8
  313. package/src/providers/integrations/perch/perch-catalog.ts +105 -0
  314. package/src/providers/integrations/perch/perch-credential.ts +25 -0
  315. package/src/providers/integrations/perch/perch-oauth.ts +170 -0
  316. package/src/providers/integrations/perch/perch-protocol.ts +208 -0
  317. package/src/providers/integrations/perch/perch-quota.ts +123 -0
  318. package/src/providers/integrations/perch/perch-realm.ts +82 -0
  319. package/src/providers/integrations/perch/perch-system-prompt.ts +13 -0
  320. package/src/providers/integrations/perch/perch.ts +305 -0
  321. package/src/providers/integrations/perch/saffron-system-prompt.txt +8 -0
  322. package/src/providers/integrations/qoder/chat-proto.ts +328 -328
  323. package/src/providers/integrations/qoder/claim-qoder-credits.ts +19 -60
  324. package/src/providers/integrations/qoder/qoder-adapter.ts +90 -40
  325. package/src/providers/integrations/qoder/qoder-auth.ts +101 -29
  326. package/src/providers/integrations/qoder/qoder-cache.ts +32 -0
  327. package/src/providers/integrations/qoder/qoder-catalog.ts +26 -6
  328. package/src/providers/integrations/qoder/qoder-chat.ts +7 -21
  329. package/src/providers/integrations/qoder/qoder-checkin.ts +6 -6
  330. package/src/providers/integrations/qoder/qoder-grpc.ts +210 -7
  331. package/src/providers/integrations/qoder/qoder-ide.ts +5 -108
  332. package/src/providers/integrations/qoder/qoder-identity-cache.ts +184 -0
  333. package/src/providers/integrations/qoder/qoder-native-identity-store.ts +83 -18
  334. package/src/providers/integrations/qoder/qoder-realm.ts +9 -6
  335. package/src/providers/integrations/qoder/qoder-umid.ts +109 -342
  336. package/src/providers/integrations/qoder/qoder-virtual.ts +33 -1
  337. package/src/providers/integrations/qoder/qoder.ts +3 -7
  338. package/src/providers/integrations/trae/trae-catalog.ts +9 -14
  339. package/src/providers/integrations/trae/trae-headers.ts +36 -11
  340. package/src/providers/integrations/trae/trae-oauth.ts +14 -17
  341. package/src/providers/integrations/trae/trae-protocol.ts +12 -11
  342. package/src/providers/integrations/trae/trae-quota.ts +96 -36
  343. package/src/providers/integrations/trae/trae-realm.ts +74 -15
  344. package/src/providers/integrations/trae/trae.ts +99 -26
  345. package/src/providers/integrations/zcode-start-plan/provider.ts +226 -23
  346. package/src/providers/integrations/zcode-start-plan/quota.ts +19 -2
  347. package/src/providers/integrations/zro/zro.ts +204 -0
  348. package/src/providers/operations/account-error-classification.ts +235 -278
  349. package/src/providers/operations/account-health-service.ts +273 -26
  350. package/src/providers/operations/account-refresh-status.ts +269 -0
  351. package/src/providers/operations/account-tier.ts +66 -0
  352. package/src/providers/operations/client-version-resolver.ts +1 -1
  353. package/src/providers/operations/client-versions.ts +42 -150
  354. package/src/providers/operations/cooldown-rules.ts +56 -0
  355. package/src/providers/operations/{model-catalog-cache.ts → provider-caches.ts} +68 -16
  356. package/src/providers/operations/provider-catalog-seeder.ts +4 -0
  357. package/src/providers/operations/provider-catalog-service.ts +1 -1
  358. package/src/providers/operations/provider-credential-service.ts +102 -32
  359. package/src/providers/operations/provider-rules/github.ts +48 -0
  360. package/src/providers/operations/provider-rules/grok.ts +33 -0
  361. package/src/providers/operations/provider-rules/inception.ts +49 -0
  362. package/src/providers/operations/provider-rules/index.ts +32 -0
  363. package/src/providers/operations/provider-rules/perch.ts +37 -0
  364. package/src/providers/operations/provider-rules/qoder.ts +70 -0
  365. package/src/providers/operations/provider-rules/trae.ts +81 -0
  366. package/src/providers/provider-contracts.ts +33 -2
  367. package/src/providers/{provider-credential-resolver.ts → provider-credentials.ts} +40 -3
  368. package/src/providers/provider-metadata.ts +99 -14
  369. package/src/providers/provider-model-definition.ts +36 -0
  370. package/src/providers/provider-probe.ts +15 -1
  371. package/src/providers/provider-registry.ts +42 -33
  372. package/src/providers/quota/quota-limit-windows.ts +16 -5
  373. package/src/providers/quota/quota-support.ts +1 -1
  374. package/src/providers/quota/quota-window-parser.ts +90 -18
  375. package/src/providers/quota/refresh.ts +96 -18
  376. package/src/security/abuse.ts +71 -0
  377. package/src/security/api-key-auth-cache.ts +1 -1
  378. package/src/security/api-key-auth.ts +6 -2
  379. package/src/transport/dispatch/attempt-finalize.ts +14 -13
  380. package/src/transport/dispatch/attempt-loop.ts +71 -17
  381. package/src/transport/dispatch/generation.ts +2 -8
  382. package/src/transport/dispatch/native-shared.ts +25 -0
  383. package/src/transport/dispatch/proxy-request.ts +9 -46
  384. package/src/transport/dispatch/responses-compact.ts +2 -8
  385. package/src/transport/dispatch/streaming/finalize.ts +9 -6
  386. package/src/transport/dispatch/streaming/stream.ts +9 -36
  387. package/src/transport/dispatch/systemone.ts +2 -8
  388. package/src/transport/dispatch/token-count.ts +2 -6
  389. package/src/transport/dispatch/upstream.ts +67 -1
  390. package/src/transport/drain-endpoint.ts +31 -4
  391. package/src/transport/failure-policy.ts +129 -37
  392. package/src/transport/guards/body-policy.ts +7 -1
  393. package/src/transport/guards/error-lifecycle.ts +13 -1
  394. package/src/transport/guards/request-context.ts +22 -14
  395. package/src/transport/request/preparer.ts +81 -71
  396. package/src/transport/routing/bounded-lru.ts +14 -0
  397. package/src/transport/routing/drain-reserve.ts +129 -0
  398. package/src/transport/routing/plan-gate.ts +7 -1
  399. package/src/transport/routing/round-robin.ts +60 -0
  400. package/src/transport/routing/route-catalog.ts +123 -111
  401. package/src/transport/routing/route-model.ts +51 -9
  402. package/src/transport/routing/router.ts +418 -33
  403. package/src/transport/surface/adapters.ts +11 -2
  404. package/src/transport/surface/chat/adapter.ts +2 -2
  405. package/src/transport/surface/chat/encode.ts +47 -4
  406. package/src/transport/surface/chat/parse.ts +2 -12
  407. package/src/transport/surface/completion.ts +44 -33
  408. package/src/transport/surface/content-parts.ts +21 -0
  409. package/src/transport/surface/messages/adapter.ts +9 -2
  410. package/src/transport/surface/messages/encode.ts +11 -3
  411. package/src/transport/surface/messages/parse.ts +2 -1
  412. package/src/transport/surface/messages/stream.ts +33 -17
  413. package/src/transport/surface/responses/encode.ts +34 -6
  414. package/src/transport/surface/responses/parse.ts +3 -11
  415. package/src/workers/daily-checkin.ts +1 -1
  416. package/src/workers/quota-refresh-sweep.ts +1 -1
  417. package/src/workers/scheduler.ts +22 -4
  418. package/dist/dashboard/assets/BannedPage-DeIbIiBU.js +0 -1
  419. package/dist/dashboard/assets/BannedPage-DeIbIiBU.js.gz +0 -0
  420. package/dist/dashboard/assets/CliToolDetailPage-BRWKPQPf.js.gz +0 -0
  421. package/dist/dashboard/assets/CliToolsPage-BP-Y5iHN.js.gz +0 -0
  422. package/dist/dashboard/assets/CombosPage-B7JHozVa.js +0 -1
  423. package/dist/dashboard/assets/CombosPage-B7JHozVa.js.gz +0 -0
  424. package/dist/dashboard/assets/ConsoleLogPage-GX7iGGcu.js +0 -1
  425. package/dist/dashboard/assets/ConsoleLogPage-GX7iGGcu.js.gz +0 -0
  426. package/dist/dashboard/assets/CustomizationPage-HVonbGEa.js +0 -1
  427. package/dist/dashboard/assets/CustomizationPage-HVonbGEa.js.gz +0 -0
  428. package/dist/dashboard/assets/HealthEventsModal-xaut4OJ9.js +0 -1
  429. package/dist/dashboard/assets/HealthEventsModal-xaut4OJ9.js.gz +0 -0
  430. package/dist/dashboard/assets/InjectSystemPromptPanel-Cv-2Ol_K.js.gz +0 -0
  431. package/dist/dashboard/assets/LoginPage-bKfz_b1K.js.gz +0 -0
  432. package/dist/dashboard/assets/ModelPicker-B-WQxx_H.js.gz +0 -0
  433. package/dist/dashboard/assets/OverviewPage-Z9pJZFo8.js +0 -1
  434. package/dist/dashboard/assets/OverviewPage-Z9pJZFo8.js.gz +0 -0
  435. package/dist/dashboard/assets/ProviderDetailPage-CMeDBxR1.js +0 -7
  436. package/dist/dashboard/assets/ProviderDetailPage-CMeDBxR1.js.gz +0 -0
  437. package/dist/dashboard/assets/ProviderIcon-jjqXJDA3.js +0 -1
  438. package/dist/dashboard/assets/ProviderIcon-jjqXJDA3.js.gz +0 -0
  439. package/dist/dashboard/assets/ProvidersPage-BXPdOpG4.js +0 -1
  440. package/dist/dashboard/assets/ProvidersPage-BXPdOpG4.js.gz +0 -0
  441. package/dist/dashboard/assets/ProxyPage-emlTuxz6.js.gz +0 -0
  442. package/dist/dashboard/assets/QuotaPage-CMJKNYWb.js +0 -1
  443. package/dist/dashboard/assets/QuotaPage-CMJKNYWb.js.gz +0 -0
  444. package/dist/dashboard/assets/SettingsPage-BSy7EwGo.js +0 -1
  445. package/dist/dashboard/assets/SettingsPage-BSy7EwGo.js.gz +0 -0
  446. package/dist/dashboard/assets/SetupPage-CUZrKs9r.js.gz +0 -0
  447. package/dist/dashboard/assets/StudioPage-jWmUdDpy.js.gz +0 -0
  448. package/dist/dashboard/assets/UsagePage-5F9tGVGG.js +0 -56
  449. package/dist/dashboard/assets/UsagePage-5F9tGVGG.js.gz +0 -0
  450. package/dist/dashboard/assets/api-keys-KvX83mRe.js +0 -1
  451. package/dist/dashboard/assets/api-keys-KvX83mRe.js.gz +0 -0
  452. package/dist/dashboard/assets/app-BEsU8hKs.css +0 -2
  453. package/dist/dashboard/assets/app-BEsU8hKs.css.gz +0 -0
  454. package/dist/dashboard/assets/assert-CidQVoNo.js.gz +0 -0
  455. package/dist/dashboard/assets/badge-DyI_mJfi.js.gz +0 -0
  456. package/dist/dashboard/assets/banner-B-yXtCqb.js.gz +0 -0
  457. package/dist/dashboard/assets/brand/bansos_page.png +0 -0
  458. package/dist/dashboard/assets/brand/denia_bansos.jpg +0 -0
  459. package/dist/dashboard/assets/brand/favicon.jpg +0 -0
  460. package/dist/dashboard/assets/brand/login_register.jpg +0 -0
  461. package/dist/dashboard/assets/brand/login_register_initial.jpg +0 -0
  462. package/dist/dashboard/assets/brand/og_bansos.jpg +0 -0
  463. package/dist/dashboard/assets/brand/og_image.jpg +0 -0
  464. package/dist/dashboard/assets/brand/sidebar_ico.jpg +0 -0
  465. package/dist/dashboard/assets/card-_HwAEqFF.js.gz +0 -0
  466. package/dist/dashboard/assets/cli-tools-Bwv5-zGZ.js +0 -1
  467. package/dist/dashboard/assets/cli-tools-Bwv5-zGZ.js.gz +0 -0
  468. package/dist/dashboard/assets/clipboard-button-ByjCk0o9.js.gz +0 -0
  469. package/dist/dashboard/assets/dialog-BZhOlfz-.js.gz +0 -0
  470. package/dist/dashboard/assets/drawer-BADqIRYP.js.gz +0 -0
  471. package/dist/dashboard/assets/entry-COaC-irN.js.gz +0 -0
  472. package/dist/dashboard/assets/entry-Ca7LSCet.js +0 -4
  473. package/dist/dashboard/assets/entry-Ca7LSCet.js.gz +0 -0
  474. package/dist/dashboard/assets/entry-PDd27uCd.js +0 -1
  475. package/dist/dashboard/assets/entry-PDd27uCd.js.gz +0 -0
  476. package/dist/dashboard/assets/formatters-DcHJyRiu.js.gz +0 -0
  477. package/dist/dashboard/assets/index-CgKmFngv.js.gz +0 -0
  478. package/dist/dashboard/assets/inline-B2BirKry.js.gz +0 -0
  479. package/dist/dashboard/assets/input-D-qV9dKY.js.gz +0 -0
  480. package/dist/dashboard/assets/layout-B7tVWhtV.js.gz +0 -0
  481. package/dist/dashboard/assets/list-row-CtkGE3bF.js.gz +0 -0
  482. package/dist/dashboard/assets/live-DGizF06-.js.gz +0 -0
  483. package/dist/dashboard/assets/minimize-2-BTmMTK4o.js +0 -1
  484. package/dist/dashboard/assets/minimize-2-BTmMTK4o.js.gz +0 -0
  485. package/dist/dashboard/assets/mutation-GU7XdEjz.js.gz +0 -0
  486. package/dist/dashboard/assets/network-C9UE-bOr.js.gz +0 -0
  487. package/dist/dashboard/assets/numbers-BnAd22a_.js.gz +0 -0
  488. package/dist/dashboard/assets/pagination-BAywyMzS.js.gz +0 -0
  489. package/dist/dashboard/assets/query-keys-YSoQOUh6.js.gz +0 -0
  490. package/dist/dashboard/assets/quota-DJ1QlAOq.js +0 -1
  491. package/dist/dashboard/assets/quota-DJ1QlAOq.js.gz +0 -0
  492. package/dist/dashboard/assets/routing-fnCsxgNO.js +0 -1
  493. package/dist/dashboard/assets/routing-fnCsxgNO.js.gz +0 -0
  494. package/dist/dashboard/assets/select-BCOcv8WK.js.gz +0 -0
  495. package/dist/dashboard/assets/settings-BxslsErn.js +0 -1
  496. package/dist/dashboard/assets/settings-BxslsErn.js.gz +0 -0
  497. package/dist/dashboard/assets/stack-ByKt3Q1g.js.gz +0 -0
  498. package/dist/dashboard/assets/state-DXbGTx7N.js.gz +0 -0
  499. package/dist/dashboard/assets/switch-ZeTsW623.js.gz +0 -0
  500. package/dist/dashboard/assets/useMutation-CbCP4w1o.js.gz +0 -0
  501. package/dist/dashboard/console-backgrounds.jpg +0 -0
  502. package/dist/dashboard/console-backgrounds_initial.jpg +0 -0
  503. package/dist/dashboard/donation/qris-risun-marke.png +0 -0
  504. package/src/providers/bundled-discovery-providers.ts +0 -98
  505. package/src/providers/configured-openai-providers.ts +0 -120
  506. package/src/providers/integrations/buddy/turing-device-token.ts +0 -262
  507. package/src/providers/integrations/buddy/workbuddy/cn-oauth.ts +0 -30
  508. package/src/providers/integrations/buddy/workbuddy/cn-provider.ts +0 -58
  509. package/src/providers/integrations/buddy/workbuddy/oauth.ts +0 -31
  510. package/src/providers/integrations/buddy/workbuddy/provider.ts +0 -209
  511. package/src/providers/integrations/buddy/workbuddy/quota.ts +0 -161
  512. package/src/providers/integrations/buddy/workbuddy/shared.ts +0 -285
  513. package/src/providers/operations/model-discovery-cache.ts +0 -34
  514. package/src/providers/operations/probe-fetch.ts +0 -15
  515. package/src/providers/operations/provider-version-cache.ts +0 -30
  516. package/src/providers/provider-capabilities.ts +0 -33
  517. package/src/providers/provider-credential-envelope.ts +0 -43
  518. /package/dist/dashboard/assets/{app-CTxoqyYB.js → app-DIXLjojg.js} +0 -0
  519. /package/dist/dashboard/assets/{app-CTxoqyYB.js.gz → app-DIXLjojg.js.gz} +0 -0
@@ -10,7 +10,7 @@ import {
10
10
  firstNonEmptyString as firstString,
11
11
  tryParseJson as safeParseJson,
12
12
  } from "../../../shared/coerce";
13
- import type { CredentialMode, ModelCatalogEntry } from "./contracts";
13
+ import type { CredentialMode, ModelCatalogEntry, ModelWarning } from "./contracts";
14
14
  import {
15
15
  FALLBACK_CONTEXT_LIMIT,
16
16
  FALLBACK_OUTPUT_LIMIT,
@@ -285,6 +285,32 @@ export function liveModelCooldowns(
285
285
  }
286
286
 
287
287
 
288
+ /**
289
+ * Reads the display-only warning blob. Postgres returns `jsonb` already
290
+ * decoded; SQLite returns the JSON string its `text` column carries, so both
291
+ * shapes are accepted. A value that is not `{ warnings: [...] }` is ignored —
292
+ * a malformed row must not break the entire model list.
293
+ */
294
+ function parseWarningMetadata(raw: unknown): { warnings: ModelWarning[] } | undefined {
295
+ let value: unknown = raw;
296
+ if (typeof value === "string") {
297
+ try {
298
+ value = JSON.parse(value) as unknown;
299
+ } catch {
300
+ return undefined;
301
+ }
302
+ }
303
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return undefined;
304
+ const warnings = (value as Record<string, unknown>)["warnings"];
305
+ if (!Array.isArray(warnings)) return undefined;
306
+ const projected = warnings.filter((warning): warning is ModelWarning => {
307
+ if (warning === null || typeof warning !== "object" || Array.isArray(warning)) return false;
308
+ const item = warning as Record<string, unknown>;
309
+ return typeof item["kind"] === "string" && typeof item["message"] === "string";
310
+ });
311
+ return projected.length > 0 ? { warnings: projected } : undefined;
312
+ }
313
+
288
314
  /**
289
315
  * Projects one persisted `models` row into the console's catalog entry.
290
316
  *
@@ -344,6 +370,9 @@ export function mapModelRow(
344
370
  // Same three-valued rule, from the provider's own directory. Qoder states
345
371
  // `is_free` per row, so a plan change moves the pill without a code change.
346
372
  ...(row.freeTier === true ? { freeTier: true } : {}),
373
+ // Display-only: a `NULL` column (or a malformed legacy value) projects no
374
+ // warnings at all rather than leaking the raw JSON shape to the dashboard.
375
+ ...(parseWarningMetadata(row.warningMetadata) ?? {}),
347
376
  sourceUpdatedAt: row.sourceUpdatedAt?.toISOString() ?? null,
348
377
  } satisfies ModelCatalogEntry;
349
378
  }
@@ -1,5 +1,6 @@
1
1
  // Drizzle-backed provider, account, model, routing, and API-key repositories.
2
- import { and, asc, eq, gte, inArray, isNull, or, sql, type SQL } from "drizzle-orm";
2
+ import { and, asc, eq, gte, inArray, isNull, ne, or, sql, type SQL } from "drizzle-orm";
3
+ import { liveModelCooldownExistsSql } from "../../../persistence/dialect";
3
4
  import { ConsoleDomainError } from "../../shared/errors";
4
5
  import { parseCustomProviderId, isBundledProviderId } from "../../../providers/provider-contracts";
5
6
  import { providerRateLimitIsIpScoped } from "../../../providers/provider-metadata";
@@ -13,11 +14,13 @@ import { buddyTaskLedger, models, networkPools, providerAccounts, providerOauthS
13
14
  import { IS_GATEWAY_ERROR_SQL } from "../../../observability/event-query";
14
15
  import type { WireFamily } from "../../../protocol/canonical-model";
15
16
  import { listAccountHealthEvents, recoverAccount, deleteAccountHealthEvents, type AccountHealthEventRecord } from "../../../providers/operations/account-health-service";
16
- import { invalidateCredentialCache } from "../../../providers/operations/provider-credential-service";
17
+ import { invalidateCredentialCache, invalidateCredentialCaches } from "../../../providers/operations/provider-credential-service";
18
+ import { invalidateQuotaCaches } from "../../../providers/quota/cache";
17
19
  import { encryptCredential, hashSecret } from "../../../security/crypto";
18
20
  import type { TelemetryBatchBuffer } from "../../../observability/telemetry-buffer";
21
+ import type { RedisClient } from "../../../persistence/redis";
19
22
  import { bumpCatalogRevision, type BundledProviderCatalog } from "../../../providers/operations/provider-catalog-service";
20
- import type { AccountInflightReading, BuddyGrowthTaskLedgerEntry, ByokConnectionTestRequest, ByokConnectionTestResult, CreateProviderAccountBatchItemResult, CreateProviderAccountBatchResult, CreateProviderAccountRequest, CredentialMode, ModelCatalogEntry, ProbeAllAccountsResult, ProbeAllModelsResult, ProbeModelRequest, ProbeModelResult, ProviderAccountResponse, ProviderAccountTokenUsage, ProviderCatalogStore, ProviderRecord, SetModelEnabledRequest, UpdateModelMetadataRequest, UpdateProviderAccountRequest, ModelMetadataOverride } from "./contracts";
23
+ import type { AccountInflightReading, BuddyGrowthTaskLedgerEntry, ByokConnectionTestRequest, ByokConnectionTestResult, CreateProviderAccountBatchItemResult, CreateProviderAccountBatchResult, CreateProviderAccountRequest, CredentialMode, DiscoverModelCandidate, ModelCatalogEntry, ProbeAllAccountsResult, ProbeAllModelsResult, ProbeModelRequest, ProbeModelResult, ProviderAccountBulkActionResult, ProviderAccountResponse, ProviderAccountTokenUsage, ProviderCatalogStore, ProviderRecord, SetModelEnabledRequest, UpdateModelMetadataRequest, UpdateProviderAccountRequest, ModelMetadataOverride } from "./contracts";
21
24
  import { isServiceKind, validateCompatibilityProfile } from "./contracts";
22
25
  import { ProviderProbingService, type ProbeOutboundResolver } from "../../../providers/discovery/probing-service";
23
26
  import {
@@ -75,6 +78,7 @@ export class DrizzleProviderCatalogStore implements ProviderCatalogStore {
75
78
  private readonly probing: ProviderProbingService;
76
79
  private readonly bundledModelCatalog: ReadonlyMap<string, readonly ModelDefinition[]>;
77
80
  private readonly providerRegistry: ProviderRegistry;
81
+ private readonly redis: RedisClient | undefined;
78
82
 
79
83
  private readonly readAccountInflight:
80
84
  | ((
@@ -97,11 +101,14 @@ export class DrizzleProviderCatalogStore implements ProviderCatalogStore {
97
101
  tenantId: string,
98
102
  ) => Promise<readonly AccountInflightReading[]>)
99
103
  | undefined;
104
+ /** Shared Redis handle for quota-cache invalidation on bulk delete. */
105
+ readonly redis?: RedisClient | undefined;
100
106
  },
101
107
  ) {
102
108
  this.bundledModelCatalog = options.bundledModelCatalog.modelsByProvider;
103
109
  this.providerRegistry = options.providerRegistry;
104
110
  this.readAccountInflight = options.readAccountInflight;
111
+ this.redis = options.redis;
105
112
  this.probing = new ProviderProbingService({
106
113
  db,
107
114
  telemetryBuffer: options.telemetryBuffer,
@@ -430,12 +437,17 @@ export class DrizzleProviderCatalogStore implements ProviderCatalogStore {
430
437
  * (warms OAuth token / connection pools), the rest run concurrently bounded
431
438
  * to 5. Registered model ids come from this store's own catalog read.
432
439
  */
433
- async probeAllModels(tenantId: string, providerId: string): Promise<ProbeAllModelsResult> {
440
+ async probeAllModels(
441
+ tenantId: string,
442
+ providerId: string,
443
+ mode?: ProbeModelRequest["mode"],
444
+ ): Promise<ProbeAllModelsResult> {
434
445
  const entries = await this.listModels(tenantId, providerId);
435
446
  const requests = entries.map((entry) => ({
436
447
  modelId: entry.modelId,
437
448
  route: entry.route,
438
449
  serviceKind: isServiceKind(entry.serviceKind) ? entry.serviceKind : ("llm" as const),
450
+ ...(mode === undefined ? {} : { mode }),
439
451
  }));
440
452
  const uniqueRequests = requests.filter(
441
453
  (request, index, all) =>
@@ -559,6 +571,8 @@ export class DrizzleProviderCatalogStore implements ProviderCatalogStore {
559
571
  const findRow = (ownership: SQL) =>
560
572
  this.db
561
573
  .select({ id: models.id, modalities: models.modalities, source: models.source })
574
+ // source is read for both the global builtin guard above and the new
575
+ // non-builtin-only warning edit.
562
576
  .from(models)
563
577
  .innerJoin(providers, eq(models.providerId, providers.id))
564
578
  .where(
@@ -594,6 +608,29 @@ export class DrizzleProviderCatalogStore implements ProviderCatalogStore {
594
608
  // per-flag field: the point of the flag is that the operator does not know
595
609
  // which capabilities the model has, so an explicit `false` alongside it is
596
610
  // either stale or a mistake.
611
+ // Builtin rows are re-seeded from the static catalog on every boot, so a
612
+ // warning edit there would be reverted. Non-builtin rows (including ones a
613
+ // discovery commit created) are operator-owned and may set or clear them.
614
+ if (override?.warnings !== undefined) {
615
+ if (row.source === "builtin") {
616
+ throw new ConsoleDomainError(
617
+ "builtin_model_immutable",
618
+ 403,
619
+ `Built-in model ${providerId}/${request.modelId} warnings cannot be edited — the bundle re-seeds them on every boot`,
620
+ );
621
+ }
622
+ set.warningMetadata =
623
+ override.warnings === null
624
+ ? null
625
+ : {
626
+ warnings: override.warnings.map((warning) => ({
627
+ kind: warning.kind,
628
+ message: warning.message,
629
+ ...(warning.severity === undefined ? {} : { severity: warning.severity }),
630
+ ...(warning.updatedAt === undefined ? {} : { updatedAt: warning.updatedAt }),
631
+ })),
632
+ };
633
+ }
597
634
  if (override?.passthrough === true) {
598
635
  set.reasoning = true;
599
636
  set.toolCall = true;
@@ -1655,8 +1692,216 @@ export class DrizzleProviderCatalogStore implements ProviderCatalogStore {
1655
1692
  return recoverAccount(this.db, accountId, "manual_operator_recovery");
1656
1693
  }
1657
1694
 
1695
+ /**
1696
+ * One `UPDATE ... WHERE <cooldown predicate>` (D07/D08/D09). The predicate
1697
+ * is evaluated in SQL, so the whole provider is handled in one statement
1698
+ * without reading rows into the process. `cooldown_until` and
1699
+ * `model_cooldowns` are left untouched: they are the evidence the operator
1700
+ * sees on a disabled row until an explicit Recover clears them. A row that
1701
+ * is already `disabled` is not a target — those columns surviving the
1702
+ * status change would otherwise keep matching the predicate forever.
1703
+ *
1704
+ * Pool-as-account rows live in `provider_pool_accounts`, not this table, so
1705
+ * they are excluded by construction. Shared (`tenant_id IS NULL`) rows are
1706
+ * counted in `globalSkipped` but never written by a tenant; the global-admin
1707
+ * pair of routes owns those.
1708
+ */
1709
+ async disableCooldowns(
1710
+ tenantId: string,
1711
+ providerId: string,
1712
+ ): Promise<ProviderAccountBulkActionResult> {
1713
+ const nowIso = new Date().toISOString();
1714
+ const [globalRows, updatedRows] = await Promise.all([
1715
+ this.db
1716
+ .select({ id: providerAccounts.id })
1717
+ .from(providerAccounts)
1718
+ .where(
1719
+ and(
1720
+ eq(providerAccounts.providerId, providerId),
1721
+ isNull(providerAccounts.tenantId),
1722
+ this.liveCooldownPredicate(nowIso),
1723
+ ),
1724
+ ),
1725
+ this.db
1726
+ .update(providerAccounts)
1727
+ .set({ status: "disabled" })
1728
+ .where(
1729
+ and(
1730
+ eq(providerAccounts.providerId, providerId),
1731
+ eq(providerAccounts.tenantId, tenantId),
1732
+ this.liveCooldownPredicate(nowIso),
1733
+ ),
1734
+ )
1735
+ .returning({ id: providerAccounts.id }),
1736
+ ]);
1737
+ // Status is part of the cached credential's admission decision, so every
1738
+ // newly disabled id must be dropped here — not only on delete.
1739
+ invalidateCredentialCaches(updatedRows.map((row) => row.id));
1740
+ return { ok: true, updated: updatedRows.length, globalSkipped: globalRows.length };
1741
+ }
1742
+
1743
+ /**
1744
+ * One `DELETE ... WHERE <strict banned predicate> RETURNING id` (D12).
1745
+ * Credential and quota caches are dropped for the deleted ids in one pass
1746
+ * each, exactly like the existing batch-delete route. A delete is strictly
1747
+ * `status='disabled' AND last_error_category='policy_blocked_banned'`;
1748
+ * `auth_invalidated` is remediated by replacing the credential, not here.
1749
+ */
1750
+ async deleteBanned(
1751
+ tenantId: string,
1752
+ providerId: string,
1753
+ ): Promise<ProviderAccountBulkActionResult> {
1754
+ const globalRows = await this.db
1755
+ .select({ id: providerAccounts.id })
1756
+ .from(providerAccounts)
1757
+ .where(
1758
+ and(
1759
+ eq(providerAccounts.providerId, providerId),
1760
+ isNull(providerAccounts.tenantId),
1761
+ this.bannedPredicate(),
1762
+ ),
1763
+ );
1764
+ const deletedRows = await this.db
1765
+ .delete(providerAccounts)
1766
+ .where(
1767
+ and(
1768
+ eq(providerAccounts.providerId, providerId),
1769
+ eq(providerAccounts.tenantId, tenantId),
1770
+ this.bannedPredicate(),
1771
+ ),
1772
+ )
1773
+ .returning({ id: providerAccounts.id });
1774
+ const deletedIds = deletedRows.map((row) => row.id);
1775
+ invalidateCredentialCaches(deletedIds);
1776
+ await invalidateQuotaCaches(tenantId, deletedIds, this.redis);
1777
+ return {
1778
+ ok: true,
1779
+ updated: 0,
1780
+ deleted: deletedIds,
1781
+ missing: [],
1782
+ globalSkipped: globalRows.length,
1783
+ };
1784
+ }
1785
+
1786
+ /** Strict banned definition shared by the tenant and global DELETE paths. */
1787
+ private bannedPredicate(): SQL {
1788
+ return and(
1789
+ eq(providerAccounts.status, "disabled"),
1790
+ eq(providerAccounts.lastErrorCategory, "policy_blocked_banned"),
1791
+ ) as SQL;
1792
+ }
1793
+
1794
+ /**
1795
+ * Not already disabled, and either `status='cooldown'`, `cooldown_until` is
1796
+ * still in the future, or `model_cooldowns` carries a live entry. Scoped to
1797
+ * the caller's own rows (the tenant path never touches `tenant_id IS NULL`
1798
+ * rows). The shared JSON subquery lives in the dialect module; only the
1799
+ * surrounding scoping is the store's.
1800
+ *
1801
+ * The disabled exclusion is load-bearing. Disable preserves the deadline
1802
+ * columns as evidence, so without it a row this action just wrote matches
1803
+ * the predicate again and the operator's count never drops to zero.
1804
+ */
1805
+ private liveCooldownPredicate(nowIso: string): SQL {
1806
+ return and(
1807
+ ne(providerAccounts.status, "disabled"),
1808
+ or(
1809
+ eq(providerAccounts.status, "cooldown"),
1810
+ sql`${providerAccounts.cooldownUntil} > ${nowIso}`,
1811
+ liveModelCooldownExistsSql(this.db, nowIso),
1812
+ ),
1813
+ ) as SQL;
1814
+ }
1658
1815
 
1659
1816
  async syncModels(tenantId: string, providerId: string): Promise<{ synced: number }> {
1660
1817
  return this.probing.syncModels(tenantId, providerId);
1661
1818
  }
1819
+
1820
+ /** Read-only discovery; delegates to the same fetch path as sync (area 05). */
1821
+ async listDiscoveryCandidates(
1822
+ tenantId: string,
1823
+ providerId: string,
1824
+ ): Promise<{
1825
+ readonly supported: boolean;
1826
+ readonly candidates: readonly DiscoverModelCandidate[];
1827
+ readonly accountLabel?: string;
1828
+ }> {
1829
+ const discovery = await this.probing.discoverModels(tenantId, providerId);
1830
+ if (!discovery.supported) return { supported: false, candidates: [] };
1831
+ const registered = await this.listModelsForTenant(tenantId, providerId);
1832
+ const ids = new Set(
1833
+ (registered.get(providerId) ?? []).map((entry) => entry.modelId),
1834
+ );
1835
+ // The picker exists to add models, so rows already on the roster are
1836
+ // excluded rather than listed disabled — a fully-registered catalog would
1837
+ // otherwise render an all-"Registered" list that reads as empty.
1838
+ const candidates: DiscoverModelCandidate[] = discovery.models
1839
+ .filter((definition) => !ids.has(definition.modelId))
1840
+ .map((definition) => ({
1841
+ modelId: definition.modelId,
1842
+ wireFamily: definition.wireFamily,
1843
+ serviceKind: definition.serviceKind ?? "llm",
1844
+ endpointPath: definition.endpointPath,
1845
+ contextLimit: definition.contextLimit,
1846
+ outputLimit: definition.outputLimit,
1847
+ modalities: definition.modalities,
1848
+ reasoning: definition.reasoning,
1849
+ toolCall: definition.toolCall,
1850
+ ...(definition.freeTier === undefined ? {} : { freeTier: definition.freeTier }),
1851
+ ...(definition.proOnly === undefined ? {} : { proOnly: definition.proOnly }),
1852
+ alreadyRegistered: false,
1853
+ }));
1854
+ return {
1855
+ supported: true,
1856
+ candidates,
1857
+ ...(discovery.accountLabel === undefined ? {} : { accountLabel: discovery.accountLabel }),
1858
+ };
1859
+ }
1860
+
1861
+ /**
1862
+ * Re-runs discovery on the server, then persists only ids the fresh roster
1863
+ * actually advertised (D28/D29/D30/D31). This is the anti-forgery gate: a
1864
+ * caller cannot attach a hand-built metadata object to an arbitrary id.
1865
+ */
1866
+ async commitDiscoveredModels(
1867
+ tenantId: string,
1868
+ providerId: string,
1869
+ modelIds: readonly string[],
1870
+ ): Promise<{
1871
+ readonly committed: readonly string[];
1872
+ readonly skipped: readonly string[];
1873
+ readonly synced: number;
1874
+ }> {
1875
+ const discovery = await this.probing.discoverModels(tenantId, providerId);
1876
+ if (!discovery.supported) {
1877
+ throw new ConsoleDomainError(
1878
+ "model_discovery_unsupported",
1879
+ 400,
1880
+ `${providerId} publishes no model-listing endpoint; add the model manually`,
1881
+ );
1882
+ }
1883
+ const byId = new Map(discovery.models.map((definition) => [definition.modelId, definition]));
1884
+ const wanted = [...new Set(modelIds)];
1885
+ const unknown = wanted.filter((id) => !byId.has(id));
1886
+ if (unknown.length > 0) {
1887
+ throw new ConsoleDomainError(
1888
+ "invalid_request",
1889
+ 400,
1890
+ `Models are no longer in the provider's listing: ${unknown.join(", ")} (request {requestId}). Fetch the model list again 🪞`,
1891
+ );
1892
+ }
1893
+ const registered = await this.listModelsForTenant(tenantId, providerId);
1894
+ const existing = new Set(
1895
+ (registered.get(providerId) ?? []).map((entry) => entry.modelId),
1896
+ );
1897
+ const selected = wanted.filter((id) => !existing.has(id));
1898
+ const skipped = wanted.filter((id) => existing.has(id));
1899
+ if (selected.length === 0) return { committed: [], skipped, synced: 0 };
1900
+ const { synced } = await this.probing.commitDiscoveredModels(
1901
+ tenantId,
1902
+ providerId,
1903
+ selected,
1904
+ );
1905
+ return { committed: selected, skipped, synced };
1906
+ }
1662
1907
  }
@@ -1,6 +1,7 @@
1
1
  import { t } from "elysia";
2
2
  import type { Elysia } from "elysia";
3
- import { and, eq, inArray, isNull } from "drizzle-orm";
3
+ import { and, eq, inArray, isNull, sql } from "drizzle-orm";
4
+ import { liveModelCooldownExistsSql } from "../../persistence/dialect";
4
5
  import { adminAuditLog, providerAccounts } from "../../persistence/schema";
5
6
  import { requireGlobalAdmin } from "../shared/errors";
6
7
  import {
@@ -22,10 +23,29 @@ import {
22
23
  } from "../../providers/quota/refresh";
23
24
  import { sanitizeGlobalAccount } from "./quota-view";
24
25
  import {
25
- ACCOUNT_STATUSES,
26
+ resolveNextAccountStatus,
26
27
  type AccountQuotaRoutesDeps,
27
- type AccountStatus,
28
28
  } from "./account-quota-shared";
29
+ import type { ProviderAccountBulkActionResult } from "../providers/catalog/contracts";
30
+
31
+ /**
32
+ * The same live-cooldown predicate as the tenant-scoped store, evaluated in
33
+ * SQL so one statement covers the whole shared-account set. `status='cooldown'`
34
+ * counts even when its deadline elapsed; a live `model_cooldowns` entry counts
35
+ * even while `status` is still `active` (D07). A row already `disabled` does
36
+ * not: disable keeps the deadline columns as evidence, and counting them would
37
+ * make a second pass look like it still has work. Scoped strictly to shared
38
+ * rows (`tenant_id IS NULL`); the shared JSON subquery lives in the dialect
39
+ * module.
40
+ */
41
+ function globalLiveCooldownPredicate(database: unknown): ReturnType<typeof and> {
42
+ const nowIso = new Date().toISOString();
43
+ return and(
44
+ isNull(providerAccounts.tenantId),
45
+ sql`${providerAccounts.status} <> 'disabled'`,
46
+ sql`${providerAccounts.status} = 'cooldown' OR ${providerAccounts.cooldownUntil} > ${nowIso} OR ${liveModelCooldownExistsSql(database, nowIso)}`,
47
+ )!;
48
+ }
29
49
 
30
50
  /**
31
51
  * Registers the platform-admin account/quota routes onto `app`. Every route here
@@ -38,6 +58,47 @@ export function registerAccountQuotaGlobalRoutes(
38
58
  refreshDeps: QuotaRefreshDeps,
39
59
  ): void {
40
60
  const { db, accessResolver, redis, snapshotInvalidator } = deps;
61
+ /**
62
+ * The two bulk actions are one statement each, scoped strictly to shared
63
+ * rows. Neither action takes a provider filter: the global-admin surface
64
+ * lists shared accounts across providers, and the same health predicate
65
+ * defines the target set everywhere.
66
+ */
67
+ const runGlobalBulk = async (
68
+ kind: "disable" | "delete",
69
+ ): Promise<ProviderAccountBulkActionResult> => {
70
+ if (kind === "disable") {
71
+ const updated = await db
72
+ .update(providerAccounts)
73
+ .set({ status: "disabled" })
74
+ .where(globalLiveCooldownPredicate(db))
75
+ .returning({ id: providerAccounts.id });
76
+ invalidateCredentialCaches(updated.map((row) => row.id));
77
+ await snapshotInvalidator?.invalidate();
78
+ return { ok: true, updated: updated.length, globalSkipped: 0 };
79
+ }
80
+ const deleted = await db
81
+ .delete(providerAccounts)
82
+ .where(
83
+ and(
84
+ isNull(providerAccounts.tenantId),
85
+ eq(providerAccounts.status, "disabled"),
86
+ eq(providerAccounts.lastErrorCategory, "policy_blocked_banned"),
87
+ ),
88
+ )
89
+ .returning({ id: providerAccounts.id });
90
+ const deletedIds = deleted.map((row) => row.id);
91
+ invalidateCredentialCaches(deletedIds);
92
+ await invalidateQuotaCaches(GLOBAL_QUOTA_LENS, deletedIds, redis);
93
+ await snapshotInvalidator?.invalidate();
94
+ return {
95
+ ok: true,
96
+ updated: 0,
97
+ deleted: deletedIds,
98
+ missing: [],
99
+ globalSkipped: 0,
100
+ };
101
+ };
41
102
  app
42
103
  .get("/global/accounts", async ({ request }) => {
43
104
  requireGlobalAdmin(accessResolver(request));
@@ -57,23 +118,12 @@ export function registerAccountQuotaGlobalRoutes(
57
118
  },
58
119
  async ({ request, params, body, set }) => {
59
120
  const access = requireGlobalAdmin(accessResolver(request));
60
- if (body.status !== undefined && !ACCOUNT_STATUSES.includes(body.status as AccountStatus)) {
61
- set.status = 400;
62
- return {
63
- error: `status must be one of: ${ACCOUNT_STATUSES.join(", ")}`,
64
- code: "invalid_status",
65
- };
66
- }
67
- const nextStatus: AccountStatus | undefined =
68
- body.active !== undefined
69
- ? body.active
70
- ? "active"
71
- : "disabled"
72
- : (body.status as AccountStatus | undefined);
73
- if (!nextStatus) {
74
- set.status = 400;
75
- return { error: "active or status is required", code: "invalid_request" };
121
+ const resolved = resolveNextAccountStatus(body);
122
+ if (!resolved.ok) {
123
+ set.status = resolved.status;
124
+ return resolved.body;
76
125
  }
126
+ const nextStatus = resolved.nextStatus;
77
127
  const updated = await db
78
128
  .update(providerAccounts)
79
129
  .set({ status: nextStatus })
@@ -157,6 +207,34 @@ export function registerAccountQuotaGlobalRoutes(
157
207
  });
158
208
  return { ok: true, deleted: deletedIds, missing };
159
209
  })
210
+ /**
211
+ * Global counterparts of the provider-detail bulk actions (D10): shared
212
+ * accounts carry `tenant_id IS NULL`, so the tenant-scoped pair can never
213
+ * match them. Both are one statement, one audit row, one snapshot
214
+ * invalidation, and authorize through `requireGlobalAdmin`.
215
+ */
216
+ .post("/global/accounts/actions/disable-cooldowns", async ({ request }) => {
217
+ const access = requireGlobalAdmin(accessResolver(request));
218
+ const { ok, updated, globalSkipped } = await runGlobalBulk("disable");
219
+ await db.insert(adminAuditLog).values({
220
+ actor: access.admissionIdentity,
221
+ action: "provider_account.bulk_disabled_cooldowns",
222
+ target: "global",
223
+ detail: { updated, globalSkipped },
224
+ });
225
+ return { ok, updated, globalSkipped };
226
+ })
227
+ .post("/global/accounts/actions/delete-banned", async ({ request }) => {
228
+ const access = requireGlobalAdmin(accessResolver(request));
229
+ const { ok, updated, deleted, missing, globalSkipped } = await runGlobalBulk("delete");
230
+ await db.insert(adminAuditLog).values({
231
+ actor: access.admissionIdentity,
232
+ action: "provider_account.bulk_deleted_banned",
233
+ target: "global",
234
+ detail: { deleted: deleted ?? [], missing: missing ?? [], globalSkipped },
235
+ });
236
+ return { ok, updated, deleted: deleted ?? [], missing: missing ?? [], globalSkipped };
237
+ })
160
238
  .post("/global/accounts/:id/quota/refresh", async ({ request, params, set }) => {
161
239
  const access = requireGlobalAdmin(accessResolver(request));
162
240
  const target = await loadQuotaTarget(db, params.id, null);
@@ -20,6 +20,55 @@ export interface AccountQuotaRoutesDeps extends QuotaRefreshDeps {
20
20
  export const ACCOUNT_STATUSES = ["active", "cooldown", "disabled"] as const;
21
21
  export type AccountStatus = (typeof ACCOUNT_STATUSES)[number];
22
22
 
23
+ /**
24
+ * The merged result of interpreting a PATCH account-status body. On success it
25
+ * carries the resolved `AccountStatus`; on failure it carries the exact 400
26
+ * response the tenant and global routes both return.
27
+ */
28
+ export type ResolveNextAccountStatusResult =
29
+ | { readonly ok: true; readonly nextStatus: AccountStatus }
30
+ | {
31
+ readonly ok: false;
32
+ readonly status: 400;
33
+ readonly body: { readonly error: string; readonly code: string };
34
+ };
35
+
36
+ /**
37
+ * Resolves the next account status from a PATCH body: an explicit `active`
38
+ * boolean wins over `status`, and a missing `status` (or an invalid one) is
39
+ * rejected with the shared 400 shape. Both account PATCH handlers interpret
40
+ * the same input contract, so it lives here once.
41
+ */
42
+ export function resolveNextAccountStatus(body: {
43
+ readonly active?: boolean;
44
+ readonly status?: string;
45
+ }): ResolveNextAccountStatusResult {
46
+ if (body.status !== undefined && !ACCOUNT_STATUSES.includes(body.status as AccountStatus)) {
47
+ return {
48
+ ok: false,
49
+ status: 400,
50
+ body: {
51
+ error: `status must be one of: ${ACCOUNT_STATUSES.join(", ")}`,
52
+ code: "invalid_status",
53
+ },
54
+ };
55
+ }
56
+ const nextStatus: AccountStatus | undefined =
57
+ body.active !== undefined
58
+ ? body.active
59
+ ? "active"
60
+ : "disabled"
61
+ : (body.status as AccountStatus | undefined);
62
+ if (nextStatus === undefined) {
63
+ return {
64
+ ok: false,
65
+ status: 400,
66
+ body: { error: "active or status is required", code: "invalid_request" },
67
+ };
68
+ }
69
+ return { ok: true, nextStatus };
70
+ }
71
+
23
72
  /**
24
73
  * Wraps a stored api_key credential in the envelope its collector expects.
25
74
  *
@@ -72,9 +72,8 @@ import {
72
72
  toQuotaView,
73
73
  } from "./quota-view";
74
74
  import {
75
- ACCOUNT_STATUSES,
75
+ resolveNextAccountStatus,
76
76
  type AccountQuotaRoutesDeps,
77
- type AccountStatus,
78
77
  } from "./account-quota-shared";
79
78
  import {
80
79
  DAILY_GROWTH_LEG_TIMEOUT_MS,
@@ -745,23 +744,12 @@ export function registerAccountQuotaTenantRoutes(
745
744
  },
746
745
  async ({ request, params, body, set }) => {
747
746
  const access = requireTenantScope(accessResolver(request), "dashboard:write");
748
- if (body.status !== undefined && !ACCOUNT_STATUSES.includes(body.status as AccountStatus)) {
749
- set.status = 400;
750
- return {
751
- error: `status must be one of: ${ACCOUNT_STATUSES.join(", ")}`,
752
- code: "invalid_status",
753
- };
754
- }
755
- const nextStatus: AccountStatus | undefined =
756
- body.active !== undefined
757
- ? body.active
758
- ? "active"
759
- : "disabled"
760
- : (body.status as AccountStatus | undefined);
761
- if (!nextStatus) {
762
- set.status = 400;
763
- return { error: "active or status is required", code: "invalid_request" };
747
+ const resolved = resolveNextAccountStatus(body);
748
+ if (!resolved.ok) {
749
+ set.status = resolved.status;
750
+ return resolved.body;
764
751
  }
752
+ const nextStatus = resolved.nextStatus;
765
753
  const updated = await db
766
754
  .update(providerAccounts)
767
755
  .set({ status: nextStatus })
@@ -16,6 +16,7 @@ import {
16
16
  DrizzleQoderNativeIdentityStore,
17
17
  type QoderNativeIdentityStore,
18
18
  } from "../../providers/integrations/qoder/qoder-native-identity-store";
19
+ import { qoderRealmForProvider } from "../../providers/integrations/qoder/qoder-realm";
19
20
  import { ConsoleDomainError, requireGlobalAdmin, requireTenantScope } from "../shared/errors";
20
21
  import type { ConsoleAccessResolver } from "../auth/access";
21
22
 
@@ -38,6 +39,7 @@ export type NativeIdentityAllocator = (args: {
38
39
  function defaultAllocator(store: QoderNativeIdentityStore): NativeIdentityAllocator {
39
40
  return ({ providerId, accountId, avoidToken }) =>
40
41
  allocateQoderNativeIdentity(store, providerId, accountId, {
42
+ realm: qoderRealmForProvider(providerId),
41
43
  ...(avoidToken !== undefined ? { avoidToken } : {}),
42
44
  });
43
45
  }