cartethyia 2.5.3 → 2.5.6

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 (456) hide show
  1. package/.env.example +13 -4
  2. package/README.md +2 -2
  3. package/dist/dashboard/assets/{BannedPage-hfCzi-Ux.js → BannedPage-CADDq4IU.js} +1 -1
  4. package/dist/dashboard/assets/BannedPage-CADDq4IU.js.gz +0 -0
  5. package/dist/dashboard/assets/CliToolDetailPage-_z1hID7E.js +1 -0
  6. package/dist/dashboard/assets/CliToolDetailPage-_z1hID7E.js.gz +0 -0
  7. package/dist/dashboard/assets/{CliToolsPage-DEDyKBlC.js → CliToolsPage-DuO1Z7dq.js} +1 -1
  8. package/dist/dashboard/assets/CliToolsPage-DuO1Z7dq.js.gz +0 -0
  9. package/dist/dashboard/assets/CombosPage-DNEPi3xU.js +1 -0
  10. package/dist/dashboard/assets/CombosPage-DNEPi3xU.js.gz +0 -0
  11. package/dist/dashboard/assets/{ConsoleLogPage-bsLuZmYH.js → ConsoleLogPage-BVdpfiz1.js} +1 -1
  12. package/dist/dashboard/assets/ConsoleLogPage-BVdpfiz1.js.gz +0 -0
  13. package/dist/dashboard/assets/{CustomizationPage-_UWhwziJ.js → CustomizationPage-BR5HHvbi.js} +1 -1
  14. package/dist/dashboard/assets/CustomizationPage-BR5HHvbi.js.gz +0 -0
  15. package/dist/dashboard/assets/HealthEventsModal-mUfcknqm.js +1 -0
  16. package/dist/dashboard/assets/HealthEventsModal-mUfcknqm.js.gz +0 -0
  17. package/dist/dashboard/assets/{LoginPage-DDVIfZGW.js → LoginPage-V5faV9V2.js} +1 -1
  18. package/dist/dashboard/assets/LoginPage-V5faV9V2.js.gz +0 -0
  19. package/dist/dashboard/assets/ModelPicker-C7pDb41R.js +1 -0
  20. package/dist/dashboard/assets/ModelPicker-C7pDb41R.js.gz +0 -0
  21. package/dist/dashboard/assets/OverviewPage-BDu20Jdx.js +1 -0
  22. package/dist/dashboard/assets/OverviewPage-BDu20Jdx.js.gz +0 -0
  23. package/dist/dashboard/assets/ProviderDetailPage-Dllr_aFO.js +3 -0
  24. package/dist/dashboard/assets/ProviderDetailPage-Dllr_aFO.js.gz +0 -0
  25. package/dist/dashboard/assets/ProvidersPage-uPiGVTTK.js +1 -0
  26. package/dist/dashboard/assets/ProvidersPage-uPiGVTTK.js.gz +0 -0
  27. package/dist/dashboard/assets/ProxyPage-BaUkO-4O.js +7 -0
  28. package/dist/dashboard/assets/ProxyPage-BaUkO-4O.js.gz +0 -0
  29. package/dist/dashboard/assets/QuotaPage-o0IFAsFb.js +1 -0
  30. package/dist/dashboard/assets/QuotaPage-o0IFAsFb.js.gz +0 -0
  31. package/dist/dashboard/assets/SettingsPage-CYWqbXjp.js +1 -0
  32. package/dist/dashboard/assets/SettingsPage-CYWqbXjp.js.gz +0 -0
  33. package/dist/dashboard/assets/{SetupPage-D6pmQ86L.js → SetupPage-UB690SE2.js} +1 -1
  34. package/dist/dashboard/assets/SetupPage-UB690SE2.js.gz +0 -0
  35. package/dist/dashboard/assets/{SortableList-DWvKiuSl.js → SortableList-BLujdX7N.js} +1 -1
  36. package/dist/dashboard/assets/SortableList-BLujdX7N.js.gz +0 -0
  37. package/dist/dashboard/assets/{StudioPage-CmkUrgdj.js → StudioPage-D5C2raAc.js} +3 -3
  38. package/dist/dashboard/assets/StudioPage-D5C2raAc.js.gz +0 -0
  39. package/dist/dashboard/assets/UsagePage-8uIcxa1W.js +56 -0
  40. package/dist/dashboard/assets/UsagePage-8uIcxa1W.js.gz +0 -0
  41. package/dist/dashboard/assets/api-keys-DQYLQxpW.js +1 -0
  42. package/dist/dashboard/assets/api-keys-DQYLQxpW.js.gz +0 -0
  43. package/dist/dashboard/assets/app-CX_Z8cMs.css +2 -0
  44. package/dist/dashboard/assets/app-CX_Z8cMs.css.gz +0 -0
  45. package/dist/dashboard/assets/{assert-NHwYq95t.js → assert-C9FD_JwJ.js} +1 -1
  46. package/dist/dashboard/assets/assert-C9FD_JwJ.js.gz +0 -0
  47. package/dist/dashboard/assets/{badge-6uzDDvqs.js → badge-CR72dasT.js} +1 -1
  48. package/dist/dashboard/assets/badge-CR72dasT.js.gz +0 -0
  49. package/dist/dashboard/assets/{banner-4_WNg2WZ.js → banner-CI9_eSfO.js} +1 -1
  50. package/dist/dashboard/assets/banner-CI9_eSfO.js.gz +0 -0
  51. package/dist/dashboard/assets/{card--AtFedAq.js → card-DS7CQEsO.js} +1 -1
  52. package/dist/dashboard/assets/card-DS7CQEsO.js.gz +0 -0
  53. package/dist/dashboard/assets/{cli-tools-B3e7dx3N.js → cli-tools-CNqzrfdb.js} +1 -1
  54. package/dist/dashboard/assets/cli-tools-CNqzrfdb.js.gz +0 -0
  55. package/dist/dashboard/assets/{clipboard-button-NreUx6c8.js → clipboard-button-611pSsaX.js} +1 -1
  56. package/dist/dashboard/assets/clipboard-button-611pSsaX.js.gz +0 -0
  57. package/dist/dashboard/assets/{fingerprint-pattern-BWuHtbW4.js → concurrency-BDF0ok7y.js} +1 -1
  58. package/dist/dashboard/assets/concurrency-BDF0ok7y.js.gz +0 -0
  59. package/dist/dashboard/assets/database-Dz9N3zGd.js +1 -0
  60. package/dist/dashboard/assets/database-Dz9N3zGd.js.gz +0 -0
  61. package/dist/dashboard/assets/dialog-k37exjNt.js +1 -0
  62. package/dist/dashboard/assets/dialog-k37exjNt.js.gz +0 -0
  63. package/dist/dashboard/assets/drawer-CX7N8ucR.js +1 -0
  64. package/dist/dashboard/assets/drawer-CX7N8ucR.js.gz +0 -0
  65. package/dist/dashboard/assets/entry-B9Xvtnfj.js +1 -0
  66. package/dist/dashboard/assets/entry-B9Xvtnfj.js.gz +0 -0
  67. package/dist/dashboard/assets/entry-CTP8AyiS.js +1 -0
  68. package/dist/dashboard/assets/entry-CTP8AyiS.js.gz +0 -0
  69. package/dist/dashboard/assets/entry-DxBHnauy.js +4 -0
  70. package/dist/dashboard/assets/entry-DxBHnauy.js.gz +0 -0
  71. package/dist/dashboard/assets/{github-badge-D_eM3IgG.js → github-badge-sltk1qUi.js} +1 -1
  72. package/dist/dashboard/assets/github-badge-sltk1qUi.js.gz +0 -0
  73. package/dist/dashboard/assets/{index-BydyKaDz.js → index-CmxZr14F.js} +2 -2
  74. package/dist/dashboard/assets/index-CmxZr14F.js.gz +0 -0
  75. package/dist/dashboard/assets/{inline-BcNGF8Gr.js → inline-BhY2sSnO.js} +1 -1
  76. package/dist/dashboard/assets/inline-BhY2sSnO.js.gz +0 -0
  77. package/dist/dashboard/assets/{input-BG41et9G.js → input-V1umFiBk.js} +1 -1
  78. package/dist/dashboard/assets/input-V1umFiBk.js.gz +0 -0
  79. package/dist/dashboard/assets/layout-BBOYycyS.js +1 -0
  80. package/dist/dashboard/assets/layout-BBOYycyS.js.gz +0 -0
  81. package/dist/dashboard/assets/{list-row-Gh_W4OR_.js → list-row-1zzZYM0B.js} +1 -1
  82. package/dist/dashboard/assets/list-row-1zzZYM0B.js.gz +0 -0
  83. package/dist/dashboard/assets/{live-BJGYRBnR.js → live-BivGfuUs.js} +1 -1
  84. package/dist/dashboard/assets/live-BivGfuUs.js.gz +0 -0
  85. package/dist/dashboard/assets/model-bans-CmffjLuG.js +1 -0
  86. package/dist/dashboard/assets/model-bans-CmffjLuG.js.gz +0 -0
  87. package/dist/dashboard/assets/{mutation-BkoLM5Ba.js → mutation-46P0oWMe.js} +1 -1
  88. package/dist/dashboard/assets/mutation-46P0oWMe.js.gz +0 -0
  89. package/dist/dashboard/assets/numbers-DkfMR5Qs.js +2 -0
  90. package/dist/dashboard/assets/numbers-DkfMR5Qs.js.gz +0 -0
  91. package/dist/dashboard/assets/{query-keys-D7wX2vM5.js → query-keys-BFhGpwR1.js} +1 -1
  92. package/dist/dashboard/assets/query-keys-BFhGpwR1.js.gz +0 -0
  93. package/dist/dashboard/assets/{routing-CcusqXA7.js → routing-CAb4F37R.js} +1 -1
  94. package/dist/dashboard/assets/routing-CAb4F37R.js.gz +0 -0
  95. package/dist/dashboard/assets/{select-BO6h8yNv.js → select-BYR_5K2c.js} +2 -2
  96. package/dist/dashboard/assets/select-BYR_5K2c.js.gz +0 -0
  97. package/dist/dashboard/assets/{settings-mpmXTgUa.js → settings-DNX9sydi.js} +1 -1
  98. package/dist/dashboard/assets/settings-DNX9sydi.js.gz +0 -0
  99. package/dist/dashboard/assets/{stack-DwLmjUFK.js → stack-BHTfXdz_.js} +1 -1
  100. package/dist/dashboard/assets/stack-BHTfXdz_.js.gz +0 -0
  101. package/dist/dashboard/assets/{state-DYdQmdAl.js → state-mPDRlQkl.js} +1 -1
  102. package/dist/dashboard/assets/state-mPDRlQkl.js.gz +0 -0
  103. package/dist/dashboard/assets/switch-DOmJxg7H.js +1 -0
  104. package/dist/dashboard/assets/switch-DOmJxg7H.js.gz +0 -0
  105. package/dist/dashboard/assets/terminal-0_IjPWTk.js +1 -0
  106. package/dist/dashboard/assets/terminal-0_IjPWTk.js.gz +0 -0
  107. package/dist/dashboard/assets/tokens-COzario9.js +1 -0
  108. package/dist/dashboard/assets/tokens-COzario9.js.gz +0 -0
  109. package/dist/dashboard/assets/use-modal-focus-By7oJoGd.js +1 -0
  110. package/dist/dashboard/assets/use-modal-focus-By7oJoGd.js.gz +0 -0
  111. package/dist/dashboard/assets/{useMutation-w4u4CVue.js → useMutation-Dem0lXQj.js} +1 -1
  112. package/dist/dashboard/assets/useMutation-Dem0lXQj.js.gz +0 -0
  113. package/dist/dashboard/assets/zap-C4EeVW4q.js +1 -0
  114. package/dist/dashboard/assets/zap-C4EeVW4q.js.gz +0 -0
  115. package/dist/dashboard/donation/qris-risun-marke.png +0 -0
  116. package/dist/dashboard/index.html +1 -1
  117. package/dist/dashboard/index.html.gz +0 -0
  118. package/dist/dashboard/precompressed-assets.json +57 -57
  119. package/migrations/README.md +52 -52
  120. package/migrations/lite-sqlite3/0000_baseline.sql +2 -2
  121. package/package.json +96 -95
  122. package/scripts/cli/admin.ts +125 -0
  123. package/scripts/cli/autostart.ts +237 -0
  124. package/scripts/cli/{ui/background.ts → background.ts} +1 -1
  125. package/scripts/cli/{command/console-menu.ts → console-menu.ts} +275 -248
  126. package/scripts/cli/doctor.ts +37 -0
  127. package/scripts/cli/{support/env.ts → env.ts} +99 -99
  128. package/scripts/cli/{gateway/spawn.ts → gateway.ts} +112 -24
  129. package/scripts/cli/instance.ts +224 -0
  130. package/scripts/cli/main.ts +88 -18
  131. package/scripts/cli/{support/platform.ts → platform.ts} +64 -64
  132. package/scripts/cli/port.ts +100 -0
  133. package/scripts/cli/{support/services.ts → services.ts} +88 -88
  134. package/scripts/cli/{command/setup.ts → setup.ts} +35 -34
  135. package/scripts/cli/{command/start.ts → start.ts} +10 -5
  136. package/scripts/cli/{support/startup-signal.ts → startup-signal.ts} +23 -23
  137. package/scripts/cli/tray/index.ts +62 -12
  138. package/scripts/cli/{command/tray.ts → tray.ts} +208 -208
  139. package/scripts/cli/update.ts +40 -0
  140. package/scripts/cli/version.ts +67 -0
  141. package/scripts/cli/{ui/wordmark.ts → wordmark.ts} +58 -58
  142. package/src/boot/app.ts +11 -7
  143. package/src/boot/dependencies.ts +50 -19
  144. package/src/boot/timeout.ts +2 -13
  145. package/src/config/cooldowns.ts +186 -138
  146. package/src/config/network.ts +8 -14
  147. package/src/config/providers.ts +7 -10
  148. package/src/config/runtime.ts +76 -76
  149. package/src/config/security.ts +15 -0
  150. package/src/config/spec.ts +52 -7
  151. package/src/config/telemetry.ts +26 -0
  152. package/src/config/timeouts.ts +0 -6
  153. package/src/console/admin/model-abuse/contracts.ts +28 -6
  154. package/src/console/admin/model-abuse/routes.ts +56 -3
  155. package/src/console/api-keys/bucket-spend.ts +15 -4
  156. package/src/console/api-keys/contracts.ts +410 -414
  157. package/src/console/api-keys/routes.ts +881 -880
  158. package/src/console/auth/credentials.ts +3 -3
  159. package/src/console/auth/setup-service.ts +2 -2
  160. package/src/console/backup/contracts.ts +15 -2
  161. package/src/console/backup/export-endpoint.ts +6 -2
  162. package/src/console/backup/nine-router.ts +2 -5
  163. package/src/console/backup/service.ts +4 -0
  164. package/src/console/backup/store.ts +192 -29
  165. package/src/console/backup/validate.ts +3 -45
  166. package/src/console/cli-tools/fs-ops.ts +487 -487
  167. package/src/console/console-router-guards.ts +1 -1
  168. package/src/console/console-router.ts +8 -0
  169. package/src/console/data-management/contracts.ts +100 -0
  170. package/src/console/data-management/operations.ts +234 -0
  171. package/src/console/data-management/routes.ts +103 -0
  172. package/src/console/domain-registration.ts +45 -1
  173. package/src/console/observability/contracts.ts +524 -485
  174. package/src/console/observability/live.ts +22 -0
  175. package/src/console/observability/store.ts +1138 -1018
  176. package/src/console/observability/usage-dimensions.ts +4 -4
  177. package/src/console/providers/catalog/catalog-projections.ts +6 -7
  178. package/src/console/providers/catalog/contracts.ts +6 -0
  179. package/src/console/providers/catalog/model-operations.ts +5 -2
  180. package/src/console/providers/catalog/provider-operations.ts +24 -4
  181. package/src/console/providers/catalog/public-model-store.ts +8 -11
  182. package/src/console/providers/catalog/routes.ts +7 -0
  183. package/src/console/providers/catalog/store.ts +1553 -1532
  184. package/src/console/providers/oauth/routes.ts +8 -12
  185. package/src/console/quota/account-quota-shared.ts +52 -39
  186. package/src/console/quota/account-quota-tenant.ts +6 -6
  187. package/src/console/quota/quota-view.ts +1 -1
  188. package/src/console/quota/refresh.ts +441 -429
  189. package/src/console/remote-support/contracts.ts +61 -0
  190. package/src/console/remote-support/routes.ts +65 -0
  191. package/src/console/remote-support/service.ts +207 -0
  192. package/src/console/response-compression.ts +0 -3
  193. package/src/console/routing/pools/import-parse.ts +206 -0
  194. package/src/console/routing/pools/operations.ts +543 -501
  195. package/src/console/routing/pools/probe-result.ts +3 -2
  196. package/src/console/routing/pools/probe-verdict.ts +132 -132
  197. package/src/console/routing/pools/routes.ts +181 -164
  198. package/src/console/routing/pools/store.ts +8 -8
  199. package/src/console/settings/contracts.ts +95 -2
  200. package/src/console/settings/store.ts +81 -3
  201. package/src/console/shared/cursor.ts +8 -8
  202. package/src/console/shared/errors.ts +1 -3
  203. package/src/console/updates/routes.ts +16 -0
  204. package/src/main.ts +10 -0
  205. package/src/network/http2-bidi.ts +2 -4
  206. package/src/network/pool/agent.ts +478 -478
  207. package/src/network/pool/selector.ts +5 -4
  208. package/src/network/retry.ts +1 -2
  209. package/src/network/ssrf.ts +5 -4
  210. package/src/observability/event-compaction.ts +317 -0
  211. package/src/observability/event-day-index.ts +440 -0
  212. package/src/observability/event-log.ts +155 -6
  213. package/src/observability/logger.ts +6 -6
  214. package/src/observability/payload-capture.ts +158 -33
  215. package/src/observability/payload-store.ts +82 -61
  216. package/src/observability/redaction.ts +5 -6
  217. package/src/observability/request-notify.ts +43 -0
  218. package/src/observability/telemetry-status.ts +8 -9
  219. package/src/observability/token-speed.ts +5 -4
  220. package/src/persistence/db-mode.ts +7 -7
  221. package/src/persistence/health-event-flush.ts +36 -0
  222. package/src/persistence/readiness.ts +1 -1
  223. package/src/persistence/schema.ts +75 -29
  224. package/src/persistence/shared-cache.ts +1 -2
  225. package/src/persistence/stores/api-key-store.ts +18 -6
  226. package/src/persistence/stores/share-store.ts +4 -3
  227. package/src/persistence/stores/telemetry-store.ts +33 -11
  228. package/src/protocol/canonical-model.ts +11 -3
  229. package/src/protocol/codecs.ts +139 -6
  230. package/src/protocol/gateway-error.ts +6 -4
  231. package/src/protocol/image-clamp.ts +19 -1
  232. package/src/protocol/registry.ts +8 -0
  233. package/src/protocol/request/chat.ts +21 -1
  234. package/src/protocol/request/codex.ts +10 -1
  235. package/src/protocol/request/gemini.ts +9 -2
  236. package/src/protocol/request/messages.ts +18 -7
  237. package/src/protocol/request/responses.ts +9 -1
  238. package/src/protocol/response/chat.ts +34 -4
  239. package/src/protocol/response/codex.ts +7 -4
  240. package/src/protocol/response/dsml.ts +140 -28
  241. package/src/protocol/response/responses.ts +11 -5
  242. package/src/protocol/stream-error-frames.ts +4 -5
  243. package/src/providers/adapters/compatible-adapter.ts +3 -0
  244. package/src/providers/authentication/jwt-validator.ts +9 -10
  245. package/src/providers/authentication/oauth-refresh-service.ts +9 -11
  246. package/src/providers/bundled-discovery-providers.ts +122 -119
  247. package/src/providers/configured-openai-providers.ts +4 -4
  248. package/src/providers/default-registry.ts +5 -4
  249. package/src/providers/discovery/catalog/catalog-blob.ts +18 -18
  250. package/src/providers/discovery/catalog/catalog-format.ts +3 -6
  251. package/src/providers/discovery/models-dev-catalog.ts +354 -354
  252. package/src/providers/discovery/openai-model-discovery.ts +5 -0
  253. package/src/providers/discovery/openrouter-free-roster.ts +15 -17
  254. package/src/providers/discovery/probe-phases.ts +4 -4
  255. package/src/providers/discovery/probe-wire.ts +6 -13
  256. package/src/providers/discovery/probing-service.ts +1117 -1045
  257. package/src/providers/integrations/alibabacp/alibabacp.ts +1 -1
  258. package/src/providers/integrations/antigravity/antigravity-quota.ts +5 -7
  259. package/src/providers/integrations/buddy/buddy-chat-shared.ts +23 -2
  260. package/src/providers/integrations/buddy/buddy-checkin.ts +1 -4
  261. package/src/providers/integrations/buddy/buddy-cli-headers.ts +23 -6
  262. package/src/providers/integrations/buddy/buddy-conversation-headers.ts +0 -1
  263. package/src/providers/integrations/buddy/buddy-identity.ts +52 -0
  264. package/src/providers/integrations/buddy/buddy-media.ts +2 -2
  265. package/src/providers/integrations/buddy/buddy-usage-store.ts +3 -4
  266. package/src/providers/integrations/buddy/codebuddy/quota.ts +2 -13
  267. package/src/providers/integrations/buddy/codebuddy/shared.ts +118 -115
  268. package/src/providers/integrations/buddy/workbuddy/quota.ts +0 -11
  269. package/src/providers/integrations/buddy/workbuddy/shared.ts +5 -2
  270. package/src/providers/integrations/claude/claude.ts +608 -608
  271. package/src/providers/integrations/cline/cline-quota.ts +27 -20
  272. package/src/providers/integrations/cline/cline.ts +537 -539
  273. package/src/providers/integrations/codex/codex-headers.ts +25 -39
  274. package/src/providers/integrations/codex/codex-oauth.ts +1 -1
  275. package/src/providers/integrations/codex/codex.ts +2 -29
  276. package/src/providers/integrations/commandcode/commandcode.ts +674 -675
  277. package/src/providers/integrations/commandcode/discovery.ts +117 -116
  278. package/src/providers/integrations/cursor/cursor-auth.ts +418 -418
  279. package/src/providers/integrations/cursor/cursor-quota.ts +309 -0
  280. package/src/providers/integrations/cursor/cursor-request.ts +616 -471
  281. package/src/providers/integrations/cursor/cursor-session-manager.ts +319 -281
  282. package/src/providers/integrations/cursor/cursor.ts +1333 -960
  283. package/src/providers/integrations/cursor/protocol/messages.ts +25 -0
  284. package/src/providers/integrations/factory/factory-models.ts +3 -5
  285. package/src/providers/integrations/factory/factory.ts +8 -1
  286. package/src/providers/integrations/gemini.ts +6 -1
  287. package/src/providers/integrations/grok/grok-install-id.ts +2 -3
  288. package/src/providers/integrations/grok/grok-quota.ts +279 -177
  289. package/src/providers/integrations/grok/grok-ratelimit-store.ts +73 -0
  290. package/src/providers/integrations/grok/grok-ratelimit.ts +218 -0
  291. package/src/providers/integrations/grok/grok.ts +6 -7
  292. package/src/providers/integrations/hermes.ts +14 -18
  293. package/src/providers/integrations/kiro/kiro-discovery.ts +1 -1
  294. package/src/providers/integrations/kiro/kiro.ts +1 -1
  295. package/src/providers/integrations/mistral.ts +6 -6
  296. package/src/providers/integrations/opencode/fingerprint.ts +3 -3
  297. package/src/providers/integrations/opencode/provider.ts +19 -20
  298. package/src/providers/integrations/qoder/qoder-catalog.ts +450 -450
  299. package/src/providers/integrations/qoder/qoder-oauth.ts +388 -310
  300. package/src/providers/integrations/qoder/qoder-quota.ts +192 -0
  301. package/src/providers/integrations/qoder/qoder.ts +884 -865
  302. package/src/providers/integrations/shared/connect.ts +6 -6
  303. package/src/providers/integrations/trae/trae-catalog.ts +1 -2
  304. package/src/providers/integrations/trae/trae-quota.ts +384 -0
  305. package/src/providers/integrations/trae/trae.ts +2 -2
  306. package/src/providers/integrations/zcode/oauth.ts +6 -1
  307. package/src/providers/operations/account-health-service.ts +75 -27
  308. package/src/providers/operations/account-reset-service.ts +1 -1
  309. package/src/providers/operations/client-versions.ts +6 -6
  310. package/src/providers/operations/custom-cli-headers.ts +3 -4
  311. package/src/providers/operations/provider-catalog-seeder.ts +95 -95
  312. package/src/providers/operations/provider-catalog-service.ts +3 -4
  313. package/src/providers/provider-contracts.ts +7 -0
  314. package/src/providers/provider-metadata.ts +10 -8
  315. package/src/providers/provider-registry.ts +6 -7
  316. package/src/providers/provider-usage.ts +1 -1
  317. package/src/providers/quota/quota-support.ts +8 -0
  318. package/src/security/abuse.ts +15 -10
  319. package/src/security/access-control.ts +144 -146
  320. package/src/security/admission/contracts.ts +11 -0
  321. package/src/security/admission/in-memory-store.ts +4 -3
  322. package/src/security/admission/lease-sweep.ts +6 -6
  323. package/src/security/admission/reasons.ts +5 -1
  324. package/src/security/admission/redis-store.ts +211 -210
  325. package/src/security/admission/service.ts +4 -0
  326. package/src/security/api-key-auth.ts +27 -17
  327. package/src/security/ip-boundary.ts +155 -15
  328. package/src/security/model-abuse.ts +366 -94
  329. package/src/security/outbound-headers.ts +3 -4
  330. package/src/shared/sleep.ts +4 -0
  331. package/src/transport/dispatch/attempt-finalize.ts +47 -5
  332. package/src/transport/dispatch/attempt-loop.ts +531 -530
  333. package/src/transport/dispatch/generation.ts +6 -0
  334. package/src/transport/dispatch/proxy-request.ts +24 -1
  335. package/src/transport/dispatch/ratelimit-capture.ts +37 -0
  336. package/src/transport/dispatch/stream-bridge.ts +1 -3
  337. package/src/transport/dispatch/streaming/finalize.ts +1 -1
  338. package/src/transport/dispatch/streaming/stream.ts +19 -6
  339. package/src/transport/dispatch/upstream.ts +9 -0
  340. package/src/transport/failure-policy.ts +162 -8
  341. package/src/transport/guards/auth-guard.ts +7 -4
  342. package/src/transport/guards/concurrency-guard.ts +0 -5
  343. package/src/transport/guards/error-lifecycle.ts +13 -6
  344. package/src/transport/guards/pipeline.ts +15 -3
  345. package/src/transport/guards/request-context.ts +8 -2
  346. package/src/transport/request/inflight.ts +5 -6
  347. package/src/transport/request/preparer.ts +9 -8
  348. package/src/transport/request/state.ts +21 -8
  349. package/src/transport/request/tenant-preferences.ts +60 -0
  350. package/src/transport/routing/admission.ts +4 -0
  351. package/src/transport/routing/route-catalog.ts +65 -10
  352. package/src/transport/routing/route-model.ts +46 -14
  353. package/src/transport/routing/router.ts +21 -14
  354. package/src/transport/surface/completion.ts +2 -3
  355. package/src/transport/surface/dialects.ts +5 -6
  356. package/src/transport/surface/responses/encode.ts +38 -0
  357. package/src/transport/surface/responses/parse.ts +4 -0
  358. package/src/transport/translation/capabilities.ts +2 -3
  359. package/src/transport/translation/quirks.ts +3 -3
  360. package/src/workers/buddy-growth-tasks.ts +5 -10
  361. package/src/workers/daily-checkin.ts +17 -23
  362. package/src/workers/quota-refresh-sweep.ts +26 -26
  363. package/dist/dashboard/assets/BannedPage-hfCzi-Ux.js.gz +0 -0
  364. package/dist/dashboard/assets/CliToolDetailPage-DLGqYEwI.js +0 -1
  365. package/dist/dashboard/assets/CliToolDetailPage-DLGqYEwI.js.gz +0 -0
  366. package/dist/dashboard/assets/CliToolsPage-DEDyKBlC.js.gz +0 -0
  367. package/dist/dashboard/assets/CombosPage-CfuYiQ8Z.js +0 -1
  368. package/dist/dashboard/assets/CombosPage-CfuYiQ8Z.js.gz +0 -0
  369. package/dist/dashboard/assets/ConsoleLogPage-bsLuZmYH.js.gz +0 -0
  370. package/dist/dashboard/assets/CustomizationPage-_UWhwziJ.js.gz +0 -0
  371. package/dist/dashboard/assets/HealthEventsModal-Cjfc5WFS.js +0 -1
  372. package/dist/dashboard/assets/HealthEventsModal-Cjfc5WFS.js.gz +0 -0
  373. package/dist/dashboard/assets/LoginPage-DDVIfZGW.js.gz +0 -0
  374. package/dist/dashboard/assets/ModelPicker-CWkTUtpX.js +0 -1
  375. package/dist/dashboard/assets/ModelPicker-CWkTUtpX.js.gz +0 -0
  376. package/dist/dashboard/assets/OverviewPage-B53YYMng.js +0 -1
  377. package/dist/dashboard/assets/OverviewPage-B53YYMng.js.gz +0 -0
  378. package/dist/dashboard/assets/ProviderDetailPage-B0KaAQf0.js +0 -3
  379. package/dist/dashboard/assets/ProviderDetailPage-B0KaAQf0.js.gz +0 -0
  380. package/dist/dashboard/assets/ProvidersPage-Dx31DgIO.js +0 -1
  381. package/dist/dashboard/assets/ProvidersPage-Dx31DgIO.js.gz +0 -0
  382. package/dist/dashboard/assets/ProxyPage-BpnR4kXT.js +0 -7
  383. package/dist/dashboard/assets/ProxyPage-BpnR4kXT.js.gz +0 -0
  384. package/dist/dashboard/assets/QuotaPage-BpOQCnun.js +0 -1
  385. package/dist/dashboard/assets/QuotaPage-BpOQCnun.js.gz +0 -0
  386. package/dist/dashboard/assets/SettingsPage-DV1eo2J_.js +0 -1
  387. package/dist/dashboard/assets/SettingsPage-DV1eo2J_.js.gz +0 -0
  388. package/dist/dashboard/assets/SetupPage-D6pmQ86L.js.gz +0 -0
  389. package/dist/dashboard/assets/SortableList-DWvKiuSl.js.gz +0 -0
  390. package/dist/dashboard/assets/StudioPage-CmkUrgdj.js.gz +0 -0
  391. package/dist/dashboard/assets/UsagePage-B47uOydr.js +0 -56
  392. package/dist/dashboard/assets/UsagePage-B47uOydr.js.gz +0 -0
  393. package/dist/dashboard/assets/api-keys-uNywYlfv.js +0 -1
  394. package/dist/dashboard/assets/api-keys-uNywYlfv.js.gz +0 -0
  395. package/dist/dashboard/assets/app-DxivpfYG.css +0 -2
  396. package/dist/dashboard/assets/app-DxivpfYG.css.gz +0 -0
  397. package/dist/dashboard/assets/assert-NHwYq95t.js.gz +0 -0
  398. package/dist/dashboard/assets/badge-6uzDDvqs.js.gz +0 -0
  399. package/dist/dashboard/assets/banner-4_WNg2WZ.js.gz +0 -0
  400. package/dist/dashboard/assets/card--AtFedAq.js.gz +0 -0
  401. package/dist/dashboard/assets/cli-tools-B3e7dx3N.js.gz +0 -0
  402. package/dist/dashboard/assets/clipboard-button-NreUx6c8.js.gz +0 -0
  403. package/dist/dashboard/assets/dialog-BtpsJU98.js +0 -1
  404. package/dist/dashboard/assets/dialog-BtpsJU98.js.gz +0 -0
  405. package/dist/dashboard/assets/download-A0SW9OCW.js +0 -1
  406. package/dist/dashboard/assets/download-A0SW9OCW.js.gz +0 -0
  407. package/dist/dashboard/assets/drawer-WwwtzihF.js +0 -1
  408. package/dist/dashboard/assets/drawer-WwwtzihF.js.gz +0 -0
  409. package/dist/dashboard/assets/entry-CkryrIea.js +0 -4
  410. package/dist/dashboard/assets/entry-CkryrIea.js.gz +0 -0
  411. package/dist/dashboard/assets/entry-TJIye_jT.js +0 -1
  412. package/dist/dashboard/assets/entry-TJIye_jT.js.gz +0 -0
  413. package/dist/dashboard/assets/entry-jy9_4-Ke.js +0 -1
  414. package/dist/dashboard/assets/entry-jy9_4-Ke.js.gz +0 -0
  415. package/dist/dashboard/assets/fingerprint-pattern-BWuHtbW4.js.gz +0 -0
  416. package/dist/dashboard/assets/github-badge-D_eM3IgG.js.gz +0 -0
  417. package/dist/dashboard/assets/index-BydyKaDz.js.gz +0 -0
  418. package/dist/dashboard/assets/inline-BcNGF8Gr.js.gz +0 -0
  419. package/dist/dashboard/assets/input-BG41et9G.js.gz +0 -0
  420. package/dist/dashboard/assets/layout-C8mer_Ak.js +0 -1
  421. package/dist/dashboard/assets/layout-C8mer_Ak.js.gz +0 -0
  422. package/dist/dashboard/assets/list-row-Gh_W4OR_.js.gz +0 -0
  423. package/dist/dashboard/assets/live-BJGYRBnR.js.gz +0 -0
  424. package/dist/dashboard/assets/model-bans-DeVLDA7I.js +0 -1
  425. package/dist/dashboard/assets/model-bans-DeVLDA7I.js.gz +0 -0
  426. package/dist/dashboard/assets/mutation-BkoLM5Ba.js.gz +0 -0
  427. package/dist/dashboard/assets/numbers-Bs4LOOuf.js +0 -2
  428. package/dist/dashboard/assets/numbers-Bs4LOOuf.js.gz +0 -0
  429. package/dist/dashboard/assets/query-keys-D7wX2vM5.js.gz +0 -0
  430. package/dist/dashboard/assets/routing-CcusqXA7.js.gz +0 -0
  431. package/dist/dashboard/assets/select-BO6h8yNv.js.gz +0 -0
  432. package/dist/dashboard/assets/settings-mpmXTgUa.js.gz +0 -0
  433. package/dist/dashboard/assets/sparkles-BRaRUxAU.js +0 -1
  434. package/dist/dashboard/assets/sparkles-BRaRUxAU.js.gz +0 -0
  435. package/dist/dashboard/assets/stack-DwLmjUFK.js.gz +0 -0
  436. package/dist/dashboard/assets/state-DYdQmdAl.js.gz +0 -0
  437. package/dist/dashboard/assets/switch-CidSfbG7.js +0 -1
  438. package/dist/dashboard/assets/switch-CidSfbG7.js.gz +0 -0
  439. package/dist/dashboard/assets/terminal-Bd4ee8TA.js +0 -1
  440. package/dist/dashboard/assets/terminal-Bd4ee8TA.js.gz +0 -0
  441. package/dist/dashboard/assets/tokens-DDxMbSRL.js +0 -1
  442. package/dist/dashboard/assets/tokens-DDxMbSRL.js.gz +0 -0
  443. package/dist/dashboard/assets/use-presence-BSZEBx7U.js +0 -1
  444. package/dist/dashboard/assets/use-presence-BSZEBx7U.js.gz +0 -0
  445. package/dist/dashboard/assets/useMutation-w4u4CVue.js.gz +0 -0
  446. package/scripts/cli/command/admin.ts +0 -65
  447. package/scripts/cli/command/doctor.ts +0 -257
  448. package/scripts/cli/command/update.ts +0 -23
  449. package/scripts/cli/gateway/drain-token.ts +0 -78
  450. package/scripts/cli/gateway/instance-lock.ts +0 -105
  451. package/scripts/cli/gateway/stop.ts +0 -51
  452. package/scripts/cli/support/bun.ts +0 -27
  453. package/scripts/cli/support/version.ts +0 -37
  454. /package/dist/dashboard/assets/{app-CafWtVXA.js → app-BIt446yE.js} +0 -0
  455. /package/dist/dashboard/assets/{app-CafWtVXA.js.gz → app-BIt446yE.js.gz} +0 -0
  456. /package/scripts/cli/{ui/prompt.ts → prompt.ts} +0 -0
