cartethyia 2.7.7 → 2.7.9

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 (372) hide show
  1. package/.env.example +0 -1
  2. package/README.md +29 -59
  3. package/dist/dashboard/assets/BannedPage-BxoG1MrL.js +1 -0
  4. package/dist/dashboard/assets/BannedPage-BxoG1MrL.js.gz +0 -0
  5. package/dist/dashboard/assets/{CliToolDetailPage-CLUhmjUy.js → CliToolDetailPage-ixwgKk53.js} +1 -1
  6. package/dist/dashboard/assets/CliToolDetailPage-ixwgKk53.js.gz +0 -0
  7. package/dist/dashboard/assets/{CliToolsPage-CQmrqqrk.js → CliToolsPage-BmttNV3U.js} +1 -1
  8. package/dist/dashboard/assets/CliToolsPage-BmttNV3U.js.gz +0 -0
  9. package/dist/dashboard/assets/CombosPage-BE3qwwfC.js +1 -0
  10. package/dist/dashboard/assets/CombosPage-BE3qwwfC.js.gz +0 -0
  11. package/dist/dashboard/assets/ConsoleLogPage-CRZdmMIX.js +1 -0
  12. package/dist/dashboard/assets/ConsoleLogPage-CRZdmMIX.js.gz +0 -0
  13. package/dist/dashboard/assets/CustomizationPage-Dv_FJ6Er.js +1 -0
  14. package/dist/dashboard/assets/CustomizationPage-Dv_FJ6Er.js.gz +0 -0
  15. package/dist/dashboard/assets/HealthEventsModal-CoVrw2sM.js +1 -0
  16. package/dist/dashboard/assets/HealthEventsModal-CoVrw2sM.js.gz +0 -0
  17. package/dist/dashboard/assets/{InjectSystemPromptPanel-C-e2AMz6.js → InjectSystemPromptPanel-BOoO8rpz.js} +2 -2
  18. package/dist/dashboard/assets/InjectSystemPromptPanel-BOoO8rpz.js.gz +0 -0
  19. package/dist/dashboard/assets/{LoginPage-J8wPbmfT.js → LoginPage-ChJuNNVN.js} +1 -1
  20. package/dist/dashboard/assets/LoginPage-ChJuNNVN.js.gz +0 -0
  21. package/dist/dashboard/assets/{ModelPicker-C6WlpU5E.js → ModelPicker-BZAJtPg1.js} +1 -1
  22. package/dist/dashboard/assets/ModelPicker-BZAJtPg1.js.gz +0 -0
  23. package/dist/dashboard/assets/OverviewPage-BE40s3u5.js +1 -0
  24. package/dist/dashboard/assets/OverviewPage-BE40s3u5.js.gz +0 -0
  25. package/dist/dashboard/assets/ProviderDetailPage-sfLoQtPV.js +8 -0
  26. package/dist/dashboard/assets/ProviderDetailPage-sfLoQtPV.js.gz +0 -0
  27. package/dist/dashboard/assets/ProviderIcon-CQ6bPUf_.js +1 -0
  28. package/dist/dashboard/assets/ProviderIcon-CQ6bPUf_.js.gz +0 -0
  29. package/dist/dashboard/assets/ProvidersPage-DcepmPWo.js +1 -0
  30. package/dist/dashboard/assets/ProvidersPage-DcepmPWo.js.gz +0 -0
  31. package/dist/dashboard/assets/{ProxyPage-BBTRBAQ3.js → ProxyPage-BJ87Pp0U.js} +1 -1
  32. package/dist/dashboard/assets/ProxyPage-BJ87Pp0U.js.gz +0 -0
  33. package/dist/dashboard/assets/{QuotaPage-B2WqMSG1.js → QuotaPage-gkSyRAKa.js} +1 -1
  34. package/dist/dashboard/assets/QuotaPage-gkSyRAKa.js.gz +0 -0
  35. package/dist/dashboard/assets/SettingsPage-C5HPWqEv.js +1 -0
  36. package/dist/dashboard/assets/SettingsPage-C5HPWqEv.js.gz +0 -0
  37. package/dist/dashboard/assets/{SetupPage-LND6_Ss7.js → SetupPage-CFNcATJJ.js} +1 -1
  38. package/dist/dashboard/assets/SetupPage-CFNcATJJ.js.gz +0 -0
  39. package/dist/dashboard/assets/{StudioPage-BzNnm6Bi.js → StudioPage-YoD4tY5l.js} +1 -1
  40. package/dist/dashboard/assets/StudioPage-YoD4tY5l.js.gz +0 -0
  41. package/dist/dashboard/assets/UsagePage-DxjaAhb_.js +56 -0
  42. package/dist/dashboard/assets/UsagePage-DxjaAhb_.js.gz +0 -0
  43. package/dist/dashboard/assets/api-keys-BTlkq_Uk.js +1 -0
  44. package/dist/dashboard/assets/api-keys-BTlkq_Uk.js.gz +0 -0
  45. package/dist/dashboard/assets/app-BX2nnKqs.css +2 -0
  46. package/dist/dashboard/assets/app-BX2nnKqs.css.gz +0 -0
  47. package/dist/dashboard/assets/assert-CndthXFQ.js +1 -0
  48. package/dist/dashboard/assets/assert-CndthXFQ.js.gz +0 -0
  49. package/dist/dashboard/assets/{badge-BaX2GTrn.js → badge-DdbT_7Am.js} +1 -1
  50. package/dist/dashboard/assets/badge-DdbT_7Am.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-DFH4Bkte.js → banner-CsGN-nbm.js} +1 -1
  54. package/dist/dashboard/assets/banner-CsGN-nbm.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-CxDw0lK6.js → card-mgZ_TLLZ.js} +1 -1
  64. package/dist/dashboard/assets/card-mgZ_TLLZ.js.gz +0 -0
  65. package/dist/dashboard/assets/cli-tools-A1-v8-03.js +1 -0
  66. package/dist/dashboard/assets/cli-tools-A1-v8-03.js.gz +0 -0
  67. package/dist/dashboard/assets/{clipboard-button-DfS1ntiq.js → clipboard-button-CJ4Zty89.js} +1 -1
  68. package/dist/dashboard/assets/clipboard-button-CJ4Zty89.js.gz +0 -0
  69. package/dist/dashboard/assets/{dialog-BK-NRr5u.js → dialog-B5L2jPQ_.js} +1 -1
  70. package/dist/dashboard/assets/dialog-B5L2jPQ_.js.gz +0 -0
  71. package/dist/dashboard/assets/{drawer-QqPul6sC.js → drawer-DkjUouAj.js} +1 -1
  72. package/dist/dashboard/assets/drawer-DkjUouAj.js.gz +0 -0
  73. package/dist/dashboard/assets/{entry-BMlYJVFj.js → entry-B-LVLV6o.js} +1 -1
  74. package/dist/dashboard/assets/entry-B-LVLV6o.js.gz +0 -0
  75. package/dist/dashboard/assets/entry-CAhdYl3B.js +1 -0
  76. package/dist/dashboard/assets/entry-CAhdYl3B.js.gz +0 -0
  77. package/dist/dashboard/assets/entry-D2xD0Tmd.js +4 -0
  78. package/dist/dashboard/assets/entry-D2xD0Tmd.js.gz +0 -0
  79. package/dist/dashboard/assets/{formatters-DP-5E01-.js → formatters-CrsdLVsn.js} +1 -1
  80. package/dist/dashboard/assets/formatters-CrsdLVsn.js.gz +0 -0
  81. package/dist/dashboard/assets/{index-D36vCZht.js → index-BU64Lqvp.js} +2 -2
  82. package/dist/dashboard/assets/index-BU64Lqvp.js.gz +0 -0
  83. package/dist/dashboard/assets/{inline-CHJQwxLa.js → inline-B2aDLyyg.js} +1 -1
  84. package/dist/dashboard/assets/inline-B2aDLyyg.js.gz +0 -0
  85. package/dist/dashboard/assets/{input-BlmlizVV.js → input-BCVkdR9d.js} +1 -1
  86. package/dist/dashboard/assets/input-BCVkdR9d.js.gz +0 -0
  87. package/dist/dashboard/assets/{layout-Bz93I-yE.js → layout-DNcAdMK-.js} +1 -1
  88. package/dist/dashboard/assets/layout-DNcAdMK-.js.gz +0 -0
  89. package/dist/dashboard/assets/{list-row-Ml16KCfu.js → list-row-DgrIH0hw.js} +1 -1
  90. package/dist/dashboard/assets/list-row-DgrIH0hw.js.gz +0 -0
  91. package/dist/dashboard/assets/{live-7_QpnYGu.js → live-Cae01iu8.js} +1 -1
  92. package/dist/dashboard/assets/live-Cae01iu8.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-CuMDaTzT.js → network-CRib7H_2.js} +1 -1
  96. package/dist/dashboard/assets/network-CRib7H_2.js.gz +0 -0
  97. package/dist/dashboard/assets/{numbers-CoqpaDMx.js → numbers-BKQE8Xpz.js} +1 -1
  98. package/dist/dashboard/assets/numbers-BKQE8Xpz.js.gz +0 -0
  99. package/dist/dashboard/assets/{pagination-B5aOoweZ.js → pagination-ecPvG_8g.js} +1 -1
  100. package/dist/dashboard/assets/pagination-ecPvG_8g.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/{query-keys-YSoQOUh6.js → query-keys-IKLh0YFd.js} +1 -1
  106. package/dist/dashboard/assets/query-keys-IKLh0YFd.js.gz +0 -0
  107. package/dist/dashboard/assets/routing-Ckm1UXuC.js +1 -0
  108. package/dist/dashboard/assets/routing-Ckm1UXuC.js.gz +0 -0
  109. package/dist/dashboard/assets/{select-C9Ssle9b.js → select-ChoFWyKz.js} +1 -1
  110. package/dist/dashboard/assets/select-ChoFWyKz.js.gz +0 -0
  111. package/dist/dashboard/assets/settings-CKjFPlKW.js +1 -0
  112. package/dist/dashboard/assets/settings-CKjFPlKW.js.gz +0 -0
  113. package/dist/dashboard/assets/{stack-j7cXjgYX.js → stack-DWCwU3fh.js} +1 -1
  114. package/dist/dashboard/assets/stack-DWCwU3fh.js.gz +0 -0
  115. package/dist/dashboard/assets/{state-DHmEnLa2.js → state-Cg3VL5J_.js} +1 -1
  116. package/dist/dashboard/assets/state-Cg3VL5J_.js.gz +0 -0
  117. package/dist/dashboard/assets/story/25817331.webp +0 -0
  118. package/dist/dashboard/assets/story/Shorekeeper.webp +0 -0
  119. package/dist/dashboard/assets/story/cartethyia-god.webp +0 -0
  120. package/dist/dashboard/assets/story/fleurdelys_plus.webp +0 -0
  121. package/dist/dashboard/assets/story/jinhsi-blossom.webp +0 -0
  122. package/dist/dashboard/assets/story/phrolova-a.webp +0 -0
  123. package/dist/dashboard/assets/story/requestdeniawokkjpg.webp +0 -0
  124. package/dist/dashboard/assets/story/wuthering-waves-hiyuki-aemeath-hiyuki-rover.webp +0 -0
  125. package/dist/dashboard/assets/{switch-DmZcyTIC.js → switch-Db70BMix.js} +1 -1
  126. package/dist/dashboard/assets/switch-Db70BMix.js.gz +0 -0
  127. package/dist/dashboard/assets/{useMutation-CbCP4w1o.js → useMutation-BqdE0LuM.js} +1 -1
  128. package/dist/dashboard/assets/useMutation-BqdE0LuM.js.gz +0 -0
  129. package/dist/dashboard/console-backgrounds.webp +0 -0
  130. package/dist/dashboard/console-backgrounds_initial.webp +0 -0
  131. package/dist/dashboard/donation/qris-risun-marke.webp +0 -0
  132. package/dist/dashboard/index.html +3 -3
  133. package/dist/dashboard/index.html.gz +0 -0
  134. package/dist/dashboard/precompressed-assets.json +57 -55
  135. package/migrations/full-postgres/0044_add_models_warning_metadata.sql +10 -0
  136. package/migrations/full-postgres/0045_migrate_workbuddy_to_codebuddy.sql +156 -0
  137. package/migrations/lite-sqlite3/0044_add_models_warning_metadata.sql +11 -0
  138. package/migrations/lite-sqlite3/0045_migrate_workbuddy_to_codebuddy.sql +156 -0
  139. package/package.json +1 -1
  140. package/src/boot/dependencies.ts +11 -3
  141. package/src/config/cooldowns.ts +1 -1
  142. package/src/config/spec.ts +0 -6
  143. package/src/console/api-keys/routes.ts +11 -4
  144. package/src/console/console-router.ts +7 -0
  145. package/src/console/dashboard-assets.ts +5 -5
  146. package/src/console/domain-registration.ts +27 -0
  147. package/src/console/ip-bans/routes.ts +99 -0
  148. package/src/console/providers/catalog/catalog-projections.ts +5 -2
  149. package/src/console/providers/catalog/contracts.ts +123 -0
  150. package/src/console/providers/catalog/model-operations.ts +122 -10
  151. package/src/console/providers/catalog/provider-operations.ts +116 -8
  152. package/src/console/providers/catalog/public-model-store.ts +0 -0
  153. package/src/console/providers/catalog/routes.ts +64 -4
  154. package/src/console/providers/catalog/store-helpers.ts +30 -1
  155. package/src/console/providers/catalog/store.ts +243 -3
  156. package/src/console/quota/account-quota-global.ts +89 -1
  157. package/src/console/quota/native-identity.ts +2 -0
  158. package/src/console/settings/contracts.ts +18 -0
  159. package/src/console/settings/store.ts +32 -0
  160. package/src/console/system-prompt/contracts.ts +0 -1
  161. package/src/main.ts +1 -0
  162. package/src/observability/event-query.ts +18 -4
  163. package/src/persistence/dialect.ts +12 -0
  164. package/src/persistence/schema.ts +20 -2
  165. package/src/protocol/dsml.ts +1 -1
  166. package/src/protocol/gateway-error.ts +1 -0
  167. package/src/protocol/reasoning.ts +1 -1
  168. package/src/protocol/request/chat.ts +4 -4
  169. package/src/protocol/request/messages.ts +1 -1
  170. package/src/protocol/response/gemini.ts +6 -1
  171. package/src/protocol/sse-decoder.ts +25 -8
  172. package/src/protocol/stream-error-frames.ts +13 -3
  173. package/src/protocol/translation/quirks.ts +1 -1
  174. package/src/protocol/translation/tool-repair.ts +2 -2
  175. package/src/protocol/upstream/send-claude-messages.ts +15 -4
  176. package/src/protocol/upstream-parsing.ts +1 -1
  177. package/src/providers/adapters/compatible-adapter.ts +7 -5
  178. package/src/providers/authentication/oauth-refresh-service.ts +149 -36
  179. package/src/providers/default-registry.ts +68 -3
  180. package/src/providers/discovery/discovery-types.ts +12 -0
  181. package/src/providers/discovery/probe-phases.ts +61 -30
  182. package/src/providers/discovery/probing-service.ts +279 -164
  183. package/src/providers/integrations/antigravity/antigravity-protocol.ts +117 -2
  184. package/src/providers/integrations/antigravity/antigravity.ts +13 -2
  185. package/src/providers/integrations/buddy/buddy-catalog-shared.ts +72 -34
  186. package/src/providers/integrations/buddy/buddy-chat-shared.ts +18 -18
  187. package/src/providers/integrations/buddy/buddy-checkin.ts +17 -39
  188. package/src/providers/integrations/buddy/buddy-conversation-headers.ts +2 -2
  189. package/src/providers/integrations/buddy/buddy-discovery-shared.ts +5 -5
  190. package/src/providers/integrations/buddy/buddy-identity.ts +70 -0
  191. package/src/providers/integrations/buddy/buddy-media.ts +4 -5
  192. package/src/providers/integrations/buddy/buddy-oauth-shared.ts +2 -2
  193. package/src/providers/integrations/buddy/buddy-quota-shared.ts +1 -1
  194. package/src/providers/integrations/buddy/buddy-usage-shared.ts +1 -3
  195. package/src/providers/integrations/buddy/capabilities.ts +4 -41
  196. package/src/providers/integrations/buddy/codebuddy/cn-catalog.ts +2 -1
  197. package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +20 -1
  198. package/src/providers/integrations/buddy/codebuddy/provider.ts +2 -2
  199. package/src/providers/integrations/buddy/codebuddy/shared.ts +6 -10
  200. package/src/providers/integrations/cursor/cursor-catalog.ts +2 -0
  201. package/src/providers/integrations/cursor/cursor-exec.ts +738 -0
  202. package/src/providers/integrations/cursor/cursor-http2.ts +2 -0
  203. package/src/providers/integrations/cursor/cursor-request.ts +9 -8
  204. package/src/providers/integrations/cursor/cursor-shared.ts +19 -1
  205. package/src/providers/integrations/cursor/cursor.ts +50 -30
  206. package/src/providers/integrations/cursor/protocol/tool-canonical.ts +26 -100
  207. package/src/providers/integrations/devin/devin-auth.ts +353 -0
  208. package/src/providers/integrations/devin/devin-catalog.ts +207 -0
  209. package/src/providers/integrations/devin/devin-http.ts +91 -0
  210. package/src/providers/integrations/devin/devin-quota.ts +214 -0
  211. package/src/providers/integrations/devin/devin-shared.ts +111 -0
  212. package/src/providers/integrations/devin/devin.ts +539 -0
  213. package/src/providers/integrations/devin/protocol/devin-proto.ts +584 -0
  214. package/src/providers/integrations/devin/protocol/framing.ts +124 -0
  215. package/src/providers/integrations/factory/factory.ts +2 -2
  216. package/src/providers/integrations/freepi/freepi-catalog.ts +60 -0
  217. package/src/providers/integrations/freepi/freepi-discovery.ts +82 -0
  218. package/src/providers/integrations/freepi/freepi-headers.ts +21 -0
  219. package/src/providers/integrations/freepi/freepi-oauth.ts +142 -0
  220. package/src/providers/integrations/freepi/freepi-protocol.ts +242 -0
  221. package/src/providers/integrations/freepi/freepi-quota.ts +126 -0
  222. package/src/providers/integrations/freepi/freepi-realm.ts +35 -0
  223. package/src/providers/integrations/freepi/freepi.ts +292 -0
  224. package/src/providers/integrations/gemini.ts +2 -2
  225. package/src/providers/integrations/hermes/hermes-oauth.ts +94 -0
  226. package/src/providers/integrations/hermes.ts +4 -12
  227. package/src/providers/integrations/opencode/fingerprint.ts +1 -1
  228. package/src/providers/integrations/perch/perch-catalog.ts +105 -0
  229. package/src/providers/integrations/perch/perch-oauth.ts +170 -0
  230. package/src/providers/integrations/perch/perch-protocol.ts +211 -0
  231. package/src/providers/integrations/perch/perch-quota.ts +137 -0
  232. package/src/providers/integrations/perch/perch-realm.ts +82 -0
  233. package/src/providers/integrations/perch/perch-system-prompt.ts +13 -0
  234. package/src/providers/integrations/perch/perch.ts +315 -0
  235. package/src/providers/integrations/perch/saffron-system-prompt.txt +8 -0
  236. package/src/providers/integrations/qoder/chat-proto.ts +328 -328
  237. package/src/providers/integrations/qoder/claim-qoder-credits.ts +1 -1
  238. package/src/providers/integrations/qoder/qoder-adapter.ts +89 -28
  239. package/src/providers/integrations/qoder/qoder-cache.ts +32 -0
  240. package/src/providers/integrations/qoder/qoder-catalog.ts +6 -1
  241. package/src/providers/integrations/qoder/qoder-checkin.ts +2 -2
  242. package/src/providers/integrations/qoder/qoder-grpc.ts +209 -5
  243. package/src/providers/integrations/qoder/qoder-ide.ts +5 -1
  244. package/src/providers/integrations/qoder/qoder-identity-cache.ts +184 -0
  245. package/src/providers/integrations/qoder/qoder-native-identity-store.ts +83 -18
  246. package/src/providers/integrations/qoder/qoder-realm.ts +9 -6
  247. package/src/providers/integrations/qoder/qoder-umid.ts +109 -342
  248. package/src/providers/integrations/qoder/qoder-virtual.ts +33 -1
  249. package/src/providers/integrations/qoder/qoder.ts +3 -6
  250. package/src/providers/integrations/trae/trae-catalog.ts +9 -14
  251. package/src/providers/integrations/trae/trae-headers.ts +36 -11
  252. package/src/providers/integrations/trae/trae-oauth.ts +14 -17
  253. package/src/providers/integrations/trae/trae-protocol.ts +3 -6
  254. package/src/providers/integrations/trae/trae-quota.ts +103 -21
  255. package/src/providers/integrations/trae/trae-realm.ts +36 -15
  256. package/src/providers/integrations/trae/trae.ts +97 -7
  257. package/src/providers/operations/account-error-classification.ts +66 -1
  258. package/src/providers/operations/account-health-service.ts +192 -26
  259. package/src/providers/operations/account-refresh-status.ts +284 -0
  260. package/src/providers/operations/account-tier.ts +67 -0
  261. package/src/providers/operations/byok-wire-profile.ts +9 -12
  262. package/src/providers/operations/client-versions.ts +37 -99
  263. package/src/providers/operations/cooldown-rules.ts +61 -0
  264. package/src/providers/operations/provider-catalog-seeder.ts +4 -0
  265. package/src/providers/provider-contracts.ts +29 -0
  266. package/src/providers/provider-metadata.ts +59 -4
  267. package/src/providers/provider-model-definition.ts +36 -0
  268. package/src/providers/quota/refresh.ts +21 -0
  269. package/src/security/abuse.ts +71 -0
  270. package/src/transport/dispatch/proxy-request.ts +1 -0
  271. package/src/transport/dispatch/streaming/stream.ts +1 -0
  272. package/src/transport/dispatch/upstream.ts +4 -1
  273. package/src/transport/failure-policy.ts +113 -7
  274. package/src/transport/routing/router.ts +25 -1
  275. package/src/transport/surface/messages/adapter.ts +2 -1
  276. package/src/transport/surface/responses/encode.ts +12 -1
  277. package/src/workers/daily-checkin.ts +1 -1
  278. package/src/workers/quota-refresh-sweep.ts +1 -1
  279. package/src/workers/scheduler.ts +22 -4
  280. package/dist/dashboard/assets/BannedPage-B05aF8Dg.js +0 -1
  281. package/dist/dashboard/assets/BannedPage-B05aF8Dg.js.gz +0 -0
  282. package/dist/dashboard/assets/CliToolDetailPage-CLUhmjUy.js.gz +0 -0
  283. package/dist/dashboard/assets/CliToolsPage-CQmrqqrk.js.gz +0 -0
  284. package/dist/dashboard/assets/CombosPage-CHG_1Wr3.js +0 -1
  285. package/dist/dashboard/assets/CombosPage-CHG_1Wr3.js.gz +0 -0
  286. package/dist/dashboard/assets/ConsoleLogPage-DPgbklad.js +0 -1
  287. package/dist/dashboard/assets/ConsoleLogPage-DPgbklad.js.gz +0 -0
  288. package/dist/dashboard/assets/CustomizationPage-pnfAqr_-.js +0 -1
  289. package/dist/dashboard/assets/CustomizationPage-pnfAqr_-.js.gz +0 -0
  290. package/dist/dashboard/assets/HealthEventsModal-DGJuU2A6.js +0 -1
  291. package/dist/dashboard/assets/HealthEventsModal-DGJuU2A6.js.gz +0 -0
  292. package/dist/dashboard/assets/InjectSystemPromptPanel-C-e2AMz6.js.gz +0 -0
  293. package/dist/dashboard/assets/LoginPage-J8wPbmfT.js.gz +0 -0
  294. package/dist/dashboard/assets/ModelPicker-C6WlpU5E.js.gz +0 -0
  295. package/dist/dashboard/assets/OverviewPage-BSQnF2Lp.js +0 -1
  296. package/dist/dashboard/assets/OverviewPage-BSQnF2Lp.js.gz +0 -0
  297. package/dist/dashboard/assets/ProviderDetailPage-DK_e0FJz.js +0 -7
  298. package/dist/dashboard/assets/ProviderDetailPage-DK_e0FJz.js.gz +0 -0
  299. package/dist/dashboard/assets/ProviderIcon-jjqXJDA3.js +0 -1
  300. package/dist/dashboard/assets/ProviderIcon-jjqXJDA3.js.gz +0 -0
  301. package/dist/dashboard/assets/ProvidersPage-D_a3r6Gc.js +0 -1
  302. package/dist/dashboard/assets/ProvidersPage-D_a3r6Gc.js.gz +0 -0
  303. package/dist/dashboard/assets/ProxyPage-BBTRBAQ3.js.gz +0 -0
  304. package/dist/dashboard/assets/QuotaPage-B2WqMSG1.js.gz +0 -0
  305. package/dist/dashboard/assets/SettingsPage-D7xp5Ij_.js +0 -1
  306. package/dist/dashboard/assets/SettingsPage-D7xp5Ij_.js.gz +0 -0
  307. package/dist/dashboard/assets/SetupPage-LND6_Ss7.js.gz +0 -0
  308. package/dist/dashboard/assets/StudioPage-BzNnm6Bi.js.gz +0 -0
  309. package/dist/dashboard/assets/UsagePage-C1TbL24N.js +0 -56
  310. package/dist/dashboard/assets/UsagePage-C1TbL24N.js.gz +0 -0
  311. package/dist/dashboard/assets/api-keys-KvX83mRe.js +0 -1
  312. package/dist/dashboard/assets/api-keys-KvX83mRe.js.gz +0 -0
  313. package/dist/dashboard/assets/app-mweRcsKp.css +0 -2
  314. package/dist/dashboard/assets/app-mweRcsKp.css.gz +0 -0
  315. package/dist/dashboard/assets/assert-CidQVoNo.js +0 -1
  316. package/dist/dashboard/assets/assert-CidQVoNo.js.gz +0 -0
  317. package/dist/dashboard/assets/badge-BaX2GTrn.js.gz +0 -0
  318. package/dist/dashboard/assets/banner-DFH4Bkte.js.gz +0 -0
  319. package/dist/dashboard/assets/brand/bansos_page.png +0 -0
  320. package/dist/dashboard/assets/brand/denia_bansos.jpg +0 -0
  321. package/dist/dashboard/assets/brand/favicon.jpg +0 -0
  322. package/dist/dashboard/assets/brand/login_register.jpg +0 -0
  323. package/dist/dashboard/assets/brand/login_register_initial.jpg +0 -0
  324. package/dist/dashboard/assets/brand/og_bansos.jpg +0 -0
  325. package/dist/dashboard/assets/brand/og_image.jpg +0 -0
  326. package/dist/dashboard/assets/brand/sidebar_ico.jpg +0 -0
  327. package/dist/dashboard/assets/card-CxDw0lK6.js.gz +0 -0
  328. package/dist/dashboard/assets/cli-tools-Bwv5-zGZ.js +0 -1
  329. package/dist/dashboard/assets/cli-tools-Bwv5-zGZ.js.gz +0 -0
  330. package/dist/dashboard/assets/clipboard-button-DfS1ntiq.js.gz +0 -0
  331. package/dist/dashboard/assets/dialog-BK-NRr5u.js.gz +0 -0
  332. package/dist/dashboard/assets/drawer-QqPul6sC.js.gz +0 -0
  333. package/dist/dashboard/assets/entry-BMlYJVFj.js.gz +0 -0
  334. package/dist/dashboard/assets/entry-Dmx7gBiN.js +0 -1
  335. package/dist/dashboard/assets/entry-Dmx7gBiN.js.gz +0 -0
  336. package/dist/dashboard/assets/entry-vqGb8yVz.js +0 -4
  337. package/dist/dashboard/assets/entry-vqGb8yVz.js.gz +0 -0
  338. package/dist/dashboard/assets/formatters-DP-5E01-.js.gz +0 -0
  339. package/dist/dashboard/assets/index-D36vCZht.js.gz +0 -0
  340. package/dist/dashboard/assets/inline-CHJQwxLa.js.gz +0 -0
  341. package/dist/dashboard/assets/input-BlmlizVV.js.gz +0 -0
  342. package/dist/dashboard/assets/layout-Bz93I-yE.js.gz +0 -0
  343. package/dist/dashboard/assets/list-row-Ml16KCfu.js.gz +0 -0
  344. package/dist/dashboard/assets/live-7_QpnYGu.js.gz +0 -0
  345. package/dist/dashboard/assets/minimize-2-BTmMTK4o.js +0 -1
  346. package/dist/dashboard/assets/minimize-2-BTmMTK4o.js.gz +0 -0
  347. package/dist/dashboard/assets/mutation-GU7XdEjz.js.gz +0 -0
  348. package/dist/dashboard/assets/network-CuMDaTzT.js.gz +0 -0
  349. package/dist/dashboard/assets/numbers-CoqpaDMx.js.gz +0 -0
  350. package/dist/dashboard/assets/pagination-B5aOoweZ.js.gz +0 -0
  351. package/dist/dashboard/assets/query-keys-YSoQOUh6.js.gz +0 -0
  352. package/dist/dashboard/assets/routing-fnCsxgNO.js +0 -1
  353. package/dist/dashboard/assets/routing-fnCsxgNO.js.gz +0 -0
  354. package/dist/dashboard/assets/select-C9Ssle9b.js.gz +0 -0
  355. package/dist/dashboard/assets/settings-BxslsErn.js +0 -1
  356. package/dist/dashboard/assets/settings-BxslsErn.js.gz +0 -0
  357. package/dist/dashboard/assets/stack-j7cXjgYX.js.gz +0 -0
  358. package/dist/dashboard/assets/state-DHmEnLa2.js.gz +0 -0
  359. package/dist/dashboard/assets/switch-DmZcyTIC.js.gz +0 -0
  360. package/dist/dashboard/assets/useMutation-CbCP4w1o.js.gz +0 -0
  361. package/dist/dashboard/console-backgrounds.jpg +0 -0
  362. package/dist/dashboard/console-backgrounds_initial.jpg +0 -0
  363. package/dist/dashboard/donation/qris-risun-marke.png +0 -0
  364. package/src/providers/integrations/buddy/turing-device-token.ts +0 -262
  365. package/src/providers/integrations/buddy/workbuddy/cn-oauth.ts +0 -30
  366. package/src/providers/integrations/buddy/workbuddy/cn-provider.ts +0 -58
  367. package/src/providers/integrations/buddy/workbuddy/oauth.ts +0 -31
  368. package/src/providers/integrations/buddy/workbuddy/provider.ts +0 -209
  369. package/src/providers/integrations/buddy/workbuddy/quota.ts +0 -161
  370. package/src/providers/integrations/buddy/workbuddy/shared.ts +0 -285
  371. /package/dist/dashboard/assets/{app-DRnQLhsc.js → app-B7kIWFm5.js} +0 -0
  372. /package/dist/dashboard/assets/{app-DRnQLhsc.js.gz → app-B7kIWFm5.js.gz} +0 -0
