cartethyia 2.7.8 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (519) hide show
  1. package/.env.example +3 -3
  2. package/README.md +29 -59
  3. package/dist/dashboard/assets/BannedPage-Cc0vKpMG.js +1 -0
  4. package/dist/dashboard/assets/BannedPage-Cc0vKpMG.js.gz +0 -0
  5. package/dist/dashboard/assets/{CliToolDetailPage-BRWKPQPf.js → CliToolDetailPage-Bqj2aa1T.js} +1 -1
  6. package/dist/dashboard/assets/CliToolDetailPage-Bqj2aa1T.js.gz +0 -0
  7. package/dist/dashboard/assets/{CliToolsPage-BP-Y5iHN.js → CliToolsPage-CXsaOAO2.js} +1 -1
  8. package/dist/dashboard/assets/CliToolsPage-CXsaOAO2.js.gz +0 -0
  9. package/dist/dashboard/assets/CombosPage-BeLpJ4ma.js +1 -0
  10. package/dist/dashboard/assets/CombosPage-BeLpJ4ma.js.gz +0 -0
  11. package/dist/dashboard/assets/ConsoleLogPage-DnV0uZ3m.js +1 -0
  12. package/dist/dashboard/assets/ConsoleLogPage-DnV0uZ3m.js.gz +0 -0
  13. package/dist/dashboard/assets/CustomizationPage-CBxAjo63.js +1 -0
  14. package/dist/dashboard/assets/CustomizationPage-CBxAjo63.js.gz +0 -0
  15. package/dist/dashboard/assets/HealthEventsModal-B5yEE0k6.js +1 -0
  16. package/dist/dashboard/assets/HealthEventsModal-B5yEE0k6.js.gz +0 -0
  17. package/dist/dashboard/assets/{InjectSystemPromptPanel-Cv-2Ol_K.js → InjectSystemPromptPanel-BWFLECmz.js} +2 -2
  18. package/dist/dashboard/assets/InjectSystemPromptPanel-BWFLECmz.js.gz +0 -0
  19. package/dist/dashboard/assets/{LoginPage-bKfz_b1K.js → LoginPage-CgoPc5B9.js} +1 -1
  20. package/dist/dashboard/assets/LoginPage-CgoPc5B9.js.gz +0 -0
  21. package/dist/dashboard/assets/{ModelPicker-B-WQxx_H.js → ModelPicker-D9bFQern.js} +1 -1
  22. package/dist/dashboard/assets/ModelPicker-D9bFQern.js.gz +0 -0
  23. package/dist/dashboard/assets/OverviewPage-B_p4tL8k.js +1 -0
  24. package/dist/dashboard/assets/OverviewPage-B_p4tL8k.js.gz +0 -0
  25. package/dist/dashboard/assets/ProviderDetailPage-rs5E2rxv.js +8 -0
  26. package/dist/dashboard/assets/ProviderDetailPage-rs5E2rxv.js.gz +0 -0
  27. package/dist/dashboard/assets/ProviderIcon-D_0PiZtg.js +1 -0
  28. package/dist/dashboard/assets/ProviderIcon-D_0PiZtg.js.gz +0 -0
  29. package/dist/dashboard/assets/ProvidersPage-CLI2bJTK.js +1 -0
  30. package/dist/dashboard/assets/ProvidersPage-CLI2bJTK.js.gz +0 -0
  31. package/dist/dashboard/assets/{ProxyPage-emlTuxz6.js → ProxyPage-BwNC_-Cx.js} +1 -1
  32. package/dist/dashboard/assets/ProxyPage-BwNC_-Cx.js.gz +0 -0
  33. package/dist/dashboard/assets/QuotaPage-DA-a1iSN.js +1 -0
  34. package/dist/dashboard/assets/QuotaPage-DA-a1iSN.js.gz +0 -0
  35. package/dist/dashboard/assets/SettingsPage-CctTga2C.js +1 -0
  36. package/dist/dashboard/assets/SettingsPage-CctTga2C.js.gz +0 -0
  37. package/dist/dashboard/assets/{SetupPage-CUZrKs9r.js → SetupPage-febLU4r1.js} +1 -1
  38. package/dist/dashboard/assets/SetupPage-febLU4r1.js.gz +0 -0
  39. package/dist/dashboard/assets/{StudioPage-jWmUdDpy.js → StudioPage-M6fvR5Wz.js} +1 -1
  40. package/dist/dashboard/assets/StudioPage-M6fvR5Wz.js.gz +0 -0
  41. package/dist/dashboard/assets/UsagePage-nyeuRqz9.js +56 -0
  42. package/dist/dashboard/assets/UsagePage-nyeuRqz9.js.gz +0 -0
  43. package/dist/dashboard/assets/api-keys-BFOlvrJC.js +1 -0
  44. package/dist/dashboard/assets/api-keys-BFOlvrJC.js.gz +0 -0
  45. package/dist/dashboard/assets/app-Dx2QgsNj.css +2 -0
  46. package/dist/dashboard/assets/app-Dx2QgsNj.css.gz +0 -0
  47. package/dist/dashboard/assets/{assert-CidQVoNo.js → assert-CqTLw5e7.js} +1 -1
  48. package/dist/dashboard/assets/assert-CqTLw5e7.js.gz +0 -0
  49. package/dist/dashboard/assets/{badge-DyI_mJfi.js → badge-C83EqLB0.js} +1 -1
  50. package/dist/dashboard/assets/badge-C83EqLB0.js.gz +0 -0
  51. package/dist/dashboard/assets/ban-durations-B58FyRxC.js +1 -0
  52. package/dist/dashboard/assets/ban-durations-B58FyRxC.js.gz +0 -0
  53. package/dist/dashboard/assets/{banner-B-yXtCqb.js → banner-D1xn7pIe.js} +1 -1
  54. package/dist/dashboard/assets/banner-D1xn7pIe.js.gz +0 -0
  55. package/dist/dashboard/assets/brand/bansos_page.webp +0 -0
  56. package/dist/dashboard/assets/brand/denia_bansos.webp +0 -0
  57. package/dist/dashboard/assets/brand/favicon.webp +0 -0
  58. package/dist/dashboard/assets/brand/login_register.webp +0 -0
  59. package/dist/dashboard/assets/brand/login_register_initial.webp +0 -0
  60. package/dist/dashboard/assets/brand/og_bansos.webp +0 -0
  61. package/dist/dashboard/assets/brand/og_image.webp +0 -0
  62. package/dist/dashboard/assets/brand/sidebar_ico.webp +0 -0
  63. package/dist/dashboard/assets/{card-_HwAEqFF.js → card-DU3Lq5Lm.js} +1 -1
  64. package/dist/dashboard/assets/card-DU3Lq5Lm.js.gz +0 -0
  65. package/dist/dashboard/assets/cli-tools-Dw1U_drK.js +1 -0
  66. package/dist/dashboard/assets/cli-tools-Dw1U_drK.js.gz +0 -0
  67. package/dist/dashboard/assets/{clipboard-button-ByjCk0o9.js → clipboard-button-CEL5Sg_J.js} +1 -1
  68. package/dist/dashboard/assets/clipboard-button-CEL5Sg_J.js.gz +0 -0
  69. package/dist/dashboard/assets/{dialog-BZhOlfz-.js → dialog-BjFR5UHy.js} +1 -1
  70. package/dist/dashboard/assets/dialog-BjFR5UHy.js.gz +0 -0
  71. package/dist/dashboard/assets/{drawer-BADqIRYP.js → drawer-HvBW0AjI.js} +1 -1
  72. package/dist/dashboard/assets/drawer-HvBW0AjI.js.gz +0 -0
  73. package/dist/dashboard/assets/entry-BSu3gFlt.js +4 -0
  74. package/dist/dashboard/assets/entry-BSu3gFlt.js.gz +0 -0
  75. package/dist/dashboard/assets/{entry-COaC-irN.js → entry-CECVELyy.js} +1 -1
  76. package/dist/dashboard/assets/entry-CECVELyy.js.gz +0 -0
  77. package/dist/dashboard/assets/entry-DYYofQ7W.js +1 -0
  78. package/dist/dashboard/assets/entry-DYYofQ7W.js.gz +0 -0
  79. package/dist/dashboard/assets/{formatters-DcHJyRiu.js → formatters-DLW4i8zK.js} +1 -1
  80. package/dist/dashboard/assets/formatters-DLW4i8zK.js.gz +0 -0
  81. package/dist/dashboard/assets/{index-CgKmFngv.js → index-1-fCqe64.js} +2 -2
  82. package/dist/dashboard/assets/index-1-fCqe64.js.gz +0 -0
  83. package/dist/dashboard/assets/{inline-B2BirKry.js → inline-DGvU2oLK.js} +1 -1
  84. package/dist/dashboard/assets/inline-DGvU2oLK.js.gz +0 -0
  85. package/dist/dashboard/assets/{input-D-qV9dKY.js → input-iNnO1X2K.js} +1 -1
  86. package/dist/dashboard/assets/input-iNnO1X2K.js.gz +0 -0
  87. package/dist/dashboard/assets/{layout-B7tVWhtV.js → layout-0q8UGypb.js} +1 -1
  88. package/dist/dashboard/assets/layout-0q8UGypb.js.gz +0 -0
  89. package/dist/dashboard/assets/{list-row-CtkGE3bF.js → list-row-C0w2bQjJ.js} +1 -1
  90. package/dist/dashboard/assets/list-row-C0w2bQjJ.js.gz +0 -0
  91. package/dist/dashboard/assets/{live-DGizF06-.js → live-M5aJ_xCa.js} +1 -1
  92. package/dist/dashboard/assets/live-M5aJ_xCa.js.gz +0 -0
  93. package/dist/dashboard/assets/{mutation-GU7XdEjz.js → mutation-BQ0ccH_A.js} +1 -1
  94. package/dist/dashboard/assets/mutation-BQ0ccH_A.js.gz +0 -0
  95. package/dist/dashboard/assets/{network-C9UE-bOr.js → network-BKeQGT3K.js} +1 -1
  96. package/dist/dashboard/assets/network-BKeQGT3K.js.gz +0 -0
  97. package/dist/dashboard/assets/{numbers-BnAd22a_.js → numbers-CivAk3da.js} +1 -1
  98. package/dist/dashboard/assets/numbers-CivAk3da.js.gz +0 -0
  99. package/dist/dashboard/assets/{pagination-BAywyMzS.js → pagination-CpzvI5ep.js} +1 -1
  100. package/dist/dashboard/assets/pagination-CpzvI5ep.js.gz +0 -0
  101. package/dist/dashboard/assets/providers/devin.svg +9 -0
  102. package/dist/dashboard/assets/providers/devin.svg.gz +0 -0
  103. package/dist/dashboard/assets/providers/windsurf.svg +10 -0
  104. package/dist/dashboard/assets/providers/windsurf.svg.gz +0 -0
  105. package/dist/dashboard/assets/providers/zro.svg +3 -0
  106. package/dist/dashboard/assets/providers/zro.svg.gz +0 -0
  107. package/dist/dashboard/assets/{query-keys-YSoQOUh6.js → query-keys-IKLh0YFd.js} +1 -1
  108. package/dist/dashboard/assets/query-keys-IKLh0YFd.js.gz +0 -0
  109. package/dist/dashboard/assets/quota-DiPD6UKH.js +1 -0
  110. package/dist/dashboard/assets/quota-DiPD6UKH.js.gz +0 -0
  111. package/dist/dashboard/assets/routing-BFQX2VBc.js +1 -0
  112. package/dist/dashboard/assets/routing-BFQX2VBc.js.gz +0 -0
  113. package/dist/dashboard/assets/{select-BCOcv8WK.js → select-CXbck0jG.js} +2 -2
  114. package/dist/dashboard/assets/select-CXbck0jG.js.gz +0 -0
  115. package/dist/dashboard/assets/settings-C_XvMHh9.js +1 -0
  116. package/dist/dashboard/assets/settings-C_XvMHh9.js.gz +0 -0
  117. package/dist/dashboard/assets/{stack-ByKt3Q1g.js → stack-DggRiCxE.js} +1 -1
  118. package/dist/dashboard/assets/stack-DggRiCxE.js.gz +0 -0
  119. package/dist/dashboard/assets/{state-DXbGTx7N.js → state-ClFzfbYp.js} +1 -1
  120. package/dist/dashboard/assets/state-ClFzfbYp.js.gz +0 -0
  121. package/dist/dashboard/assets/story/25817331.webp +0 -0
  122. package/dist/dashboard/assets/story/Shorekeeper.webp +0 -0
  123. package/dist/dashboard/assets/story/cartethyia-god.webp +0 -0
  124. package/dist/dashboard/assets/story/fleurdelys_plus.webp +0 -0
  125. package/dist/dashboard/assets/story/jinhsi-blossom.webp +0 -0
  126. package/dist/dashboard/assets/story/phrolova-a.webp +0 -0
  127. package/dist/dashboard/assets/story/requestdeniawokkjpg.webp +0 -0
  128. package/dist/dashboard/assets/story/wuthering-waves-hiyuki-aemeath-hiyuki-rover.webp +0 -0
  129. package/dist/dashboard/assets/{switch-ZeTsW623.js → switch-CTIFxKqS.js} +1 -1
  130. package/dist/dashboard/assets/switch-CTIFxKqS.js.gz +0 -0
  131. package/dist/dashboard/assets/{useMutation-CbCP4w1o.js → useMutation-BClG3yWj.js} +1 -1
  132. package/dist/dashboard/assets/useMutation-BClG3yWj.js.gz +0 -0
  133. package/dist/dashboard/console-backgrounds.webp +0 -0
  134. package/dist/dashboard/console-backgrounds_initial.webp +0 -0
  135. package/dist/dashboard/donation/qris-risun-marke.webp +0 -0
  136. package/dist/dashboard/index.html +3 -3
  137. package/dist/dashboard/index.html.gz +0 -0
  138. package/dist/dashboard/precompressed-assets.json +59 -56
  139. package/migrations/full-postgres/0044_add_models_warning_metadata.sql +10 -0
  140. package/migrations/full-postgres/0045_migrate_workbuddy_to_codebuddy.sql +156 -0
  141. package/migrations/full-postgres/0046_add_accounts_quota_plan_fetched_at.sql +7 -0
  142. package/migrations/lite-sqlite3/0044_add_models_warning_metadata.sql +11 -0
  143. package/migrations/lite-sqlite3/0045_migrate_workbuddy_to_codebuddy.sql +156 -0
  144. package/migrations/lite-sqlite3/0046_add_accounts_quota_plan_fetched_at.sql +4 -0
  145. package/package.json +1 -1
  146. package/src/boot/app.ts +10 -13
  147. package/src/boot/dependencies.ts +12 -4
  148. package/src/boot/flush.ts +20 -0
  149. package/src/boot/lifecycle.ts +7 -5
  150. package/src/config/cooldowns.ts +1 -1
  151. package/src/config/routing.ts +26 -0
  152. package/src/config/spec-metadata.ts +72 -0
  153. package/src/config/spec.ts +20 -6
  154. package/src/console/api-keys/contracts.ts +12 -1
  155. package/src/console/api-keys/routes.ts +50 -31
  156. package/src/console/auth/account-reset.ts +7 -9
  157. package/src/console/backup/contracts.ts +4 -0
  158. package/src/console/backup/export-endpoint.ts +7 -5
  159. package/src/console/cli-tools/service.ts +6 -12
  160. package/src/console/console-router-guards.ts +23 -16
  161. package/src/console/console-router.ts +7 -0
  162. package/src/console/dashboard-assets.ts +5 -5
  163. package/src/console/domain-registration.ts +27 -2
  164. package/src/console/inferhub/normalize.ts +24 -28
  165. package/src/console/inferhub/types.ts +3 -0
  166. package/src/console/ip-bans/routes.ts +93 -0
  167. package/src/console/observability/store.ts +0 -53
  168. package/src/console/providers/catalog/catalog-projections.ts +5 -2
  169. package/src/console/providers/catalog/contracts.ts +123 -0
  170. package/src/console/providers/catalog/discover-constants.ts +6 -0
  171. package/src/console/providers/catalog/model-operations.ts +122 -10
  172. package/src/console/providers/catalog/provider-operations.ts +116 -8
  173. package/src/console/providers/catalog/public-model-store.ts +0 -0
  174. package/src/console/providers/catalog/routes.ts +65 -4
  175. package/src/console/providers/catalog/store-helpers.ts +30 -1
  176. package/src/console/providers/catalog/store.ts +249 -4
  177. package/src/console/quota/account-quota-global.ts +97 -19
  178. package/src/console/quota/account-quota-shared.ts +49 -0
  179. package/src/console/quota/account-quota-tenant.ts +6 -18
  180. package/src/console/quota/native-identity.ts +2 -0
  181. package/src/console/quota/quota-view.ts +19 -3
  182. package/src/console/routing/pools/operations.ts +31 -46
  183. package/src/console/settings/contracts.ts +18 -0
  184. package/src/console/settings/store.ts +32 -0
  185. package/src/console/shared/errors.ts +33 -6
  186. package/src/console/system-prompt/contracts.ts +0 -1
  187. package/src/main.ts +3 -3
  188. package/src/network/pool/byte-accounting.ts +3 -2
  189. package/src/network/pool/selector.ts +182 -39
  190. package/src/network/pool-cooldown-store.ts +89 -14
  191. package/src/network/pool-health.ts +49 -61
  192. package/src/network/rate-limit-verdict.ts +1 -1
  193. package/src/network/ssrf.ts +6 -6
  194. package/src/observability/event-log.ts +7 -361
  195. package/src/observability/event-query.ts +18 -4
  196. package/src/observability/performance-metrics.ts +9 -0
  197. package/src/observability/runtime-metrics.ts +14 -9
  198. package/src/observability/telemetry-buffer.ts +15 -25
  199. package/src/observability/telemetry-reconciliation.ts +9 -37
  200. package/src/observability/telemetry-row.ts +125 -0
  201. package/src/persistence/db-mode.ts +4 -1
  202. package/src/persistence/dialect.ts +30 -0
  203. package/src/persistence/migrate.ts +6 -3
  204. package/src/persistence/schema.ts +28 -2
  205. package/src/persistence/ttl-cache.ts +20 -2
  206. package/src/protocol/codecs.ts +1 -1
  207. package/src/protocol/dsml.ts +35 -87
  208. package/src/protocol/gateway-error.ts +1 -0
  209. package/src/protocol/markup-stream-parser.ts +195 -0
  210. package/src/protocol/reasoning-text.ts +19 -0
  211. package/src/protocol/reasoning.ts +1 -1
  212. package/src/protocol/request/chat.ts +14 -14
  213. package/src/protocol/request/gemini.ts +8 -2
  214. package/src/protocol/request/messages.ts +4 -5
  215. package/src/protocol/response/chat.ts +12 -20
  216. package/src/protocol/response/gemini.ts +6 -1
  217. package/src/protocol/response/omp-tool-call-markup.ts +43 -86
  218. package/src/protocol/response/responses.ts +18 -2
  219. package/src/protocol/sse-decoder.ts +25 -8
  220. package/src/protocol/stop-reason.ts +23 -1
  221. package/src/protocol/stream-error-frames.ts +13 -3
  222. package/src/protocol/think-split.ts +112 -0
  223. package/src/protocol/translation/dialect/qwen.ts +26 -58
  224. package/src/protocol/translation/quirks.ts +1 -1
  225. package/src/protocol/translation/tool-repair.ts +2 -2
  226. package/src/protocol/upstream/send-claude-messages.ts +15 -4
  227. package/src/protocol/upstream-parsing.ts +1 -1
  228. package/src/providers/adapters/compatible-adapter.ts +78 -17
  229. package/src/providers/authentication/oauth-client.ts +1 -1
  230. package/src/providers/authentication/oauth-device-flow.ts +98 -8
  231. package/src/providers/authentication/oauth-refresh-service.ts +153 -59
  232. package/src/providers/configured-provider.ts +148 -0
  233. package/src/providers/default-registry.ts +117 -27
  234. package/src/providers/discovery/discovery-types.ts +12 -0
  235. package/src/providers/discovery/probe-phases.ts +74 -33
  236. package/src/providers/discovery/probing-service.ts +293 -165
  237. package/src/providers/integrations/alibabacp/alibabacp-models.ts +1 -2
  238. package/src/providers/integrations/{dashscope-models.ts → alibabacp/dashscope-models.ts} +2 -2
  239. package/src/providers/integrations/antigravity/antigravity-protocol.ts +121 -2
  240. package/src/providers/integrations/antigravity/antigravity.ts +13 -2
  241. package/src/providers/integrations/buddy/buddy-catalog-shared.ts +72 -34
  242. package/src/providers/integrations/buddy/buddy-chat-shared.ts +191 -35
  243. package/src/providers/integrations/buddy/buddy-checkin.ts +17 -39
  244. package/src/providers/integrations/buddy/buddy-conversation-headers.ts +2 -2
  245. package/src/providers/integrations/buddy/buddy-discovery-shared.ts +5 -5
  246. package/src/providers/integrations/buddy/buddy-identity.ts +89 -6
  247. package/src/providers/integrations/buddy/buddy-media.ts +4 -5
  248. package/src/providers/integrations/buddy/buddy-oauth-shared.ts +2 -2
  249. package/src/providers/integrations/buddy/buddy-quota-shared.ts +1 -1
  250. package/src/providers/integrations/buddy/buddy-usage-shared.ts +1 -3
  251. package/src/providers/integrations/buddy/capabilities.ts +6 -43
  252. package/src/providers/integrations/buddy/codebuddy/cn-catalog.ts +2 -1
  253. package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +23 -1
  254. package/src/providers/integrations/buddy/codebuddy/provider.ts +6 -2
  255. package/src/providers/integrations/buddy/codebuddy/shared.ts +38 -27
  256. package/src/providers/integrations/claude/claude-quota.ts +14 -17
  257. package/src/providers/integrations/cline/cline.ts +2 -2
  258. package/src/providers/integrations/commandcode/quota.ts +22 -28
  259. package/src/providers/integrations/cursor/cursor-auth.ts +0 -8
  260. package/src/providers/integrations/cursor/cursor-catalog.ts +2 -0
  261. package/src/providers/integrations/cursor/cursor-exec.ts +745 -0
  262. package/src/providers/integrations/cursor/cursor-http2.ts +2 -0
  263. package/src/providers/integrations/cursor/cursor-request.ts +35 -20
  264. package/src/providers/integrations/cursor/cursor-shared.ts +26 -5
  265. package/src/providers/integrations/cursor/cursor.ts +139 -70
  266. package/src/providers/integrations/cursor/protocol/messages.ts +22 -29
  267. package/src/providers/integrations/cursor/protocol/protobuf-schema.ts +37 -0
  268. package/src/providers/integrations/cursor/protocol/tool-canonical.ts +26 -100
  269. package/src/providers/integrations/devin/devin-auth.ts +353 -0
  270. package/src/providers/integrations/devin/devin-catalog.ts +207 -0
  271. package/src/providers/integrations/devin/devin-http.ts +91 -0
  272. package/src/providers/integrations/devin/devin-quota.ts +214 -0
  273. package/src/providers/integrations/devin/devin-shared.ts +111 -0
  274. package/src/providers/integrations/devin/devin.ts +539 -0
  275. package/src/providers/integrations/devin/protocol/devin-proto.ts +558 -0
  276. package/src/providers/integrations/devin/protocol/framing.ts +124 -0
  277. package/src/providers/integrations/factory/factory-oauth.ts +17 -44
  278. package/src/providers/integrations/factory/factory.ts +2 -2
  279. package/src/providers/integrations/freebuff/assets/official-system-manager.txt +88 -0
  280. package/src/providers/integrations/freebuff/assets/official-system-worker.txt +57 -0
  281. package/src/providers/integrations/freebuff/assets/official-tools.json +1070 -0
  282. package/src/providers/integrations/freebuff/freebuff-catalog.ts +120 -0
  283. package/src/providers/integrations/freebuff/freebuff-constants.ts +1233 -0
  284. package/src/providers/integrations/freebuff/freebuff-discovery.ts +108 -0
  285. package/src/providers/integrations/freebuff/freebuff-oauth.ts +240 -0
  286. package/src/providers/integrations/freebuff/freebuff-quota.ts +260 -0
  287. package/src/providers/integrations/freebuff/freebuff-virtual.ts +89 -0
  288. package/src/providers/integrations/freebuff/freebuff.ts +682 -0
  289. package/src/providers/integrations/freepi/freepi-catalog.ts +60 -0
  290. package/src/providers/integrations/freepi/freepi-discovery.ts +82 -0
  291. package/src/providers/integrations/freepi/freepi-headers.ts +21 -0
  292. package/src/providers/integrations/freepi/freepi-oauth.ts +142 -0
  293. package/src/providers/integrations/freepi/freepi-protocol.ts +242 -0
  294. package/src/providers/integrations/freepi/freepi-quota.ts +126 -0
  295. package/src/providers/integrations/freepi/freepi-realm.ts +35 -0
  296. package/src/providers/integrations/freepi/freepi.ts +292 -0
  297. package/src/providers/integrations/gemini.ts +2 -2
  298. package/src/providers/integrations/grok/grok-oauth.ts +48 -45
  299. package/src/providers/integrations/grok/grok-ratelimit-store.ts +10 -17
  300. package/src/providers/integrations/grok/grok-turn-index.ts +11 -16
  301. package/src/providers/integrations/hermes/hermes-oauth.ts +94 -0
  302. package/src/providers/integrations/{hermes.ts → hermes/hermes.ts} +13 -21
  303. package/src/providers/integrations/hermes/index.ts +2 -0
  304. package/src/providers/integrations/kiro/kiro-catalog.ts +17 -1
  305. package/src/providers/integrations/kiro/kiro-discovery.ts +10 -12
  306. package/src/providers/integrations/kiro/kiro-quota.ts +8 -4
  307. package/src/providers/integrations/kiro/kiro.ts +15 -19
  308. package/src/providers/integrations/ollamacloud/index.ts +2 -0
  309. package/src/providers/integrations/{ollamacloud-models.ts → ollamacloud/models.ts} +2 -2
  310. package/src/providers/integrations/{ollamacloud-quota.ts → ollamacloud/quota.ts} +3 -3
  311. package/src/providers/integrations/opencode/fingerprint.ts +1 -1
  312. package/src/providers/integrations/opencode/opencode-go-quota.ts +2 -8
  313. package/src/providers/integrations/perch/perch-catalog.ts +105 -0
  314. package/src/providers/integrations/perch/perch-credential.ts +25 -0
  315. package/src/providers/integrations/perch/perch-oauth.ts +170 -0
  316. package/src/providers/integrations/perch/perch-protocol.ts +208 -0
  317. package/src/providers/integrations/perch/perch-quota.ts +123 -0
  318. package/src/providers/integrations/perch/perch-realm.ts +82 -0
  319. package/src/providers/integrations/perch/perch-system-prompt.ts +13 -0
  320. package/src/providers/integrations/perch/perch.ts +305 -0
  321. package/src/providers/integrations/perch/saffron-system-prompt.txt +8 -0
  322. package/src/providers/integrations/qoder/chat-proto.ts +328 -328
  323. package/src/providers/integrations/qoder/claim-qoder-credits.ts +19 -60
  324. package/src/providers/integrations/qoder/qoder-adapter.ts +90 -40
  325. package/src/providers/integrations/qoder/qoder-auth.ts +101 -29
  326. package/src/providers/integrations/qoder/qoder-cache.ts +32 -0
  327. package/src/providers/integrations/qoder/qoder-catalog.ts +26 -6
  328. package/src/providers/integrations/qoder/qoder-chat.ts +7 -21
  329. package/src/providers/integrations/qoder/qoder-checkin.ts +6 -6
  330. package/src/providers/integrations/qoder/qoder-grpc.ts +210 -7
  331. package/src/providers/integrations/qoder/qoder-ide.ts +5 -108
  332. package/src/providers/integrations/qoder/qoder-identity-cache.ts +184 -0
  333. package/src/providers/integrations/qoder/qoder-native-identity-store.ts +83 -18
  334. package/src/providers/integrations/qoder/qoder-realm.ts +9 -6
  335. package/src/providers/integrations/qoder/qoder-umid.ts +109 -342
  336. package/src/providers/integrations/qoder/qoder-virtual.ts +33 -1
  337. package/src/providers/integrations/qoder/qoder.ts +3 -7
  338. package/src/providers/integrations/trae/trae-catalog.ts +9 -14
  339. package/src/providers/integrations/trae/trae-headers.ts +36 -11
  340. package/src/providers/integrations/trae/trae-oauth.ts +14 -17
  341. package/src/providers/integrations/trae/trae-protocol.ts +12 -11
  342. package/src/providers/integrations/trae/trae-quota.ts +96 -36
  343. package/src/providers/integrations/trae/trae-realm.ts +74 -15
  344. package/src/providers/integrations/trae/trae.ts +99 -26
  345. package/src/providers/integrations/zcode-start-plan/provider.ts +226 -23
  346. package/src/providers/integrations/zcode-start-plan/quota.ts +19 -2
  347. package/src/providers/integrations/zro/zro.ts +204 -0
  348. package/src/providers/operations/account-error-classification.ts +235 -278
  349. package/src/providers/operations/account-health-service.ts +273 -26
  350. package/src/providers/operations/account-refresh-status.ts +269 -0
  351. package/src/providers/operations/account-tier.ts +66 -0
  352. package/src/providers/operations/client-version-resolver.ts +1 -1
  353. package/src/providers/operations/client-versions.ts +42 -150
  354. package/src/providers/operations/cooldown-rules.ts +56 -0
  355. package/src/providers/operations/{model-catalog-cache.ts → provider-caches.ts} +68 -16
  356. package/src/providers/operations/provider-catalog-seeder.ts +4 -0
  357. package/src/providers/operations/provider-catalog-service.ts +1 -1
  358. package/src/providers/operations/provider-credential-service.ts +102 -32
  359. package/src/providers/operations/provider-rules/github.ts +48 -0
  360. package/src/providers/operations/provider-rules/grok.ts +33 -0
  361. package/src/providers/operations/provider-rules/inception.ts +49 -0
  362. package/src/providers/operations/provider-rules/index.ts +32 -0
  363. package/src/providers/operations/provider-rules/perch.ts +37 -0
  364. package/src/providers/operations/provider-rules/qoder.ts +70 -0
  365. package/src/providers/operations/provider-rules/trae.ts +81 -0
  366. package/src/providers/provider-contracts.ts +33 -2
  367. package/src/providers/{provider-credential-resolver.ts → provider-credentials.ts} +40 -3
  368. package/src/providers/provider-metadata.ts +99 -14
  369. package/src/providers/provider-model-definition.ts +36 -0
  370. package/src/providers/provider-probe.ts +15 -1
  371. package/src/providers/provider-registry.ts +42 -33
  372. package/src/providers/quota/quota-limit-windows.ts +16 -5
  373. package/src/providers/quota/quota-support.ts +1 -1
  374. package/src/providers/quota/quota-window-parser.ts +90 -18
  375. package/src/providers/quota/refresh.ts +96 -18
  376. package/src/security/abuse.ts +71 -0
  377. package/src/security/api-key-auth-cache.ts +1 -1
  378. package/src/security/api-key-auth.ts +6 -2
  379. package/src/transport/dispatch/attempt-finalize.ts +14 -13
  380. package/src/transport/dispatch/attempt-loop.ts +71 -17
  381. package/src/transport/dispatch/generation.ts +2 -8
  382. package/src/transport/dispatch/native-shared.ts +25 -0
  383. package/src/transport/dispatch/proxy-request.ts +9 -46
  384. package/src/transport/dispatch/responses-compact.ts +2 -8
  385. package/src/transport/dispatch/streaming/finalize.ts +9 -6
  386. package/src/transport/dispatch/streaming/stream.ts +9 -36
  387. package/src/transport/dispatch/systemone.ts +2 -8
  388. package/src/transport/dispatch/token-count.ts +2 -6
  389. package/src/transport/dispatch/upstream.ts +67 -1
  390. package/src/transport/drain-endpoint.ts +31 -4
  391. package/src/transport/failure-policy.ts +129 -37
  392. package/src/transport/guards/body-policy.ts +7 -1
  393. package/src/transport/guards/error-lifecycle.ts +13 -1
  394. package/src/transport/guards/request-context.ts +22 -14
  395. package/src/transport/request/preparer.ts +81 -71
  396. package/src/transport/routing/bounded-lru.ts +14 -0
  397. package/src/transport/routing/drain-reserve.ts +129 -0
  398. package/src/transport/routing/plan-gate.ts +7 -1
  399. package/src/transport/routing/round-robin.ts +60 -0
  400. package/src/transport/routing/route-catalog.ts +123 -111
  401. package/src/transport/routing/route-model.ts +51 -9
  402. package/src/transport/routing/router.ts +418 -33
  403. package/src/transport/surface/adapters.ts +11 -2
  404. package/src/transport/surface/chat/adapter.ts +2 -2
  405. package/src/transport/surface/chat/encode.ts +47 -4
  406. package/src/transport/surface/chat/parse.ts +2 -12
  407. package/src/transport/surface/completion.ts +44 -33
  408. package/src/transport/surface/content-parts.ts +21 -0
  409. package/src/transport/surface/messages/adapter.ts +9 -2
  410. package/src/transport/surface/messages/encode.ts +11 -3
  411. package/src/transport/surface/messages/parse.ts +2 -1
  412. package/src/transport/surface/messages/stream.ts +33 -17
  413. package/src/transport/surface/responses/encode.ts +34 -6
  414. package/src/transport/surface/responses/parse.ts +3 -11
  415. package/src/workers/daily-checkin.ts +1 -1
  416. package/src/workers/quota-refresh-sweep.ts +1 -1
  417. package/src/workers/scheduler.ts +22 -4
  418. package/dist/dashboard/assets/BannedPage-DeIbIiBU.js +0 -1
  419. package/dist/dashboard/assets/BannedPage-DeIbIiBU.js.gz +0 -0
  420. package/dist/dashboard/assets/CliToolDetailPage-BRWKPQPf.js.gz +0 -0
  421. package/dist/dashboard/assets/CliToolsPage-BP-Y5iHN.js.gz +0 -0
  422. package/dist/dashboard/assets/CombosPage-B7JHozVa.js +0 -1
  423. package/dist/dashboard/assets/CombosPage-B7JHozVa.js.gz +0 -0
  424. package/dist/dashboard/assets/ConsoleLogPage-GX7iGGcu.js +0 -1
  425. package/dist/dashboard/assets/ConsoleLogPage-GX7iGGcu.js.gz +0 -0
  426. package/dist/dashboard/assets/CustomizationPage-HVonbGEa.js +0 -1
  427. package/dist/dashboard/assets/CustomizationPage-HVonbGEa.js.gz +0 -0
  428. package/dist/dashboard/assets/HealthEventsModal-xaut4OJ9.js +0 -1
  429. package/dist/dashboard/assets/HealthEventsModal-xaut4OJ9.js.gz +0 -0
  430. package/dist/dashboard/assets/InjectSystemPromptPanel-Cv-2Ol_K.js.gz +0 -0
  431. package/dist/dashboard/assets/LoginPage-bKfz_b1K.js.gz +0 -0
  432. package/dist/dashboard/assets/ModelPicker-B-WQxx_H.js.gz +0 -0
  433. package/dist/dashboard/assets/OverviewPage-Z9pJZFo8.js +0 -1
  434. package/dist/dashboard/assets/OverviewPage-Z9pJZFo8.js.gz +0 -0
  435. package/dist/dashboard/assets/ProviderDetailPage-CMeDBxR1.js +0 -7
  436. package/dist/dashboard/assets/ProviderDetailPage-CMeDBxR1.js.gz +0 -0
  437. package/dist/dashboard/assets/ProviderIcon-jjqXJDA3.js +0 -1
  438. package/dist/dashboard/assets/ProviderIcon-jjqXJDA3.js.gz +0 -0
  439. package/dist/dashboard/assets/ProvidersPage-BXPdOpG4.js +0 -1
  440. package/dist/dashboard/assets/ProvidersPage-BXPdOpG4.js.gz +0 -0
  441. package/dist/dashboard/assets/ProxyPage-emlTuxz6.js.gz +0 -0
  442. package/dist/dashboard/assets/QuotaPage-CMJKNYWb.js +0 -1
  443. package/dist/dashboard/assets/QuotaPage-CMJKNYWb.js.gz +0 -0
  444. package/dist/dashboard/assets/SettingsPage-BSy7EwGo.js +0 -1
  445. package/dist/dashboard/assets/SettingsPage-BSy7EwGo.js.gz +0 -0
  446. package/dist/dashboard/assets/SetupPage-CUZrKs9r.js.gz +0 -0
  447. package/dist/dashboard/assets/StudioPage-jWmUdDpy.js.gz +0 -0
  448. package/dist/dashboard/assets/UsagePage-5F9tGVGG.js +0 -56
  449. package/dist/dashboard/assets/UsagePage-5F9tGVGG.js.gz +0 -0
  450. package/dist/dashboard/assets/api-keys-KvX83mRe.js +0 -1
  451. package/dist/dashboard/assets/api-keys-KvX83mRe.js.gz +0 -0
  452. package/dist/dashboard/assets/app-BEsU8hKs.css +0 -2
  453. package/dist/dashboard/assets/app-BEsU8hKs.css.gz +0 -0
  454. package/dist/dashboard/assets/assert-CidQVoNo.js.gz +0 -0
  455. package/dist/dashboard/assets/badge-DyI_mJfi.js.gz +0 -0
  456. package/dist/dashboard/assets/banner-B-yXtCqb.js.gz +0 -0
  457. package/dist/dashboard/assets/brand/bansos_page.png +0 -0
  458. package/dist/dashboard/assets/brand/denia_bansos.jpg +0 -0
  459. package/dist/dashboard/assets/brand/favicon.jpg +0 -0
  460. package/dist/dashboard/assets/brand/login_register.jpg +0 -0
  461. package/dist/dashboard/assets/brand/login_register_initial.jpg +0 -0
  462. package/dist/dashboard/assets/brand/og_bansos.jpg +0 -0
  463. package/dist/dashboard/assets/brand/og_image.jpg +0 -0
  464. package/dist/dashboard/assets/brand/sidebar_ico.jpg +0 -0
  465. package/dist/dashboard/assets/card-_HwAEqFF.js.gz +0 -0
  466. package/dist/dashboard/assets/cli-tools-Bwv5-zGZ.js +0 -1
  467. package/dist/dashboard/assets/cli-tools-Bwv5-zGZ.js.gz +0 -0
  468. package/dist/dashboard/assets/clipboard-button-ByjCk0o9.js.gz +0 -0
  469. package/dist/dashboard/assets/dialog-BZhOlfz-.js.gz +0 -0
  470. package/dist/dashboard/assets/drawer-BADqIRYP.js.gz +0 -0
  471. package/dist/dashboard/assets/entry-COaC-irN.js.gz +0 -0
  472. package/dist/dashboard/assets/entry-Ca7LSCet.js +0 -4
  473. package/dist/dashboard/assets/entry-Ca7LSCet.js.gz +0 -0
  474. package/dist/dashboard/assets/entry-PDd27uCd.js +0 -1
  475. package/dist/dashboard/assets/entry-PDd27uCd.js.gz +0 -0
  476. package/dist/dashboard/assets/formatters-DcHJyRiu.js.gz +0 -0
  477. package/dist/dashboard/assets/index-CgKmFngv.js.gz +0 -0
  478. package/dist/dashboard/assets/inline-B2BirKry.js.gz +0 -0
  479. package/dist/dashboard/assets/input-D-qV9dKY.js.gz +0 -0
  480. package/dist/dashboard/assets/layout-B7tVWhtV.js.gz +0 -0
  481. package/dist/dashboard/assets/list-row-CtkGE3bF.js.gz +0 -0
  482. package/dist/dashboard/assets/live-DGizF06-.js.gz +0 -0
  483. package/dist/dashboard/assets/minimize-2-BTmMTK4o.js +0 -1
  484. package/dist/dashboard/assets/minimize-2-BTmMTK4o.js.gz +0 -0
  485. package/dist/dashboard/assets/mutation-GU7XdEjz.js.gz +0 -0
  486. package/dist/dashboard/assets/network-C9UE-bOr.js.gz +0 -0
  487. package/dist/dashboard/assets/numbers-BnAd22a_.js.gz +0 -0
  488. package/dist/dashboard/assets/pagination-BAywyMzS.js.gz +0 -0
  489. package/dist/dashboard/assets/query-keys-YSoQOUh6.js.gz +0 -0
  490. package/dist/dashboard/assets/quota-DJ1QlAOq.js +0 -1
  491. package/dist/dashboard/assets/quota-DJ1QlAOq.js.gz +0 -0
  492. package/dist/dashboard/assets/routing-fnCsxgNO.js +0 -1
  493. package/dist/dashboard/assets/routing-fnCsxgNO.js.gz +0 -0
  494. package/dist/dashboard/assets/select-BCOcv8WK.js.gz +0 -0
  495. package/dist/dashboard/assets/settings-BxslsErn.js +0 -1
  496. package/dist/dashboard/assets/settings-BxslsErn.js.gz +0 -0
  497. package/dist/dashboard/assets/stack-ByKt3Q1g.js.gz +0 -0
  498. package/dist/dashboard/assets/state-DXbGTx7N.js.gz +0 -0
  499. package/dist/dashboard/assets/switch-ZeTsW623.js.gz +0 -0
  500. package/dist/dashboard/assets/useMutation-CbCP4w1o.js.gz +0 -0
  501. package/dist/dashboard/console-backgrounds.jpg +0 -0
  502. package/dist/dashboard/console-backgrounds_initial.jpg +0 -0
  503. package/dist/dashboard/donation/qris-risun-marke.png +0 -0
  504. package/src/providers/bundled-discovery-providers.ts +0 -98
  505. package/src/providers/configured-openai-providers.ts +0 -120
  506. package/src/providers/integrations/buddy/turing-device-token.ts +0 -262
  507. package/src/providers/integrations/buddy/workbuddy/cn-oauth.ts +0 -30
  508. package/src/providers/integrations/buddy/workbuddy/cn-provider.ts +0 -58
  509. package/src/providers/integrations/buddy/workbuddy/oauth.ts +0 -31
  510. package/src/providers/integrations/buddy/workbuddy/provider.ts +0 -209
  511. package/src/providers/integrations/buddy/workbuddy/quota.ts +0 -161
  512. package/src/providers/integrations/buddy/workbuddy/shared.ts +0 -285
  513. package/src/providers/operations/model-discovery-cache.ts +0 -34
  514. package/src/providers/operations/probe-fetch.ts +0 -15
  515. package/src/providers/operations/provider-version-cache.ts +0 -30
  516. package/src/providers/provider-capabilities.ts +0 -33
  517. package/src/providers/provider-credential-envelope.ts +0 -43
  518. /package/dist/dashboard/assets/{app-CTxoqyYB.js → app-DIXLjojg.js} +0 -0
  519. /package/dist/dashboard/assets/{app-CTxoqyYB.js.gz → app-DIXLjojg.js.gz} +0 -0
