cartethyia 2.7.8 → 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 (371) 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-BRWKPQPf.js → CliToolDetailPage-ixwgKk53.js} +1 -1
  6. package/dist/dashboard/assets/CliToolDetailPage-ixwgKk53.js.gz +0 -0
  7. package/dist/dashboard/assets/{CliToolsPage-BP-Y5iHN.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-Cv-2Ol_K.js → InjectSystemPromptPanel-BOoO8rpz.js} +2 -2
  18. package/dist/dashboard/assets/InjectSystemPromptPanel-BOoO8rpz.js.gz +0 -0
  19. package/dist/dashboard/assets/{LoginPage-bKfz_b1K.js → LoginPage-ChJuNNVN.js} +1 -1
  20. package/dist/dashboard/assets/LoginPage-ChJuNNVN.js.gz +0 -0
  21. package/dist/dashboard/assets/{ModelPicker-B-WQxx_H.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-emlTuxz6.js → ProxyPage-BJ87Pp0U.js} +1 -1
  32. package/dist/dashboard/assets/ProxyPage-BJ87Pp0U.js.gz +0 -0
  33. package/dist/dashboard/assets/{QuotaPage-CMJKNYWb.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-CUZrKs9r.js → SetupPage-CFNcATJJ.js} +1 -1
  38. package/dist/dashboard/assets/SetupPage-CFNcATJJ.js.gz +0 -0
  39. package/dist/dashboard/assets/{StudioPage-jWmUdDpy.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-DyI_mJfi.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-B-yXtCqb.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-_HwAEqFF.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-ByjCk0o9.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-BZhOlfz-.js → dialog-B5L2jPQ_.js} +1 -1
  70. package/dist/dashboard/assets/dialog-B5L2jPQ_.js.gz +0 -0
  71. package/dist/dashboard/assets/{drawer-BADqIRYP.js → drawer-DkjUouAj.js} +1 -1
  72. package/dist/dashboard/assets/drawer-DkjUouAj.js.gz +0 -0
  73. package/dist/dashboard/assets/{entry-COaC-irN.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-DcHJyRiu.js → formatters-CrsdLVsn.js} +1 -1
  80. package/dist/dashboard/assets/formatters-CrsdLVsn.js.gz +0 -0
  81. package/dist/dashboard/assets/{index-CgKmFngv.js → index-BU64Lqvp.js} +2 -2
  82. package/dist/dashboard/assets/index-BU64Lqvp.js.gz +0 -0
  83. package/dist/dashboard/assets/{inline-B2BirKry.js → inline-B2aDLyyg.js} +1 -1
  84. package/dist/dashboard/assets/inline-B2aDLyyg.js.gz +0 -0
  85. package/dist/dashboard/assets/{input-D-qV9dKY.js → input-BCVkdR9d.js} +1 -1
  86. package/dist/dashboard/assets/input-BCVkdR9d.js.gz +0 -0
  87. package/dist/dashboard/assets/{layout-B7tVWhtV.js → layout-DNcAdMK-.js} +1 -1
  88. package/dist/dashboard/assets/layout-DNcAdMK-.js.gz +0 -0
  89. package/dist/dashboard/assets/{list-row-CtkGE3bF.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-DGizF06-.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-C9UE-bOr.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-BnAd22a_.js → numbers-BKQE8Xpz.js} +1 -1
  98. package/dist/dashboard/assets/numbers-BKQE8Xpz.js.gz +0 -0
  99. package/dist/dashboard/assets/{pagination-BAywyMzS.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-BCOcv8WK.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-ByKt3Q1g.js → stack-DWCwU3fh.js} +1 -1
  114. package/dist/dashboard/assets/stack-DWCwU3fh.js.gz +0 -0
  115. package/dist/dashboard/assets/{state-DXbGTx7N.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-ZeTsW623.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 +210 -6
  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/client-versions.ts +37 -99
  262. package/src/providers/operations/cooldown-rules.ts +61 -0
  263. package/src/providers/operations/provider-catalog-seeder.ts +4 -0
  264. package/src/providers/provider-contracts.ts +29 -0
  265. package/src/providers/provider-metadata.ts +59 -4
  266. package/src/providers/provider-model-definition.ts +36 -0
  267. package/src/providers/quota/refresh.ts +21 -0
  268. package/src/security/abuse.ts +71 -0
  269. package/src/transport/dispatch/proxy-request.ts +1 -0
  270. package/src/transport/dispatch/streaming/stream.ts +1 -0
  271. package/src/transport/dispatch/upstream.ts +4 -1
  272. package/src/transport/failure-policy.ts +113 -7
  273. package/src/transport/routing/router.ts +25 -1
  274. package/src/transport/surface/messages/adapter.ts +2 -1
  275. package/src/transport/surface/responses/encode.ts +12 -1
  276. package/src/workers/daily-checkin.ts +1 -1
  277. package/src/workers/quota-refresh-sweep.ts +1 -1
  278. package/src/workers/scheduler.ts +22 -4
  279. package/dist/dashboard/assets/BannedPage-DeIbIiBU.js +0 -1
  280. package/dist/dashboard/assets/BannedPage-DeIbIiBU.js.gz +0 -0
  281. package/dist/dashboard/assets/CliToolDetailPage-BRWKPQPf.js.gz +0 -0
  282. package/dist/dashboard/assets/CliToolsPage-BP-Y5iHN.js.gz +0 -0
  283. package/dist/dashboard/assets/CombosPage-B7JHozVa.js +0 -1
  284. package/dist/dashboard/assets/CombosPage-B7JHozVa.js.gz +0 -0
  285. package/dist/dashboard/assets/ConsoleLogPage-GX7iGGcu.js +0 -1
  286. package/dist/dashboard/assets/ConsoleLogPage-GX7iGGcu.js.gz +0 -0
  287. package/dist/dashboard/assets/CustomizationPage-HVonbGEa.js +0 -1
  288. package/dist/dashboard/assets/CustomizationPage-HVonbGEa.js.gz +0 -0
  289. package/dist/dashboard/assets/HealthEventsModal-xaut4OJ9.js +0 -1
  290. package/dist/dashboard/assets/HealthEventsModal-xaut4OJ9.js.gz +0 -0
  291. package/dist/dashboard/assets/InjectSystemPromptPanel-Cv-2Ol_K.js.gz +0 -0
  292. package/dist/dashboard/assets/LoginPage-bKfz_b1K.js.gz +0 -0
  293. package/dist/dashboard/assets/ModelPicker-B-WQxx_H.js.gz +0 -0
  294. package/dist/dashboard/assets/OverviewPage-Z9pJZFo8.js +0 -1
  295. package/dist/dashboard/assets/OverviewPage-Z9pJZFo8.js.gz +0 -0
  296. package/dist/dashboard/assets/ProviderDetailPage-CMeDBxR1.js +0 -7
  297. package/dist/dashboard/assets/ProviderDetailPage-CMeDBxR1.js.gz +0 -0
  298. package/dist/dashboard/assets/ProviderIcon-jjqXJDA3.js +0 -1
  299. package/dist/dashboard/assets/ProviderIcon-jjqXJDA3.js.gz +0 -0
  300. package/dist/dashboard/assets/ProvidersPage-BXPdOpG4.js +0 -1
  301. package/dist/dashboard/assets/ProvidersPage-BXPdOpG4.js.gz +0 -0
  302. package/dist/dashboard/assets/ProxyPage-emlTuxz6.js.gz +0 -0
  303. package/dist/dashboard/assets/QuotaPage-CMJKNYWb.js.gz +0 -0
  304. package/dist/dashboard/assets/SettingsPage-BSy7EwGo.js +0 -1
  305. package/dist/dashboard/assets/SettingsPage-BSy7EwGo.js.gz +0 -0
  306. package/dist/dashboard/assets/SetupPage-CUZrKs9r.js.gz +0 -0
  307. package/dist/dashboard/assets/StudioPage-jWmUdDpy.js.gz +0 -0
  308. package/dist/dashboard/assets/UsagePage-5F9tGVGG.js +0 -56
  309. package/dist/dashboard/assets/UsagePage-5F9tGVGG.js.gz +0 -0
  310. package/dist/dashboard/assets/api-keys-KvX83mRe.js +0 -1
  311. package/dist/dashboard/assets/api-keys-KvX83mRe.js.gz +0 -0
  312. package/dist/dashboard/assets/app-BEsU8hKs.css +0 -2
  313. package/dist/dashboard/assets/app-BEsU8hKs.css.gz +0 -0
  314. package/dist/dashboard/assets/assert-CidQVoNo.js +0 -1
  315. package/dist/dashboard/assets/assert-CidQVoNo.js.gz +0 -0
  316. package/dist/dashboard/assets/badge-DyI_mJfi.js.gz +0 -0
  317. package/dist/dashboard/assets/banner-B-yXtCqb.js.gz +0 -0
  318. package/dist/dashboard/assets/brand/bansos_page.png +0 -0
  319. package/dist/dashboard/assets/brand/denia_bansos.jpg +0 -0
  320. package/dist/dashboard/assets/brand/favicon.jpg +0 -0
  321. package/dist/dashboard/assets/brand/login_register.jpg +0 -0
  322. package/dist/dashboard/assets/brand/login_register_initial.jpg +0 -0
  323. package/dist/dashboard/assets/brand/og_bansos.jpg +0 -0
  324. package/dist/dashboard/assets/brand/og_image.jpg +0 -0
  325. package/dist/dashboard/assets/brand/sidebar_ico.jpg +0 -0
  326. package/dist/dashboard/assets/card-_HwAEqFF.js.gz +0 -0
  327. package/dist/dashboard/assets/cli-tools-Bwv5-zGZ.js +0 -1
  328. package/dist/dashboard/assets/cli-tools-Bwv5-zGZ.js.gz +0 -0
  329. package/dist/dashboard/assets/clipboard-button-ByjCk0o9.js.gz +0 -0
  330. package/dist/dashboard/assets/dialog-BZhOlfz-.js.gz +0 -0
  331. package/dist/dashboard/assets/drawer-BADqIRYP.js.gz +0 -0
  332. package/dist/dashboard/assets/entry-COaC-irN.js.gz +0 -0
  333. package/dist/dashboard/assets/entry-Ca7LSCet.js +0 -4
  334. package/dist/dashboard/assets/entry-Ca7LSCet.js.gz +0 -0
  335. package/dist/dashboard/assets/entry-PDd27uCd.js +0 -1
  336. package/dist/dashboard/assets/entry-PDd27uCd.js.gz +0 -0
  337. package/dist/dashboard/assets/formatters-DcHJyRiu.js.gz +0 -0
  338. package/dist/dashboard/assets/index-CgKmFngv.js.gz +0 -0
  339. package/dist/dashboard/assets/inline-B2BirKry.js.gz +0 -0
  340. package/dist/dashboard/assets/input-D-qV9dKY.js.gz +0 -0
  341. package/dist/dashboard/assets/layout-B7tVWhtV.js.gz +0 -0
  342. package/dist/dashboard/assets/list-row-CtkGE3bF.js.gz +0 -0
  343. package/dist/dashboard/assets/live-DGizF06-.js.gz +0 -0
  344. package/dist/dashboard/assets/minimize-2-BTmMTK4o.js +0 -1
  345. package/dist/dashboard/assets/minimize-2-BTmMTK4o.js.gz +0 -0
  346. package/dist/dashboard/assets/mutation-GU7XdEjz.js.gz +0 -0
  347. package/dist/dashboard/assets/network-C9UE-bOr.js.gz +0 -0
  348. package/dist/dashboard/assets/numbers-BnAd22a_.js.gz +0 -0
  349. package/dist/dashboard/assets/pagination-BAywyMzS.js.gz +0 -0
  350. package/dist/dashboard/assets/query-keys-YSoQOUh6.js.gz +0 -0
  351. package/dist/dashboard/assets/routing-fnCsxgNO.js +0 -1
  352. package/dist/dashboard/assets/routing-fnCsxgNO.js.gz +0 -0
  353. package/dist/dashboard/assets/select-BCOcv8WK.js.gz +0 -0
  354. package/dist/dashboard/assets/settings-BxslsErn.js +0 -1
  355. package/dist/dashboard/assets/settings-BxslsErn.js.gz +0 -0
  356. package/dist/dashboard/assets/stack-ByKt3Q1g.js.gz +0 -0
  357. package/dist/dashboard/assets/state-DXbGTx7N.js.gz +0 -0
  358. package/dist/dashboard/assets/switch-ZeTsW623.js.gz +0 -0
  359. package/dist/dashboard/assets/useMutation-CbCP4w1o.js.gz +0 -0
  360. package/dist/dashboard/console-backgrounds.jpg +0 -0
  361. package/dist/dashboard/console-backgrounds_initial.jpg +0 -0
  362. package/dist/dashboard/donation/qris-risun-marke.png +0 -0
  363. package/src/providers/integrations/buddy/turing-device-token.ts +0 -262
  364. package/src/providers/integrations/buddy/workbuddy/cn-oauth.ts +0 -30
  365. package/src/providers/integrations/buddy/workbuddy/cn-provider.ts +0 -58
  366. package/src/providers/integrations/buddy/workbuddy/oauth.ts +0 -31
  367. package/src/providers/integrations/buddy/workbuddy/provider.ts +0 -209
  368. package/src/providers/integrations/buddy/workbuddy/quota.ts +0 -161
  369. package/src/providers/integrations/buddy/workbuddy/shared.ts +0 -285
  370. /package/dist/dashboard/assets/{app-CTxoqyYB.js → app-B7kIWFm5.js} +0 -0
  371. /package/dist/dashboard/assets/{app-CTxoqyYB.js.gz → app-B7kIWFm5.js.gz} +0 -0
@@ -35,12 +35,18 @@ import type { ProviderRegistry } from "../providers/provider-registry";
35
35
  import type { BundledProviderCatalog } from "../providers/operations/provider-catalog-service";
36
36
  import type { ValidatedNetworkBindingFactory } from "../network/pool/resolver";
37
37
  import type { TrustedProxyBoundary } from "../config/network";
38
+ import type { IpAbuseStore } from "../security/abuse";
38
39
  import { gzipResponse } from "./response-compression";
39
40
  import { beginConsoleActivity } from "../shared/invoke-guard";
40
41
 
41
42
  export interface ConsoleApiCompositionDeps {
42
43
  readonly db: CartethyiaDatabase;
43
44
  readonly accessResolver: ConsoleAccessResolver;
45
+ /**
46
+ * The IP-abuse store backing manual IP bans and the live ban list. Absent
47
+ * (reduced compositions) means the IP-ban routes are not offered.
48
+ */
49
+ readonly ipAbuseStore?: IpAbuseStore;
44
50
  /**
45
51
  * Drops the cached route snapshot after a console write changes the catalog.
46
52
  * Structurally satisfied by `RouteSnapshotService`; declared here so the
@@ -222,6 +228,7 @@ export function createConsoleRouter(deps: ConsoleApiCompositionDeps): AnyElysia
222
228
  telemetryBuffer,
223
229
  ...(deps.stateStore ? { stateStore: deps.stateStore } : {}),
224
230
  ...(deps.shareStore ? { shareStore: deps.shareStore } : {}),
231
+ ...(deps.ipAbuseStore ? { ipAbuseStore: deps.ipAbuseStore } : {}),
225
232
  providerRegistry: deps.providerRegistry,
226
233
  bundledModelCatalog: deps.bundledModelCatalog,
227
234
  networkBindingFactory: deps.networkBindingFactory,
@@ -62,13 +62,13 @@ export const SHARE_SOCIAL_META: SocialMeta = {
62
62
  title: "Cartethyia — Bansos Token",
63
63
  description:
64
64
  "Come and save your tokens 💖 Shared Cartethyia API key — Bansos Token with live quota, usage and model allowlist.",
65
- image: "/assets/brand/og_bansos.jpg",
66
- imageType: "image/jpeg",
65
+ image: "/assets/brand/og_bansos.webp",
66
+ imageType: "image/webp",
67
67
  // Read from the file's own header, not carried over from a previous image:
68
68
  // og:image:width/height are what a crawler uses to allocate the card before
69
69
  // the bytes arrive, so a copied size makes every preview crop.
70
- imageWidth: 1760,
71
- imageHeight: 576,
70
+ imageWidth: 1200,
71
+ imageHeight: 393,
72
72
  imageAlt: "Cartethyia Bansos Token — Come and save your tokens",
73
73
  };
74
74
 
@@ -405,7 +405,7 @@ function getMimeType(filePath: string): string {
405
405
  * and the modern Vite spelling `app-BCnqn3N3.js` — plus `_`, and then required
406
406
  * to *look generated*: the segment must contain a digit or mix upper and lower
407
407
  * case. The second half is what keeps a plain word out of the set, since
408
- * `console-backgrounds.jpg` and `openai-light.svg` both match the separator
408
+ * `console-backgrounds.webp` and `openai-light.svg` both match the separator
409
409
  * alone and are emphatically not immutable.
410
410
  *
411
411
  * Getting this wrong in either direction costs real money: a hashed asset
@@ -36,6 +36,8 @@ import { createLiveRoutes } from "./observability/live";
36
36
  import { createLogRoutes } from "./observability/logs";
37
37
  import { createAuditRoutes } from "./audit/routes";
38
38
  import { DrizzleAuditReadStore } from "./audit/store";
39
+ import { createIpBanRoutes } from "./ip-bans/routes";
40
+ import type { IpAbuseStore } from "../security/abuse";
39
41
  import { createPerformanceRoutes } from "./observability/performance";
40
42
  import { createUpdateRoutes } from "./updates/routes";
41
43
  import { createRemoteSupportRoutes } from "./remote-support/routes";
@@ -55,6 +57,7 @@ import {
55
57
  createAccountExportCredentialsResolver,
56
58
  createAccountSecretResolver,
57
59
  } from "../providers/operations/provider-credential-service";
60
+ import { refreshAccountStatus } from "../providers/operations/account-refresh-status";
58
61
  import { syncByokProvider } from "../providers/operations/provider-catalog-service";
59
62
  import { DrizzleApiKeyStore } from "../persistence/stores/api-key-store";
60
63
  import {
@@ -82,6 +85,11 @@ import type { ConsoleAccessResolver } from "./auth/access";
82
85
  export interface ConsoleDomainContext {
83
86
  readonly db: CartethyiaDatabase;
84
87
  readonly accessResolver: ConsoleAccessResolver;
88
+ /**
89
+ * The IP-abuse store backing manual IP bans and the live ban list. Absent
90
+ * (reduced compositions) means the IP-ban routes are not offered.
91
+ */
92
+ readonly ipAbuseStore?: IpAbuseStore;
85
93
  readonly providerRegistry: ProviderRegistry;
86
94
  readonly bundledModelCatalog: BundledProviderCatalog;
87
95
  readonly networkBindingFactory: ValidatedNetworkBindingFactory;
@@ -159,6 +167,17 @@ export function registerConsoleDomains(
159
167
  resolveRefresher: (id) => ctx.providerRegistry.resolveRefresher(id),
160
168
  refreshService: ctx.oauthRefreshService,
161
169
  });
170
+ /** Manual OAuth token refresh for the catalog route (area 04). */
171
+ const refreshAccountStatusForConsole = (accountId: string, opts?: { readonly origin?: "ui" | "sweep" | "dispatch" }) =>
172
+ refreshAccountStatus(
173
+ {
174
+ db: ctx.db,
175
+ refreshService: ctx.oauthRefreshService,
176
+ resolveRefresher: (id) => ctx.providerRegistry.resolveRefresher(id),
177
+ },
178
+ accountId,
179
+ opts,
180
+ );
162
181
  const providerCatalogStore = new DrizzleProviderCatalogStore(ctx.db, {
163
182
  telemetryBuffer: ctx.telemetryBuffer,
164
183
  bundledModelCatalog: ctx.bundledModelCatalog,
@@ -238,6 +257,7 @@ export function registerConsoleDomains(
238
257
  };
239
258
  },
240
259
  snapshotInvalidator: ctx.routeSnapshotService,
260
+ redis: ctx.redis,
241
261
  readAccountInflight: async (providerId, tenantId) =>
242
262
  ctx.readRoutingAccountInflight ? ctx.readRoutingAccountInflight(providerId, tenantId) : [],
243
263
  });