@@ -15,6 +15,7 @@ import {
15
15
  ConsoleDomainError,
16
16
  requireGlobalAdmin,
17
17
  requireTenantAnyScope,
18
+ requireTenantScope,
18
19
  } from "../../shared/errors";
19
20
  import { cachedReadWithEtag, conditionalResponse } from "../../shared/read-through-cache";
20
21
  import type { RedisClient } from "../../../persistence/redis";
@@ -24,10 +25,11 @@ import { isBundledProviderId } from "../../../providers/provider-contracts";
24
25
  import type { ProviderRegistry } from "../../../providers/provider-registry";
25
26
  import { getProviderClientVersion, getRouteIdentityVersions } from "../../../providers/operations/client-versions";
26
27
  import type { OAuthLoginField } from "../../../providers/authentication/oauth-flow-store";
27
- import { providerCredentialHint, providerCredentialUrl, providerDisplayName, providerHasAdapterUserAgent, providerOperatorNotice, providerServiceKinds, type CompatibilityProfile } from "../../../providers/provider-metadata";
28
+ import { providerCredentialHint, providerCredentialUrl, providerDisplayName, providerHasAdapterUserAgent, providerOperatorNotice, providerServiceKinds, customProviderServiceKinds, type CompatibilityProfile } from "../../../providers/provider-metadata";
28
29
  import type { AccountHealthEventRecord } from "../../../providers/operations/account-health-service";