@@ -31,12 +31,12 @@ const FRAME_BUFFER_SPARE_BYTES = 1024 * 1024;
31
31
  /**
32
32
  * Growable byte buffer for reassembling Connect frames across stream reads.
33
33
  *
34
- * Streaming callers used to prepend the unconsumed tail to every read with
35
- * `Buffer.concat([pending, chunk])`, which copies the entire partial frame on
36
- * every chunk. A frame that spans many reads (the payload cap allows up to 16 MiB)
37
- * therefore cost O(n²) copying and peaked at twice the frame size. Appending
38
- * into spare capacity costs one copy per byte instead, and `consume` only
39
- * advances an offset, so dropping parsed bytes moves nothing.
34
+ * Appending into spare capacity costs one copy per byte, where prepending the
35
+ * unconsumed tail to every read (`Buffer.concat([pending, chunk])`) copies the
36
+ * entire partial frame on every chunk. A frame that spans many reads (the
37
+ * payload cap allows up to 16 MiB) therefore costs O(n²) copying and peaks at
38
+ * twice the frame size. `consume` only advances an offset, so dropping parsed
39
+ * bytes moves nothing.
40
40
  *
41
41
  * The view returned by {@link view} is a subarray of the backing store and
42
42
  * stays valid until the next {@link append}, which may relocate the bytes.
@@ -71,8 +71,7 @@ const TRAE_CN_ROWS: readonly TraeModelRow[] = [
71
71
  * are rejected here with `4001 param is invalid`, and the agent ids are
72
72
  * rejected there with `4023 model is unknown`. There is no call that serves
73
73
  * both, so a single-surface catalog is the only honest one; offering the chat
74
- * ids would advertise models this provider's dispatch cannot reach. See
75
- * `documentation/research/trae-agent-vs-chat-surface.md`.
74
+ * ids would advertise models this provider's dispatch cannot reach.
76
75
  *
77
76
  * `ctx` is the **selectable maximum** (`context_window_tokens.max`), not the
78
77
  * active `dev` window. Upstream advertises both and they differ by up to 5x
@@ -0,0 +1,384 @@
1
+ // Trae quota.
2
+ //
3
+ // Two surfaces, on two different hosts, and neither is optional if the account
4
+ // is to be read honestly:
5
+ //
6
+ // - `get_session_usage` on the **core** host is the running meter — what the
7
+ // account has left of its advanced/premium request buckets and its dollar
8
+ // balances.
9
+ // - `ide_user_ent_usage` on the **pay** host is the entitlement: which package
10
+ // the account holds and when it lapses.
11
+ //
12
+ // The donor reads both and lets either fail without the other, and that
13
+ // tolerance is load-bearing here too: entitlement is a *pay* host, which is the
14
+ // first thing a region-blocked or unpaid account loses. Letting a dead pay host
15
+ // fail the whole read would report "no quota" for an account whose session
16
+ // meter is perfectly readable — and "no quota" is what the router parks on.
17
+ //
18
+ // Headers come from `buildTraeHeaders` rather than being rebuilt here: the
19
+ // device fingerprint is derived from the uid, and an account that reports one
20
+ // device here and another at dispatch is exactly the correlation this gateway
21
+ // exists to avoid.
22
+
23
+ import type {
24
+ FetchLike,
25
+ ProviderQuotaResult,
26
+ ProviderQuotaWindow,
27
+ } from "../../quota/quota-contracts";
28
+ import {
29
+ cleanError,
30
+ isoDate,
31
+ number,
32
+ percentWindow,
33
+ record,
34
+ text,
35
+ } from "../../quota/quota-contracts";
36
+ import type { QuotaCollectionContext } from "../../quota/quota-support";
37
+ import { decodeJwtPayload } from "../../authentication/oauth-flow-store";
38
+ import { buildTraeHeaders } from "./trae-headers";
39
+ import {
40
+ TRAE_ENT_USAGE_PATH,
41
+ TRAE_REGION_CORE_HOSTS,
42
+ TRAE_REALMS,
43
+ TRAE_SESSION_USAGE_PATH,
44
+ type TraeRealm,
45
+ } from "./trae-realm";
46
+
47
+ /**
48
+ * The buckets `remain_usage` carries, in the order the donor reads them.
49
+ *
50
+ * `advanced_model` and `premium_model_*` are request counts; `dollar_basic` and
51
+ * `dollar_bonus` are spend balances. They are separate budgets and must never
52
+ * be summed — a dollar balance is not a request count.
53
+ */
54
+ const REQUEST_BUCKETS: readonly (readonly [string, string])[] = [
55
+ ["advanced_model", "Advanced model requests"],
56
+ ["premium_model_fast_request", "Premium fast requests"],
57
+ ["premium_model_slow_request", "Premium slow requests"],
58
+ ];
59
+
60
+ const DOLLAR_BUCKETS: readonly (readonly [string, string])[] = [
61
+ ["dollar_basic", "Basic spend"],
62
+ ["dollar_bonus", "Bonus spend"],
63
+ ];
64
+
65
+ interface TraeQuotaAuth {
66
+ readonly realm: TraeRealm;
67
+ readonly uid: string;
68
+ /** Provisioned region (`sg`, `us`), which selects the core host. */
69
+ readonly region: string;
70
+ }
71
+
72
+ /**
73
+ * Reads the account's realm and uid for the quota call.
74
+ *
75
+ * Both come from `auth_state`, the same place dispatch reads them: a Trae
76
+ * token is realm-bound, so guessing `intl` for a CN account sends it to a host
77
+ * that will not authenticate it.
78
+ */
79
+ function quotaAuth(context: QuotaCollectionContext | undefined): TraeQuotaAuth {
80
+ const raw = context?.auth_state ?? {};
81
+ // `realm` is not persisted by the OAuth client — it is a property of which
82
+ // provider id the account was registered under, so the CN wrapper forces it
83
+ // rather than trusting an absent field.
84
+ const realm = raw["realm"] === "cn" || raw["region"] === "CN" ? "cn" : "intl";
85
+ // The OAuth client stores the account id as `userId`; `uid` is the name the
86
+ // dispatch header builder uses. Reading only one of them left the device
87
+ // fingerprint empty for every account.
88
+ const uidRaw = raw["uid"] ?? raw["userId"] ?? raw["user_id"];
89
+ const uid = typeof uidRaw === "string" && uidRaw.trim() ? uidRaw.trim() : "";
90
+ // `region` is provisioned, not chosen: an intl account is created into one
91
+ // and its token only authenticates against that region's core host. `us` and
92
+ // `cn` are served by the realm's default host, but `sg` is a different host
93
+ // entirely — sending it to the US core answers 401 code 1001.
94
+ const region = typeof raw["region"] === "string" ? raw["region"].trim().toLowerCase() : "";
95
+ return { realm, uid, region };
96
+ }
97
+
98
+ /**
99
+ * The account id out of the access token itself.
100
+ *
101
+ * The device fingerprint is derived from the uid, so an empty one presents no
102
+ * device at all — and this surface correlates the credential against its
103
+ * device, answering `1001` for a token that is perfectly valid. The JWT carries
104
+ * the id, so it is read there when the auth state does not have it.
105
+ */
106
+ function uidFromToken(credential: string): string | undefined {
107
+ const payload = decodeJwtPayload(credential);
108
+ if (!payload) return undefined;
109
+ // The id is nested: this token's payload is `{data:{id,type,tenant_id},exp,
110
+ // iat}` — there is no `sub` and no top-level `userId`, so a flat read found
111
+ // nothing and the request went out with no device identity at all.
112
+ for (const source of [payload, record(payload["data"])]) {
113
+ if (!source) continue;
114
+ for (const key of ["userId", "user_id", "uid", "sub", "id"] as const) {
115
+ const value = source[key];
116
+ if (typeof value === "string" && value.trim()) return value.trim();
117
+ if (typeof value === "number" && Number.isFinite(value)) return String(value);
118
+ }
119
+ }
120
+ return undefined;
121
+ }
122
+
123
+ /** One bucket of `remain_usage`, paired with its cap from the sibling object. */
124
+ function bucketWindow(
125
+ label: string,
126
+ remaining: number | null,
127
+ limit: number | null,
128
+ resetsAt: string | null,
129
+ ): ProviderQuotaWindow | null {
130
+ if (remaining === null && limit === null) return null;
131
+ const used = remaining !== null && limit !== null ? limit - remaining : null;
132
+ return percentWindow(
133
+ `trae:${label}`,
134
+ label,
135
+ null,
136
+ resetsAt,
137
+ used,
138
+ limit,
139
+ // Independent: each bucket is its own budget. Summing a request count into
140
+ // a dollar balance would produce a number that means nothing.
141
+ "independent",
142
+ );
143
+ }
144
+
145
+ /**
146
+ * Pulls `remaining`/`limit` for one key out of either usage object.
147
+ *
148
+ * `remain_usage` carries a **bare number** per bucket (`advanced_model: 40`),
149
+ * not a nested object — the caps live in the sibling `usage` object. Reading a
150
+ * nested `remaining` here is what dropped every balance and left a limit with
151
+ * no remaining, i.e. a window that cannot say how much is left.
152
+ */
153
+ function bucketValues(
154
+ source: Record<string, unknown>,
155
+ key: string,
156
+ ): { readonly remaining: number | null; readonly limit: number | null } {
157
+ const direct = record(source[key]);
158
+ if (direct) {
159
+ return {
160
+ remaining: number(direct["remaining"] ?? direct["remain"] ?? direct["remain_usage"]),
161
+ limit: number(direct["limit"] ?? direct["total"] ?? direct["cap"]),
162
+ };
163
+ }
164
+ return {
165
+ remaining: number(source[key] ?? source[`${key}_remaining`]),
166
+ limit: number(source[`${key}_limit`] ?? source[`${key}_total`]),
167
+ };
168
+ }
169
+
170
+ /**
171
+ * The session-usage meter: `remain_usage` holds the remaining balance per
172
+ * bucket, with the caps alongside it on the same object.
173
+ */
174
+ function parseSessionUsage(payload: unknown): {
175
+ readonly windows: readonly ProviderQuotaWindow[];
176
+ readonly plan: string | null;
177
+ } {
178
+ // Deliberately *not* `quotaRecord`: it unwraps a `usage` envelope, and on
179
+ // this surface `usage` is the sibling object holding the caps — unwrapping it
180
+ // discards `remain_usage` entirely and leaves a limit with no remaining.
181
+ const root = record(payload) ?? {};
182
+ const windows: ProviderQuotaWindow[] = [];
183
+ // The meter is nested under `data` on some realms and at the root on others;
184
+ // both shapes are live, so look in each rather than picking one.
185
+ const candidates = [root, record(root["data"]), record(root["quota"])].filter(
186
+ (value): value is Record<string, unknown> => value !== null,
187
+ );
188
+ const resetsAt = candidates
189
+ .map((value) => isoDate(value["reset_time"] ?? value["resetTime"] ?? value["next_reset_time"]))
190
+ .find((value) => value !== null) ?? null;
191
+
192
+ for (const source of candidates) {
193
+ const remain = record(source["remain_usage"]) ?? source;
194
+ const usage = record(source["usage"]) ?? {};
195
+ for (const [key, label] of [...REQUEST_BUCKETS, ...DOLLAR_BUCKETS]) {
196
+ // Remaining is the bare number under `remain_usage`; the cap is the
197
+ // sibling object under `usage`. Two objects, one per side of the same
198
+ // budget, so both are read before a window is built.
199
+ const remaining = bucketValues(remain, key).remaining ?? number(usage[key]);
200
+ const limit = bucketValues(usage, key).limit ?? bucketValues(remain, key).limit;
201
+ const window = bucketWindow(label, remaining, limit, resetsAt);
202
+ if (window) windows.push(window);
203
+ }
204
+ if (windows.length > 0) break;
205
+ }
206
+ const plan = candidates
207
+ .map((value) => text(value["plan_name"] ?? value["planName"] ?? value["user_type"]))
208
+ .find((value) => value !== null) ?? null;
209
+ return { windows, plan };
210
+ }
211
+
212
+ /** The entitlement package name and lapse, from the pay host. */
213
+ function parseEntitlement(payload: unknown): {
214
+ readonly windows: readonly ProviderQuotaWindow[];
215
+ readonly plan: string | null;
216
+ } {
217
+ const root = record(payload) ?? {};
218
+ const data = record(root["data"]) ?? root;
219
+ const items = Array.isArray(data["entitlement_list"])
220
+ ? data["entitlement_list"]
221
+ : Array.isArray(data["list"])
222
+ ? data["list"]
223
+ : [];
224
+ const windows: ProviderQuotaWindow[] = [];
225
+ let plan: string | null = null;
226
+ for (const entry of items) {
227
+ const item = record(entry);
228
+ if (!item) continue;
229
+ const name = text(item["ent_title"] ?? item["name"] ?? item["package_name"]);
230
+ if (!plan && name) plan = name;
231
+ const total = number(item["total"] ?? item["quota"] ?? item["advanced_model_request_limit"]);
232
+ const remaining = number(item["remain"] ?? item["remaining"] ?? item["remain_usage"]);
233
+ const resetsAt = isoDate(item["end_time"] ?? item["endTime"] ?? item["expires_at"]);
234
+ const window = bucketWindow(name ?? "Entitlement", remaining, total, resetsAt);
235
+ if (window) windows.push(window);
236
+ }
237
+ return { windows, plan };
238
+ }
239
+
240
+ /**
241
+ * Records a quiet surface where an operator can act on it.
242
+ *
243
+ * Deliberately not the returned `error`: that field is what flags the account
244
+ * `quota_fetch_failed` and cools it. A 403 from the wrong realm, or a pay host
245
+ * that is unreachable, says nothing about whether the account can serve.
246
+ */
247
+ function logTraeQuotaMiss(errors: readonly string[]): void {
248
+ if (errors.length === 0) return;
249
+ console.warn(`[quota] trae surfaces unavailable: ${errors.join("; ")}`);
250
+ }
251
+
252
+ /**
253
+ * Reads one Trae account's quota.
254
+ *
255
+ * The session meter and the entitlement are fetched independently: whichever
256
+ * answers contributes, and a failure is only fatal if *both* fail. An account
257
+ * whose pay host is unreachable still has a readable meter, and reporting
258
+ * "no quota" for it would park an account that can still serve.
259
+ */
260
+ /**
261
+ * The core host for this account.
262
+ *
263
+ * A provisioned region overrides the realm default: an `sg` account sent to the
264
+ * US core is answered 401 code 1001, which looks exactly like a dead token.
265
+ */
266
+ function coreHost(region: string, config: (typeof TRAE_REALMS)[TraeRealm]): string {
267
+ return (region !== "" ? TRAE_REGION_CORE_HOSTS[region] : undefined) ?? config.core;
268
+ }
269
+
270
+ export async function fetchTraeQuota(
271
+ credential: string,
272
+ fetcher: FetchLike,
273
+ context?: QuotaCollectionContext,
274
+ ): Promise<ProviderQuotaResult> {
275
+ const { realm, uid, region } = quotaAuth(context);
276
+ const resolvedUid = uid || uidFromToken(credential) || "";
277
+ const errors: string[] = [];
278
+
279
+ const call = async (
280
+ realm: TraeRealm,
281
+ url: string,
282
+ surface: string,
283
+ ): Promise<unknown> => {
284
+ const response = await fetcher(url, {
285
+ // POST with an empty JSON body, not GET: this is the commercial surface
286
+ // the desktop client calls, and it answers a GET with an error rather
287
+ // than a meter — which read as "no quota" for every account.
288
+ method: "POST",
289
+ headers: {
290
+ // The commercial surface authenticates with `Cloud-IDE-JWT` on both
291
+ // realms.
292
+ ...buildTraeHeaders({ realm, token: credential, uid: resolvedUid }),
293
+ // Applied after the builder, which switches intl to `Bearer` for raw
294
+ // chat: a Bearer here is answered 401 code 1001, so the account shows
295
+ // no windows while its token is perfectly valid.
296
+ authorization: `Cloud-IDE-JWT ${credential}`,
297
+ "content-type": "application/json",
298
+ // The builder pins `us` for every intl account. An `sg` account sent
299
+ // with a US country code is refused with 401 code 1001 — the country
300
+ // code is part of the identity this surface checks, not a hint.
301
+ "store-country-code": region || "us",
302
+ },
303
+ body: "{}",
304
+ });
305
+ if (!response.ok) {
306
+ // 403 here is a verdict about *how we asked*, not about the balance: the
307
+ // commercial surface correlates the credential against its realm and its
308
+ // device fingerprint, and a CN token sent to an intl host — or one sent
309
+ // with no device id — is refused outright. Reporting that as a failed
310
+ // read parks the account, so it is reported as unreadable-for-now and
311
+ // left alone by the cooldown path.
312
+ const body = await response.text().catch(() => "");
313
+ throw new Error(
314
+ response.status === 403
315
+ ? `${surface}: HTTP 403 (realm or device mismatch, not a balance)`
316
+ : `${surface}: HTTP ${response.status}${body ? ` ${body.slice(0, 80)}` : ""}`,
317
+ );
318
+ }
319
+ return await response.json();
320
+ };
321
+
322
+ /**
323
+ * Tries the realm we believe, then the other one.
324
+ *
325
+ * A Trae token is realm-bound and the auth state does not record which — the
326
+ * provider id does, and a collector cannot see it. The wrong host answers 401
327
+ * code 1001, which is indistinguishable from a dead token unless the other
328
+ * realm is tried: one of them accepts, and that is the answer.
329
+ */
330
+ const tryRealms = async (
331
+ path: (config: (typeof TRAE_REALMS)[TraeRealm]) => string,
332
+ surface: string,
333
+ parse: (payload: unknown) => { readonly windows: readonly ProviderQuotaWindow[]; readonly plan: string | null },
334
+ ): Promise<{ readonly windows: readonly ProviderQuotaWindow[]; readonly plan: string | null } | null> => {
335
+ for (const candidate of realm === "cn" ? (["cn", "intl"] as const) : (["intl", "cn"] as const)) {
336
+ try {
337
+ const payload = await call(candidate, path(TRAE_REALMS[candidate]), surface);
338
+ return parse(payload);
339
+ } catch (error) {
340
+ errors.push(`${candidate} ${surface}: ${cleanError(error)}`);
341
+ }
342
+ }
343
+ return null;
344
+ };
345
+
346
+ const session = await tryRealms((config) => coreHost(region, config) + TRAE_SESSION_USAGE_PATH, "session usage", parseSessionUsage);
347
+ const entitlement = await tryRealms((config) => config.pay + TRAE_ENT_USAGE_PATH, "entitlement", parseEntitlement);
348
+
349
+ const windows = [...(session?.windows ?? []), ...(entitlement?.windows ?? [])];
350
+ if (!session && !entitlement) {
351
+ // No window and *no error*: a quota read that could not reach upstream is
352
+ // not evidence about the account, and `error` here is what flags the
353
+ // account `quota_fetch_failed` and cools it down. An unreachable or
354
+ // refusing surface must leave the account alone — otherwise an account is
355
+ // parked by its own monitor, for a reason that has nothing to do with it.
356
+ // The detail is kept for the log instead, where it cannot be read as a
357
+ // balance.
358
+ logTraeQuotaMiss(errors);
359
+ return { source: "trae", plan: null, windows: [], error: null };
360
+ }
361
+ return {
362
+ source: "trae",
363
+ plan: session?.plan ?? entitlement?.plan ?? null,
364
+ windows,
365
+ // A partial read is still a read, and the quiet surface is a diagnostic —
366
+ // never a reason to park the account.
367
+ error: null,
368
+ };
369
+ }
370
+
371
+ /**
372
+ * The `traecn` provider's quota.
373
+ *
374
+ * Same collector, realm forced to `cn`: a CN token is refused by the intl
375
+ * hosts, and the provider id is not something the collector can see — the
376
+ * context carries no provider, only auth state.
377
+ */
378
+ export function fetchTraeCnQuota(
379
+ credential: string,
380
+ fetcher: FetchLike,
381
+ context?: QuotaCollectionContext,
382
+ ): Promise<ProviderQuotaResult> {
383
+ return fetchTraeQuota(credential, fetcher, { ...context, auth_state: { ...(context?.auth_state ?? {}), realm: "cn" } });
384
+ }
@@ -242,7 +242,7 @@ class TraeAdapter implements ProviderAdapter {
242
242
  yield {
243
243
  type: "content_delta",
244
244
  sequence_number: seq++,
245
- content: { kind: "reasoning", payload: flatReasoning, summary: flatReasoning },
245
+ content: { kind: "reasoning", payload: null, summary: flatReasoning },
246
246
  } as CanonicalEvent;
247
247
  }
