cartethyia 2.4.5 → 2.5.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 (415) hide show
  1. package/.env.example +4 -1
  2. package/README.md +63 -13
  3. package/dist/dashboard/assets/AccountCooldown-ChpdzxRH.js.gz +0 -0
  4. package/dist/dashboard/assets/BannedPage-9igfctfZ.js +1 -0
  5. package/dist/dashboard/assets/BannedPage-9igfctfZ.js.gz +0 -0
  6. package/dist/dashboard/assets/{CliToolDetailPage-BzGevuql.js → CliToolDetailPage-ZRr2K2sF.js} +1 -1
  7. package/dist/dashboard/assets/CliToolDetailPage-ZRr2K2sF.js.gz +0 -0
  8. package/dist/dashboard/assets/{CliToolsPage-BaRM_7We.js → CliToolsPage-D3xqZbN-.js} +1 -1
  9. package/dist/dashboard/assets/CliToolsPage-D3xqZbN-.js.gz +0 -0
  10. package/dist/dashboard/assets/CombosPage-CVnQPBBz.js +1 -0
  11. package/dist/dashboard/assets/CombosPage-CVnQPBBz.js.gz +0 -0
  12. package/dist/dashboard/assets/ConsoleLogPage-u6W1UXSc.js +1 -0
  13. package/dist/dashboard/assets/ConsoleLogPage-u6W1UXSc.js.gz +0 -0
  14. package/dist/dashboard/assets/{CustomizationPage-DqUGcoH0.js → CustomizationPage-CTTpCb25.js} +1 -1
  15. package/dist/dashboard/assets/CustomizationPage-CTTpCb25.js.gz +0 -0
  16. package/dist/dashboard/assets/HealthEventsModal-C5XpWlvy.js +1 -0
  17. package/dist/dashboard/assets/HealthEventsModal-C5XpWlvy.js.gz +0 -0
  18. package/dist/dashboard/assets/{LoginPage-DbhTGfhb.js → LoginPage-DHoB3Agn.js} +1 -1
  19. package/dist/dashboard/assets/LoginPage-DHoB3Agn.js.gz +0 -0
  20. package/dist/dashboard/assets/ModelPicker-oQ-SDXp8.js +1 -0
  21. package/dist/dashboard/assets/ModelPicker-oQ-SDXp8.js.gz +0 -0
  22. package/dist/dashboard/assets/OverviewPage-BrY4SgIt.js +1 -0
  23. package/dist/dashboard/assets/OverviewPage-BrY4SgIt.js.gz +0 -0
  24. package/dist/dashboard/assets/ProviderDetailPage-D9VSt7T_.js +3 -0
  25. package/dist/dashboard/assets/ProviderDetailPage-D9VSt7T_.js.gz +0 -0
  26. package/dist/dashboard/assets/ProviderIcon-CmfzxpVM.js +1 -0
  27. package/dist/dashboard/assets/ProviderIcon-CmfzxpVM.js.gz +0 -0
  28. package/dist/dashboard/assets/ProvidersPage-Df77V5I5.js +1 -0
  29. package/dist/dashboard/assets/ProvidersPage-Df77V5I5.js.gz +0 -0
  30. package/dist/dashboard/assets/ProxyPage-CDfxP7AC.js +7 -0
  31. package/dist/dashboard/assets/ProxyPage-CDfxP7AC.js.gz +0 -0
  32. package/dist/dashboard/assets/QuotaPage-CY4o2t4p.js +1 -0
  33. package/dist/dashboard/assets/QuotaPage-CY4o2t4p.js.gz +0 -0
  34. package/dist/dashboard/assets/{SettingsPage-YxRml8AT.js → SettingsPage-CvjZxHE4.js} +1 -1
  35. package/dist/dashboard/assets/SettingsPage-CvjZxHE4.js.gz +0 -0
  36. package/dist/dashboard/assets/SetupPage-D3nza7LH.js +1 -0
  37. package/dist/dashboard/assets/SetupPage-D3nza7LH.js.gz +0 -0
  38. package/dist/dashboard/assets/{SortableList-Bol1ASmI.js → SortableList-Dtt12WT1.js} +1 -1
  39. package/dist/dashboard/assets/SortableList-Dtt12WT1.js.gz +0 -0
  40. package/dist/dashboard/assets/StudioPage-CbK3hXvU.js +31 -0
  41. package/dist/dashboard/assets/StudioPage-CbK3hXvU.js.gz +0 -0
  42. package/dist/dashboard/assets/ToolIcon-K1ckcDKg.js.gz +0 -0
  43. package/dist/dashboard/assets/UsagePage-BQlWbWg2.js +56 -0
  44. package/dist/dashboard/assets/UsagePage-BQlWbWg2.js.gz +0 -0
  45. package/dist/dashboard/assets/api-keys-BmDbsior.js +1 -0
  46. package/dist/dashboard/assets/api-keys-BmDbsior.js.gz +0 -0
  47. package/dist/dashboard/assets/app-BbpSarVV.css +2 -0
  48. package/dist/dashboard/assets/app-BbpSarVV.css.gz +0 -0
  49. package/dist/dashboard/assets/{app-BDJWMThu.js → app-C-Y4oZT0.js} +3 -3
  50. package/dist/dashboard/assets/app-C-Y4oZT0.js.gz +0 -0
  51. package/dist/dashboard/assets/arrow-left-CpBxkoxI.js.gz +0 -0
  52. package/dist/dashboard/assets/arrow-right-Dl5yKnp8.js.gz +0 -0
  53. package/dist/dashboard/assets/assert-BHfaei-X.js +1 -0
  54. package/dist/dashboard/assets/assert-BHfaei-X.js.gz +0 -0
  55. package/dist/dashboard/assets/backup-file-gdKIlg8U.js.gz +0 -0
  56. package/dist/dashboard/assets/{badge-BwmwEOgA.js → badge-DpXT4Ixt.js} +1 -1
  57. package/dist/dashboard/assets/badge-DpXT4Ixt.js.gz +0 -0
  58. package/dist/dashboard/assets/{banner-BKBfKTqn.js → banner-CSAAWLJv.js} +1 -1
  59. package/dist/dashboard/assets/banner-CSAAWLJv.js.gz +0 -0
  60. package/dist/dashboard/assets/brain-Cwed_72z.js.gz +0 -0
  61. package/dist/dashboard/assets/{card-BTdbRFFg.js → card-DEhRXXw4.js} +1 -1
  62. package/dist/dashboard/assets/card-DEhRXXw4.js.gz +0 -0
  63. package/dist/dashboard/assets/check-D69FzdvC.js.gz +0 -0
  64. package/dist/dashboard/assets/chevron-right-BXIy5l3h.js.gz +0 -0
  65. package/dist/dashboard/assets/circle-alert-CntYHV5v.js.gz +0 -0
  66. package/dist/dashboard/assets/circle-check-BU3v_mFW.js.gz +0 -0
  67. package/dist/dashboard/assets/cli-tools-B0wBq-RL.js +1 -0
  68. package/dist/dashboard/assets/cli-tools-B0wBq-RL.js.gz +0 -0
  69. package/dist/dashboard/assets/clipboard-button-DOo6EU8O.js +1 -0
  70. package/dist/dashboard/assets/clipboard-button-DOo6EU8O.js.gz +0 -0
  71. package/dist/dashboard/assets/cog-CdWhsajs.js.gz +0 -0
  72. package/dist/dashboard/assets/copy-ngmQ9zcZ.js.gz +0 -0
  73. package/dist/dashboard/assets/{dialog-iocKA3jr.js → dialog-BvQeBMfP.js} +1 -1
  74. package/dist/dashboard/assets/dialog-BvQeBMfP.js.gz +0 -0
  75. package/dist/dashboard/assets/dialog-actions-B1axpEg7.js.gz +0 -0
  76. package/dist/dashboard/assets/download-A0SW9OCW.js.gz +0 -0
  77. package/dist/dashboard/assets/download-dRFo5_Wu.js.gz +0 -0
  78. package/dist/dashboard/assets/{drawer-CDKOTZdY.js → drawer-CMwPXwQB.js} +1 -1
  79. package/dist/dashboard/assets/drawer-CMwPXwQB.js.gz +0 -0
  80. package/dist/dashboard/assets/entry-D4O2229X.js +1 -0
  81. package/dist/dashboard/assets/entry-D4O2229X.js.gz +0 -0
  82. package/dist/dashboard/assets/entry-DKvr5Q9K.js +1 -0
  83. package/dist/dashboard/assets/entry-DKvr5Q9K.js.gz +0 -0
  84. package/dist/dashboard/assets/entry-FQCejoDY.js +4 -0
  85. package/dist/dashboard/assets/entry-FQCejoDY.js.gz +0 -0
  86. package/dist/dashboard/assets/eye-C_hawfH4.js.gz +0 -0
  87. package/dist/dashboard/assets/eye-off-Bk5g2luF.js.gz +0 -0
  88. package/dist/dashboard/assets/fingerprint-pattern-BWuHtbW4.js.gz +0 -0
  89. package/dist/dashboard/assets/gauge-BbIMy7MB.js.gz +0 -0
  90. package/dist/dashboard/assets/github-badge-zliLvBwr.js +1 -0
  91. package/dist/dashboard/assets/github-badge-zliLvBwr.js.gz +0 -0
  92. package/dist/dashboard/assets/image-BPaouViG.js.gz +0 -0
  93. package/dist/dashboard/assets/{index-C4hXfAMs.js → index-CXm3FIyt.js} +2 -2
  94. package/dist/dashboard/assets/index-CXm3FIyt.js.gz +0 -0
  95. package/dist/dashboard/assets/{inline-DCejSLlA.js → inline-D1IlZZ8H.js} +1 -1
  96. package/dist/dashboard/assets/inline-D1IlZZ8H.js.gz +0 -0
  97. package/dist/dashboard/assets/{input-Bn4_kNav.js → input-BpVZdrbc.js} +1 -1
  98. package/dist/dashboard/assets/input-BpVZdrbc.js.gz +0 -0
  99. package/dist/dashboard/assets/jsx-runtime-CXUMngI_.js.gz +0 -0
  100. package/dist/dashboard/assets/key-round-UkavsGxp.js.gz +0 -0
  101. package/dist/dashboard/assets/layers-2-glBbjl.js.gz +0 -0
  102. package/dist/dashboard/assets/{layout-Bv-NxqZ5.js → layout-BioLW7gN.js} +1 -1
  103. package/dist/dashboard/assets/layout-BioLW7gN.js.gz +0 -0
  104. package/dist/dashboard/assets/{list-row-BLA0sldD.js → list-row-CwVcGAIc.js} +1 -1
  105. package/dist/dashboard/assets/list-row-CwVcGAIc.js.gz +0 -0
  106. package/dist/dashboard/assets/{live-YlifPYox.js → live-yfEJFF9T.js} +1 -1
  107. package/dist/dashboard/assets/live-yfEJFF9T.js.gz +0 -0
  108. package/dist/dashboard/assets/log-in-BmRjy60c.js.gz +0 -0
  109. package/dist/dashboard/assets/message-circle-CCxGwRvv.js.gz +0 -0
  110. package/dist/dashboard/assets/model-bans-CT-TxSNX.js +1 -0
  111. package/dist/dashboard/assets/model-bans-CT-TxSNX.js.gz +0 -0
  112. package/dist/dashboard/assets/mutation-BkoLM5Ba.js +1 -0
  113. package/dist/dashboard/assets/mutation-BkoLM5Ba.js.gz +0 -0
  114. package/dist/dashboard/assets/numbers-CFL4_Dqc.js.gz +0 -0
  115. package/dist/dashboard/assets/pencil-768ysRAr.js.gz +0 -0
  116. package/dist/dashboard/assets/plus-DIdX_g-x.js.gz +0 -0
  117. package/dist/dashboard/assets/power-off-Yjw9GMJh.js +1 -0
  118. package/dist/dashboard/assets/power-off-Yjw9GMJh.js.gz +0 -0
  119. package/dist/dashboard/assets/privacy-mode-GM8bej85.js +1 -0
  120. package/dist/dashboard/assets/privacy-mode-GM8bej85.js.gz +0 -0
  121. package/dist/dashboard/assets/providers/agentrouter.svg.gz +0 -0
  122. package/dist/dashboard/assets/providers/aihubmix.svg.gz +0 -0
  123. package/dist/dashboard/assets/providers/alibaba-coding-plan.svg.gz +0 -0
  124. package/dist/dashboard/assets/providers/alibaba.svg.gz +0 -0
  125. package/dist/dashboard/assets/providers/antigravity.svg.gz +0 -0
  126. package/dist/dashboard/assets/providers/bai.svg.gz +0 -0
  127. package/dist/dashboard/assets/providers/blackbox.svg.gz +0 -0
  128. package/dist/dashboard/assets/providers/claude-code.svg.gz +0 -0
  129. package/dist/dashboard/assets/providers/cursor.webp +0 -0
  130. package/dist/dashboard/assets/providers/exa.svg.gz +0 -0
  131. package/dist/dashboard/assets/providers/gmi-cloud.svg.gz +0 -0
  132. package/dist/dashboard/assets/providers/inferhub.svg.gz +0 -0
  133. package/dist/dashboard/assets/providers/muse.svg.gz +0 -0
  134. package/dist/dashboard/assets/providers/nous-research.svg.gz +0 -0
  135. package/dist/dashboard/assets/providers/openai-light.svg.gz +0 -0
  136. package/dist/dashboard/assets/providers/parallel.svg.gz +0 -0
  137. package/dist/dashboard/assets/providers/tokenharbor.svg.gz +0 -0
  138. package/dist/dashboard/assets/providers/trae.webp +0 -0
  139. package/dist/dashboard/assets/providers/zai-coding-plan.svg.gz +0 -0
  140. package/dist/dashboard/assets/{query-keys-ROf-g1Wi.js → query-keys-D7wX2vM5.js} +1 -1
  141. package/dist/dashboard/assets/query-keys-D7wX2vM5.js.gz +0 -0
  142. package/dist/dashboard/assets/quota-DZGyp0N5.js.gz +0 -0
  143. package/dist/dashboard/assets/refresh-cw-BqJFk4sU.js.gz +0 -0
  144. package/dist/dashboard/assets/rotate-ccw-CufcqITi.js.gz +0 -0
  145. package/dist/dashboard/assets/route-CNSNj9Zm.js.gz +0 -0
  146. package/dist/dashboard/assets/routing-Cs6jl49L.js +1 -0
  147. package/dist/dashboard/assets/routing-Cs6jl49L.js.gz +0 -0
  148. package/dist/dashboard/assets/scaling-nWE4Cx8f.js.gz +0 -0
  149. package/dist/dashboard/assets/search-JOKus9rb.js.gz +0 -0
  150. package/dist/dashboard/assets/select-C0Rer3gF.js +41 -0
  151. package/dist/dashboard/assets/select-C0Rer3gF.js.gz +0 -0
  152. package/dist/dashboard/assets/settings-CfcTcOyU.js +1 -0
  153. package/dist/dashboard/assets/settings-CfcTcOyU.js.gz +0 -0
  154. package/dist/dashboard/assets/shield-check-DVS0Foqh.js.gz +0 -0
  155. package/dist/dashboard/assets/sparkles-BRaRUxAU.js.gz +0 -0
  156. package/dist/dashboard/assets/{stack-C1kTcOmP.js → stack-C9_uUjaA.js} +1 -1
  157. package/dist/dashboard/assets/stack-C9_uUjaA.js.gz +0 -0
  158. package/dist/dashboard/assets/{state-DHKdg-pm.js → state-DA0B95UQ.js} +1 -1
  159. package/dist/dashboard/assets/state-DA0B95UQ.js.gz +0 -0
  160. package/dist/dashboard/assets/stream-retry-5QnVuSSo.js.gz +0 -0
  161. package/dist/dashboard/assets/{switch-DIDeAiz9.js → switch-Cittxy-x.js} +1 -1
  162. package/dist/dashboard/assets/switch-Cittxy-x.js.gz +0 -0
  163. package/dist/dashboard/assets/terminal-Bd4ee8TA.js +1 -0
  164. package/dist/dashboard/assets/terminal-Bd4ee8TA.js.gz +0 -0
  165. package/dist/dashboard/assets/toast-CNt_jzO4.js +2 -0
  166. package/dist/dashboard/assets/toast-CNt_jzO4.js.gz +0 -0
  167. package/dist/dashboard/assets/tokens-DDxMbSRL.js.gz +0 -0
  168. package/dist/dashboard/assets/trash-Cppn5c3R.js.gz +0 -0
  169. package/dist/dashboard/assets/use-clipboard-B0Sy03fE.js.gz +0 -0
  170. package/dist/dashboard/assets/use-presence-BSZEBx7U.js.gz +0 -0
  171. package/dist/dashboard/assets/use-reduced-motion-C4B0R6Sz.js.gz +0 -0
  172. package/dist/dashboard/assets/use-timeout-BwcBBRrT.js.gz +0 -0
  173. package/dist/dashboard/assets/{useMutation-DVmwOKU-.js → useMutation-BQKMcJ1H.js} +1 -1
  174. package/dist/dashboard/assets/useMutation-BQKMcJ1H.js.gz +0 -0
  175. package/dist/dashboard/assets/wrench-B6E1oEuY.js.gz +0 -0
  176. package/dist/dashboard/index.html +1 -1
  177. package/dist/dashboard/index.html.gz +0 -0
  178. package/dist/dashboard/precompressed-assets.json +122 -0
  179. package/migrations/full-postgres/0013_add_providers_tenant_id_index.sql +22 -0
  180. package/migrations/full-postgres/0014_add_provider_accounts_tenant_id_index.sql +21 -0
  181. package/migrations/full-postgres/0015_add_payload_stages.sql +18 -0
  182. package/migrations/full-postgres/0016_add_share_link_slug.sql +20 -0
  183. package/migrations/full-postgres/0017_add_models_pro_only.sql +18 -0
  184. package/migrations/full-postgres/0018_add_accounts_quota_plan.sql +18 -0
  185. package/migrations/full-postgres/0019_add_models_free_tier.sql +15 -0
  186. package/migrations/lite-sqlite3/0000_baseline.sql +4 -2
  187. package/migrations/lite-sqlite3/0013_add_providers_tenant_id_index.sql +4 -0
  188. package/migrations/lite-sqlite3/0014_add_provider_accounts_tenant_id_index.sql +4 -0
  189. package/migrations/lite-sqlite3/0015_add_payload_stages.sql +18 -0
  190. package/migrations/lite-sqlite3/0016_add_share_link_slug.sql +20 -0
  191. package/migrations/lite-sqlite3/0017_add_models_pro_only.sql +18 -0
  192. package/migrations/lite-sqlite3/0018_add_accounts_quota_plan.sql +18 -0
  193. package/migrations/lite-sqlite3/0019_add_models_free_tier.sql +15 -0
  194. package/package.json +26 -30
  195. package/scripts/cli/{carte-launcher.cjs → bin/carte.cjs} +8 -8
  196. package/scripts/cli/{cli-launcher.cjs → bin/cartethyia.cjs} +167 -134
  197. package/scripts/cli/command/admin.ts +65 -0
  198. package/scripts/cli/command/console-menu.ts +246 -0
  199. package/scripts/cli/{doctor.ts → command/doctor.ts} +257 -258
  200. package/scripts/cli/{setup.ts → command/setup.ts} +8 -12
  201. package/scripts/cli/command/start.ts +77 -0
  202. package/scripts/cli/command/tray.ts +165 -0
  203. package/scripts/cli/command/update.ts +23 -0
  204. package/scripts/cli/{runtime-state.ts → gateway/drain-token.ts} +78 -78
  205. package/scripts/cli/gateway/instance-lock.ts +105 -0
  206. package/scripts/cli/gateway/spawn.ts +173 -0
  207. package/scripts/cli/gateway/stop.ts +51 -0
  208. package/scripts/cli/main.ts +94 -0
  209. package/scripts/cli/support/bun.ts +27 -0
  210. package/scripts/cli/support/env.ts +99 -0
  211. package/scripts/cli/support/platform.ts +64 -0
  212. package/scripts/cli/support/services.ts +88 -0
  213. package/scripts/cli/support/startup-signal.ts +23 -0
  214. package/scripts/cli/{version.ts → support/version.ts} +1 -1
  215. package/scripts/cli/tray/hidden-window.vbs +28 -0
  216. package/scripts/cli/{tray.ts → tray/index.ts} +3 -3
  217. package/scripts/cli/{tray.ps1 → tray/windows.ps1} +107 -98
  218. package/scripts/cli/ui/background.ts +110 -0
  219. package/src/boot/app.ts +23 -7
  220. package/src/boot/dependencies.ts +55 -13
  221. package/src/boot/elysia-typebox-bridge.ts +37 -0
  222. package/src/boot/lifecycle.ts +2 -0
  223. package/src/config/cooldowns.ts +138 -138
  224. package/src/config/providers.ts +31 -5
  225. package/src/config/runtime.ts +19 -3
  226. package/src/config/spec.ts +20 -0
  227. package/src/console/api-keys/contracts.ts +414 -391
  228. package/src/console/api-keys/routes.ts +880 -870
  229. package/src/console/auth/account-reset-store.ts +14 -0
  230. package/src/console/auth/account-reset.ts +3 -2
  231. package/src/console/auth/setup-service.ts +6 -3
  232. package/src/console/backup/routes.ts +8 -0
  233. package/src/console/cli-tools/contracts.ts +6 -6
  234. package/src/console/console-router.ts +5 -3
  235. package/src/console/dashboard-assets.ts +81 -11
  236. package/src/console/domain-registration.ts +9 -2
  237. package/src/console/observability/contracts.ts +475 -423
  238. package/src/console/observability/routes.ts +31 -6
  239. package/src/console/observability/store.ts +1015 -912
  240. package/src/console/providers/catalog/contracts.ts +31 -0
  241. package/src/console/providers/catalog/provider-operations.ts +8 -0
  242. package/src/console/providers/catalog/public-model-store.ts +91 -60
  243. package/src/console/providers/catalog/store.ts +1532 -1521
  244. package/src/console/providers/oauth/callback-listener.ts +18 -3
  245. package/src/console/providers/oauth/routes.ts +38 -15
  246. package/src/console/quota/account-quota-tenant.ts +9 -6
  247. package/src/console/quota/refresh.ts +19 -0
  248. package/src/console/response-compression.ts +69 -2
  249. package/src/console/routing/pools/probe-verdict.ts +72 -0
  250. package/src/console/share/share-router.ts +7 -4
  251. package/src/console/share/share-stats.ts +12 -5
  252. package/src/console/shared/read-through-cache.ts +133 -7
  253. package/src/console/updates/repository-updates.ts +39 -34
  254. package/src/console/updates/routes.ts +162 -4
  255. package/src/main.ts +3 -0
  256. package/src/network/http2-bidi.ts +85 -0
  257. package/src/network/outbound-fetch.ts +87 -24
  258. package/src/network/pool/agent.ts +478 -460
  259. package/src/network/pool/resolver.ts +1104 -544
  260. package/src/network/pool-health-machine.ts +12 -5
  261. package/src/observability/event-log.ts +29 -8
  262. package/src/observability/metrics.ts +40 -13
  263. package/src/observability/payload-capture.ts +344 -324
  264. package/src/observability/runtime-metrics.ts +7 -0
  265. package/src/persistence/db-pg.ts +9 -3
  266. package/src/persistence/db-sqlite.ts +497 -495
  267. package/src/persistence/health-event-flush.ts +137 -0
  268. package/src/persistence/migrate.ts +181 -129
  269. package/src/persistence/schema.ts +1360 -1257
  270. package/src/persistence/settings-revision.ts +25 -0
  271. package/src/persistence/shared-cache.ts +217 -0
  272. package/src/persistence/stores/share-store.ts +556 -528
  273. package/src/persistence/usage-flush.ts +61 -1
  274. package/src/protocol/canonical-model.ts +23 -1
  275. package/src/protocol/response/chat.ts +143 -10
  276. package/src/protocol/response/codex.ts +9 -75
  277. package/src/protocol/response/omp-tool-call-markup.ts +221 -0
  278. package/src/protocol/tool-identity.ts +43 -9
  279. package/src/providers/authentication/jwt-validator.ts +130 -187
  280. package/src/providers/authentication/oauth-flow-store.ts +16 -9
  281. package/src/providers/default-registry.ts +177 -1
  282. package/src/providers/discovery/catalog/catalog-format.ts +20 -0
  283. package/src/providers/discovery/models-dev-catalog.ts +39 -22
  284. package/src/providers/discovery/openai-model-discovery.ts +39 -3
  285. package/src/providers/discovery/openrouter-free-roster.ts +98 -0
  286. package/src/providers/discovery/probe-wire.ts +29 -5
  287. package/src/providers/discovery/probing-service.ts +1022 -1001
  288. package/src/providers/integrations/buddy/buddy-catalog-shared.ts +42 -2
  289. package/src/providers/integrations/buddy/buddy-chat-shared.ts +913 -914
  290. package/src/providers/integrations/buddy/buddy-cli-headers.ts +88 -0
  291. package/src/providers/integrations/buddy/buddy-conversation-headers.ts +22 -5
  292. package/src/providers/integrations/buddy/buddy-discovery-shared.ts +33 -1
  293. package/src/providers/integrations/buddy/buddy-identity.ts +46 -0
  294. package/src/providers/integrations/buddy/buddy-oauth-shared.ts +37 -1
  295. package/src/providers/integrations/buddy/buddy-usage-store.ts +40 -5
  296. package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +4 -0
  297. package/src/providers/integrations/buddy/codebuddy/provider.ts +180 -175
  298. package/src/providers/integrations/buddy/codebuddy/shared.ts +115 -73
  299. package/src/providers/integrations/buddy/workbuddy/cn-provider.ts +5 -2
  300. package/src/providers/integrations/buddy/workbuddy/provider.ts +206 -204
  301. package/src/providers/integrations/buddy/workbuddy/quota.ts +172 -159
  302. package/src/providers/integrations/buddy/workbuddy/shared.ts +200 -171
  303. package/src/providers/integrations/claude/claude.ts +606 -582
  304. package/src/providers/integrations/cline/cline-quota.ts +18 -16
  305. package/src/providers/integrations/cline/cline.ts +539 -468
  306. package/src/providers/integrations/codex/codex.ts +5 -8
  307. package/src/providers/integrations/commandcode/commandcode.ts +675 -541
  308. package/src/providers/integrations/commandcode/discovery.ts +116 -0
  309. package/src/providers/integrations/commandcode/quota.ts +160 -0
  310. package/src/providers/integrations/cursor/cursor-auth.ts +418 -0
  311. package/src/providers/integrations/cursor/cursor-catalog.ts +368 -0
  312. package/src/providers/integrations/cursor/cursor-http2.ts +300 -0
  313. package/src/providers/integrations/cursor/cursor-identity.ts +92 -0
  314. package/src/providers/integrations/cursor/cursor-request.ts +471 -0
  315. package/src/providers/integrations/cursor/cursor-session-manager.ts +281 -0
  316. package/src/providers/integrations/cursor/cursor-shared.ts +108 -0
  317. package/src/providers/integrations/cursor/cursor.ts +960 -0
  318. package/src/providers/integrations/cursor/protocol/framing.ts +129 -0
  319. package/src/providers/integrations/cursor/protocol/messages.ts +2355 -0
  320. package/src/providers/integrations/cursor/protocol/struct.ts +305 -0
  321. package/src/providers/integrations/factory/factory-oauth.ts +7 -11
  322. package/src/providers/integrations/github/github.ts +4 -4
  323. package/src/providers/integrations/hermes.ts +79 -8
  324. package/src/providers/integrations/kilo/kilo-discovery.ts +10 -1
  325. package/src/providers/integrations/kilo/kilo.ts +153 -146
  326. package/src/providers/integrations/opencode/opencode-go-quota.ts +130 -0
  327. package/src/providers/integrations/opencode/provider.ts +532 -499
  328. package/src/providers/integrations/openrouter/provider.ts +139 -281
  329. package/src/providers/integrations/qoder/qoder-catalog.ts +450 -0
  330. package/src/providers/integrations/qoder/qoder-oauth.ts +310 -263
  331. package/src/providers/integrations/qoder/qoder-realm.ts +106 -0
  332. package/src/providers/integrations/qoder/qoder-virtual.ts +77 -0
  333. package/src/providers/integrations/qoder/qoder.ts +865 -730
  334. package/src/providers/integrations/shared/install-id.ts +29 -2
  335. package/src/providers/integrations/trae/trae-catalog.ts +190 -0
  336. package/src/providers/integrations/trae/trae-headers.ts +149 -0
  337. package/src/providers/integrations/trae/trae-oauth.ts +773 -0
  338. package/src/providers/integrations/trae/trae-protocol.ts +400 -0
  339. package/src/providers/integrations/trae/trae-realm.ts +220 -0
  340. package/src/providers/integrations/trae/trae.ts +458 -0
  341. package/src/providers/operations/account-health-service.ts +107 -10
  342. package/src/providers/operations/client-versions.ts +143 -6
  343. package/src/providers/operations/model-catalog-cache.ts +18 -3
  344. package/src/providers/operations/provider-catalog-seeder.ts +95 -86
  345. package/src/providers/operations/provider-catalog-service.ts +76 -0
  346. package/src/providers/operations/provider-credential-service.ts +46 -7
  347. package/src/providers/provider-contracts.ts +76 -0
  348. package/src/providers/provider-metadata.ts +78 -6
  349. package/src/providers/provider-model-definition.ts +373 -349
  350. package/src/security/api-key-auth.ts +126 -8
  351. package/src/transport/contracts.ts +3 -0
  352. package/src/transport/dispatch/attempt-finalize.ts +39 -2
  353. package/src/transport/dispatch/attempt-loop.ts +530 -513
  354. package/src/transport/dispatch/empty-stream.ts +17 -26
  355. package/src/transport/dispatch/generation.ts +88 -21
  356. package/src/transport/dispatch/leases.ts +99 -2
  357. package/src/transport/dispatch/proxy-request.ts +60 -3
  358. package/src/transport/dispatch/streaming/finalize.ts +16 -1
  359. package/src/transport/dispatch/streaming/stream.ts +22 -2
  360. package/src/transport/dispatch/upstream.ts +19 -1
  361. package/src/transport/failure-policy.ts +20 -0
  362. package/src/transport/guards/auth-guard.ts +26 -1
  363. package/src/transport/guards/body-policy.ts +5 -4
  364. package/src/transport/request/preparer.ts +103 -0
  365. package/src/transport/request/state.ts +14 -0
  366. package/src/transport/routing/plan-gate.ts +45 -0
  367. package/src/transport/routing/route-catalog.ts +799 -738
  368. package/src/transport/routing/route-model.ts +52 -1
  369. package/src/transport/routing/router.ts +51 -0
  370. package/src/transport/surface/chat/encode.ts +18 -31
  371. package/src/transport/surface/messages/stream.ts +3 -2
  372. package/src/transport/translation/tool-repair.ts +37 -16
  373. package/src/workers/account-identity-sweep.ts +14 -12
  374. package/src/workers/daily-checkin.ts +62 -0
  375. package/src/workers/quota-refresh-sweep.ts +27 -2
  376. package/dist/dashboard/assets/BannedPage-DUrXpp-X.js +0 -1
  377. package/dist/dashboard/assets/CombosPage-B7ETkfVH.js +0 -1
  378. package/dist/dashboard/assets/ConsoleLogPage-AisQzCI1.js +0 -1
  379. package/dist/dashboard/assets/HealthEventsModal-BNflDm_f.js +0 -1
  380. package/dist/dashboard/assets/ModelPicker-CAiPR5Vd.js +0 -1
  381. package/dist/dashboard/assets/OverviewPage-C3NIzsDx.js +0 -1
  382. package/dist/dashboard/assets/ProviderDetailPage-BK8i5B5X.js +0 -3
  383. package/dist/dashboard/assets/ProviderIcon-paOCq8I0.js +0 -1
  384. package/dist/dashboard/assets/ProvidersPage-CvCVgm1e.js +0 -1
  385. package/dist/dashboard/assets/ProxyPage-DjabkuzP.js +0 -7
  386. package/dist/dashboard/assets/QuotaPage-Dxe0B0iu.js +0 -1
  387. package/dist/dashboard/assets/SetupPage-BaD4P_jW.js +0 -1
  388. package/dist/dashboard/assets/StudioPage-CLP4le3U.js +0 -31
  389. package/dist/dashboard/assets/UsagePage-Djes-Epy.js +0 -56
  390. package/dist/dashboard/assets/api-keys-CjIGQhe7.js +0 -1
  391. package/dist/dashboard/assets/app-Cjgo4-Lf.css +0 -2
  392. package/dist/dashboard/assets/assert-Bwh6DJTK.js +0 -1
  393. package/dist/dashboard/assets/cli-tools-DccaOfhX.js +0 -1
  394. package/dist/dashboard/assets/clipboard-button-DQmMikNQ.js +0 -1
  395. package/dist/dashboard/assets/entry-Cpi0rph1.js +0 -4
  396. package/dist/dashboard/assets/entry-Dm3PpDCO.js +0 -1
  397. package/dist/dashboard/assets/entry-keNDZ_Wz.js +0 -1
  398. package/dist/dashboard/assets/github-badge-BuAJr8oU.js +0 -1
  399. package/dist/dashboard/assets/model-bans-CVvHZ-qz.js +0 -1
  400. package/dist/dashboard/assets/mutation-BIizFZav.js +0 -1
  401. package/dist/dashboard/assets/privacy-mode-CQjbZ3xk.js +0 -1
  402. package/dist/dashboard/assets/routing-jntw9-Ui.js +0 -1
  403. package/dist/dashboard/assets/select-CGUuTQmj.js +0 -41
  404. package/dist/dashboard/assets/settings-BAS6QbmJ.js +0 -1
  405. package/dist/dashboard/assets/sliders-horizontal-ClG7dBeJ.js +0 -1
  406. package/dist/dashboard/assets/terminal-C9mZ4Ugm.js +0 -1
  407. package/dist/dashboard/assets/toast-DKAdP2mo.js +0 -2
  408. package/scripts/cli/cli.ts +0 -771
  409. package/scripts/operations/probe-egress.ts +0 -335
  410. package/scripts/operations/restart.ts +0 -120
  411. package/scripts/shared/env.ts +0 -269
  412. /package/{tray-assets → scripts/cli/tray/assets}/cartethyia.ico +0 -0
  413. /package/{tray-assets → scripts/cli/tray/assets}/cartethyia.png +0 -0
  414. /package/scripts/cli/{menu.ts → ui/prompt.ts} +0 -0
  415. /package/scripts/cli/{wordmark.ts → ui/wordmark.ts} +0 -0