@@ -271,6 +291,12 @@ export function registerConsoleDomains(
271
291
  service: createRemoteSupportService({ port: resolvePort() }),
272
292
  }));
273
293
  console.use(createAuditRoutes({ store: auditReadStore, accessResolver: ctx.accessResolver }));
294
+ if (ctx.ipAbuseStore) {
295
+ console.use(createIpBanRoutes({
296
+ accessResolver: ctx.accessResolver,
297
+ store: ctx.ipAbuseStore,
298
+ }));
299
+ }
274
300
  // InferHub market data. On demand only: a route fetches when its snapshot is
275
301
  // past its freshness window, so a page nobody opens causes no upstream calls.
276
302
  const inferhubKeys = createInferhubKeyStore(ctx.db);
@@ -287,6 +313,7 @@ export function registerConsoleDomains(
287
313
  redis: ctx.redis,
288
314
  snapshotInvalidator: ctx.routeSnapshotService,
289
315
  resolveExportCredentials,
316
+ refreshAccountStatus: refreshAccountStatusForConsole,
290
317
  // Without this, a custom provider only becomes dispatchable after a
291
318
  // restart: boot registers BYOK rows once, and the catalog routes are the
292
319
  syncByokProvider: (providerId) =>
@@ -0,0 +1,99 @@
1
+ // IP-ban domain: manual IP bans visible in the Banned Users modal.
2
+ //
3
+ // The gateway's abuse store keeps automatic bans (escalation after a burst).
4
+ // This domain exposes the same store to the operator so an address can be
5
+ // banned BY HAND and listed/unbanned from the console. IP bans are instance
6
+ // state, not tenant data, so every route is guarded by `platform:admin` like
7
+ // the other instance-level operations.
8
+
9
+ import { Elysia, t, type AnyElysia } from "elysia";
10
+ import { errorResponse, requireGlobalAdmin } from "../shared/errors";
11
+ import type { IpAbuseStore } from "../../security/abuse";
12
+ import type { ConsoleAccessResolver } from "../auth/access";
13
+
14
+ /** One active IP ban, as the console returns it. */
15
+ export interface IpBanResponse {
16
+ /** The banned address. */
17
+ readonly ip: string;
18
+ /** When the ban lifts, as ISO timestamp; null = permanent. */
19
+ readonly bannedUntil: string | null;
20
+ /** True when the ban was set by hand rather than by abuse escalation. */
21
+ readonly manual: boolean;
22
+ }
23
+
24
+ export interface IpBanRoutesConfig {
25
+ readonly accessResolver: ConsoleAccessResolver;
26
+ readonly store: IpAbuseStore;
27
+ }
28
+
29
+ const BAN_DURATION_CHOICES: readonly { readonly minutes: number | null; readonly label: string }[] = [
30
+ { minutes: 60, label: "1 hour" },
31
+ { minutes: 6 * 60, label: "6 hours" },
32
+ { minutes: 12 * 60, label: "12 hours" },
33
+ { minutes: 24 * 60, label: "1 day" },
34
+ { minutes: 7 * 24 * 60, label: "7 days" },
35
+ { minutes: null, label: "Permanent" },
36
+ ] as const;
37
+
38
+ /** Parses a duration choice into minutes; null = permanent. */
39
+ export function parseBanDuration(value: unknown): number | null | undefined {
40
+ if (value === null || value === undefined) return value === null ? null : undefined;
41
+ const minutes = Number(value);
42
+ if (!Number.isFinite(minutes) || minutes <= 0) return undefined;
43
+ return Math.round(minutes);
44
+ }
45
+
46
+ export function createIpBanRoutes(config: IpBanRoutesConfig): AnyElysia {
47
+ const { accessResolver, store } = config;
48
+ return new Elysia()
49
+ .get("/ip-bans", async ({ request, set }) => {
50
+ try {
51
+ requireGlobalAdmin(accessResolver(request));
52
+ const bans = await store.listBans();
53
+ return bans.map((ban) => ({
54
+ ip: ban.identity,
55
+ bannedUntil: ban.bannedUntil === null ? null : new Date(ban.bannedUntil).toISOString(),
56
+ manual: true,
57
+ }));
58
+ } catch (e) {
59
+ return errorResponse(e, set, "Could not list IP bans");
60
+ }
61
+ })
62
+ .post(
63
+ "/ip-bans",
64
+ { body: t.Object({ ip: t.String(), minutes: t.Optional(t.Nullable(t.Number())) }) },
65
+ async ({ request, body, set }) => {
66
+ try {
67
+ requireGlobalAdmin(accessResolver(request));
68
+ const ip = body.ip.trim();
69
+ if (ip.length === 0)
70
+ return errorResponse(new Error("ip is required"), set, "Invalid IP ban request");
71
+ const minutes = body.minutes === undefined || body.minutes === null ? null : Math.round(body.minutes);
72
+ if (minutes !== null && (!Number.isFinite(minutes) || minutes <= 0))
73
+ return errorResponse(new Error("minutes must be a positive integer or null"), set, "Invalid IP ban request");
74
+ await store.ban(ip, minutes === null ? 0 : minutes * 60_000);
75
+ return {
76
+ ip,
77
+ bannedUntil: minutes === null ? null : new Date(Date.now() + minutes * 60_000).toISOString(),
78
+ manual: true,
79
+ };
80
+ } catch (e) {
81
+ return errorResponse(e, set, "Could not ban IP");
82
+ }
83
+ },
84
+ )
85
+ .delete("/ip-bans/:ip", async ({ request, params, set }) => {
86
+ try {
87
+ requireGlobalAdmin(accessResolver(request));
88
+ const ip = typeof params?.["ip"] === "string" ? decodeURIComponent(params["ip"]) : "";
89
+ if (ip.length === 0)
90
+ return errorResponse(new Error("ip is required"), set, "Invalid unban request");
91
+ await store.unban(ip);
92
+ return { success: true, ip };
93
+ } catch (e) {
94
+ return errorResponse(e, set, "Could not unban IP");
95
+ }
96
+ });
97
+ }
98
+
99
+ export { BAN_DURATION_CHOICES };
@@ -11,7 +11,7 @@
11
11
  import { isBundledProviderId } from "../../../providers/provider-contracts";
12
12
  import { BUILTIN_DEFAULT_ENDPOINTS } from "../../../protocol/codecs";
13
13
 
14
- import { providerDisplayName, providerServiceKinds } from "../../../providers/provider-metadata";
14
+ import { providerDisplayName, providerServiceKinds, customProviderServiceKinds } from "../../../providers/provider-metadata";
15
15
  import type { CompatibilityProfile } from "../../../providers/provider-metadata";
16
16
  import { supportedWireFamiliesForProvider } from "../../../providers/discovery/probe-wire";
17
17
  import { providers } from "../../../persistence/schema";
@@ -74,6 +74,7 @@ export function mapProviderRow(
74
74
  row.wireFamilyDefault,
75
75
  row.compatibilityProfile,
76
76
  );
77
+ const compatibilityProfile = row.compatibilityProfile as CompatibilityProfile | null;
77
78
  return {
78
79
  providerId: row.id,
79
80
  displayName: providerDisplayName(row.id),
@@ -82,7 +83,9 @@ export function mapProviderRow(
82
83
  isBuiltIn: isBundledProviderId(row.id),
83
84
  configured: false,
84
85
  requiresAccount: row.requiresAccount,
85
- serviceKinds: providerServiceKinds(row.id),
86
+ serviceKinds: isBundledProviderId(row.id)
87
+ ? providerServiceKinds(row.id)
88
+ : customProviderServiceKinds(compatibilityProfile),
86
89
  // No model advertises search; see the note in provider-metadata.ts.
87
90
  supportsWebSearch: false,
88
91
  supportsModelDiscovery:
@@ -62,6 +62,72 @@ export const ACCOUNT_STATUSES = ["active", "disabled"] as const;
62
62
  /** One operator-settable account status. */
63
63
  export type AccountStatus = (typeof ACCOUNT_STATUSES)[number];
64
64
 
65
+ /** Display-only warning type shared with the dashboard contract mirror. */
66
+ export type { ModelWarning, ModelWarningSeverity } from "../../../providers/provider-model-definition";
67
+ import type { ModelWarning } from "../../../providers/provider-model-definition";
68
+
69
+ /**
70
+ * Outcome of one provider-scoped bulk account action (area 02).
71
+ *
72
+ * A tenant-scoped action never touches shared (`tenant_id IS NULL`) rows; the
73
+ * global-admin pair of routes does. `globalSkipped` counts those rows on the
74
+ * tenant path so the console can say why its visible list and the DB disagree.
75
+ * `deleted`/`missing` are present on delete; `updated` on disable. Both actions
76
+ * are idempotent: a second call finds no matching row and reports zero.
77
+ */
78
+ export interface ProviderAccountBulkActionResult {
79
+ readonly ok: boolean;
80
+ readonly updated: number;
81
+ readonly deleted?: readonly string[];
82
+ readonly missing?: readonly string[];
83
+ readonly globalSkipped: number;
84
+ }
85
+
86
+ /** Cap on one discovery-commit request (D29). */
87
+ export const DISCOVER_MODEL_COMMIT_MAX = 100;
88
+
89
+ /**
90
+ * One model the provider's own listing advertised, projected for the Add-Model
91
+ * picker. Read-only: `GET .../models/discover` writes nothing. `models`
92
+ * metadata here is the fetched `ModelDefinition`, not the static catalog's
93
+ * guess, so the operator can see exactly what the commit will store.
94
+ */
95
+ export interface DiscoverModelCandidate {
96
+ readonly modelId: string;
97
+ readonly wireFamily: string;
98
+ readonly serviceKind: string;
99
+ readonly endpointPath: string;
100
+ readonly contextLimit: number | null;
101
+ readonly outputLimit: number | null;
102
+ readonly modalities: { readonly input: readonly string[]; readonly output: readonly string[] };
103
+ readonly reasoning: boolean;
104
+ readonly toolCall: boolean;
105
+ readonly freeTier?: boolean;
106
+ readonly proOnly?: boolean;
107
+ readonly alreadyRegistered: boolean;
108
+ }
109
+
110
+ /** Result of `GET .../models/discover`. */
111
+ export interface DiscoverModelCandidatesResponse {
112
+ readonly supported: boolean;
113
+ readonly candidates: readonly DiscoverModelCandidate[];
114
+ /** Label of the usable account whose credential fetched the listing (D27). */
115
+ readonly accountLabel?: string;
116
+ }
117
+
118
+ /** Body of `POST .../models/discover/commit`. */
119
+ export interface DiscoverModelCommitRequest {
120
+ readonly modelIds: readonly string[];
121
+ }
122
+
123
+ /** Outcome of a discovery commit: the ids persisted and the ones skipped. */
124
+ export interface DiscoverModelCommitResult {
125
+ readonly ok: boolean;
126
+ readonly committed: readonly string[];
127
+ readonly skipped: readonly string[];
128
+ readonly synced: number;
129
+ }
130
+
65
131
  const VALID_WIRE_FAMILIES: Record<WireFamily, true> = {
66
132
  chat: true,
67
133
  responses: true,
@@ -121,6 +187,16 @@ const ADAPTER_OWNED: Record<string, true> = {
121
187
  };
122
188
 
123
189
  export function validateCompatibilityProfile(profile: CompatibilityProfile): void {
190
+ if (profile.service_kinds !== undefined) {
191
+ if (!Array.isArray(profile.service_kinds)) {
192
+ throw new GatewayError("invalid_request", 400, "service_kinds must be an array (request {requestId}). Check the shape of what you sent — then resend 🪞");
193
+ }
194
+ for (const kind of profile.service_kinds) {
195
+ if (!isServiceKind(kind)) {
196
+ throw new GatewayError("invalid_request", 400, `unknown service kind ${String(kind)} (request {requestId}). Check the shape of what you sent — then resend 🪞`);
197
+ }
198
+ }
199
+ }
124
200
  if (profile.cli_identity !== undefined) {
125
201
  if (typeof profile.cli_identity !== "boolean") {
126
202
  throw new GatewayError("invalid_request", 400, "cli_identity must be boolean (request {requestId}). Check the shape of what you sent — then resend 🪞");
@@ -358,6 +434,8 @@ export interface ModelCatalogEntry {
358
434
  * provider's own model directory states it.
359
435
  */
360
436
  freeTier?: boolean;
437
+ /** Display-only warnings; `null` in storage means none. */
438
+ warnings?: readonly ModelWarning[];
361
439
  }
362
440
  export interface FlatModelCatalogEntry {
363
441
  readonly providerId: string;
@@ -407,6 +485,11 @@ export interface ModelMetadataOverride {
407
485
  readonly image?: boolean;
408
486
  readonly reasoning?: boolean;
409
487
  readonly toolCall?: boolean;
488
+ /**
489
+ * Display-only warnings, editable only on non-builtin rows. `null` clears
490
+ * the stored value; absence leaves it alone.
491
+ */
492
+ readonly warnings?: readonly ModelWarning[] | null;
410
493
  }
411
494
 
412
495
  /** Register payload: model ids plus optional operator metadata. */
@@ -484,6 +567,27 @@ export interface UpdateProviderAccountRequest {
484
567
  */
485
568
  staticToken?: boolean;
486
569
  }
570
+ /**
571
+ * Response of `POST /providers/:providerId/accounts/:accountId/refresh-token`
572
+ * (area 04). The refresh never changes `status`: `statusAfter` is re-read from
573
+ * the database after the attempt and is never `"active"` by fiat — a disabled
574
+ * account stays disabled, only an operator can re-enable it.
575
+ */
576
+ export interface RefreshAccountTokenResponse {
577
+ readonly ok: boolean;
578
+ readonly statusAfter: string;
579
+ /** True when an upstream token refresh actually minted a fresh token. */
580
+ readonly tokenRefreshed?: boolean;
581
+ /** ISO timestamp of the fresh access-token expiry, when known. */
582
+ readonly expiresAt?: string;
583
+ /** ISO timestamp of the refresh attempt. */
584
+ readonly refreshedAt?: string;
585
+ /** Structured reason when `ok` is false (`not_oauth`, `static_token`, …). */
586
+ readonly error?: string;
587
+ /** Human detail (failure message) when available. */
588
+ readonly category?: string;
589
+ }
590
+
487
591
  export interface ProviderAccountTokenUsage {
488
592
  readonly requests: number;
489
593
  readonly errors: number;
@@ -786,6 +890,7 @@ export interface ProviderCatalogStore {
786
890
  probeAllModels(
787
891
  tenantId: string,
788
892
  providerId: string,
893
+ mode?: ProbeModelRequest["mode"],
789
894
  ): Promise<ProbeAllModelsResult>;
790
895
  probeAllAccounts(
791
896
  tenantId: string,
@@ -817,6 +922,20 @@ export interface ProviderCatalogStore {
817
922
  request: UpdateModelMetadataRequest,
818
923
  ): Promise<boolean>;
819
924
  syncModels(tenantId: string, providerId: string): Promise<{ synced: number }>;
925
+ /**
926
+ * Read-only `/models` discovery. Never persists. `supported: false` names a
927
+ * provider with no listing endpoint so the UI can fall back to manual add.
928
+ */
929
+ listDiscoveryCandidates(
930
+ tenantId: string,
931
+ providerId: string,
932
+ ): Promise<{ readonly supported: boolean; readonly candidates: readonly DiscoverModelCandidate[]; readonly accountLabel?: string }>;
933
+ /** Persists a verified subset of a fresh discovery as `source="manual"`. */
934
+ commitDiscoveredModels(
935
+ tenantId: string,
936
+ providerId: string,
937
+ modelIds: readonly string[],
938
+ ): Promise<{ readonly committed: readonly string[]; readonly skipped: readonly string[]; readonly synced: number }>;
820
939
  listAccounts(tenantId: string, providerId: string): Promise<readonly ProviderAccountResponse[]>;
821
940
  listAllAccounts(tenantId: string): Promise<readonly ProviderAccountResponse[]>;
822
941
  /**
@@ -891,6 +1010,10 @@ export interface ProviderCatalogStore {
891
1010
  dayKey: string,
892
1011
  ): Promise<readonly BuddyGrowthTaskLedgerEntry[]>;
893
1012
  recoverAccount(tenantId: string, providerId: string, accountId: string): Promise<boolean>;
1013
+ /** One UPDATE over this tenant's cooling accounts (D07/D08); evidence kept. */
1014
+ disableCooldowns(tenantId: string, providerId: string): Promise<ProviderAccountBulkActionResult>;
1015
+ /** One DELETE over this tenant's banned accounts (D12); idempotent. */
1016
+ deleteBanned(tenantId: string, providerId: string): Promise<ProviderAccountBulkActionResult>;
894
1017
  }
895
1018
 
896
1019
  /**
@@ -11,7 +11,17 @@
11
11
  import type { AccessDecision } from "../../../security/access-control";
12
12
  import { ConsoleDomainError, requireScope, requireTenantAnyScope } from "../../shared/errors";
13
13
  import { GatewayError } from "../../../protocol/gateway-error";
14
- import { MODEL_READ_SCOPES, MODEL_WRITE_SCOPES } from "./contracts";
14
+ import {
15
+ DISCOVER_MODEL_COMMIT_MAX,
16
+ MODEL_READ_SCOPES,
17
+ MODEL_WRITE_SCOPES,
18
+ } from "./contracts";
19
+ import type { DiscoverModelCandidatesResponse, DiscoverModelCommitResult } from "./contracts";
20
+ import {
21
+ MODEL_WARNING_KIND_MAX_LENGTH,
22
+ MODEL_WARNING_MESSAGE_MAX_LENGTH,
23
+ MODEL_WARNING_SEVERITIES,
24
+ } from "../../../providers/provider-model-definition";
15
25
  import type {
16
26
  ByokConnectionTestRequest,
17
27
  ByokConnectionTestResult,
@@ -47,6 +57,60 @@ function validateModelMetadata(metadata: ModelMetadataOverride | undefined): voi
47
57
  `${field} must be a positive integer, or null for unknown`,
48
58
  );
49
59
  }
60
+ // `null` clears the stored blob; an absent field leaves it alone. Only a
61
+ // non-builtin row can actually carry an edit — the store enforces that
62
+ // invariant. Bounds here stop a hostile payload from persisting a multi-MB
63
+ // display banner; severity is a closed whitelist.
64
+ if (metadata.warnings === null || metadata.warnings === undefined) return;
65
+ if (!Array.isArray(metadata.warnings)) {
66
+ throw new ConsoleDomainError("invalid_request", 400, "warnings must be an array or null");
67
+ }
68
+ for (const [index, warning] of metadata.warnings.entries()) {
69
+ if (warning === null || typeof warning !== "object" || Array.isArray(warning)) {
70
+ throw new ConsoleDomainError("invalid_request", 400, `warnings[${index}] must be an object`);
71
+ }
72
+ const kind = (warning as { kind?: unknown }).kind;
73
+ const message = (warning as { message?: unknown }).message;
74
+ const severity = (warning as { severity?: unknown }).severity;
75
+ if (typeof kind !== "string" || kind.trim().length === 0 || kind.length > MODEL_WARNING_KIND_MAX_LENGTH) {
76
+ throw new ConsoleDomainError(
77
+ "invalid_request",
78
+ 400,
79
+ `warnings[${index}].kind must be a non-empty string up to ${MODEL_WARNING_KIND_MAX_LENGTH} chars`,
80
+ );
81
+ }
82
+ if (typeof message !== "string" || message.trim().length === 0 || message.length > MODEL_WARNING_MESSAGE_MAX_LENGTH) {
83
+ throw new ConsoleDomainError(
84
+ "invalid_request",
85
+ 400,
86
+ `warnings[${index}].message must be a non-empty string up to ${MODEL_WARNING_MESSAGE_MAX_LENGTH} chars`,
87
+ );
88
+ }
89
+ if (
90
+ severity !== undefined &&
91
+ (typeof severity !== "string" ||
92
+ !(MODEL_WARNING_SEVERITIES as readonly string[]).includes(severity))
93
+ ) {
94
+ throw new ConsoleDomainError(
95
+ "invalid_request",
96
+ 400,
97
+ `warnings[${index}].severity must be one of ${MODEL_WARNING_SEVERITIES.join(", ")}`,
98
+ );
99
+ }
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Discovery lives in the provider layer and raises `GatewayError`; the console
105
+ * speaks `ConsoleDomainError`. Keeping the exact code/status/message lets the
106
+ * dashboard toast name the real upstream reason rather than a generic failure.
107
+ */
108
+ function translateDiscoveryError(error: unknown): unknown {
109
+ if (error instanceof ConsoleDomainError) return error;
110
+ if (error instanceof GatewayError) {
111
+ return new ConsoleDomainError(error.code, error.status, error.message);
112
+ }
113
+ return error;
50
114
  }
51
115
 
52
116
  export function createModelCatalogOperations(config: ProviderCatalogConfig) {
@@ -164,14 +228,7 @@ export function createModelCatalogOperations(config: ProviderCatalogConfig) {
164
228
  await config.snapshotInvalidator?.invalidate();
165
229
  return result;
166
230
  } catch (error) {
167
- // Discovery runs in the provider layer and raises `GatewayError`; the
168
- // console speaks `ConsoleDomainError`. Translating here keeps the
169
- // upstream-vs-operator distinction and the status code intact, so the
170
- // dashboard toast names the real reason instead of a generic failure.
171
- if (error instanceof GatewayError) {
172
- throw new ConsoleDomainError(error.code, error.status, error.message);
173
- }
174
- throw error;
231
+ throw translateDiscoveryError(error);
175
232
  }
176
233
  }
177
234
  /** Manually registers additional model ids on an existing provider (tenant-bound, no upstream call). */,
@@ -247,9 +304,10 @@ export function createModelCatalogOperations(config: ProviderCatalogConfig) {
247
304
  async probeAllModels(
248
305
  access: AccessDecision | undefined,
249
306
  providerId: string,
307
+ mode?: ProbeModelRequest["mode"],
250
308
  ): Promise<ProbeAllModelsResult> {
251
309
  const a = requireTenantAnyScope(access, MODEL_WRITE_SCOPES);
252
- return config.store.probeAllModels(a.tenantId, providerId);
310
+ return config.store.probeAllModels(a.tenantId, providerId, mode);
253
311
  },
254
312
  async probeAllAccounts(
255
313
  access: AccessDecision | undefined,
@@ -299,6 +357,60 @@ export function createModelCatalogOperations(config: ProviderCatalogConfig) {
299
357
  await config.snapshotInvalidator?.invalidate();
300
358
  return { success: true };
301
359
  },
360
+ /**
361
+ * `GET .../models/discover`: fetch the provider's own listing without any
362
+ * write (D30). A provider with no listing endpoint answers `supported:false`
363
+ * so the UI can fall back to manual add instead of showing an error.
364
+ */
365
+ async listDiscoveryCandidates(
366
+ access: AccessDecision | undefined,
367
+ providerId: string,
368
+ ): Promise<DiscoverModelCandidatesResponse> {
369
+ const a = requireTenantAnyScope(access, MODEL_READ_SCOPES);
370
+ try {
371
+ return await config.store.listDiscoveryCandidates(a.tenantId, providerId);
372
+ } catch (error) {
373
+ throw translateDiscoveryError(error);
374
+ }
375
+ },
376
+ /**
377
+ * `POST .../models/discover/commit`: the store re-fetches the roster and
378
+ * rejects ids outside it (D31), so no hand-forged id can be persisted.
379
+ */
380
+ async commitDiscoveredModels(
381
+ access: AccessDecision | undefined,
382
+ providerId: string,
383
+ modelIds: readonly string[],
384
+ ): Promise<DiscoverModelCommitResult> {
385
+ const a = requireTenantAnyScope(access, MODEL_WRITE_SCOPES);
386
+ if (modelIds.some((id) => typeof id !== "string" || id.trim().length === 0)) {
387
+ throw new ConsoleDomainError(
388
+ "invalid_request",
389
+ 400,
390
+ "modelIds must be non-empty model ids",
391
+ );
392
+ }
393
+ if (modelIds.length === 0 || modelIds.length > DISCOVER_MODEL_COMMIT_MAX) {
394
+ throw new ConsoleDomainError(
395
+ "invalid_request",
396
+ 400,
397
+ `modelIds must contain 1-${DISCOVER_MODEL_COMMIT_MAX} entries`,
398
+ );
399
+ }
400
+ try {
401
+ const result = await config.store.commitDiscoveredModels(a.tenantId, providerId, modelIds);
402
+ await config.auditSink?.record({
403
+ access: a,
404
+ action: "provider.models.registered",
405
+ target: providerId,
406
+ detail: { modelIds: result.committed, source: "discover-commit" },
407
+ });
408
+ await config.snapshotInvalidator?.invalidate();
409
+ return { ok: true, ...result };
410
+ } catch (error) {
411
+ throw translateDiscoveryError(error);
412
+ }
413
+ },
302
414
  async deleteModelsBulk(
303
415
  access: AccessDecision | undefined,
304
416
  providerId: string,