248
248
  yield* this.#toolCallDeltas(record["tool_calls"], announced, () => seq++);
@@ -273,7 +273,7 @@ class TraeAdapter implements ProviderAdapter {
273
273
  yield {
274
274
  type: "content_delta",
275
275
  sequence_number: seq++,
276
- content: { kind: "reasoning", payload: reasoning, summary: reasoning },
276
+ content: { kind: "reasoning", payload: null, summary: reasoning },
277
277
  } as CanonicalEvent;
278
278
  }
279
279
  if (typeof payload["finish_reason"] === "string") {
@@ -73,7 +73,12 @@ function unwrapEnvelope(body: unknown, operation: string): unknown {
73
73
  const message = nonEmptyTrimmedString(root.msg) ?? `${operation} failed`;
74
74
  // `cartethyia` origin so the dialog shows the provider's own reason
75
75
  // instead of the generic "check the console log" fallback.
76
- throw new GatewayError("authentication_failed", 400, `Z.AI ${message}`, { providerId: "zcode" });
76
+ // 401, not the 400 this used to send: an authentication failure is not a
77
+ // malformed request, and every other `authentication_failed` in the
78
+ // codebase is 401. A 400 told the client to fix its body when the real
79
+ // action is to fix the credential — and HTTP itself reserves 401 for
80
+ // exactly this, so a standard client reads the class from the status.
81
+ throw new GatewayError("authentication_failed", 401, `Z.AI ${message}`, { providerId: "zcode" });
77
82
  }
78
83
  }