@@ -0,0 +1,458 @@
1
+ // Trae adapter.
2
+ //
3
+ // Realm-aware: `trae` (intl) and `traecn` (CN) share this code but nothing of
4
+ // the address, the authorization scheme, or the roster. The intl deployment
5
+ // additionally gates on the *current* client version, which is why the version
6
+ // here comes from `getTraeVersion()` (a live resolver) rather than a constant.
7
+
8
+ import { GatewayError } from "../../../protocol/gateway-error";
9
+ import type { CanonicalEvent, CanonicalRequest, CanonicalStopReason } from "../../../protocol/canonical-model";
10
+ import { usageFromProvider } from "../../provider-usage";
11
+ import {
12
+ readCredentialSecret,
13
+ type ProviderAdapter,
14
+ type ProviderDispatchContext,
15
+ type ProviderDispatchTarget,
16
+ type ProviderId,
17
+ } from "../../provider-contracts";
18
+ import { mapUpstreamHttpError } from "../../../transport/failure-policy";
19
+ import { getTraeVersion } from "../../operations/client-versions";
20
+ import { abortGatewayError, createUpstreamDeadlineLifecycle } from "../../operations/upstream-deadline";
21
+ import { TRAE_CN_MODELS, TRAE_INTL_MODELS, traeMaxContextWindow } from "./trae-catalog";
22
+ import { buildTraeHeaders } from "./trae-headers";
23
+ import {
24
+ buildTraeChatBody,
25
+ isTraeAgentEndpoint,
26
+ parseTraeSse,
27
+ toTraeAgentBody,
28
+ TRAE_AGENT_ENDPOINT,
29
+ TRAE_CHAT_ENDPOINT,
30
+ } from "./trae-protocol";
31
+ import { TRAE_REGION_CORE_HOSTS, traeCoreCandidates, type TraeRealm } from "./trae-realm";
32
+
33
+ export interface TraeAdapterOptions {
34
+ readonly providerId?: string;
35
+ readonly realm?: TraeRealm;
36
+ readonly fetch?: typeof fetch;
37
+ }
38
+
39
+ /** Identity resolved from a persisted credential. */
40
+ interface TraeAuth {
41
+ readonly token: string;
42
+ readonly uid: string;
43
+ }
44
+
45
+ const TRAE_PROVIDER_ID = "trae";
46
+ const TRAE_CN_PROVIDER_ID = "traecn";
47
+
48
+ /** Upstream finish reason → canonical stop reason. */
49
+ const STOP_REASONS: Readonly<Record<string, CanonicalStopReason>> = {
50
+ stop: "stop",
51
+ length: "length",
52
+ tool_calls: "tool_use",
53
+ function_call: "tool_use",
54
+ content_filter: "content_filter",
55
+ };
56
+
57
+ /**
58
+ * Reads the credential.
59
+ *
60
+ * Trae credentials are OAuth token pairs, so the whole JSON export is the
61
+ * secret: extracting only the access token would drop the refresh token and
62
+ * leave the account permanently unrefreshable. The refresh itself runs through
63
+ * the registered OAuth refresher (`trae-oauth.ts`), not here — this only needs
64
+ * the access token to authorize a request.
65
+ */
66
+ function traeAuth(secret: string, accountId: string | undefined): TraeAuth {
67
+ const token = extractToken(secret);
68
+ if (token === undefined) {
69
+ throw new GatewayError("authentication_failed", 401, "Trae credential has no access token", {}, "upstream");
70
+ }
71
+ return { token, uid: accountId ?? "" };
72
+ }
73
+
74
+ function extractToken(secret: string): string | undefined {
75
+ const trimmed = secret.trim();
76
+ if (trimmed.startsWith("{")) {
77
+ try {
78
+ const parsed = JSON.parse(trimmed) as Record<string, unknown>;
79
+ const candidates = ["accessToken", "access_token", "token", "x-ide-token"];
80
+ for (const key of candidates) {
81
+ const value = parsed[key];
82
+ if (typeof value === "string" && value.length > 0) return value;
83
+ }
84
+ } catch {
85
+ // Fall through to the bare-token reading below.
86
+ }
87
+ }
88
+ return trimmed.length > 0 ? trimmed : undefined;
89
+ }
90
+
91
+ class TraeAdapter implements ProviderAdapter {
92
+ readonly provider_id: ProviderId;
93
+ private readonly realm: TraeRealm;
94
+ private readonly fetchFn: typeof fetch;
95
+ /**
96
+ * Maps a frame's `index` to the call id seen for it.
97
+ *
98
+ * Argument fragments arrive without the id, so the association has to be
99
+ * remembered between frames rather than read from the fragment itself.
100
+ */
101
+ readonly #toolCallIds = new Map<string, string>();
102
+
103
+ constructor(options: TraeAdapterOptions = {}) {
104
+ this.provider_id = (options.providerId ?? TRAE_PROVIDER_ID) as ProviderId;
105
+ this.realm = options.realm ?? "intl";
106
+ this.fetchFn = options.fetch ?? globalThis.fetch;
107
+ }
108
+
109
+ async *dispatch(
110
+ request: CanonicalRequest,
111
+ candidate: ProviderDispatchTarget,
112
+ context: ProviderDispatchContext,
113
+ ): AsyncIterable<CanonicalEvent> {
114
+ if (candidate.wire_family !== "chat") {
115
+ throw new GatewayError(
116
+ "capability_unsupported",
117
+ 400,
118
+ `trae supports chat only, got ${candidate.wire_family}`,
119
+ );
120
+ }
121
+ const secret = readCredentialSecret(context.credential, "Trae credential is required");
122
+ const modelId = candidate.model_id || request.model;
123
+ const lifecycle = createUpstreamDeadlineLifecycle(context);
124
+ const outboundFetch =
125
+ (context.outbound_fetch as unknown as typeof fetch | undefined) ?? this.fetchFn;
126
+
127
+ // intl refuses a stale client version with 4015, so the resolved version is
128
+ // part of every request rather than a startup detail.
129
+ const version = getTraeVersion();
130
+ const auth = traeAuth(secret, context.credential.account_id);
131
+ const body = buildTraeChatBody(modelId, request, this.realm);
132
+ /**
133
+ * The endpoint follows the model, not the provider.
134
+ *
135
+ * Trae's two surfaces are disjoint and neither serves the other's ids, so
136
+ * the roster's own `endpoint_path` decides. Falling back to the agent path
137
+ * rather than the chat one matters: the intl catalog is agent-only, and a
138
+ * caller whose target carries no route would otherwise land on a surface
139
+ * that rejects every model in it.
140
+ */
141
+ const agentSurface = isTraeAgentEndpoint(candidate.endpoint_path);
142
+ const payload = agentSurface ? toTraeAgentBody(body) : body;
143
+ const headers = buildTraeHeaders({
144
+ realm: this.realm,
145
+ token: auth.token,
146
+ uid: auth.uid,
147
+ clientVersion: version,
148
+ stream: true,
149
+ });
150
+
151
+ const primary = traeCoreForAccount(this.realm, context.credential.auth_state);
152
+ const path = agentSurface ? TRAE_AGENT_ENDPOINT : TRAE_CHAT_ENDPOINT;
153
+ const url =
154
+ candidate.endpoint_path && candidate.endpoint_path.startsWith("http")
155
+ ? candidate.endpoint_path
156
+ : `${primary}${path}`;
157
+
158
+ try {
159
+ const response = await outboundFetch(url, {
160
+ method: "POST",
161
+ headers,
162
+ body: JSON.stringify(payload),
163
+ signal: lifecycle.signal,
164
+ });
165
+ if (!response.ok) throw await mapUpstreamHttpError(response, this.provider_id);
166
+ if (!response.body) {
167
+ throw new GatewayError("platform_unavailable", 502, "Trae returned empty body", {}, "upstream");
168
+ }
169
+ // Headers arrived, so the pre-stream deadline has served its purpose;
170
+ // from here the gateway's own stall watchdog governs the body.
171
+ lifecycle.release();
172
+ yield* this.#events(
173
+ response.body as ReadableStream<Uint8Array>,
174
+ lifecycle.signal,
175
+ request.model,
176
+ );
177
+ } catch (err: unknown) {
178
+ const abortError = abortGatewayError(lifecycle, err, context.abort_signal);
179
+ if (abortError) throw abortError;
180
+ throw err;
181
+ } finally {
182
+ lifecycle.release();
183
+ }
184
+ }
185
+
186
+ async *#events(
187
+ body: ReadableStream<Uint8Array>,
188
+ _signal: AbortSignal,
189
+ requestModel: string,
190
+ ): AsyncIterable<CanonicalEvent> {
191
+ let seq = 1;
192
+ yield { type: "response_start", sequence_number: seq++, model: requestModel } as CanonicalEvent;
193
+ let stopReason: CanonicalStopReason = "stop";
194
+ let usage: Record<string, unknown> | undefined;
195
+ /**
196
+ * Tool calls arrive fragmented across frames and the id is not repeated.
197
+ *
198
+ * Upstream streams one `tool_calls` array per frame where only the first
199
+ * carries `id`, only early frames carry `name`, and `arguments` arrives as
200
+ * successive JSON fragments. So name is announced once on first sight of
201
+ * the id and arguments are accumulated; emitting each frame verbatim would
202
+ * produce several calls with empty ids.
203
+ */
204
+ const announced = new Set<string>();
205
+
206
+ for await (const frame of parseTraeSse(body)) {
207
+ if (frame.event === "done") {
208
+ const data = frame.data as { finish_reason?: unknown };
209
+ if (typeof data.finish_reason === "string") {
210
+ stopReason = STOP_REASONS[data.finish_reason] ?? "stop";
211
+ }
212
+ continue;
213
+ }
214
+ if (frame.event === "error") {
215
+ const data = frame.data as { message?: unknown };
216
+ const message = typeof data.message === "string" ? data.message : "Trae stream error";
217
+ throw new GatewayError("platform_unavailable", 502, message, {}, "upstream");
218
+ }
219
+ if (typeof frame.data !== "object" || frame.data === null) continue;
220
+ const record = frame.data as Record<string, unknown>;
221
+ /**
222
+ * Two frame layouts reach here and both are live.
223
+ *
224
+ * The donor's client nests the answer under `chunk`; the region host this
225
+ * gateway talks to sends it flat — `event: output` with
226
+ * `{"response":"…"}` — and reports usage as a separate `token_usage`
227
+ * frame with the OpenAI spelling at the top level. Reading only `chunk`
228
+ * made a working upstream look like an empty stream.
229
+ */
230
+ const chunk = record["chunk"];
231
+ if (typeof chunk !== "object" || chunk === null) {
232
+ const flat = record["response"];
233
+ if (typeof flat === "string" && flat.length > 0) {
234
+ yield {
235
+ type: "content_delta",
236
+ sequence_number: seq++,
237
+ content: { kind: "text", text: flat },
238
+ } as CanonicalEvent;
239
+ }
240
+ const flatReasoning = record["reasoning_content"];
241
+ if (typeof flatReasoning === "string" && flatReasoning.length > 0) {
242
+ yield {
243
+ type: "content_delta",
244
+ sequence_number: seq++,
245
+ content: { kind: "reasoning", payload: flatReasoning, summary: flatReasoning },
246
+ } as CanonicalEvent;
247
+ }
248
+ yield* this.#toolCallDeltas(record["tool_calls"], announced, () => seq++);
249
+ if (typeof record["finish_reason"] === "string") {
250
+ stopReason = STOP_REASONS[record["finish_reason"] as string] ?? "stop";
251
+ }
252
+ // `token_usage` frames carry the counters themselves, not under `usage`.
253
+ const counters =
254
+ record["usage"] !== undefined && typeof record["usage"] === "object"
255
+ ? (record["usage"] as Record<string, unknown>)
256
+ : frame.event === "token_usage" || record["prompt_tokens"] !== undefined
257
+ ? record
258
+ : undefined;
259
+ if (counters !== undefined) usage = counters;
260
+ continue;
261
+ }
262
+ const payload = chunk as Record<string, unknown>;
263
+ const text = payload["content"];
264
+ if (typeof text === "string" && text.length > 0) {
265
+ yield {
266
+ type: "content_delta",
267
+ sequence_number: seq++,
268
+ content: { kind: "text", text },
269
+ } as CanonicalEvent;
270
+ }
271
+ const reasoning = payload["reasoning_content"];
272
+ if (typeof reasoning === "string" && reasoning.length > 0) {
273
+ yield {
274
+ type: "content_delta",
275
+ sequence_number: seq++,
276
+ content: { kind: "reasoning", payload: reasoning, summary: reasoning },
277
+ } as CanonicalEvent;
278
+ }
279
+ if (typeof payload["finish_reason"] === "string") {
280
+ stopReason = STOP_REASONS[payload["finish_reason"]] ?? "stop";
281
+ }
282
+ if (typeof payload["usage"] === "object" && payload["usage"] !== null) {
283
+ usage = payload["usage"] as Record<string, unknown>;
284
+ }
285
+ }
286
+
287
+ if (usage !== undefined) {
288
+ yield* this.#usageEvent(usage, seq++);
289
+ }
290
+ // The canonical terminal is `terminal`; a stream that ends without one is
291
+ // read as "the upstream never reported a result".
292
+ yield {
293
+ type: "terminal",
294
+ sequence_number: seq,
295
+ state: "complete",
296
+ stop_reason: stopReason,
297
+ } as unknown as CanonicalEvent;
298
+ }
299
+
300
+ /**
301
+ * Emits canonical tool-call deltas from one `tool_calls` array.
302
+ *
303
+ * Upstream nests the payload under `function_call`, not OpenAI's `function`,
304
+ * and spreads one call over many frames with three different shapes: the
305
+ * first carries id and name, the middle ones carry `arguments` as successive
306
+ * JSON fragments with an **empty** id, and the last repeats the id with no
307
+ * arguments. Grouping by `index` is what ties them together — the id is
308
+ * absent exactly when the arguments are present, so keying on id would drop
309
+ * every fragment and yield `arguments: "{}"`.
310
+ */
311
+ async *#toolCallDeltas(
312
+ raw: unknown,
313
+ announced: Set<string>,
314
+ nextSeq: () => number,
315
+ ): AsyncIterable<CanonicalEvent> {
316
+ if (!Array.isArray(raw)) return;
317
+ for (const entry of raw) {
318
+ if (typeof entry !== "object" || entry === null) continue;
319
+ const call = entry as Record<string, unknown>;
320
+ const fn = call["function_call"];
321
+ const fnRecord = typeof fn === "object" && fn !== null ? (fn as Record<string, unknown>) : undefined;
322
+ const name = typeof fnRecord?.["name"] === "string" ? (fnRecord["name"] as string) : undefined;
323
+ const args = typeof fnRecord?.["arguments"] === "string" ? (fnRecord["arguments"] as string) : "";
324
+ const declaredId = typeof call["id"] === "string" ? call["id"] : "";
325
+ const index = typeof call["index"] === "number" ? call["index"] : 0;
326
+ const slot = `idx:${index}`;
327
+
328
+ if (declaredId.length > 0) {
329
+ this.#toolCallIds.set(slot, declaredId);
330
+ // The closing frame repeats a known id with no payload; emitting it
331
+ // again would append a duplicate empty call.
332
+ if (announced.has(declaredId)) continue;
333
+ announced.add(declaredId);
334
+ yield {
335
+ type: "tool_call_delta",
336
+ sequence_number: nextSeq(),
337
+ call_id: declaredId,
338
+ name: name ?? "",
339
+ arguments_delta: "",
340
+ } as CanonicalEvent;
341
+ continue;
342
+ }
343
+
344
+ if (args.length === 0) continue;
345
+ const id = this.#toolCallIds.get(slot);
346
+ if (id === undefined) continue;
347
+ yield {
348
+ type: "tool_call_delta",
349
+ sequence_number: nextSeq(),
350
+ call_id: id,
351
+ arguments_delta: args,
352
+ } as CanonicalEvent;
353
+ }
354
+ }
355
+
356
+ /**
357
+ * Emits the terminal usage event, or none at all.
358
+ *
359
+ * Trae reports usage with the OpenAI spelling (`prompt_tokens` /
360
+ * `completion_tokens`). `usageFromProvider` normalizes both spellings and
361
+ * returns `undefined` when nothing was actually reported, and an absent
362
+ * usage event is correct in that case — emitting zeros would record a
363
+ * request that cost nothing.
364
+ */
365
+ async *#usageEvent(usage: Record<string, unknown>, seq: number): AsyncIterable<CanonicalEvent> {
366
+ const record = usageFromProvider(usage);
367
+ if (record === undefined) return;
368
+ yield { type: "usage", sequence_number: seq, usage: record } as unknown as CanonicalEvent;
369
+ }
370
+ }
371
+
372
+ export function createTraeAdapter(options: TraeAdapterOptions = {}): ProviderAdapter {
373
+ return new TraeAdapter(options);
374
+ }
375
+
376
+ /** Adapter for the China deployment: different host, auth scheme, and roster. */
377
+ export function createTraeCnAdapter(options: TraeAdapterOptions = {}): ProviderAdapter {
378
+ return new TraeAdapter({
379
+ ...options,
380
+ realm: "cn",
381
+ providerId: TRAE_CN_PROVIDER_ID,
382
+ });
383
+ }
384
+
385
+ /**
386
+ * Live model discovery.
387
+ *
388
+ * The listing is per account region, not per realm: it answers on CN and on the
389
+ * intl region host that serves the account (`coresg-normal.trae.ai` verified),
390
+ * while the generic intl core answers `403 no schema found`. An empty result is
391
+ * therefore a real "this account has no listing", and the static roster stays
392
+ * the fallback rather than being replaced by nothing.
393
+ */
394
+ export async function discoverTraeModels(input: {
395
+ readonly realm: TraeRealm;
396
+ readonly token: string;
397
+ readonly uid: string;
398
+ readonly fetcher: typeof fetch;
399
+ readonly signal?: AbortSignal;
400
+ readonly providerId?: string;
401
+ /** Account region, so the listing is asked of the host that serves it. */
402
+ readonly region?: string;
403
+ }): Promise<readonly { id: string; ctx: number | undefined }[]> {
404
+ const headers = buildTraeHeaders({
405
+ realm: input.realm,
406
+ token: input.token,
407
+ uid: input.uid,
408
+ });
409
+ const url = `${traeCoreForAccount(input.realm, input.region === undefined ? undefined : { region: input.region })}/api/ide/v1/model_list`;
410
+ const response = await input.fetcher(url, {
411
+ method: "POST",
412
+ headers,
413
+ body: JSON.stringify({ type: "chat" }),
414
+ ...(input.signal ? { signal: input.signal } : {}),
415
+ });
416
+ if (!response.ok) return [];
417
+ const parsed = (await response.json()) as {
418
+ data?: unknown;
419
+ models?: unknown;
420
+ model_configs?: unknown;
421
+ };
422
+ const rows = Array.isArray(parsed.model_configs)
423
+ ? parsed.model_configs
424
+ : Array.isArray(parsed.models)
425
+ ? parsed.models
426
+ : Array.isArray(parsed.data)
427
+ ? parsed.data
428
+ : [];
429
+ return rows
430
+ .filter((row): row is Record<string, unknown> => typeof row === "object" && row !== null)
431
+ .map((row) => ({
432
+ // `model_name` is the key upstream's listing uses; the others are the
433
+ // spellings the older donors assumed.
434
+ id: String(row["model_name"] ?? row["name"] ?? row["id"] ?? "").trim(),
435
+ ctx: traeMaxContextWindow(row),
436
+ }))
437
+ .filter((row) => row.id.length > 0);
438
+ }
439
+
440
+ export { TRAE_CN_MODELS, TRAE_INTL_MODELS };
441
+
442
+ /**
443
+ * Core host for one account.
444
+ *
445
+ * An intl account is provisioned into a region and its token only authenticates
446
+ * against that region's host, so the account's own `region` wins over the
447
+ * realm default. Falling back to the realm keeps older accounts (stored before
448
+ * the region was captured) working exactly as before.
449
+ */
450
+ function traeCoreForAccount(
451
+ realm: TraeRealm,
452
+ authState: Readonly<Record<string, unknown>> | undefined,
453
+ ): string {
454
+ const region = typeof authState?.["region"] === "string" ? authState["region"].toLowerCase() : "";
455
+ const regional = TRAE_REGION_CORE_HOSTS[region];
456
+ if (regional !== undefined) return regional;
457
+ return traeCoreCandidates(realm)[0] ?? "";
458
+ }
@@ -1,3 +1,4 @@
1
+ import { healthEventBuffer } from "../../persistence/health-event-flush";
1
2
  // Account health: error classification, failure/success recording, recovery and cooldown sweeps.