29
30
  import { healthEventBuffer } from "../../../persistence/health-event-flush";
30
- import { CREDENTIAL_MODES, PROVIDER_READ_SCOPES, PROVIDER_WRITE_SCOPES, type BuddyGrowthTaskLedgerEntry } from "./contracts";
31
+ import { CREDENTIAL_MODES, PROVIDER_READ_SCOPES, PROVIDER_WRITE_SCOPES, type BuddyGrowthTaskLedgerEntry, type ProviderAccountBulkActionResult, type RefreshAccountTokenResponse } from "./contracts";
32
+ import type { RefreshAccountStatusOutcome } from "../../../providers/operations/account-refresh-status";
31
33
  import { enqueueCheckin } from "../../../workers/daily-checkin";
32
34
  import {
33
35
  isWireFamily,
@@ -87,12 +89,16 @@ export function sanitizeProviderResponse(
87
89
  const credentialHint = providerCredentialHint(p.providerId);
88
90
  const operatorNotice = providerOperatorNotice(p.providerId);
89
91
  const clientVersion = getProviderClientVersion(p.providerId);
92
+ const compatibilityProfile = p.compatibilityProfile as CompatibilityProfile | undefined;
93
+ const isBuiltIn = (p.isBuiltIn as boolean | undefined) ?? false;
90
94
  const response: ProviderResponse = {
91
95
  providerId: p.providerId,
92
- serviceKinds: providerServiceKinds(p.providerId),
96
+ serviceKinds: isBuiltIn
97
+ ? providerServiceKinds(p.providerId)
98
+ : customProviderServiceKinds(compatibilityProfile),
93
99
  displayName: providerDisplayName(p.providerId),
94
100
  enabled: (p.enabled as boolean | undefined) ?? true,
95
- isBuiltIn: (p.isBuiltIn as boolean | undefined) ?? false,
101
+ isBuiltIn,
96
102
  requiresAccount: (p.requiresAccount as boolean | undefined) ?? true,
97
103
  hasAdapterUserAgent: providerHasAdapterUserAgent(p.providerId),
98
104
  // No model advertises search; see the note in provider-metadata.ts.
@@ -102,7 +108,7 @@ export function sanitizeProviderResponse(
102
108
  ...(operatorNotice === undefined ? {} : { operatorNotice }),
103
109
  ...(clientVersion === undefined ? {} : { clientVersion }),
104
110
  routeIdentityVersions: getRouteIdentityVersions(),
105
- supportsModelDiscovery: (p.supportsModelDiscovery as boolean | undefined) ?? !p.isBuiltIn,
111
+ supportsModelDiscovery: (p.supportsModelDiscovery as boolean | undefined) ?? !isBuiltIn,
106
112
  ...(typeof p.createdAt === "string" ? { createdAt: p.createdAt } : {}),
107
113
  ...(typeof p.updatedAt === "string" ? { updatedAt: p.updatedAt } : {}),
108
114
  };
@@ -182,6 +188,15 @@ export interface ProviderCatalogConfig {
182
188
  providerId: string,
183
189
  accountId: string,
184
190
  ) => Promise<{ readonly accessToken: string; readonly refreshToken?: string }>;
191
+ /**
192
+ * One-shot manual OAuth token refresh (area 04). Provided by the composition
193
+ * root; when absent the refresh-token route answers 501 rather than silently
194
+ * reporting success.
195
+ */
196
+ readonly refreshAccountStatus?: (
197
+ accountId: string,
198
+ opts?: { readonly origin?: "ui" | "sweep" | "dispatch" },
199
+ ) => Promise<RefreshAccountStatusOutcome>;
185
200
  }
186
201
  async function attachProviderCapabilities(
187
202
  response: ProviderResponse,
@@ -715,6 +730,101 @@ export function createProviderCatalogOperations(config: ProviderCatalogConfig) {
715
730
  await config.snapshotInvalidator?.invalidate();
716
731
  return { success: true };
717
732
  },
733
+ /**
734
+ * One UPDATE over the tenant's cooling accounts, with `cooldownUntil` and
735
+ * `modelCooldowns` preserved as evidence (D07/D08). Shared rows are not
736
+ * writable here: the platform-admin pair of routes owns them (D10).
737
+ */
738
+ async disableCooldowns(
739
+ access: AccessDecision | undefined,
740
+ providerId: string,
741
+ ): Promise<ProviderAccountBulkActionResult> {
742
+ const a = requireTenantAnyScope(access, PROVIDER_WRITE_SCOPES);
743
+ const result = await config.store.disableCooldowns(a.tenantId, providerId);
744
+ await config.auditSink?.record({
745
+ access: a,
746
+ action: "provider_account.bulk_disabled_cooldowns",
747
+ target: providerId,
748
+ detail: { providerId, updated: result.updated, globalSkipped: result.globalSkipped },
749
+ });
750
+ await config.snapshotInvalidator?.invalidate();
751
+ return { ok: true, updated: result.updated, globalSkipped: result.globalSkipped };
752
+ },
753
+ /**
754
+ * One DELETE over the tenant's strictly banned accounts (D12). Empty
755
+ * matches are a successful no-op, so a second click is idempotent.
756
+ */
757
+ async deleteBanned(
758
+ access: AccessDecision | undefined,
759
+ providerId: string,
760
+ ): Promise<ProviderAccountBulkActionResult> {
761
+ const a = requireTenantAnyScope(access, PROVIDER_WRITE_SCOPES);
762
+ const result = await config.store.deleteBanned(a.tenantId, providerId);
763
+ await config.auditSink?.record({
764
+ access: a,
765
+ action: "provider_account.bulk_deleted_banned",
766
+ target: providerId,
767
+ detail: {
768
+ providerId,
769
+ deleted: result.deleted ?? [],
770
+ missing: result.missing ?? [],
771
+ globalSkipped: result.globalSkipped,
772
+ },
773
+ });
774
+ await config.snapshotInvalidator?.invalidate();
775
+ return {
776
+ ok: true,
777
+ updated: 0,
778
+ deleted: result.deleted ?? [],
779
+ missing: result.missing ?? [],
780
+ globalSkipped: result.globalSkipped,
781
+ };
782
+ },
783
+ async refreshAccountToken(
784
+ access: AccessDecision | undefined,
785
+ providerId: string,
786
+ accountId: string,
787
+ ): Promise<RefreshAccountTokenResponse> {
788
+ // The refresh changes no routing state and is a per-account operator
789
+ // action, so the browser session's own write scope is sufficient.
790
+ const a = requireTenantScope(access, "dashboard:write");
791
+ // 404 unless the account exists and belongs to this provider/tenant.
792
+ const accounts = await config.store.listAccounts(a.tenantId, providerId);
793
+ const owned = accounts.find((account) => account.id === accountId && account.tenantId === a.tenantId);
794
+ if (!owned)
795
+ throw new ConsoleDomainError("account_not_found", 404, `Account ${accountId} not found`);
796
+ // 409 for a static token (nothing to re-mint), 400 for non-OAuth —
797
+ // before calling the refresher, so the row is never touched on a
798
+ // rejectable request.
799
+ if (owned.staticToken === true)
800
+ throw new ConsoleDomainError("static_token", 409, "Account carries a static token that cannot be refreshed");
801
+ if (owned.credentialKind !== "oauth")
802
+ throw new ConsoleDomainError("not_oauth", 400, "Account is not OAuth; token refresh is not applicable");
803
+ if (!config.refreshAccountStatus)
804
+ throw new ConsoleDomainError("refresh_unavailable", 501, "Token refresh is not configured on this host");
805
+ const outcome = await config.refreshAccountStatus(accountId, { origin: "ui" });
806
+ await config.auditSink?.record({
807
+ access: a,
808
+ action: "provider_account.token_refreshed",
809
+ target: accountId,
810
+ detail: {
811
+ providerId,
812
+ ok: outcome.ok,
813
+ statusAfter: outcome.statusAfter,
814
+ ...(outcome.error === undefined ? {} : { error: outcome.error }),
815
+ },
816
+ });
817
+ await config.snapshotInvalidator?.invalidate();
818
+ return {
819
+ ok: outcome.ok,
820
+ statusAfter: outcome.statusAfter,
821
+ ...(outcome.tokenRefreshed ? { tokenRefreshed: true } : {}),
822
+ ...(outcome.expiresAt === undefined ? {} : { expiresAt: outcome.expiresAt }),
823
+ ...(outcome.refreshedAt === undefined ? {} : { refreshedAt: outcome.refreshedAt }),
824
+ ...(outcome.error === undefined ? {} : { error: outcome.error }),
825
+ ...(outcome.category === undefined ? {} : { category: outcome.category }),
826
+ };
827
+ },
718
828
  /**
719
829
  * Exports the requested accounts with their decrypted credentials.
720
830
  *
@@ -778,9 +888,7 @@ export function createProviderCatalogOperations(config: ProviderCatalogConfig) {
778
888
  // channel check (11128) rejects the imported account.
779
889
  const materialized =
780
890
  account.providerId === "cb" ||
781
- account.providerId === "cbcn" ||
782
- account.providerId === "workbuddy" ||
783
- account.providerId === "workbuddycn"
891
+ account.providerId === "cbcn"
784
892
  // dynamic-import: B-02 lazy graph — buddy device-binding materializer, buddy-family only.
785
893
  // The buddy-identity graph is lazy-loaded: the device-binding
786
894
  // materializer only runs for buddy-family accounts (B-02).
@@ -2,9 +2,10 @@ import { consoleErrorHandler } from "../../shared/errors";
2
2
  import { literalUnion } from "../../shared/elysia-schema";
3
3
  import { Elysia, t, type AnyElysia } from "elysia";
4
4
  import { SERVICE_KINDS, WIRE_FAMILIES } from "../../../protocol/canonical-model";
5
- import { PROBE_REASONING_EFFORTS } from "../../../providers/discovery/discovery-types";
5
+ import { PROBE_MODES, PROBE_REASONING_EFFORTS } from "../../../providers/discovery/discovery-types";
6
6
  import {
7
7
  ACCOUNT_STATUSES,
8
+ DISCOVER_MODEL_COMMIT_MAX,
8
9
  CREDENTIAL_KINDS,
9
10
  CREDENTIAL_MODES,
10
11
  } from "./contracts";
@@ -18,6 +19,7 @@ import type {
18
19
  ByokConnectionTestRequest,
19
20
  CreateProviderAccountRequest,
20
21
  CreateProviderRequest,
22
+ DiscoverModelCommitRequest,
21
23
  ProbeModelRequest,
22
24
  RegisterModelsRequest,
23
25
  SetModelEnabledRequest,
@@ -110,12 +112,28 @@ const modelMetadataOverrideBody = t.Object({
110
112
  image: t.Optional(t.Boolean()),
111
113
  reasoning: t.Optional(t.Boolean()),
112
114
  toolCall: t.Optional(t.Boolean()),
115
+ warnings: t.Optional(
116
+ t.Union([
117
+ t.Null(),
118
+ t.Array(
119
+ t.Object({
120
+ kind: t.String(),
121
+ message: t.String(),
122
+ severity: t.Optional(literalUnion(["info", "warn", "critical"])),
123
+ updatedAt: t.Optional(t.String()),
124
+ }),
125
+ ),
126
+ ]),
127
+ ),
113
128
  });
114
129
  const registerModelsBody = t.Object({
115
130
  modelIds: t.Array(t.String()),
116
131
  wireFamily: t.Optional(t.String()),
117
132
  metadata: t.Optional(modelMetadataOverrideBody),
118
133
  });
134
+ const discoverCommitBody = t.Object({
135
+ modelIds: t.Array(t.String(), { minItems: 1, maxItems: DISCOVER_MODEL_COMMIT_MAX }),
136
+ });
119
137
  const probeModelBody = t.Object({
120
138
  modelId: t.String(),
121
139
  serviceKind: t.Optional(literalUnion(SERVICE_KINDS)),
@@ -127,6 +145,7 @@ const probeModelBody = t.Object({
127
145
  maxOutputTokens: t.Optional(t.Integer({ minimum: 1, maximum: 131072 })),
128
146
  stream: t.Optional(t.Boolean()),
129
147
  surface: t.Optional(literalUnion(["default", "cosy", "grpc", "ide"])),
148
+ mode: t.Optional(literalUnion(PROBE_MODES)),
130
149
  });
131
150
  const byokConnectionTestBody = t.Object({
132
151
  baseUrl: t.String(),
@@ -275,9 +294,42 @@ export function createProviderCatalogRoutes(config: ProviderCatalogConfig): AnyE
275
294
  );
276
295
  },
277
296
  )
278
- .post("/:providerId/models/test-models", async ({ request, params }) => {
279
- return await modelFactory.probeAllModels(config.accessResolver(request), params.providerId);
280
- })
297
+ .post(
298
+ "/:providerId/models/test-models",
299
+ { body: t.Optional(t.Object({ mode: t.Optional(literalUnion(PROBE_MODES)) })) },
300
+ async ({ request, params, body }) => {
301
+ return await modelFactory.probeAllModels(
302
+ config.accessResolver(request),
303
+ params.providerId,
304
+ body?.mode,
305
+ );
306
+ },
307
+ )
308
+ // Discovery must be declared before `/:providerId/models/discover/commit`
309
+ // would be shadowed by a dynamic segment; Elysia resolves both here.
310
+ .get("/:providerId/models/discover", async ({ request, params }) => {
311
+ return await modelFactory.listDiscoveryCandidates(
312
+ config.accessResolver(request),
313
+ params.providerId,
314
+ );
315
+ })
316
+ .post(
317
+ "/:providerId/models/discover/commit",
318
+ { body: discoverCommitBody },
319
+ async ({ request, params, body }) => {
320
+ return await modelFactory.commitDiscoveredModels(
321
+ config.accessResolver(request),
322
+ params.providerId,
323
+ (body as DiscoverModelCommitRequest).modelIds,
324
+ );
325
+ },
326
+ )
327
+ .post("/:providerId/accounts/actions/disable-cooldowns", async ({ request, params }) => {
328
+ return await factory.disableCooldowns(config.accessResolver(request), params.providerId);
329
+ })
330
+ .post("/:providerId/accounts/actions/delete-banned", async ({ request, params }) => {
331
+ return await factory.deleteBanned(config.accessResolver(request), params.providerId);
332
+ })
281
333
  .post(
282
334
  "/:providerId/accounts/probe",
283
335
  { body: probeModelBody },
@@ -408,6 +460,14 @@ export function createProviderCatalogRoutes(config: ProviderCatalogConfig): AnyE
408
460
  params.accountId,
409
461
  );
410
462
  return recovered;
463
+ })
464
+ .post("/:providerId/accounts/:accountId/refresh-token", async ({ request, params }) => {
465
+ const refreshed = await factory.refreshAccountToken(
466
+ config.accessResolver(request),
467
+ params.providerId,
468
+ params.accountId,
469
+ );
470
+ return refreshed;
411
471
  })
412
472
  .post(
413
473
  "/:providerId/accounts/export",
@@ -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
2
  import { and, asc, eq, gte, inArray, isNull, or, sql, type SQL } from "drizzle-orm";
3
+ import { isSqliteDatabase } 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,211 @@ 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.
1701
+ *
1702
+ * Pool-as-account rows live in `provider_pool_accounts`, not this table, so
1703
+ * they are excluded by construction. Shared (`tenant_id IS NULL`) rows are
1704
+ * counted in `globalSkipped` but never written by a tenant; the global-admin
1705
+ * pair of routes owns those.
1706
+ */
1707
+ async disableCooldowns(
1708
+ tenantId: string,
1709
+ providerId: string,
1710
+ ): Promise<ProviderAccountBulkActionResult> {
1711
+ const nowIso = new Date().toISOString();
1712
+ const [globalRows, updatedRows] = await Promise.all([
1713
+ this.db
1714
+ .select({ id: providerAccounts.id })
1715
+ .from(providerAccounts)
1716
+ .where(
1717
+ and(
1718
+ eq(providerAccounts.providerId, providerId),
1719
+ isNull(providerAccounts.tenantId),
1720
+ this.liveCooldownPredicate(nowIso),
1721
+ ),
1722
+ ),
1723
+ this.db
1724
+ .update(providerAccounts)
1725
+ .set({ status: "disabled" })
1726
+ .where(
1727
+ and(
1728
+ eq(providerAccounts.providerId, providerId),
1729
+ eq(providerAccounts.tenantId, tenantId),
1730
+ this.liveCooldownPredicate(nowIso),
1731
+ ),
1732
+ )
1733
+ .returning({ id: providerAccounts.id }),
1734
+ ]);
1735
+ // Status is part of the cached credential's admission decision, so every
1736
+ // newly disabled id must be dropped here — not only on delete.
1737
+ invalidateCredentialCaches(updatedRows.map((row) => row.id));
1738
+ return { ok: true, updated: updatedRows.length, globalSkipped: globalRows.length };
1739
+ }
1740
+
1741
+ /**
1742
+ * One `DELETE ... WHERE <strict banned predicate> RETURNING id` (D12).
1743
+ * Credential and quota caches are dropped for the deleted ids in one pass
1744
+ * each, exactly like the existing batch-delete route. A delete is strictly
1745
+ * `status='disabled' AND last_error_category='policy_blocked_banned'`;
1746
+ * `auth_invalidated` is remediated by replacing the credential, not here.
1747
+ */
1748
+ async deleteBanned(
1749
+ tenantId: string,
1750
+ providerId: string,
1751
+ ): Promise<ProviderAccountBulkActionResult> {
1752
+ const globalRows = await this.db
1753
+ .select({ id: providerAccounts.id })
1754
+ .from(providerAccounts)
1755
+ .where(
1756
+ and(
1757
+ eq(providerAccounts.providerId, providerId),
1758
+ isNull(providerAccounts.tenantId),
1759
+ this.bannedPredicate(),
1760
+ ),
1761
+ );
1762
+ const deletedRows = await this.db
1763
+ .delete(providerAccounts)
1764
+ .where(
1765
+ and(
1766
+ eq(providerAccounts.providerId, providerId),
1767
+ eq(providerAccounts.tenantId, tenantId),
1768
+ this.bannedPredicate(),
1769
+ ),
1770
+ )
1771
+ .returning({ id: providerAccounts.id });
1772
+ const deletedIds = deletedRows.map((row) => row.id);
1773
+ invalidateCredentialCaches(deletedIds);
1774
+ await invalidateQuotaCaches(tenantId, deletedIds, this.redis);
1775
+ return {
1776
+ ok: true,
1777
+ updated: 0,
1778
+ deleted: deletedIds,
1779
+ missing: [],
1780
+ globalSkipped: globalRows.length,
1781
+ };
1782
+ }
1783
+
1784
+ /** Strict banned definition shared by the tenant and global DELETE paths. */
1785
+ private bannedPredicate(): SQL {
1786
+ return and(
1787
+ eq(providerAccounts.status, "disabled"),
1788
+ eq(providerAccounts.lastErrorCategory, "policy_blocked_banned"),
1789
+ ) as SQL;
1790
+ }
1791
+
1792
+ /**
1793
+ * `status='cooldown' OR cooldown_until > now OR model_cooldowns` carries a
1794
+ * live entry. The JSON check is dialect-specific: PostgreSQL uses
1795
+ * `jsonb_each_text` (with a shape guard), SQLite `json_each` on its `text`
1796
+ * column. Only entries whose ISO deadline is still in the future count, mirroring
1797
+ * `liveModelCooldowns`. Timestamps are stored as ISO strings on both backends,
1798
+ * so the lexical comparison against `nowIso` is stable.
1799
+ */
1800
+ private liveCooldownPredicate(nowIso: string): SQL {
1801
+ const liveModelCooldown = isSqliteDatabase(this.db)
1802
+ ? sql`EXISTS (SELECT 1 FROM json_each(${providerAccounts.modelCooldowns}) AS entry WHERE json_valid(${providerAccounts.modelCooldowns}) AND json_type(${providerAccounts.modelCooldowns}) = 'object' AND entry.value > ${nowIso})`
1803
+ : sql`EXISTS (SELECT 1 FROM jsonb_each_text(${providerAccounts.modelCooldowns}) AS entry WHERE jsonb_typeof(${providerAccounts.modelCooldowns}) = 'object' AND entry.value > ${nowIso})`;
1804
+ return or(
1805
+ eq(providerAccounts.status, "cooldown"),
1806
+ sql`${providerAccounts.cooldownUntil} > ${nowIso}`,
1807
+ liveModelCooldown,
1808
+ ) as SQL;
1809
+ }
1658
1810
 
1659
1811
  async syncModels(tenantId: string, providerId: string): Promise<{ synced: number }> {
1660
1812
  return this.probing.syncModels(tenantId, providerId);
1661
1813
  }
1814
+
1815
+ /** Read-only discovery; delegates to the same fetch path as sync (area 05). */
1816
+ async listDiscoveryCandidates(
1817
+ tenantId: string,
1818
+ providerId: string,
1819
+ ): Promise<{
1820
+ readonly supported: boolean;
1821
+ readonly candidates: readonly DiscoverModelCandidate[];
1822
+ readonly accountLabel?: string;
1823
+ }> {
1824
+ const discovery = await this.probing.discoverModels(tenantId, providerId);
1825
+ if (!discovery.supported) return { supported: false, candidates: [] };
1826
+ const registered = await this.listModelsForTenant(tenantId, providerId);
1827
+ const ids = new Set(
1828
+ (registered.get(providerId) ?? []).map((entry) => entry.modelId),
1829
+ );
1830
+ // The picker exists to add models, so rows already on the roster are
1831
+ // excluded rather than listed disabled — a fully-registered catalog would
1832
+ // otherwise render an all-"Registered" list that reads as empty.
1833
+ const candidates: DiscoverModelCandidate[] = discovery.models
1834
+ .filter((definition) => !ids.has(definition.modelId))
1835
+ .map((definition) => ({
1836
+ modelId: definition.modelId,
1837
+ wireFamily: definition.wireFamily,
1838
+ serviceKind: definition.serviceKind ?? "llm",
1839
+ endpointPath: definition.endpointPath,
1840
+ contextLimit: definition.contextLimit,
1841
+ outputLimit: definition.outputLimit,
1842
+ modalities: definition.modalities,
1843
+ reasoning: definition.reasoning,
1844
+ toolCall: definition.toolCall,
1845
+ ...(definition.freeTier === undefined ? {} : { freeTier: definition.freeTier }),
1846
+ ...(definition.proOnly === undefined ? {} : { proOnly: definition.proOnly }),
1847
+ alreadyRegistered: false,
1848
+ }));
1849
+ return {
1850
+ supported: true,
1851
+ candidates,
1852
+ ...(discovery.accountLabel === undefined ? {} : { accountLabel: discovery.accountLabel }),
1853
+ };
1854
+ }
1855
+
1856
+ /**
1857
+ * Re-runs discovery on the server, then persists only ids the fresh roster
1858
+ * actually advertised (D28/D29/D30/D31). This is the anti-forgery gate: a
1859
+ * caller cannot attach a hand-built metadata object to an arbitrary id.
1860
+ */
1861
+ async commitDiscoveredModels(
1862
+ tenantId: string,
1863
+ providerId: string,
1864
+ modelIds: readonly string[],
1865
+ ): Promise<{
1866
+ readonly committed: readonly string[];
1867
+ readonly skipped: readonly string[];
1868
+ readonly synced: number;
1869
+ }> {
1870
+ const discovery = await this.probing.discoverModels(tenantId, providerId);
1871
+ if (!discovery.supported) {
1872
+ throw new ConsoleDomainError(
1873
+ "model_discovery_unsupported",
1874
+ 400,
1875
+ `${providerId} publishes no model-listing endpoint; add the model manually`,
1876
+ );
1877
+ }
1878
+ const byId = new Map(discovery.models.map((definition) => [definition.modelId, definition]));
1879
+ const wanted = [...new Set(modelIds)];
1880
+ const unknown = wanted.filter((id) => !byId.has(id));
1881
+ if (unknown.length > 0) {
1882
+ throw new ConsoleDomainError(
1883
+ "invalid_request",
1884
+ 400,
1885
+ `Models are no longer in the provider's listing: ${unknown.join(", ")} (request {requestId}). Fetch the model list again 🪞`,
1886
+ );
1887
+ }
1888
+ const registered = await this.listModelsForTenant(tenantId, providerId);
1889
+ const existing = new Set(
1890
+ (registered.get(providerId) ?? []).map((entry) => entry.modelId),
1891
+ );
1892
+ const selected = wanted.filter((id) => !existing.has(id));
1893
+ const skipped = wanted.filter((id) => existing.has(id));
1894
+ if (selected.length === 0) return { committed: [], skipped, synced: 0 };
1895
+ const { synced } = await this.probing.commitDiscoveredModels(
1896
+ tenantId,
1897
+ providerId,
1898
+ selected,
1899
+ );
1900
+ return { committed: selected, skipped, synced };
1901
+ }
1662
1902
  }