79
84
  return root.data ?? body;
@@ -1,6 +1,6 @@
1
1
  import { healthEventBuffer } from "../../persistence/health-event-flush";
2
2
  // Account health: error classification, failure/success recording, recovery and cooldown sweeps.
3
- import { and, desc, eq, inArray, isNotNull, lte, or, sql } from "drizzle-orm";
3
+ import { and, count, desc, eq, inArray, isNotNull, lte, or, sql } from "drizzle-orm";
4
4
  import { affectedRows, pruneExpiredCooldowns } from "../../persistence/dialect";
5
5
  import type { CartethyiaDatabase } from "../../persistence/db-handle";
6
6
  import { isRecord } from "../../protocol/codecs";
@@ -310,9 +310,9 @@ function latestDeadlineMs(...candidates: readonly (number | undefined | null)[])
310
310
  * {@link latestDeadlineMs} cannot express *no* deadline — it floors at 0 — and
311
311
  * the account-wide write has one state that needs it. With
312
312
  * `CARTETHYIA_ACCOUNT_DISABLED_RECOVERY_MS=0` an automatic disable is meant to
313
- * be terminal again, and the classifier correctly reports `retryAt: null`; the
314
- * write used to derive the deadline from `Date.now() + cooldownMs` anyway,
315
- * which stamps *now*. The recovery sweep then treats that elapsed deadline as
313
+ * be terminal again, and the classifier correctly reports `retryAt: null`.
314
+ * Deriving the deadline from `Date.now() + cooldownMs` instead stamps *now*.
315
+ * The recovery sweep then treats that elapsed deadline as
316
316
  * an automatic disable awaiting recovery and returns the row to rotation on its