@@ -1,18 +1,66 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { healthEventBuffer } from "../../persistence/health-event-flush";
3
- import { and, count, desc, eq, inArray, isNotNull, lte, sql } from "drizzle-orm";
3
+ import { and, count, desc, eq, inArray, isNotNull, like, lte, or, sql } from "drizzle-orm";
4
4
  import { affectedRows, pruneExpiredCooldowns } from "../../persistence/dialect";
5
5
  import type { CartethyiaDatabase } from "../../persistence/db-handle";
6
- import { healthEvents, providerAccounts } from "../../persistence/schema";
6
+ import { consoleSettings, healthEvents, providerAccounts } from "../../persistence/schema";
7
7
  import {
8
8
  COOLDOWN_RECOVERY_BATCH,
9
9
  accountDeadlineMs,
10
+ applyRuleEscalation,
10
11
  classifyAccountError,
11
- escalateFallbackCooldown,
12
12
  latestDeadlineMs,
13
13
  unsupportedModelCooldownMs,
14
14
  } from "./account-error-classification";
15
+ import { resolveAccountDisabledRecoveryMs } from "../../config/cooldowns";
15
16
  import type { AccountErrorClassification, AccountFailureEvidence } from "./account-error-classification";
17
+ import { resolveCooldownRule } from "./cooldown-rules";
18
+ import { mapPlanToTier } from "./account-tier";
19
+
20
+ /**
21
+ * Categories the auto-disable toggle (area 01) targets (D01). Model-scoped
22
+ * verdicts (`model_unreachable`) are not here — they belong to the tier rules
23
+ * (area 03, R-C).
24
+ */
25
+ const AUTO_DISABLE_TARGET_CATEGORIES: ReadonlySet<string> = new Set([
26
+ "quota_exhausted",
27
+ "rate_limit_transient",
28
+ ]);
29
+
30
+ /** Prefix marking an auto-disable written by the rate-limited intercept (D03/R-B). */
31
+ export const AUTO_DISABLE_MARKER_PREFIX = "auto-disable (rate-limited): ";
32
+
33
+ /** An upstream-stated window shorter than this is not auto-disabled (D04). */
34
+ const AUTO_DISABLE_STATED_MIN_MS = 5 * 60 * 1000;
35
+
36
+ /**
37
+ * Whether the auto-disable preference is on for a tenant (and for shared
38
+ * accounts — `tenantId === null` — when any tenant has it on, D06).
39
+ *
40
+ * The preference lives in `console_settings.preferences` (the C2 cluster added
41
+ * the typed field to `ConsoleSettingsPreferences`). Reading is fail-safe (D01
42
+ * §3.1): a missing row, a failed read, or a preference that is not exactly
43
+ * `true` all mean off — a settings failure must never break a health write.
44
+ */
45
+ export async function autoDisableRateLimitedEnabled(
46
+ db: CartethyiaDatabase,
47
+ tenantId: string | null,
48
+ ): Promise<boolean> {
49
+ try {
50
+ const rows = await db
51
+ .select({ tenantId: consoleSettings.tenantId, preferences: consoleSettings.preferences })
52
+ .from(consoleSettings);
53
+ return rows.some((row) => {
54
+ if (tenantId !== null && row.tenantId !== tenantId) return false;
55
+ const prefs = row.preferences as {
56
+ readonly autoDisableRateLimited?: boolean;
57
+ } | null | undefined;
58
+ return prefs?.autoDisableRateLimited === true;
59
+ });
60
+ } catch {
61
+ return false;
62
+ }
63
+ }
16
64
 