2
3
  import { and, desc, eq, inArray, isNotNull, lte, or, sql } from "drizzle-orm";
3
4
  import { affectedRows, pruneExpiredCooldowns } from "../../persistence/dialect";
@@ -214,6 +215,20 @@ export interface AccountErrorClassification {
214
215
  readonly reason: string;
215
216
  readonly retryAt: Date | null;
216
217
  readonly mutatesAccount: boolean;
218
+ /**
219
+ * True when this write changed a field the routing snapshot actually reads.
220
+ *
221
+ * The snapshot projects `provider_accounts.status` and
222
+ * `.cooldownUntil` and nothing else (`route-catalog.ts:286-287`), so only a
223
+ * write that moves one of those has to drop it. Everything else a failure
224
+ * persists — the failure streak, the error text, the category, a
225
+ * model-scoped cooldown — is invisible to routing, and invalidating for it
226
+ * forced the next request to rebuild an 11-SELECT snapshot for no reason.
227
+ *
228
+ * That made a failure wave self-sustaining: N failed attempts produced N
229
+ * invalidations, and every following request paid the full rebuild.
230
+ */
231
+ readonly routesVisible: boolean;
217
232
  /**
218
233
  * True when `cooldownMs` came from the provider's own statement — a parsed
219
234
  * `Retry-After`-family header, or a reset quoted in its message — rather than
@@ -390,6 +405,11 @@ export function classifyAccountError(
390
405
  reason: origin === "upstream" ? rawUpstreamResponse ?? message.slice(0, 160) : explanation,
391
406
  retryAt,
392
407
  mutatesAccount,
408
+ // Set below by the caller that knows which columns it wrote, not here:
409
+ // the same classification feeds both the account-wide path (status +
410
+ // cooldownUntil — routing-visible) and the model-scoped path (neither —
411
+ // invisible). Defaulting true keeps existing callers conservative.
412
+ routesVisible: true,
393
413
  providerStated,
394
414
  modelUnsupported,
395
415
  });
@@ -457,6 +477,16 @@ export function classifyAccountError(
457
477
  !priceRefusal &&
458
478
  !deploymentInfraError &&
459
479
  (statusCode === 402 ||
480
+ // Qoder answers an exhausted credit balance with
481
+ // `{"code":"112","message":"{\"pricingUrl\":...}"}` — the body points at
482
+ // the pricing page, i.e. "buy more", which is a balance verdict, not an
483
+ // auth one. It fell through to `auth_invalidated` and the console showed
484
+ // "Re-login required" on an account whose credential was fine, sending
485
+ // operators to re-authenticate instead of topping up.
486
+ // Recognising it here keeps it out of `auth_invalidated`; the branch
487
+ // below scopes the resulting cooldown to the model, because the account
488
+ // keeps serving its other models.
489
+ providerCode === "112" ||
460
490
  providerCode === "insufficient_quota" ||
461
491
  providerCode === "quota_exceeded" ||
462
492
  providerCode === "subscription:free-usage-exhausted" ||
@@ -489,7 +519,9 @@ export function classifyAccountError(
489
519
  lower.includes("insufficient balance") ||
490
520
  lower.includes("out of credits") ||
491
521
  lower.includes("credits exhausted") ||
492
- lower.includes("spending limit"));
522
+ lower.includes("spending limit") ||
523
+ // Same Qoder family reached by body text when the code is not parsed out.
524
+ (lower.includes("pricingurl") && lower.includes("qoder.com/pricing")));
493
525
  // A deterministic content-policy rejection is NOT credential invalidation.
494
526
  // CodeBuddy returns HTTP 403 with provider code 11140 ("request illegal",
495
527
  // "did not pass the safety review") — refreshing the token cannot change
@@ -687,6 +719,31 @@ export function classifyAccountError(
687
719
  }
688
720
 
689
721
  if (quotaSignal) {
722
+ /**
723
+ * Qoder's `112` is a verdict on the (account, model) pair, not on the
724
+ * credential or the balance as a whole.
725
+ *
726
+ * The body is `{"code":"112","message":"{\"pricingUrl\":…}"}` — a link to
727
+ * the pricing page, i.e. "this model is not in your plan". Observed
728
+ * behaviour agrees: the account keeps serving every other model after the
729
+ * refusal. Parking the whole account therefore took healthy models down
730
+ * with the one that is not entitled, which is the same mistake the
731
+ * `model_unreachable` branch already avoids for Cursor and buddy.
732
+ */
733
+ const qoderPlanGated = providerCode === "112";
734
+ if (qoderPlanGated) {
735
+ const hits = Math.max(1, Math.floor(options?.modelBlockHits ?? 1));
736
+ const cooldownMs = unsupportedModelCooldownMs(hits);
737
+ return result(
738
+ "model_unreachable",
739
+ "cooldown",
740
+ cooldownMs,
741
+ new Date(Date.now() + cooldownMs),
742
+ reason("Model not included in this account's plan"),
743
+ origin === "upstream" && options?.modelId !== undefined,
744
+ true,
745
+ );
746
+ }
690
747
  // xAI Grok Build's free tier resets on a rolling window. Use a duration
691
748
  // stated by the provider when present; otherwise park for the conservative
692
749
  // 24-hour fallback.
@@ -997,8 +1054,8 @@ export async function recordAccountFailure(
997
1054
  lastErrorAt: now,
998
1055
  })