317
317
  * next tick — the escape hatch enabled exactly the behavior it documents
318
318
  * itself as disabling.
@@ -515,6 +515,15 @@ export function classifyAccountError(
515
515
  lower.includes("free-usage") ||
516
516
  lower.includes("free usage") ||
517
517
  lower.includes("exceeded your current quota") ||
518
+ // Trae answers an exhausted quota with **HTTP 502** and the body `Your
519
+ // requests have exceeded the quota.` The status is what misled the
520
+ // classifier: 502 read as a gateway failure, so this fell through to
521
+ // `server_error` — a 30s cooldown that does not mutate the account, so
522
+ // the account stayed in rotation and every other model on it kept being
523
+ // invoked inside a window the provider had already closed. The body is
524
+ // the verdict; the 502 is incidental. Matched on the phrase, not on the
525
+ // status, which is why this sits in `quotaSignal` ahead of the 5xx arm.
526
+ lower.includes("have exceeded the quota") ||
518
527
  lower.includes("balance exhausted") ||
519
528
  lower.includes("insufficient balance") ||
520
529
  lower.includes("out of credits") ||
@@ -539,11 +548,12 @@ export function classifyAccountError(
539
548
  options?.providerId !== undefined &&
540
549
  BUDDY_PROVIDER_IDS.has(options.providerId.toLowerCase());
541
550
  if (policyBlock) {
551
+ const policyBlockMs = resolveAccountPolicyBlockCooldownMs();
542
552
  return result(
543
553
  "policy_blocked",
544
554
  "cooldown",
545
- resolveAccountPolicyBlockCooldownMs(),
546
- new Date(Date.now() + resolveAccountPolicyBlockCooldownMs()),
555
+ policyBlockMs,
556
+ new Date(Date.now() + policyBlockMs),
547
557
  reason("Provider content-policy block"),
548
558
  origin === "upstream",
549
559
  );
@@ -590,10 +600,10 @@ export function classifyAccountError(
590
600
  }
591
601
  // A channel rejection (`11128`) is the same shape: the upstream is refusing
592
602
  // the caller for this account, not this particular prompt, and it keeps
593
- // refusing on every invocation. It used to fall through to `unknown` with
594
- // `mutates=false`, so the account stayed in rotation and failed every
595
- // request — and since every account failed the same way, the whole pool
596
- // looked dead with nothing ever parked.
603
+ // refusing on every invocation. Classifying it as `unknown` with
604
+ // `mutates=false` would leave the account in rotation failing every request —
605
+ // and since every account fails the same way, the whole pool looks dead with
606
+ // nothing ever parked.
597
607
  const channelRejection =
598
608
  providerCode === "11128" || lower.includes("unapproved channel");
599
609
  const channelBlock =
@@ -601,11 +611,12 @@ export function classifyAccountError(
601
611
  options?.providerId !== undefined &&
602
612
  BUDDY_PROVIDER_IDS.has(options.providerId.toLowerCase());
603
613
  if (channelBlock) {
614
+ const channelBlockMs = resolveAccountPolicyBlockCooldownMs();
604
615
  return result(
605
616
  "policy_blocked",
606
617
  "cooldown",
607
- resolveAccountPolicyBlockCooldownMs(),
608
- new Date(Date.now() + resolveAccountPolicyBlockCooldownMs()),
618
+ channelBlockMs,
619
+ new Date(Date.now() + channelBlockMs),
609
620
  reason("Provider channel rejection"),
610
621
  origin === "upstream",
611
622
  );
@@ -691,10 +702,11 @@ export function classifyAccountError(
691
702
  // rotated a moment ago, an edge answering before the credential loads) and
692
703
  // cools down instead.
693
704
  //
694
- // This distinction is the whole fix. The generic `401`/`403` branch used to
695
- // reach `disabled` too, which no sweep recovers, so one transient rejection
696
- // removed the account from rotation until an operator noticed — the exact
697
- // outcome the comment above says a static token must not suffer.
705
+ // This distinction is the whole fix. Reaching `disabled` from the generic
706
+ // `401`/`403` branch would be unrecoverable by any sweep, so one transient
707
+ // rejection would remove the account from rotation until an operator
708
+ // noticed — the exact outcome the comment above says a static token must
709
+ // not suffer.
698
710
  const revoked = revocationNamed;
699
711
  const recoverable = staticTokenRecovery || oauthRecovery || !revoked;
700
712
  // An automatic disable is **not** terminal any more, but the deadline is what
@@ -918,13 +930,14 @@ export function classifyAccountError(
918
930
  // scoped to the account (an edge/WAF challenge answered to one account's
919
931
  // requests while its siblings on the same egress served fine) must cool
920
932
  // that account down, or it stays `active` and is re-dialled for as long as
921
- // the block lasts. Provider-scoped 5xx keeps the old behavior: the upstream
922
- // is indiscriminately failing and no single account is at fault.
933
+ // the block lasts. Provider-scoped 5xx does not: the upstream is
934
+ // indiscriminately failing and no single account is at fault.
935
+ const transientMs = TRANSIENT_ERROR_COOLDOWN_MS();
923
936
  return result(
924
937
  lower.includes("timeout") ? "timeout" : "server_error",
925
938
  "cooldown",
926
- TRANSIENT_ERROR_COOLDOWN_MS(),
927
- new Date(Date.now() + TRANSIENT_ERROR_COOLDOWN_MS()),
939
+ transientMs,
940
+ new Date(Date.now() + transientMs),
928
941
  reason(`Provider ${statusCode ?? "network"} failure`),
929
942
  canMutate,
930
943
  );
@@ -932,11 +945,12 @@ export function classifyAccountError(
932
945
 
933
946
  // Same reasoning as above, with the shortest backoff: an unmapped failure
934
947
  // must still be retried automatically rather than parked forever.
948
+ const unclassifiedMs = UNCLASSIFIED_COOLDOWN_MS();
935
949
  return result(
936
950
  "unknown",
937
951
  "cooldown",
938
- UNCLASSIFIED_COOLDOWN_MS(),
939
- new Date(Date.now() + UNCLASSIFIED_COOLDOWN_MS()),
952
+ unclassifiedMs,
953
+ new Date(Date.now() + unclassifiedMs),
940
954
  reason("Unclassified provider failure"),
941
955
  false,
942
956
  );
@@ -1303,8 +1317,8 @@ export async function sweepExpiredCooldowns(db: CartethyiaDatabase): Promise<num
1303
1317
  isNotNull(providerAccounts.cooldownUntil),
1304
1318
  lte(providerAccounts.cooldownUntil, now),
1305
1319
  ),
1306
- // An **automatic** disable, which used to be terminal. Selected only
1307
- // when it carries a deadline: an operator's disable leaves
1320
+ // An **automatic** disable. Selected only when it carries a deadline:
1321
+ // an operator's disable leaves
1308
1322
  // `cooldownUntil` null, so a human's decision still outranks the
1309
1323
  // machine and is never swept away. The deadline is written by the two
1310
1324
  // paths that disable on evidence — the classifier's revocation branch
@@ -1362,9 +1376,9 @@ export async function sweepExpiredCooldowns(db: CartethyiaDatabase): Promise<num
1362
1376
  // its locks for the whole pass. Recomputing the object in SQL does the same
1363
1377
  // work in one statement.
1364
1378
  //
1365
- // `jsonb_typeof(...) = 'object'` guards the shape the same way the old
1366
- // `typeof raw !== "object"` check did: a malformed value must not abort the
1367
- // whole sweep. `IS DISTINCT FROM` keeps the update from rewriting rows it
1379
+ // `jsonb_typeof(...) = 'object'` guards the shape: a malformed value must
1380
+ // not abort the whole sweep. `IS DISTINCT FROM` keeps the update from
1381
+ // rewriting rows it
1368
1382
  // did not change, so the returned row count is the number of accounts whose
1369
1383
  // cooldown set actually shrank — the same thing the loop counted.
1370
1384
  const pruning = pruneExpiredCooldowns(sql`${providerAccounts.modelCooldowns}`, sql`${nowIso}`);
@@ -1478,3 +1492,37 @@ export async function listAccountHealthEvents(
1478
1492
  return [];
1479
1493
  }
1480
1494
  }
1495
+
1496
+ /** Every stored row across accounts and pools — the size the purge offers to delete. */
1497
+ export async function countAllHealthEvents(db: CartethyiaDatabase): Promise<number> {
1498
+ try {
1499
+ const rows = await db.select({ total: count() }).from(healthEvents);
1500
+ return rows[0]?.total ?? 0;
1501
+ } catch {
1502
+ return 0;
1503
+ }
1504
+ }
1505
+
1506
+ /**
1507
+ * Deletes stored health & error rows.
1508
+ *
1509
+ * Unlike the read above, a failed delete surfaces: an operator who purged
1510
+ * believes the trail is gone, so "it failed" must not read as "it succeeded
1511
+ * with zero rows". Callers own the flush-buffer barrier (`drain`) so a queued
1512
+ * write cannot land after these rows are gone.
1513
+ */
1514
+ export async function deleteAccountHealthEvents(
1515
+ db: CartethyiaDatabase,
1516
+ accountId: string,
1517
+ ): Promise<number> {
1518
+ const rows = await db
1519
+ .delete(healthEvents)
1520
+ .where(and(eq(healthEvents.entityKind, "account"), eq(healthEvents.accountId, accountId)))
1521
+ .returning({ id: healthEvents.id });
1522
+ return rows.length;
1523
+ }
1524
+
1525
+ export async function deleteAllHealthEvents(db: CartethyiaDatabase): Promise<number> {
1526
+ const rows = await db.delete(healthEvents).returning({ id: healthEvents.id });
1527
+ return rows.length;
1528
+ }
@@ -608,7 +608,7 @@ function describeResetCode(code: string): string {
608
608
  /**
609
609
  * Writes the outcome to `health_events` and, on success, restores the account to
610
610
  * `active` so it rejoins dispatch rotation immediately rather than waiting out a
611
- * cooldown that no longer reflects reality.
611
+ * cooldown that the reset has already cleared.
612
612
  */
613
613
  async function recordResetActivity(
614
614
  db: CartethyiaDatabase,