17
65
  export { classifyAccountError, unsupportedModelCooldownMs } from "./account-error-classification";
18
66
  export type {
@@ -110,6 +158,11 @@ export async function recordAccountFailure(
110
158
  // counted", which is the common case and must stay cheap.
111
159
  modelBlockHits: providerAccounts.modelBlockHits,
112
160
  cooldownUntil: providerAccounts.cooldownUntil,
161
+ // Required by the auto-disable toggle (tenant-level preference read)
162
+ // and by the tier-aware cooldown rules (plan → tier).
163
+ providerId: providerAccounts.providerId,
164
+ tenantId: providerAccounts.tenantId,
165
+ quotaPlan: providerAccounts.quotaPlan,
113
166
  })
114
167
  .from(providerAccounts)
115
168
  .where(eq(providerAccounts.id, accountId))
@@ -128,6 +181,43 @@ export async function recordAccountFailure(
128
181
  const fromStatus = account.status;
129
182
  const now = new Date();
130
183
 
184
+ // ---- Auto-disable intercept (area 01, D01–D06) -------------------------
185
+ // Evaluated after classification and before the model-scoped/account-wide
186
+ // branch, so a 429/quota verdict carrying a modelId is rewritten to an
187
+ // account-wide disable instead of sinking into a per-model cooldown (D02).
188
+ //
189
+ // Only fires when every condition holds:
190
+ // - the tenant's toggle is on (fail-safe read: off on any error),
191
+ // - the classification mutates the account and the origin is upstream,
192
+ // - the category is a target (quota_exhausted / rate_limit_transient),
193
+ // - the verdict is a cooldown (rule-level `disabled` verdicts — bans,
194
+ // revoked credentials — are untouched),
195
+ // - the provider did not state a sub-5-minute window (D04: Inception's
196
+ // 1m limit must keep its short cooldown, not park the account for 24h).
197
+ const autoDisableTarget =
198
+ AUTO_DISABLE_TARGET_CATEGORIES.has(effective.category) &&
199
+ effective.modelScoped !== true &&
200
+ effective.status === "cooldown" &&
201
+ effective.mutatesAccount &&
202
+ options?.origin === "upstream";
203
+ const statedTooShort =
204
+ effective.providerStated === true &&
205
+ effective.cooldownMs > 0 &&
206
+ effective.cooldownMs < AUTO_DISABLE_STATED_MIN_MS;
207
+ let autoDisableActive = false;
208
+ if (autoDisableTarget && !statedTooShort) {
209
+ autoDisableActive = await autoDisableRateLimitedEnabled(client, account.tenantId);
210
+ }
211
+ // Rewritten classification used by the write paths below.
212
+ const interception = autoDisableActive
213
+ ? {
214
+ ...effective,
215
+ status: "disabled" as const,
216
+ cooldownMs: resolveAccountDisabledRecoveryMs(),
217
+ ruleSource: "fallback" as const,
218
+ }
219
+ : effective;
220
+
131
221
  const modelId = options?.modelId;
132
222
  // Every category here is a verdict about one (account, model) pair rather
133
223
  // than about the credential, so all three back off that pair and leave the
@@ -138,16 +228,59 @@ export async function recordAccountFailure(
138
228
  effective.category === "rate_limit_transient" ||
139
229
  effective.category === "model_capacity" ||
140
230
  effective.category === "model_unreachable";
141
- const shouldModelCooldown = modelId !== undefined && isThrottle;
231
+ // D02: when the auto-disable intercept fired, the target category must not
232
+ // sink into a model-scoped cooldown — the outcome is an account-wide
233
+ // disable.
234
+ const shouldModelCooldown =
235
+ !autoDisableActive &&
236
+ modelId !== undefined &&
237
+ (isThrottle || effective.modelScoped === true);
142
238
  // The account-wide streak, escalated for a provider-stated window. Read once
143
239
  // so the model-scoped and account-wide paths below cannot disagree about it:
144
240
  // this is the count *before* this failure, which the UPDATE below increments.
145
241
  const streak = Math.max(0, account.consecutiveFailures ?? 0);
146
- const escalatedCooldownMs = escalateFallbackCooldown(
147
- effective.cooldownMs,
148
- effective.providerStated === true,
149
- streak,
150
- );
242
+ // Tier-aware rule resolution (area 03, D15–D19): the account's plan is
243
+ // projected on this row, mapped to a tier, and — when a rule exists for
244
+ // the (provider, category, tier) combination — replaces the classifier's
245
+ // duration. The rule duration is a policy decision and never escalates
246
+ // (D18); escalation applies only to the fallback.
247
+ const accountTier =
248
+ account.quotaPlan === null || account.quotaPlan === undefined
249
+ ? null
250
+ : mapPlanToTier(account.providerId, account.quotaPlan);
251
+ // A provider-stated window always wins over a tier rule (D18), so the rule
252
+ // is only consulted when the upstream named nothing.
253
+ const ruleCooldownMs =
254
+ interception.providerStated === true
255
+ ? undefined
256
+ : resolveCooldownRule({
257
+ providerId: account.providerId,
258
+ category: effective.category,
259
+ tier: accountTier,
260
+ });
261
+ const ruleSource = interception.ruleSource;
262
+ const sourceCooldownMs =
263
+ ruleCooldownMs !== undefined ? ruleCooldownMs : interception.cooldownMs;
264
+ const sourceRuleKind: "provider-rule" | "fallback" | undefined =
265
+ ruleCooldownMs !== undefined ? "provider-rule" : ruleSource;
266
+ // The auto-disable recovery window is a policy duration like a tier rule:
267
+ // it must never pass through the fallback escalation ladder, or the 1h cap
268
+ // would truncate the default 24h recovery on the first unstated 429 (D18).
269
+ const resolvedCooldownMs = autoDisableActive
270
+ ? interception.cooldownMs
271
+ : applyRuleEscalation(
272
+ sourceCooldownMs,
273
+ interception.providerStated === true,
274
+ streak,
275
+ sourceRuleKind,
276
+ );
277
+ // When a rule replaces the duration, the classifier's `retryAt` (computed
278
+ // from the old constant/fallback) is stale: a shorter rule would lose to it
279
+ // in the monotonic-max deadline. Recompute from the rule duration instead.
280
+ const retryAtMs =
281
+ ruleCooldownMs !== undefined
282
+ ? Date.now() + ruleCooldownMs
283
+ : effective.retryAt?.getTime();
151
284
 
152
285
  // The unsupported-model ladder, computed before the branch: the classifier
153
286
  // could only state the base window because the authoritative hit count
@@ -173,8 +306,8 @@ export async function recordAccountFailure(
173
306
  // of the two whenever the provider really parked the pair longer.
174
307
  const previousDeadline = existing[modelId] === undefined ? undefined : Date.parse(existing[modelId]);
175
308
  const nextDeadline = latestDeadlineMs(
176
- effective.retryAt?.getTime(),
177
- Date.now() + (unsupportedCooldownMs ?? escalatedCooldownMs),
309
+ retryAtMs,
310
+ Date.now() + (unsupportedCooldownMs ?? resolvedCooldownMs),
178
311
  previousDeadline,
179
312
  );
180
313
  const nextUntil = new Date(nextDeadline).toISOString();
@@ -240,15 +373,35 @@ export async function recordAccountFailure(
240
373
  // The route catalog projects modelCooldowns and stamps this candidate as
241
374
  // model_cooldown. Invalidate when its deadline changes, or the cached
242
375
  // snapshot keeps routing this pair until some unrelated write rebuilds it.
243
- return { ...classification, routesVisible: previousDeadline !== nextDeadline };
376
+ return {
377
+ ...classification,
378
+ routesVisible: previousDeadline !== nextDeadline,
379
+ ...(sourceRuleKind === undefined ? {} : { ruleSource: sourceRuleKind }),
380
+ };
244
381
  }
245
382
 
383
+ // When the intercept fired, the reason is prefixed with the auto-disable
384
+ // marker — `sweepExpiredCooldowns` (D03/R-B) uses the prefix to tell an
385
+ // auto-disable apart from an operator disable, which stays terminal.
386
+ const writtenReason = autoDisableActive
387
+ ? `${AUTO_DISABLE_MARKER_PREFIX}${effective.reason}`
388
+ : effective.reason;
389
+ // A disabled account's deadline is the auto-disable recovery window — the
390
+ // classifier's `retryAt`/cooldown duration must not leak in, or a terminal
391
+ // disable (`recovery = 0`) would gain a deadline and be swept back.
392
+ const deadlineCandidates = autoDisableActive
393
+ ? [interception.cooldownMs > 0 ? Date.now() + interception.cooldownMs : undefined]
394
+ : [
395
+ retryAtMs,
396
+ resolvedCooldownMs > 0 ? Date.now() + resolvedCooldownMs : undefined,
397
+ account.cooldownUntil?.getTime?.(),
398
+ ];
246
399
  await client
247
400
  .update(providerAccounts)
248
401
  .set({
249
- status: effective.status,
402
+ status: interception.status,
250
403
  consecutiveFailures: sql`${providerAccounts.consecutiveFailures} + 1`,
251
- lastError: effective.reason,
404
+ lastError: writtenReason,
252
405
  lastErrorCategory: effective.category,
253
406
  lastErrorAt: now,
254
407
  // Monotonic against the account's own in-force deadline, for the same
@@ -259,11 +412,7 @@ export async function recordAccountFailure(
259
412
  // (see `accountDeadlineMs`): the one state that asks for it is a
260
413
  // terminal automatic disable, and an elapsed deadline there would hand
261
414
  // the row straight back to the recovery sweep.
262
- cooldownUntil: accountDeadlineMs([
263
- effective.retryAt?.getTime(),
264
- escalatedCooldownMs > 0 ? Date.now() + escalatedCooldownMs : undefined,
265
- account.cooldownUntil?.getTime?.(),
266
- ]),
415
+ cooldownUntil: accountDeadlineMs(deadlineCandidates),
267
416
  })
268
417
  .where(eq(providerAccounts.id, accountId));
269
418
 
@@ -272,8 +421,8 @@ export async function recordAccountFailure(
272
421
  entityKind: "account",
273
422
  accountId,
274
423
  fromStatus: fromStatus as "active" | "cooldown" | "disabled",
275
- toStatus: effective.status,
276
- reason: effective.reason,
424
+ toStatus: interception.status,
425
+ reason: writtenReason,
277
426
  errorCategory: effective.category,
278
427
  modelId: modelId ?? null,
279
428
  createdAt: now,
@@ -281,7 +430,11 @@ export async function recordAccountFailure(
281
430
  }
282
431
  // This path wrote `status` and `cooldownUntil`, both of which the snapshot
283
432
  // projects, so routing has to see it.
284
- return { ...classification, routesVisible: true };
433
+ return {
434
+ ...classification,
435
+ routesVisible: true,
436
+ ...(sourceRuleKind === undefined ? {} : { ruleSource: sourceRuleKind }),
437
+ };
285
438
  };
286
439
 
287
440
  return withTransaction(db, persist, null);
@@ -414,11 +567,28 @@ export async function sweepExpiredCooldowns(db: CartethyiaDatabase): Promise<num
414
567
  })
415
568
  .from(providerAccounts)
416
569
  .where(
417
- // An ordinary cooldown, recovered as soon as its window elapses.
570
+ // An ordinary cooldown, recovered as soon as its window elapses; plus
571
+ // (D03/R-B) an account the *auto-disable* intercept disabled — only
572
+ // when its category is a target AND the reason carries the auto-disable
573
+ // marker AND its recovery deadline has elapsed. An auto-disable is the
574
+ // machine's own verdict, so the sweep may undo it; an operator disable
575
+ // (no marker) stays terminal.
418
576
  and(
419
- eq(providerAccounts.status, "cooldown"),
420
- isNotNull(providerAccounts.cooldownUntil),
421
- lte(providerAccounts.cooldownUntil, now),
577
+ or(
578
+ and(
579
+ eq(providerAccounts.status, "cooldown"),
580
+ isNotNull(providerAccounts.cooldownUntil),
581
+ lte(providerAccounts.cooldownUntil, now),
582
+ ),
583
+ and(
584
+ eq(providerAccounts.status, "disabled"),
585
+ inArray(providerAccounts.lastErrorCategory, [...AUTO_DISABLE_TARGET_CATEGORIES]),
586
+ isNotNull(providerAccounts.lastError),
587
+ like(providerAccounts.lastError, `${AUTO_DISABLE_MARKER_PREFIX}%`),
588
+ isNotNull(providerAccounts.cooldownUntil),
589
+ lte(providerAccounts.cooldownUntil, now),
590
+ ),
591
+ ),
422
592
  ),
423
593
  )
424
594
  // One pass recovers a bounded batch. The sweep runs every 30s, so a
@@ -528,6 +698,83 @@ async function writeHealthEvent(
528
698
  await client.insert(healthEvents).values({ id: randomUUID(), ...row });
529
699
  }
530
700
 
701
+ /**
702
+ * The one internal seam that applies a health verdict to an account row.
703
+ *
704
+ * Every producer that persists account-state semantics — dispatch failure
705
+ * classification, OAuth refresh definitive failure, quota refresh parking —
706
+ * converges here instead of writing `status`/`cooldownUntil`/error fields
707
+ * independently. Producers may still classify differently; they may not
708
+ * persist account-state semantics separately.
709
+ *
710
+ * This writes the account-wide health state plus a health audit row, inside the
711
+ * caller's transaction (`client`). It returns `true` when the account row was
712
+ * actually updated (i.e. the account exists and is not operator-disabled with
713
+ * `preserveOperatorDisable`), so the caller knows whether routing must be
714
+ * invalidated (routesVisible) or the lease released.
715
+ */
716
+ export async function applyAccountVerdict(
717
+ client: CartethyiaDatabase,
718
+ verdict: {
719
+ readonly accountId: string;
720
+ readonly status: "cooldown" | "disabled";
721
+ readonly reason: string;
722
+ readonly category: string | null;
723
+ readonly cooldownUntil?: Date | null;
724
+ /** When true, an operator-disable (status `disabled`, no deadline) is left untouched. */
725
+ readonly preserveOperatorDisable?: boolean;
726
+ /** Health event transition status (defaults to the verdict status). */
727
+ readonly fromStatus?: "active" | "cooldown" | "disabled" | null;
728
+ readonly modelId?: string | null;
729
+ readonly createdAt?: Date;
730
+ },
731
+ ): Promise<boolean> {
732
+ return withTransaction(
733
+ client,
734
+ async (tx) => {
735
+ const rows = await tx
736
+ .select({
737
+ status: providerAccounts.status,
738
+ cooldownUntil: providerAccounts.cooldownUntil,
739
+ })
740
+ .from(providerAccounts)
741
+ .where(eq(providerAccounts.id, verdict.accountId))
742
+ .limit(1);
743
+ const row = rows[0];
744
+ if (!row) return false;
745
+ if (verdict.preserveOperatorDisable === true) {
746
+ const isOperatorDisable = row.status === "disabled" && row.cooldownUntil == null;
747
+ if (isOperatorDisable) return false;
748
+ }
749
+ const now = verdict.createdAt ?? new Date();
750
+ await tx
751
+ .update(providerAccounts)
752
+ .set({
753
+ status: verdict.status,
754
+ lastError: (verdict.reason ?? "Account failed").slice(0, 500),
755
+ lastErrorCategory: verdict.category ?? "check_failed",
756
+ lastErrorAt: now,
757
+ ...(verdict.cooldownUntil === undefined
758
+ ? {}
759
+ : { cooldownUntil: verdict.cooldownUntil }),
760
+ })
761
+ .where(eq(providerAccounts.id, verdict.accountId));
762
+ await writeHealthEvent(tx, {
763
+ entityKind: "account",
764
+ accountId: verdict.accountId,
765
+ fromStatus: verdict.fromStatus ?? (row.status as "active" | "cooldown" | "disabled" | null),
766
+ toStatus: verdict.status,
767
+ reason: verdict.reason,
768
+ errorCategory: verdict.category,
769
+ modelId: verdict.modelId ?? null,
770
+ createdAt: now,
771
+ });
772
+ return true;
773
+ },
774
+ false,
775
+ );
776
+ }
777
+
531
778
  export async function reportAttemptOutcome(
532
779
  db: CartethyiaDatabase,
533
780
  report: AttemptHealthReport,
@@ -0,0 +1,269 @@
1
+ /**
2
+ * Manual account-status observation: refresh an OAuth token on demand and
3
+ * report the account's status AFTER the attempt — never assume it.
4
+ *
5
+ * Area 04 (D21–D26): a refresh is not a health verdict. This module is the
6
+ * explicit "refresh token" affordance for accounts the OAuth sweep excludes
7
+ * (`disabled`), and it is where the invariant nobody else had to is held:
8
+ * `statusAfter` is always re-read from the database, so no caller can ever
9
+ * report `"active"` just because a refresh succeeded.
10
+ */
11
+ import { randomUUID } from "node:crypto";
12
+ import type { CartethyiaDatabase } from "../../persistence/db-handle";
13
+ import { healthEvents } from "../../persistence/schema";
14
+ import { pushStructuredConsoleLog } from "../../observability/log-ring";
15
+ import { loadAccountWithOauthState } from "./provider-credential-service";
16
+ import type { OAuthRefreshService, OAuthTokenRefresher } from "../authentication/oauth-refresh-service";
17
+
18
+ export type RefreshAccountStatusCode =
19
+ | "ok"
20
+ | "account_missing"
21
+ | "not_oauth"
22
+ | "static_token"
23
+ | "no_refresher"
24
+ | "refresh_failed";
25
+
26
+ export interface RefreshAccountStatusOutcome {
27
+ readonly ok: boolean;
28
+ readonly tokenRefreshed: boolean;
29
+ /**
30
+ * Account status re-read from the DB after the attempt. Never assumed from
31
+ * the input row and never `"active"` by fiat — a refresh cannot re-enable.
32
+ */
33
+ readonly statusAfter: string;
34
+ readonly code: RefreshAccountStatusCode;
35
+ /** IS0 timestamp of the fresh access-token expiry, when known. */
36
+ readonly expiresAt?: string;
37
+ /** ISO timestamp of the attempt, when it happened. */
38
+ readonly refreshedAt?: string;
39
+ /** Structured reason when `ok` is false (`not_oauth`, `static_token`, …). */
40
+ readonly error?: string;
41
+ /** Human detail (failure message) when available. */
42
+ readonly category?: string;
43
+ }
44
+
45
+ export interface RefreshAccountStatusDeps {
46
+ readonly db: CartethyiaDatabase;
47
+ readonly refreshService: Pick<OAuthRefreshService, "ensureFreshAccessToken">;
48
+ readonly resolveRefresher: (providerId: string) => Promise<OAuthTokenRefresher | undefined>;
49
+ }
50
+
51
+ interface LoadedAccount {
52
+ readonly id: string;
53
+ readonly providerId: string;
54
+ readonly credentialKind: string;
55
+ readonly status: string;
56
+ readonly staticToken: boolean;
57
+ readonly refreshCiphertext: Buffer | null;
58
+ readonly expiresAt: Date | null;
59
+ }
60
+
61
+ async function loadAccount(
62
+ db: CartethyiaDatabase,
63
+ accountId: string,
64
+ ): Promise<LoadedAccount | undefined> {
65
+ const loaded = await loadAccountWithOauthState(db, accountId, {
66
+ includeStatus: true,
67
+ includeExpiry: true,
68
+ });
69
+ if (!loaded || loaded.status === undefined || loaded.expiresAt === undefined) return undefined;
70
+ return {
71
+ id: loaded.id,
72
+ providerId: loaded.providerId,
73
+ credentialKind: loaded.credentialKind,
74
+ status: loaded.status,
75
+ staticToken: loaded.staticToken,
76
+ refreshCiphertext: loaded.refreshCiphertext,
77
+ expiresAt: loaded.expiresAt,
78
+ };
79
+ }
80
+
81
+ async function readStatusAfter(
82
+ db: CartethyiaDatabase,
83
+ accountId: string,
84
+ ): Promise<{ status: string; expiresAt: Date | null } | undefined> {
85
+ const loaded = await loadAccountWithOauthState(db, accountId, {
86
+ includeStatus: true,
87
+ includeExpiry: true,
88
+ });
89
+ if (!loaded || loaded.status === undefined || loaded.expiresAt === undefined) return undefined;
90
+ return { status: loaded.status, expiresAt: loaded.expiresAt };
91
+ }
92
+
93
+ /** Non-mutating health event: records the attempt, never changes status. */
94
+ async function writeRefreshHealthEvent(
95
+ db: CartethyiaDatabase,
96
+ accountId: string,
97
+ fromStatus: string,
98
+ toStatus: string,
99
+ reason: string,
100
+ ): Promise<void> {
101
+ if (typeof db.insert !== "function") return;
102
+ await db.insert(healthEvents).values({
103
+ id: randomUUID(),
104
+ entityKind: "account",
105
+ accountId,
106
+ fromStatus: fromStatus as "active" | "cooldown" | "disabled" | null,
107
+ toStatus: toStatus as "active" | "cooldown" | "disabled",
108
+ reason,
109
+ errorCategory: null,
110
+ createdAt: new Date(),
111
+ });
112
+ }
113
+
114
+ /**
115
+ * Refreshes an OAuth account's token on demand and reports the resulting
116
+ * status. Works for any status, `disabled` included — the account stays
117
+ * `disabled`; only an operator can re-enable it.
118
+ *
119
+ * Rejections are structured (`{ ok: false, code: "not_oauth" | "static_token" |
120
+ * ... }`), and every outcome carries `statusAfter` re-read from the DB.
121
+ */
122
+ export async function refreshAccountStatus(
123
+ deps: RefreshAccountStatusDeps,
124
+ accountId: string,
125
+ opts: { readonly origin?: "ui" | "sweep" | "dispatch" } = {},
126
+ ): Promise<RefreshAccountStatusOutcome> {
127
+ const origin = opts.origin ?? "ui";
128
+ const account = await loadAccount(deps.db, accountId);
129
+ if (!account) {
130
+ return {
131
+ ok: false,
132
+ tokenRefreshed: false,
133
+ statusAfter: "disabled",
134
+ code: "account_missing",
135
+ error: "Account not found",
136
+ };
137
+ }
138
+ const statusBefore = account.status;
139
+
140
+ if (account.credentialKind !== "oauth") {
141
+ return {
142
+ ok: false,
143
+ tokenRefreshed: false,
144
+ statusAfter: statusBefore,
145
+ code: "not_oauth",
146
+ error: "not_oauth",
147
+ };
148
+ }
149
+ if (account.staticToken || account.refreshCiphertext == null) {
150
+ // No refresh grant (or an explicit static marker): there is nothing to
151
+ // re-mint, and forcing the service to try would mutate the row into a
152
+ // static token. Refuse instead of changing the account.
153
+ return {
154
+ ok: false,
155
+ tokenRefreshed: false,
156
+ statusAfter: statusBefore,
157
+ code: "static_token",
158
+ error: "static_token",
159
+ ...(account.refreshCiphertext == null && !account.staticToken
160
+ ? { category: "No refresh token on record" }
161
+ : {}),
162
+ };
163
+ }
164
+
165
+ const refresher = await deps.resolveRefresher(account.providerId);
166
+ if (!refresher) {
167
+ return {
168
+ ok: false,
169
+ tokenRefreshed: false,
170
+ statusAfter: statusBefore,
171
+ code: "no_refresher",
172
+ error: "no_refresher",
173
+ };
174
+ }
175
+
176
+ // `ensureFreshAccessToken` deliberately swallows a failing refresher and
177
+ // returns null (it must not break a dispatch). For this explicit affordance
178
+ // the operator needs the reason, so keep the last failure message here
179
+ // rather than generalizing it — same "message as-is" rule as area 08.
180
+ let refreshFailureMessage: string | undefined;
181
+ const reportingRefresher: OAuthTokenRefresher = {
182
+ refresh: async (...args) => {
183
+ try {
184
+ return await refresher.refresh(...args);
185
+ } catch (error) {
186
+ refreshFailureMessage = error instanceof Error ? error.message : String(error);
187
+ throw error;
188
+ }
189
+ },
190
+ };
191
+
192
+ const startedAt = new Date();
193
+ try {
194
+ const refreshed = await deps.refreshService.ensureFreshAccessToken(accountId, reportingRefresher, {
195
+ force: true,
196
+ origin,
197
+ });
198
+ // D25: status is always re-read after the attempt — never assumed.
199
+ const after = await readStatusAfter(deps.db, accountId);
200
+ const statusAfter = after?.status ?? statusBefore;
201
+ if (refreshed !== null) {
202
+ pushStructuredConsoleLog("info", "OAuth token refreshed", {
203
+ event: "token_refresh",
204
+ accountId,
205
+ providerId: account.providerId,
206
+ details: {
207
+ origin,
208
+ statusBefore,
209
+ statusAfter,
210
+ expiresAt: after?.expiresAt?.toISOString() ?? "",
211
+ },
212
+ });
213
+ await writeRefreshHealthEvent(
214
+ deps.db,
215
+ accountId,
216
+ statusBefore,
217
+ statusAfter,
218
+ "Token refreshed — status unchanged",
219
+ );
220
+ return {
221
+ ok: true,
222
+ tokenRefreshed: true,
223
+ statusAfter,
224
+ code: "ok",
225
+ ...(after?.expiresAt ? { expiresAt: after.expiresAt.toISOString() } : {}),
226
+ refreshedAt: startedAt.toISOString(),
227
+ };
228
+ }
229
+ // `ensureFreshAccessToken` returned null: the refresh failed (transient
230
+ // upstream error, lease loss) or the credential became static mid-flight.
231
+ // Either way the account's own status is the truth, re-read above.
232
+ pushStructuredConsoleLog("warn", "OAuth token refresh did not complete", {
233
+ event: "token_refresh",
234
+ accountId,
235
+ providerId: account.providerId,
236
+ errorCode: "refresh_failed",
237
+ details: { origin, statusBefore, statusAfter },
238
+ });
239
+ return {
240
+ ok: false,
241
+ tokenRefreshed: false,
242
+ statusAfter,
243
+ code: "refresh_failed",
244
+ error: "refresh_failed",
245
+ ...(refreshFailureMessage
246
+ ? { category: refreshFailureMessage.slice(0, 500) }
247
+ : { category: "Token refresh did not complete" }),
248
+ };
249
+ } catch (error) {
250
+ const after = await readStatusAfter(deps.db, accountId);
251
+ const statusAfter = after?.status ?? statusBefore;
252
+ const message = error instanceof Error ? error.message : String(error);
253
+ pushStructuredConsoleLog("error", "OAuth token refresh failed", {
254
+ event: "token_refresh",
255
+ accountId,
256
+ providerId: account.providerId,
257
+ errorCode: "refresh_failed",
258
+ details: { origin, statusBefore, statusAfter, error: message.slice(0, 500) },
259
+ });
260
+ return {
261
+ ok: false,
262
+ tokenRefreshed: false,
263
+ statusAfter,
264
+ code: "refresh_failed",
265
+ error: "refresh_failed",
266
+ category: message.slice(0, 500),
267
+ };
268
+ }
269
+ }