999
1056
  .where(eq(providerAccounts.id, accountId));
1000
- if (!alreadyCooling && typeof client.insert === "function") {
1001
- await client.insert(healthEvents).values({
1057
+ if (!alreadyCooling) {
1058
+ await writeHealthEvent(client, {
1002
1059
  entityKind: "account",
1003
1060
  accountId,
1004
1061
  // The account's own status, on both sides: a model-scoped throttle
@@ -1015,7 +1072,10 @@ export async function recordAccountFailure(
1015
1072
  createdAt: now,
1016
1073
  });
1017
1074
  }
1018
- return classification;
1075
+ // `status` and `cooldownUntil` are untouched on this path — only the
1076
+ // (account, model) pair backed off — so the snapshot is still accurate
1077
+ // and must not be dropped.
1078
+ return { ...classification, routesVisible: false };
1019
1079
  }
1020
1080
 
1021
1081
  await client
@@ -1042,8 +1102,8 @@ export async function recordAccountFailure(
1042
1102
  })
1043
1103
  .where(eq(providerAccounts.id, accountId));
1044
1104
 
1045
- if (typeof client.insert === "function") {
1046
- await client.insert(healthEvents).values({
1105
+ {
1106
+ await writeHealthEvent(client, {
1047
1107
  entityKind: "account",
1048
1108
  accountId,
1049
1109
  fromStatus: fromStatus as "active" | "cooldown" | "disabled",
@@ -1054,7 +1114,9 @@ export async function recordAccountFailure(
1054
1114
  createdAt: now,
1055
1115
  });
1056
1116
  }
1057
- return classification;
1117
+ // This path wrote `status` and `cooldownUntil`, both of which the snapshot
1118
+ // projects, so routing has to see it.
1119
+ return { ...classification, routesVisible: true };
1058
1120
  };
1059
1121
 
1060
1122
  try {
@@ -1176,8 +1238,8 @@ export async function recoverAccount(
1176
1238
  lastErrorAt: null,
1177
1239
  })
1178
1240
  .where(eq(providerAccounts.id, accountId));
1179
- if (typeof client.insert === "function") {
1180
- await client.insert(healthEvents).values({
1241
+ {
1242
+ await writeHealthEvent(client, {
1181
1243
  entityKind: "account",
1182
1244
  accountId,
1183
1245
  fromStatus: fromStatus as "active" | "cooldown" | "disabled",
@@ -1314,6 +1376,36 @@ export interface AttemptHealthReport {
1314
1376
  readonly snapshotService?: { invalidate(): unknown };
1315
1377
  }
1316
1378
 
1379
+ /**
1380
+ * Writes one health audit row, off the request path when a buffer is installed.
1381
+ *
1382
+ * `health_events` is append-only and read by nobody in the request path, so it
1383
+ * does not need to land before the request finishes — unlike the
1384
+ * `provider_accounts` UPDATE beside it, which routing reads and which therefore
1385
+ * stays synchronous and transactional.
1386
+ */
1387
+ async function writeHealthEvent(
1388
+ client: CartethyiaDatabase,
1389
+ row: {
1390
+ readonly entityKind: "account";
1391
+ readonly accountId: string;
1392
+ readonly fromStatus: string;
1393
+ readonly toStatus: string;
1394
+ readonly reason: string;
1395
+ readonly errorCategory: string | null;
1396
+ readonly modelId?: string | null;
1397
+ readonly createdAt: Date;
1398
+ },
1399
+ ): Promise<void> {
1400
+ const buffer = healthEventBuffer();
1401
+ if (buffer !== undefined) {
1402
+ buffer.enqueue({ ...row, modelId: row.modelId ?? null });
1403
+ return;
1404
+ }
1405
+ if (typeof client.insert !== "function") return;
1406
+ await client.insert(healthEvents).values(row as never);
1407
+ }
1408
+
1317
1409
  export async function reportAttemptOutcome(
1318
1410
  db: CartethyiaDatabase,
1319
1411
  report: AttemptHealthReport,
@@ -1328,7 +1420,12 @@ export async function reportAttemptOutcome(
1328
1420
  ...report.evidence,
1329
1421
  ...(report.modelId === undefined ? {} : { modelId: report.modelId }),
1330
1422
  }).catch(() => null);
1331
- if (classification?.mutatesAccount) await report.snapshotService?.invalidate();
1423
+ // Gated on `routesVisible`, not on `mutatesAccount`: a failure that only
1424
+ // cooled one (account, model) pair, or only advanced the streak, changed
1425
+ // nothing routing reads. Invalidating for those rebuilt an 11-SELECT snapshot
1426
+ // on the next request, and under a failure wave each rebuild fed the next
1427
+ // invalidation.
1428
+ if (classification?.routesVisible) await report.snapshotService?.invalidate();
1332
1429
  }
1333
1430
 
1334
1431
  export async function listAccountHealthEvents(