cartethyia 2.7.9 → 2.8.1

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 (496) hide show
  1. package/.env.example +6 -5
  2. package/dist/dashboard/assets/{BannedPage-BxoG1MrL.js → BannedPage-Dq530awD.js} +1 -1
  3. package/dist/dashboard/assets/BannedPage-Dq530awD.js.gz +0 -0
  4. package/dist/dashboard/assets/{CliToolDetailPage-ixwgKk53.js → CliToolDetailPage-DZzaS-7S.js} +1 -1
  5. package/dist/dashboard/assets/CliToolDetailPage-DZzaS-7S.js.gz +0 -0
  6. package/dist/dashboard/assets/{CliToolsPage-BmttNV3U.js → CliToolsPage-DaZZ3cqP.js} +1 -1
  7. package/dist/dashboard/assets/CliToolsPage-DaZZ3cqP.js.gz +0 -0
  8. package/dist/dashboard/assets/{CombosPage-BE3qwwfC.js → CombosPage-BlC_xdkX.js} +1 -1
  9. package/dist/dashboard/assets/CombosPage-BlC_xdkX.js.gz +0 -0
  10. package/dist/dashboard/assets/{ConsoleLogPage-CRZdmMIX.js → ConsoleLogPage-D-rqeVtG.js} +1 -1
  11. package/dist/dashboard/assets/ConsoleLogPage-D-rqeVtG.js.gz +0 -0
  12. package/dist/dashboard/assets/{CustomizationPage-Dv_FJ6Er.js → CustomizationPage-7xT5yg2E.js} +1 -1
  13. package/dist/dashboard/assets/CustomizationPage-7xT5yg2E.js.gz +0 -0
  14. package/dist/dashboard/assets/HealthEventsModal-Clq8E9cW.js +1 -0
  15. package/dist/dashboard/assets/HealthEventsModal-Clq8E9cW.js.gz +0 -0
  16. package/dist/dashboard/assets/{InjectSystemPromptPanel-BOoO8rpz.js → InjectSystemPromptPanel-6Teoq9Se.js} +2 -2
  17. package/dist/dashboard/assets/InjectSystemPromptPanel-6Teoq9Se.js.gz +0 -0
  18. package/dist/dashboard/assets/{LoginPage-ChJuNNVN.js → LoginPage-CZuI0MAR.js} +1 -1
  19. package/dist/dashboard/assets/LoginPage-CZuI0MAR.js.gz +0 -0
  20. package/dist/dashboard/assets/{ModelPicker-BZAJtPg1.js → ModelPicker-C1hNTlkr.js} +1 -1
  21. package/dist/dashboard/assets/ModelPicker-C1hNTlkr.js.gz +0 -0
  22. package/dist/dashboard/assets/{OverviewPage-BE40s3u5.js → OverviewPage-0yfhSI1C.js} +1 -1
  23. package/dist/dashboard/assets/OverviewPage-0yfhSI1C.js.gz +0 -0
  24. package/dist/dashboard/assets/ProviderDetailPage-xqTxdQ4c.js +8 -0
  25. package/dist/dashboard/assets/ProviderDetailPage-xqTxdQ4c.js.gz +0 -0
  26. package/dist/dashboard/assets/{ProviderIcon-CQ6bPUf_.js → ProviderIcon-Ddhm1InL.js} +1 -1
  27. package/dist/dashboard/assets/ProviderIcon-Ddhm1InL.js.gz +0 -0
  28. package/dist/dashboard/assets/ProvidersPage-C3fYuu6i.js +1 -0
  29. package/dist/dashboard/assets/ProvidersPage-C3fYuu6i.js.gz +0 -0
  30. package/dist/dashboard/assets/ProxyPage-CHtJtZ3u.js +9 -0
  31. package/dist/dashboard/assets/ProxyPage-CHtJtZ3u.js.gz +0 -0
  32. package/dist/dashboard/assets/QuotaPage-D1RV_2BX.js +1 -0
  33. package/dist/dashboard/assets/QuotaPage-D1RV_2BX.js.gz +0 -0
  34. package/dist/dashboard/assets/{SettingsPage-C5HPWqEv.js → SettingsPage-BzZAvYSX.js} +1 -1
  35. package/dist/dashboard/assets/SettingsPage-BzZAvYSX.js.gz +0 -0
  36. package/dist/dashboard/assets/{SetupPage-CFNcATJJ.js → SetupPage--s-EfEIL.js} +1 -1
  37. package/dist/dashboard/assets/SetupPage--s-EfEIL.js.gz +0 -0
  38. package/dist/dashboard/assets/{StudioPage-YoD4tY5l.js → StudioPage-BjXFGSK4.js} +1 -1
  39. package/dist/dashboard/assets/StudioPage-BjXFGSK4.js.gz +0 -0
  40. package/dist/dashboard/assets/{UsagePage-DxjaAhb_.js → UsagePage-CP8F8BN7.js} +1 -1
  41. package/dist/dashboard/assets/UsagePage-CP8F8BN7.js.gz +0 -0
  42. package/dist/dashboard/assets/{api-keys-BTlkq_Uk.js → api-keys-2hc12Uz2.js} +1 -1
  43. package/dist/dashboard/assets/api-keys-2hc12Uz2.js.gz +0 -0
  44. package/dist/dashboard/assets/app-DGXbNfAR.css +2 -0
  45. package/dist/dashboard/assets/app-DGXbNfAR.css.gz +0 -0
  46. package/dist/dashboard/assets/assert-BMcj1xZE.js +1 -0
  47. package/dist/dashboard/assets/assert-BMcj1xZE.js.gz +0 -0
  48. package/dist/dashboard/assets/{badge-DdbT_7Am.js → badge-DkRUuVfz.js} +1 -1
  49. package/dist/dashboard/assets/badge-DkRUuVfz.js.gz +0 -0
  50. package/dist/dashboard/assets/{banner-CsGN-nbm.js → banner-CDjW841Q.js} +1 -1
  51. package/dist/dashboard/assets/banner-CDjW841Q.js.gz +0 -0
  52. package/dist/dashboard/assets/{card-mgZ_TLLZ.js → card-Bto5UlL9.js} +1 -1
  53. package/dist/dashboard/assets/card-Bto5UlL9.js.gz +0 -0
  54. package/dist/dashboard/assets/{cli-tools-A1-v8-03.js → cli-tools-DaH6g9hL.js} +1 -1
  55. package/dist/dashboard/assets/cli-tools-DaH6g9hL.js.gz +0 -0
  56. package/dist/dashboard/assets/{clipboard-button-CJ4Zty89.js → clipboard-button-BYKBCzjk.js} +1 -1
  57. package/dist/dashboard/assets/clipboard-button-BYKBCzjk.js.gz +0 -0
  58. package/dist/dashboard/assets/{dialog-B5L2jPQ_.js → dialog-DL0Zftwv.js} +1 -1
  59. package/dist/dashboard/assets/dialog-DL0Zftwv.js.gz +0 -0
  60. package/dist/dashboard/assets/{drawer-DkjUouAj.js → drawer-Dpx_UXaS.js} +1 -1
  61. package/dist/dashboard/assets/drawer-Dpx_UXaS.js.gz +0 -0
  62. package/dist/dashboard/assets/{entry-B-LVLV6o.js → entry-BV7-1NoM.js} +1 -1
  63. package/dist/dashboard/assets/entry-BV7-1NoM.js.gz +0 -0
  64. package/dist/dashboard/assets/{entry-CAhdYl3B.js → entry-CGvdNU6r.js} +1 -1
  65. package/dist/dashboard/assets/entry-CGvdNU6r.js.gz +0 -0
  66. package/dist/dashboard/assets/entry-DScKYvZj.js +4 -0
  67. package/dist/dashboard/assets/entry-DScKYvZj.js.gz +0 -0
  68. package/dist/dashboard/assets/{formatters-CrsdLVsn.js → formatters-BS3DZlZj.js} +1 -1
  69. package/dist/dashboard/assets/formatters-BS3DZlZj.js.gz +0 -0
  70. package/dist/dashboard/assets/{index-BU64Lqvp.js → index-SQnvvTWX.js} +2 -2
  71. package/dist/dashboard/assets/index-SQnvvTWX.js.gz +0 -0
  72. package/dist/dashboard/assets/{inline-B2aDLyyg.js → inline-Dl-cuLK0.js} +1 -1
  73. package/dist/dashboard/assets/inline-Dl-cuLK0.js.gz +0 -0
  74. package/dist/dashboard/assets/{input-BCVkdR9d.js → input-Bfx0EzRc.js} +1 -1
  75. package/dist/dashboard/assets/input-Bfx0EzRc.js.gz +0 -0
  76. package/dist/dashboard/assets/{layout-DNcAdMK-.js → layout-BxhbQ_JS.js} +1 -1
  77. package/dist/dashboard/assets/layout-BxhbQ_JS.js.gz +0 -0
  78. package/dist/dashboard/assets/{list-row-DgrIH0hw.js → list-row-D49Y4ptn.js} +1 -1
  79. package/dist/dashboard/assets/list-row-D49Y4ptn.js.gz +0 -0
  80. package/dist/dashboard/assets/{live-Cae01iu8.js → live-CbVTdX9V.js} +1 -1
  81. package/dist/dashboard/assets/live-CbVTdX9V.js.gz +0 -0
  82. package/dist/dashboard/assets/{network-CRib7H_2.js → network-JGvDveof.js} +1 -1
  83. package/dist/dashboard/assets/network-JGvDveof.js.gz +0 -0
  84. package/dist/dashboard/assets/{numbers-BKQE8Xpz.js → numbers-Dw6kiew-.js} +1 -1
  85. package/dist/dashboard/assets/numbers-Dw6kiew-.js.gz +0 -0
  86. package/dist/dashboard/assets/{pagination-ecPvG_8g.js → pagination-iUoI8GeE.js} +1 -1
  87. package/dist/dashboard/assets/pagination-iUoI8GeE.js.gz +0 -0
  88. package/dist/dashboard/assets/providers/sensenova-intl.png +0 -0
  89. package/dist/dashboard/assets/providers/sensenova.png +0 -0
  90. package/dist/dashboard/assets/providers/zro.svg +3 -0
  91. package/dist/dashboard/assets/providers/zro.svg.gz +0 -0
  92. package/dist/dashboard/assets/quota-DiPD6UKH.js +1 -0
  93. package/dist/dashboard/assets/quota-DiPD6UKH.js.gz +0 -0
  94. package/dist/dashboard/assets/{routing-Ckm1UXuC.js → routing-8jZr-3WE.js} +1 -1
  95. package/dist/dashboard/assets/routing-8jZr-3WE.js.gz +0 -0
  96. package/dist/dashboard/assets/{select-ChoFWyKz.js → select-oO1FxMVb.js} +2 -2
  97. package/dist/dashboard/assets/select-oO1FxMVb.js.gz +0 -0
  98. package/dist/dashboard/assets/{settings-CKjFPlKW.js → settings-CLr3kdIx.js} +1 -1
  99. package/dist/dashboard/assets/settings-CLr3kdIx.js.gz +0 -0
  100. package/dist/dashboard/assets/{stack-DWCwU3fh.js → stack-Y3pDteZr.js} +1 -1
  101. package/dist/dashboard/assets/stack-Y3pDteZr.js.gz +0 -0
  102. package/dist/dashboard/assets/{state-Cg3VL5J_.js → state-CArX_Iak.js} +1 -1
  103. package/dist/dashboard/assets/state-CArX_Iak.js.gz +0 -0
  104. package/dist/dashboard/assets/{switch-Db70BMix.js → switch-DwlwmDO-.js} +1 -1
  105. package/dist/dashboard/assets/switch-DwlwmDO-.js.gz +0 -0
  106. package/dist/dashboard/assets/{useMutation-BqdE0LuM.js → useMutation-DJ9GYEgf.js} +1 -1
  107. package/dist/dashboard/assets/useMutation-DJ9GYEgf.js.gz +0 -0
  108. package/dist/dashboard/index.html +2 -2
  109. package/dist/dashboard/index.html.gz +0 -0
  110. package/dist/dashboard/precompressed-assets.json +54 -53
  111. package/migrations/full-postgres/0046_add_accounts_quota_plan_fetched_at.sql +7 -0
  112. package/migrations/full-postgres/0047_add_provider_device_flow_proxy.sql +17 -0
  113. package/migrations/lite-sqlite3/0046_add_accounts_quota_plan_fetched_at.sql +4 -0
  114. package/migrations/lite-sqlite3/0047_add_provider_device_flow_proxy.sql +19 -0
  115. package/package.json +1 -1
  116. package/src/boot/app.ts +11 -14
  117. package/src/boot/dependencies.ts +5 -7
  118. package/src/boot/flush.ts +20 -0
  119. package/src/boot/lifecycle.ts +7 -5
  120. package/src/config/providers.ts +6 -5
  121. package/src/config/routing.ts +26 -0
  122. package/src/config/spec-metadata.ts +72 -0
  123. package/src/config/spec.ts +28 -8
  124. package/src/console/api-keys/contracts.ts +12 -1
  125. package/src/console/api-keys/routes.ts +40 -28
  126. package/src/console/auth/account-reset.ts +7 -9
  127. package/src/console/backup/contracts.ts +4 -0
  128. package/src/console/backup/export-endpoint.ts +7 -5
  129. package/src/console/backup/nine-router.ts +71 -10
  130. package/src/console/backup/service.ts +8 -1
  131. package/src/console/cli-tools/service.ts +6 -12
  132. package/src/console/console-router-guards.ts +23 -16
  133. package/src/console/console-router.ts +1 -1
  134. package/src/console/domain-registration.ts +7 -4
  135. package/src/console/inferhub/client.ts +1 -1
  136. package/src/console/inferhub/normalize.ts +24 -28
  137. package/src/console/inferhub/types.ts +3 -0
  138. package/src/console/ip-bans/routes.ts +7 -13
  139. package/src/console/observability/live.ts +2 -2
  140. package/src/console/observability/store.ts +0 -53
  141. package/src/console/providers/catalog/contracts.ts +9 -3
  142. package/src/console/providers/catalog/model-operations.ts +2 -3
  143. package/src/console/providers/catalog/provider-operations.ts +1 -1
  144. package/src/console/providers/catalog/routes.ts +3 -2
  145. package/src/console/providers/catalog/store.ts +1914 -1902
  146. package/src/console/providers/detail/routes.ts +1 -0
  147. package/src/console/providers/detail/store.ts +4 -0
  148. package/src/console/providers/oauth/callback-listener.ts +30 -11
  149. package/src/console/providers/oauth/device-egress.ts +123 -0
  150. package/src/console/providers/oauth/routes.ts +123 -16
  151. package/src/console/quota/account-quota-global.ts +14 -24
  152. package/src/console/quota/account-quota-shared.ts +49 -0
  153. package/src/console/quota/account-quota-tenant.ts +11 -23
  154. package/src/console/quota/account-test-egress.ts +2 -2
  155. package/src/console/quota/daily-growth-job.ts +1 -1
  156. package/src/console/quota/quota-view.ts +19 -3
  157. package/src/console/routing/pools/contracts.ts +1 -1
  158. package/src/console/routing/pools/operations.ts +32 -47
  159. package/src/console/routing/pools/probe-verdict.ts +2 -2
  160. package/src/console/routing/pools/relay-deploy.ts +1 -1
  161. package/src/console/routing/pools/store.ts +4 -4
  162. package/src/console/shared/errors.ts +33 -6
  163. package/src/main.ts +2 -3
  164. package/src/network/client.ts +67 -0
  165. package/src/network/egress/bidi-session.ts +322 -0
  166. package/src/network/egress/dial.ts +201 -0
  167. package/src/network/egress/factory.ts +191 -0
  168. package/src/network/{outbound-fetch.ts → egress/fetch.ts} +195 -29
  169. package/src/network/egress/tls-profile.ts +123 -0
  170. package/src/network/egress/ws.ts +175 -0
  171. package/src/network/pool/{byte-accounting.ts → accounting.ts} +3 -2
  172. package/src/network/{types.ts → pool/agent-config.ts} +1 -1
  173. package/src/network/pool/agent.ts +2 -2
  174. package/src/network/{pool-cooldown-store.ts → pool/cooldown-store.ts} +95 -20
  175. package/src/network/{pool-health-machine.ts → pool/health.ts} +205 -6
  176. package/src/network/pool/resolver.ts +10 -849
  177. package/src/network/pool/selector.ts +184 -41
  178. package/src/network/ssrf.ts +7 -7
  179. package/src/network/{retry.ts → util/retry.ts} +1 -1
  180. package/src/network/{rate-limit-verdict.ts → verdict.ts} +1 -1
  181. package/src/observability/event-log.ts +7 -361
  182. package/src/observability/performance-metrics.ts +9 -0
  183. package/src/observability/runtime-metrics.ts +14 -9
  184. package/src/observability/telemetry-buffer.ts +16 -26
  185. package/src/observability/telemetry-reconciliation.ts +9 -37
  186. package/src/observability/telemetry-row.ts +125 -0
  187. package/src/persistence/db-mode.ts +4 -1
  188. package/src/persistence/dialect.ts +18 -0
  189. package/src/persistence/migrate.ts +6 -3
  190. package/src/persistence/schema.ts +17 -0
  191. package/src/persistence/ttl-cache.ts +21 -3
  192. package/src/protocol/codecs.ts +59 -2
  193. package/src/protocol/dsml.ts +34 -86
  194. package/src/protocol/markup-stream-parser.ts +195 -0
  195. package/src/protocol/reasoning-text.ts +19 -0
  196. package/src/protocol/request/chat.ts +10 -10
  197. package/src/protocol/request/gemini.ts +8 -2
  198. package/src/protocol/request/messages.ts +34 -22
  199. package/src/protocol/response/chat.ts +12 -20
  200. package/src/protocol/response/messages.ts +34 -7
  201. package/src/protocol/response/omp-tool-call-markup.ts +43 -86
  202. package/src/protocol/response/responses.ts +18 -2
  203. package/src/protocol/stop-reason.ts +23 -1
  204. package/src/protocol/think-split.ts +112 -0
  205. package/src/protocol/translation/dialect/qwen.ts +26 -58
  206. package/src/protocol/translation/quirks.ts +15 -0
  207. package/src/protocol/upstream/send-claude-messages.ts +2 -2
  208. package/src/providers/adapters/compatible-adapter.ts +76 -12
  209. package/src/providers/authentication/oauth-client.ts +40 -6
  210. package/src/providers/authentication/oauth-device-flow.ts +105 -11
  211. package/src/providers/authentication/oauth-flow-store.ts +82 -1
  212. package/src/providers/authentication/oauth-refresh-service.ts +766 -758
  213. package/src/providers/configured-provider.ts +148 -0
  214. package/src/providers/default-registry.ts +137 -36
  215. package/src/providers/discovery/discovery-types.ts +7 -0
  216. package/src/providers/discovery/probe-phases.ts +20 -6
  217. package/src/providers/discovery/probing-service.ts +22 -2
  218. package/src/providers/integrations/alibabacp/alibabacp-models.ts +1 -2
  219. package/src/providers/integrations/{dashscope-models.ts → alibabacp/dashscope-models.ts} +2 -2
  220. package/src/providers/integrations/antigravity/antigravity-protocol.ts +16 -1
  221. package/src/providers/integrations/buddy/account.ts +969 -0
  222. package/src/providers/integrations/buddy/capabilities.ts +12 -12
  223. package/src/providers/integrations/buddy/{buddy-catalog-shared.ts → catalog.ts} +198 -136
  224. package/src/providers/integrations/buddy/{buddy-chat-shared.ts → chat.ts} +176 -20
  225. package/src/providers/integrations/buddy/codebuddy/index.ts +429 -0
  226. package/src/providers/integrations/buddy/codebuddy/shared.ts +41 -26
  227. package/src/providers/integrations/buddy/headers.ts +311 -0
  228. package/src/providers/integrations/buddy/lifecycle.ts +1018 -0
  229. package/src/providers/integrations/buddy/{buddy-media.ts → media.ts} +1 -1
  230. package/src/providers/integrations/claude/claude-quota.ts +14 -17
  231. package/src/providers/integrations/cline/cline-oauth.ts +10 -5
  232. package/src/providers/integrations/cline/cline.ts +5 -6
  233. package/src/providers/integrations/clixad/clixad-oauth.ts +98 -0
  234. package/src/providers/integrations/clixad/clixad-quota.ts +132 -0
  235. package/src/providers/integrations/clixad/provider.ts +145 -0
  236. package/src/providers/integrations/codex/codex-oauth.ts +8 -5
  237. package/src/providers/integrations/commandcode/quota.ts +22 -28
  238. package/src/providers/integrations/cursor/cursor-auth.ts +1 -9
  239. package/src/providers/integrations/cursor/cursor-exec.ts +9 -2
  240. package/src/providers/integrations/cursor/cursor-hardening.ts +279 -0
  241. package/src/providers/integrations/cursor/cursor-http2.ts +605 -6
  242. package/src/providers/integrations/cursor/cursor-request.ts +26 -12
  243. package/src/providers/integrations/cursor/cursor-shared.ts +8 -5
  244. package/src/providers/integrations/cursor/cursor.ts +574 -110
  245. package/src/providers/integrations/cursor/protocol/messages.ts +22 -29
  246. package/src/providers/integrations/cursor/protocol/protobuf-schema.ts +37 -0
  247. package/src/providers/integrations/deepai/deepai-catalog.ts +149 -0
  248. package/src/providers/integrations/deepai/deepai-common.ts +145 -0
  249. package/src/providers/integrations/deepai/deepai-oauth.ts +129 -0
  250. package/src/providers/integrations/deepai/deepai-quota.ts +97 -0
  251. package/src/providers/integrations/deepai/provider.ts +42 -0
  252. package/src/providers/integrations/devin/devin-auth.ts +7 -0
  253. package/src/providers/integrations/devin/protocol/devin-proto.ts +1 -27
  254. package/src/providers/integrations/factory/factory-oauth.ts +33 -51
  255. package/src/providers/integrations/freebuff/assets/official-system-manager.txt +88 -0
  256. package/src/providers/integrations/freebuff/assets/official-system-worker.txt +57 -0
  257. package/src/providers/integrations/freebuff/assets/official-tools.json +1070 -0
  258. package/src/providers/integrations/freebuff/freebuff-catalog.ts +120 -0
  259. package/src/providers/integrations/freebuff/freebuff-constants.ts +1233 -0
  260. package/src/providers/integrations/freebuff/freebuff-discovery.ts +108 -0
  261. package/src/providers/integrations/freebuff/freebuff-oauth.ts +240 -0
  262. package/src/providers/integrations/freebuff/freebuff-quota.ts +260 -0
  263. package/src/providers/integrations/freebuff/freebuff-virtual.ts +89 -0
  264. package/src/providers/integrations/freebuff/freebuff.ts +682 -0
  265. package/src/providers/integrations/freepi/freepi-catalog.ts +5 -0
  266. package/src/providers/integrations/freepi/freepi-discovery.ts +2 -2
  267. package/src/providers/integrations/freepi/freepi-headers.ts +2 -2
  268. package/src/providers/integrations/freepi/freepi-protocol.ts +11 -2
  269. package/src/providers/integrations/freepi/freepi-realm.ts +0 -3
  270. package/src/providers/integrations/freepi/freepi-session.ts +158 -0
  271. package/src/providers/integrations/freepi/freepi.ts +80 -54
  272. package/src/providers/integrations/github/github-discovery.ts +30 -13
  273. package/src/providers/integrations/github/github-oauth.ts +217 -44
  274. package/src/providers/integrations/github/github-quota.ts +22 -5
  275. package/src/providers/integrations/github/github.ts +40 -32
  276. package/src/providers/integrations/grok/grok-oauth.ts +52 -49
  277. package/src/providers/integrations/grok/grok-ratelimit-store.ts +10 -17
  278. package/src/providers/integrations/grok/grok-turn-index.ts +11 -16
  279. package/src/providers/integrations/hermes/hermes-oauth.ts +3 -3
  280. package/src/providers/integrations/{hermes.ts → hermes/hermes.ts} +42 -13
  281. package/src/providers/integrations/hermes/index.ts +2 -0
  282. package/src/providers/integrations/kilo/kilo-oauth.ts +4 -4
  283. package/src/providers/integrations/kilo/kilo.ts +1 -1
  284. package/src/providers/integrations/kimi/kimi-oauth.ts +3 -3
  285. package/src/providers/integrations/kiro/kiro-catalog.ts +17 -1
  286. package/src/providers/integrations/kiro/kiro-discovery.ts +10 -12
  287. package/src/providers/integrations/kiro/kiro-oauth.ts +2 -2
  288. package/src/providers/integrations/kiro/kiro-quota.ts +8 -4
  289. package/src/providers/integrations/kiro/kiro.ts +15 -19
  290. package/src/providers/integrations/muse/muse-oauth.ts +5 -5
  291. package/src/providers/integrations/ollamacloud/index.ts +2 -0
  292. package/src/providers/integrations/{ollamacloud-models.ts → ollamacloud/models.ts} +2 -2
  293. package/src/providers/integrations/{ollamacloud-quota.ts → ollamacloud/quota.ts} +3 -3
  294. package/src/providers/integrations/opencode/fingerprint.ts +1 -1
  295. package/src/providers/integrations/opencode/opencode-go-quota.ts +2 -8
  296. package/src/providers/integrations/openrouter/provider.ts +1 -1
  297. package/src/providers/integrations/perch/perch-credential.ts +25 -0
  298. package/src/providers/integrations/perch/perch-protocol.ts +0 -6
  299. package/src/providers/integrations/perch/perch-quota.ts +2 -16
  300. package/src/providers/integrations/perch/perch.ts +6 -16
  301. package/src/providers/integrations/qoder/claim-qoder-credits.ts +19 -60
  302. package/src/providers/integrations/qoder/qoder-adapter.ts +20 -125
  303. package/src/providers/integrations/qoder/qoder-auth.ts +109 -30
  304. package/src/providers/integrations/qoder/qoder-catalog.ts +21 -6
  305. package/src/providers/integrations/qoder/qoder-chat.ts +32 -29
  306. package/src/providers/integrations/qoder/qoder-checkin.ts +5 -5
  307. package/src/providers/integrations/qoder/qoder-cosy.ts +37 -9
  308. package/src/providers/integrations/qoder/qoder-oauth.ts +9 -2
  309. package/src/providers/integrations/qoder/qoder-realm.ts +13 -36
  310. package/src/providers/integrations/qoder/qoder-sse.ts +447 -68
  311. package/src/providers/integrations/qoder/qoder-types.ts +9 -0
  312. package/src/providers/integrations/qoder/qoder.ts +3 -26
  313. package/src/providers/integrations/sensenova/provider.ts +144 -0
  314. package/src/providers/integrations/trae/trae-protocol.ts +9 -5
  315. package/src/providers/integrations/trae/trae-quota.ts +21 -29
  316. package/src/providers/integrations/trae/trae-realm.ts +38 -0
  317. package/src/providers/integrations/trae/trae.ts +7 -24
  318. package/src/providers/integrations/vercel/provider.ts +117 -0
  319. package/src/providers/integrations/vercel/vercel-quota.ts +96 -0
  320. package/src/providers/integrations/xai/xai-oauth.ts +5 -5
  321. package/src/providers/integrations/zcode-start-plan/gifts.ts +1 -1
  322. package/src/providers/integrations/zcode-start-plan/oauth.ts +3 -3
  323. package/src/providers/integrations/zcode-start-plan/provider.ts +226 -23
  324. package/src/providers/integrations/zcode-start-plan/quota.ts +19 -2
  325. package/src/providers/integrations/zro/zro.ts +205 -0
  326. package/src/providers/operations/account-error-classification.ts +199 -307
  327. package/src/providers/operations/account-health-service.ts +82 -1
  328. package/src/providers/operations/account-refresh-status.ts +36 -37
  329. package/src/providers/operations/account-tier.ts +15 -16
  330. package/src/providers/operations/client-version-resolver.ts +1 -1
  331. package/src/providers/operations/client-versions.ts +100 -81
  332. package/src/providers/operations/cooldown-rules.ts +11 -16
  333. package/src/providers/operations/oauth-refresh-lead.ts +46 -22
  334. package/src/providers/operations/{model-catalog-cache.ts → provider-caches.ts} +68 -16
  335. package/src/providers/operations/provider-catalog-service.ts +1 -1
  336. package/src/providers/operations/provider-credential-service.ts +118 -51
  337. package/src/providers/operations/provider-rules/github.ts +48 -0
  338. package/src/providers/operations/provider-rules/grok.ts +33 -0
  339. package/src/providers/operations/provider-rules/inception.ts +49 -0
  340. package/src/providers/operations/provider-rules/index.ts +32 -0
  341. package/src/providers/operations/provider-rules/perch.ts +37 -0
  342. package/src/providers/operations/provider-rules/qoder.ts +70 -0
  343. package/src/providers/operations/provider-rules/trae.ts +81 -0
  344. package/src/providers/operations/session-resolution.ts +17 -1
  345. package/src/providers/provider-contracts.ts +6 -4
  346. package/src/providers/{provider-credential-resolver.ts → provider-credentials.ts} +40 -3
  347. package/src/providers/provider-metadata.ts +70 -10
  348. package/src/providers/provider-probe.ts +15 -1
  349. package/src/providers/provider-registry.ts +42 -33
  350. package/src/providers/quota/quota-contracts.ts +2 -0
  351. package/src/providers/quota/quota-limit-windows.ts +16 -5
  352. package/src/providers/quota/quota-support.ts +1 -1
  353. package/src/providers/quota/quota-window-parser.ts +90 -18
  354. package/src/providers/quota/refresh.ts +100 -21
  355. package/src/security/api-key-auth-cache.ts +1 -1
  356. package/src/security/api-key-auth.ts +6 -2
  357. package/src/security/ip-boundary.ts +1 -1
  358. package/src/transport/dispatch/attempt-finalize.ts +15 -14
  359. package/src/transport/dispatch/attempt-loop.ts +73 -19
  360. package/src/transport/dispatch/generation.ts +2 -8
  361. package/src/transport/dispatch/handler-deps.ts +1 -1
  362. package/src/transport/dispatch/leases.ts +1 -1
  363. package/src/transport/dispatch/native-shared.ts +25 -0
  364. package/src/transport/dispatch/proxy-request.ts +8 -46
  365. package/src/transport/dispatch/responses-compact.ts +2 -8
  366. package/src/transport/dispatch/retry-policy.ts +1 -1
  367. package/src/transport/dispatch/streaming/finalize.ts +10 -7
  368. package/src/transport/dispatch/streaming/stream.ts +9 -37
  369. package/src/transport/dispatch/systemone.ts +2 -8
  370. package/src/transport/dispatch/token-count.ts +2 -6
  371. package/src/transport/dispatch/upstream.ts +63 -0
  372. package/src/transport/drain-endpoint.ts +31 -4
  373. package/src/transport/failure-policy.ts +16 -30
  374. package/src/transport/guards/body-policy.ts +7 -1
  375. package/src/transport/guards/error-lifecycle.ts +13 -1
  376. package/src/transport/guards/request-context.ts +22 -14
  377. package/src/transport/request/preparer.ts +81 -71
  378. package/src/transport/routing/bounded-lru.ts +14 -0
  379. package/src/transport/routing/drain-reserve.ts +129 -0
  380. package/src/transport/routing/plan-gate.ts +7 -1
  381. package/src/transport/routing/round-robin.ts +60 -0
  382. package/src/transport/routing/route-catalog.ts +123 -111
  383. package/src/transport/routing/route-model.ts +51 -9
  384. package/src/transport/routing/router.ts +393 -32
  385. package/src/transport/shutdown-notice.ts +1 -1
  386. package/src/transport/surface/adapters.ts +11 -2
  387. package/src/transport/surface/chat/adapter.ts +2 -2
  388. package/src/transport/surface/chat/encode.ts +47 -4
  389. package/src/transport/surface/chat/parse.ts +2 -12
  390. package/src/transport/surface/completion.ts +44 -33
  391. package/src/transport/surface/content-parts.ts +21 -0
  392. package/src/transport/surface/messages/adapter.ts +7 -1
  393. package/src/transport/surface/messages/encode.ts +11 -3
  394. package/src/transport/surface/messages/parse.ts +2 -1
  395. package/src/transport/surface/messages/stream.ts +33 -17
  396. package/src/transport/surface/responses/encode.ts +22 -5
  397. package/src/transport/surface/responses/parse.ts +3 -11
  398. package/src/workers/buddy-growth-tasks.ts +8 -8
  399. package/src/workers/daily-checkin.ts +4 -4
  400. package/src/workers/quota-refresh-sweep.ts +1 -1
  401. package/src/workers/zcode-gift-claim.ts +1 -1
  402. package/dist/dashboard/assets/BannedPage-BxoG1MrL.js.gz +0 -0
  403. package/dist/dashboard/assets/CliToolDetailPage-ixwgKk53.js.gz +0 -0
  404. package/dist/dashboard/assets/CliToolsPage-BmttNV3U.js.gz +0 -0
  405. package/dist/dashboard/assets/CombosPage-BE3qwwfC.js.gz +0 -0
  406. package/dist/dashboard/assets/ConsoleLogPage-CRZdmMIX.js.gz +0 -0
  407. package/dist/dashboard/assets/CustomizationPage-Dv_FJ6Er.js.gz +0 -0
  408. package/dist/dashboard/assets/HealthEventsModal-CoVrw2sM.js +0 -1
  409. package/dist/dashboard/assets/HealthEventsModal-CoVrw2sM.js.gz +0 -0
  410. package/dist/dashboard/assets/InjectSystemPromptPanel-BOoO8rpz.js.gz +0 -0
  411. package/dist/dashboard/assets/LoginPage-ChJuNNVN.js.gz +0 -0
  412. package/dist/dashboard/assets/ModelPicker-BZAJtPg1.js.gz +0 -0
  413. package/dist/dashboard/assets/OverviewPage-BE40s3u5.js.gz +0 -0
  414. package/dist/dashboard/assets/ProviderDetailPage-sfLoQtPV.js +0 -8
  415. package/dist/dashboard/assets/ProviderDetailPage-sfLoQtPV.js.gz +0 -0
  416. package/dist/dashboard/assets/ProviderIcon-CQ6bPUf_.js.gz +0 -0
  417. package/dist/dashboard/assets/ProvidersPage-DcepmPWo.js +0 -1
  418. package/dist/dashboard/assets/ProvidersPage-DcepmPWo.js.gz +0 -0
  419. package/dist/dashboard/assets/ProxyPage-BJ87Pp0U.js +0 -9
  420. package/dist/dashboard/assets/ProxyPage-BJ87Pp0U.js.gz +0 -0
  421. package/dist/dashboard/assets/QuotaPage-gkSyRAKa.js +0 -1
  422. package/dist/dashboard/assets/QuotaPage-gkSyRAKa.js.gz +0 -0
  423. package/dist/dashboard/assets/SettingsPage-C5HPWqEv.js.gz +0 -0
  424. package/dist/dashboard/assets/SetupPage-CFNcATJJ.js.gz +0 -0
  425. package/dist/dashboard/assets/StudioPage-YoD4tY5l.js.gz +0 -0
  426. package/dist/dashboard/assets/UsagePage-DxjaAhb_.js.gz +0 -0
  427. package/dist/dashboard/assets/api-keys-BTlkq_Uk.js.gz +0 -0
  428. package/dist/dashboard/assets/app-BX2nnKqs.css +0 -2
  429. package/dist/dashboard/assets/app-BX2nnKqs.css.gz +0 -0
  430. package/dist/dashboard/assets/assert-CndthXFQ.js +0 -1
  431. package/dist/dashboard/assets/assert-CndthXFQ.js.gz +0 -0
  432. package/dist/dashboard/assets/badge-DdbT_7Am.js.gz +0 -0
  433. package/dist/dashboard/assets/banner-CsGN-nbm.js.gz +0 -0
  434. package/dist/dashboard/assets/card-mgZ_TLLZ.js.gz +0 -0
  435. package/dist/dashboard/assets/cli-tools-A1-v8-03.js.gz +0 -0
  436. package/dist/dashboard/assets/clipboard-button-CJ4Zty89.js.gz +0 -0
  437. package/dist/dashboard/assets/dialog-B5L2jPQ_.js.gz +0 -0
  438. package/dist/dashboard/assets/drawer-DkjUouAj.js.gz +0 -0
  439. package/dist/dashboard/assets/entry-B-LVLV6o.js.gz +0 -0
  440. package/dist/dashboard/assets/entry-CAhdYl3B.js.gz +0 -0
  441. package/dist/dashboard/assets/entry-D2xD0Tmd.js +0 -4
  442. package/dist/dashboard/assets/entry-D2xD0Tmd.js.gz +0 -0
  443. package/dist/dashboard/assets/formatters-CrsdLVsn.js.gz +0 -0
  444. package/dist/dashboard/assets/index-BU64Lqvp.js.gz +0 -0
  445. package/dist/dashboard/assets/inline-B2aDLyyg.js.gz +0 -0
  446. package/dist/dashboard/assets/input-BCVkdR9d.js.gz +0 -0
  447. package/dist/dashboard/assets/layout-DNcAdMK-.js.gz +0 -0
  448. package/dist/dashboard/assets/list-row-DgrIH0hw.js.gz +0 -0
  449. package/dist/dashboard/assets/live-Cae01iu8.js.gz +0 -0
  450. package/dist/dashboard/assets/network-CRib7H_2.js.gz +0 -0
  451. package/dist/dashboard/assets/numbers-BKQE8Xpz.js.gz +0 -0
  452. package/dist/dashboard/assets/pagination-ecPvG_8g.js.gz +0 -0
  453. package/dist/dashboard/assets/quota-DJ1QlAOq.js +0 -1
  454. package/dist/dashboard/assets/quota-DJ1QlAOq.js.gz +0 -0
  455. package/dist/dashboard/assets/routing-Ckm1UXuC.js.gz +0 -0
  456. package/dist/dashboard/assets/select-ChoFWyKz.js.gz +0 -0
  457. package/dist/dashboard/assets/settings-CKjFPlKW.js.gz +0 -0
  458. package/dist/dashboard/assets/stack-DWCwU3fh.js.gz +0 -0
  459. package/dist/dashboard/assets/state-Cg3VL5J_.js.gz +0 -0
  460. package/dist/dashboard/assets/switch-Db70BMix.js.gz +0 -0
  461. package/dist/dashboard/assets/useMutation-BqdE0LuM.js.gz +0 -0
  462. package/src/network/pool-health.ts +0 -210
  463. package/src/providers/bundled-discovery-providers.ts +0 -98
  464. package/src/providers/configured-openai-providers.ts +0 -120
  465. package/src/providers/integrations/buddy/buddy-checkin.ts +0 -521
  466. package/src/providers/integrations/buddy/buddy-cli-headers.ts +0 -76
  467. package/src/providers/integrations/buddy/buddy-conversation-headers.ts +0 -130
  468. package/src/providers/integrations/buddy/buddy-discovery-shared.ts +0 -234
  469. package/src/providers/integrations/buddy/buddy-growth-shared.ts +0 -496
  470. package/src/providers/integrations/buddy/buddy-identity.ts +0 -300
  471. package/src/providers/integrations/buddy/buddy-oauth-shared.ts +0 -341
  472. package/src/providers/integrations/buddy/buddy-quota-shared.ts +0 -417
  473. package/src/providers/integrations/buddy/buddy-server-tools.ts +0 -240
  474. package/src/providers/integrations/buddy/buddy-usage-shared.ts +0 -221
  475. package/src/providers/integrations/buddy/buddy-usage-store.ts +0 -181
  476. package/src/providers/integrations/buddy/codebuddy/cn-catalog.ts +0 -37
  477. package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +0 -185
  478. package/src/providers/integrations/buddy/codebuddy/oauth.ts +0 -47
  479. package/src/providers/integrations/buddy/codebuddy/provider.ts +0 -175
  480. package/src/providers/integrations/buddy/codebuddy/quota.ts +0 -154
  481. package/src/providers/integrations/qoder/chat-proto.ts +0 -328
  482. package/src/providers/integrations/qoder/qoder-grpc.ts +0 -871
  483. package/src/providers/integrations/qoder/qoder-ide.ts +0 -588
  484. package/src/providers/operations/model-discovery-cache.ts +0 -34
  485. package/src/providers/operations/probe-fetch.ts +0 -15
  486. package/src/providers/operations/provider-version-cache.ts +0 -30
  487. package/src/providers/provider-capabilities.ts +0 -33
  488. package/src/providers/provider-credential-envelope.ts +0 -43
  489. /package/dist/dashboard/assets/{app-B7kIWFm5.js → app-C0gTXEoN.js} +0 -0
  490. /package/dist/dashboard/assets/{app-B7kIWFm5.js.gz → app-C0gTXEoN.js.gz} +0 -0
  491. /package/src/network/{http2-bidi.ts → bidi.ts} +0 -0
  492. /package/src/network/pool/{pool-scripts.ts → scripts.ts} +0 -0
  493. /package/src/network/pool/{pool-state.ts → state.ts} +0 -0
  494. /package/src/network/{backoff.ts → util/backoff.ts} +0 -0
  495. /package/src/network/{deduplication.ts → util/dedupe.ts} +0 -0
  496. /package/src/network/{ip-utils.ts → util/ip.ts} +0 -0
@@ -0,0 +1,175 @@
1
+ /**
2
+ * Validated WebSocket egress — WSS session + the dial that opens one.
3
+ *
4
+ * The dial resolves the target through the SSRF policy (done by the caller,
5
+ * the binding factory) and then either connects through the selected pool
6
+ * agent's tunnel or dials direct with pinned, SSRF-validated address.
7
+ */
8
+ import * as tls from "node:tls";
9
+ import WebSocket, { type ClientOptions, type RawData } from "ws";
10
+ import { GatewayError } from "../../protocol/gateway-error";
11
+ import { isProxyAgentPair, type PoolAgent } from "../pool/agent";
12
+ import { Agent as HttpsAgent } from "node:https";
13
+ import type { ProviderWebSocketSession } from "../../providers/provider-contracts";
14
+ import type { ValidatedDestination } from "../ssrf";
15
+
16
+ class WsSession implements ProviderWebSocketSession {
17
+ readonly #socket: WebSocket;
18
+ readonly #signal: AbortSignal;
19
+ readonly #messages: string[] = [];
20
+ readonly #waiters: Array<{
21
+ resolve(value: string): void;
22
+ reject(reason: Error): void;
23
+ cleanup(): void;
24
+ }> = [];
25
+ #closed: Error | undefined;
26
+
27
+ constructor(socket: WebSocket, signal: AbortSignal) {
28
+ this.#socket = socket;
29
+ this.#signal = signal;
30
+ socket.on("message", (data: RawData, isBinary: boolean) => {
31
+ if (isBinary) {
32
+ this.#fail(new GatewayError("transport_unavailable", 502, "unexpected binary WebSocket frame (request {requestId}). The connection ghosted us — come back in a moment 👻"));
33
+ return;
34
+ }
35
+ const message = data.toString();
36
+ const waiter = this.#waiters.shift();
37
+ if (waiter) {
38
+ waiter.cleanup();
39
+ waiter.resolve(message);
40
+ } else {
41
+ this.#messages.push(message);
42
+ }
43
+ });
44
+ socket.on("close", () => this.#fail(new GatewayError("transport_closed", 502, "WebSocket connection closed (request {requestId}). The connection ghosted us — come back in a moment 👻")));
45
+ socket.on("error", () => this.#fail(new GatewayError("transport_unavailable", 502, "WebSocket connection failed (request {requestId}). The connection ghosted us — come back in a moment 👻")));
46
+ }
47
+
48
+ send(message: string): void {
49
+ if (this.#closed) throw this.#closed;
50
+ if (this.#socket.readyState !== WebSocket.OPEN) {
51
+ throw new GatewayError("transport_closed", 499, "WebSocket connection is not open (request {requestId}). Send it again whenever ready 🙃");
52
+ }
53
+ this.#socket.send(message, { binary: false });
54
+ }
55
+
56
+ receive(signal: AbortSignal): Promise<string> {
57
+ const queued = this.#messages.shift();
58
+ if (queued !== undefined) return Promise.resolve(queued);
59
+ if (this.#closed) return Promise.reject(this.#closed);
60
+ const combined = AbortSignal.any([this.#signal, signal]);
61
+ return new Promise<string>((resolve, reject) => {
62
+ const timeout = setTimeout(() => {
63
+ cleanup();
64
+ reject(new GatewayError("transport_unavailable", 504, "WebSocket receive timed out (request {requestId}). Some things just take longer 🐢"));
65
+ }, 45_000);
66
+ const onAbort = (): void => {
67
+ cleanup();
68
+ reject(new GatewayError("transport_closed", 499, "WebSocket receive was cancelled (request {requestId}). Send it again whenever ready 🙃"));
69
+ };
70
+ const cleanup = (): void => {
71
+ clearTimeout(timeout);
72
+ combined.removeEventListener("abort", onAbort);
73
+ };
74
+ combined.addEventListener("abort", onAbort, { once: true });
75
+ this.#waiters.push({ resolve, reject, cleanup });
76
+ if (combined.aborted) onAbort();
77
+ });
78
+ }
79
+
80
+ close(code?: number, reason?: string): void {
81
+ if (this.#socket.readyState === WebSocket.CLOSED) return;
82
+ this.#socket.close(code, reason);
83
+ }
84
+
85
+ #fail(error: Error): void {
86
+ if (this.#closed) return;
87
+ this.#closed = error;
88
+ for (const waiter of this.#waiters.splice(0)) {
89
+ waiter.cleanup();
90
+ waiter.reject(error);
91
+ }
92
+ }
93
+ }
94
+
95
+ function connectValidatedWebSocket(
96
+ url: URL,
97
+ headers: Readonly<Record<string, string>>,
98
+ signal: AbortSignal,
99
+ destination: ValidatedDestination,
100
+ agent?: PoolAgent,
101
+ ): Promise<ProviderWebSocketSession> {
102
+ const targetUrl = new URL(url.toString());
103
+ targetUrl.protocol = "wss:";
104
+ targetUrl.port = "";
105
+ targetUrl.hash = "";
106
+ const connectionOptions: ClientOptions = {
107
+ handshakeTimeout: 15_000,
108
+ perMessageDeflate: false,
109
+ followRedirects: false,
110
+ headers: { ...headers },
111
+ ...(agent === undefined ? {} : { agent: agentForWebSocket(agent) }),
112
+ ...(agent === undefined
113
+ ? {
114
+ createConnection: () => {
115
+ const socket = tls.connect({
116
+ host: destination.resolvedAddress,
117
+ port: 443,
118
+ servername: destination.hostname,
119
+ rejectUnauthorized: true,
120
+ ALPNProtocols: ["http/1.1"],
121
+ });
122
+ const onAbort = (): void => {
123
+ socket.destroy(new Error("connection aborted"));
124
+ };
125
+ signal.addEventListener("abort", onAbort, { once: true });
126
+ socket.once("secureConnect", () => signal.removeEventListener("abort", onAbort));
127
+ return socket;
128
+ },
129
+ }
130
+ : {}),
131
+ };
132
+ return new Promise<ProviderWebSocketSession>((resolve, reject) => {
133
+ const socket = new WebSocket(targetUrl, connectionOptions);
134
+ let settled = false;
135
+ const timeout = setTimeout(() => {
136
+ if (settled) return;
137
+ settled = true;
138
+ socket.terminate();
139
+ reject(new GatewayError("transport_unavailable", 502, "WebSocket handshake timed out (request {requestId}). Some things just take longer 🐢"));
140
+ }, 15_000);
141
+ const abort = (): void => {
142
+ if (settled) return;
143
+ settled = true;
144
+ clearTimeout(timeout);
145
+ socket.terminate();
146
+ reject(new GatewayError("transport_closed", 499, "WebSocket connection was cancelled (request {requestId}). Send it again whenever ready 🙃"));
147
+ };
148
+ const onError = (): void => {
149
+ if (settled) return;
150
+ settled = true;
151
+ clearTimeout(timeout);
152
+ signal.removeEventListener("abort", abort);
153
+ reject(new GatewayError("transport_unavailable", 502, "WebSocket connection failed (request {requestId}). The connection ghosted us — come back in a moment 👻"));
154
+ };
155
+ socket.once("open", () => {
156
+ if (settled) return;
157
+ settled = true;
158
+ clearTimeout(timeout);
159
+ signal.removeEventListener("abort", abort);
160
+ resolve(new WsSession(socket, signal));
161
+ });
162
+ socket.once("error", onError);
163
+ socket.once("unexpected-response", onError);
164
+ signal.addEventListener("abort", abort, { once: true });
165
+ if (signal.aborted) abort();
166
+ });
167
+ }
168
+
169
+ function agentForWebSocket(agent: PoolAgent): import("http").Agent | import("https").Agent {
170
+ if (isProxyAgentPair(agent)) return agent.https;
171
+ if (agent instanceof HttpsAgent) return agent;
172
+ throw new GatewayError("proxy_pool_unhealthy", 503, "configured WebSocket proxy pool does not support WSS (request {requestId}). This bridge doesn't speak WSS — reconfigure the pool 🔧");
173
+ }
174
+
175
+ export { connectValidatedWebSocket, WsSession };
@@ -13,6 +13,8 @@
13
13
  * byte, and nothing is allocated.
14
14
  */
15
15
 
16
+ import { evictOldestEntry } from "../../transport/routing/bounded-lru";
17
+
16
18
  /** Running totals for one pool. */
17
19
  interface PoolByteTotals {
18
20
  /** Bytes sent to the proxy (uploads, TLS handshake, CONNECT). */
@@ -33,8 +35,7 @@ function entry(poolId: string): { sent: number; received: number } {
33
35
  let current = totals.get(poolId);
34
36
  if (!current) {
35
37
  if (totals.size >= MAX_BYTE_ENTRY_COUNT) {
36
- const oldest = totals.keys().next().value;
37
- if (oldest !== undefined) totals.delete(oldest);
38
+ evictOldestEntry(totals, MAX_BYTE_ENTRY_COUNT - 1);
38
39
  }
39
40
  current = { sent: 0, received: 0 };
40
41
  totals.set(poolId, current);
@@ -11,7 +11,7 @@
11
11
  * and the agent resolver, so a malformed row fails loudly instead of producing
12
12
  * a silently-wrong agent.
13
13
  */
14
- import { isRecord } from "../protocol/codecs";
14
+ import { isRecord } from "../../protocol/codecs";
15
15
 
16
16
  /** Thrown when a stored/requested pool config cannot be decoded. */
17
17
  export class ProxyConfigError extends Error {
@@ -4,7 +4,7 @@
4
4
  * caches instances of these; the selector owns admission.
5
5
  */
6
6
  import { isIP } from "node:net";
7
- import { accountSocketBytes, accountSocketWrites, recordPoolBytes } from "./byte-accounting";
7
+ import { accountSocketBytes, accountSocketWrites, recordPoolBytes } from "./accounting";
8
8
  import {
9
9
  Agent as HttpAgent,
10
10
  request as httpRequest,
@@ -19,7 +19,7 @@ import { SocksProxyAgent } from "socks-proxy-agent";
19
19
  import { resolveAllAddresses, validateResolvedAddresses } from "../ssrf";
20
20
  import type { SsrfPolicy } from "../../config/network";
21
21
  import { resolveProxyMaxFreeSockets, resolveProxyMaxSockets } from "../../config/network";
22
- import type { AgentConfig } from "../types";
22
+ import type { AgentConfig } from "./agent-config";
23
23
 
24
24
  /**
25
25
  * Idle keep-alive socket timeout for every pool agent. Matches the 60s DB pool
@@ -14,12 +14,13 @@
14
14
  */
15
15
  import { randomUUID } from "node:crypto";
16
16
  import { eq } from "drizzle-orm";
17
- import type { CartethyiaDatabase } from "../persistence/db-handle";
18
- import { networkPools, healthEvents } from "../persistence/schema";
19
- import { log } from "../observability/logger";
20
- import { providerRateLimitIsIpScoped } from "../providers/provider-metadata";
21
- import { resolvePoolProviderRateLimitCooldownMs } from "../config/cooldowns";
22
- import { PROVIDER_BLOCK_MS, type ProxyCooldownEntry } from "./pool/selector";
17
+ import type { CartethyiaDatabase } from "../../persistence/db-handle";
18
+ import { networkPools, healthEvents } from "../../persistence/schema";
19
+ import { log } from "../../observability/logger";
20
+ import { metrics } from "../../observability/metrics";
21
+ import { providerRateLimitIsIpScoped } from "../../providers/provider-metadata";
22
+ import { resolvePoolProviderRateLimitCooldownMs } from "../../config/cooldowns";
23
+ import { PROVIDER_BLOCK_MS, type ProxyCooldownEntry } from "./selector";
23
24
 
24
25
  type PoolProviderCooldownMap = NonNullable<
25
26
  (typeof networkPools.$inferSelect)["providerCooldowns"]
@@ -152,6 +153,88 @@ export async function pruneExpiredProviderCooldownMarkers(
152
153
  }
153
154
  }
154
155
 
156
+ /** Max simultaneously tracked (pool, provider) coalescing keys. */
157
+ const MAX_TRACKED_PAIRS = 5_000;
158
+
159
+ /** Last coalesced row timestamp per (pool, provider), for the cooldown window only. */
160
+ const lastRecordedByPoolProvider = new Map<string, number>();
161
+
162
+ /**
163
+ * Appends one provider-scoped egress verdict row to `health_events` — the
164
+ * durable audit log the console's pool page reads.
165
+ *
166
+ * Shared by the three writers of this row shape: the dispatch-side 429
167
+ * recorder (`recordPoolCooldownEvent`), the egress-block recorder
168
+ * (`recordPoolBlockEvent`), and the probe verdict recorder
169
+ * (`recordProbeVerdict`). The row is always `entityKind: "pool"`,
170
+ * `fromStatus: null`, `toStatus: "cooldown"`, and the category is chosen by
171
+ * `blocked`; the only divergences are the optional code suffix (block path)
172
+ * and the optional coalescing window (cooldown path).
173
+ *
174
+ * Coalescing: `coalesceWindowMs` suppresses a duplicate row for the same
175
+ * (pool, provider) pair within the window — used by the cooldown path, where
176
+ * dispatch flags the same pair on every throttled request and the audit log
177
+ * needs one row per episode, not one per request. The default `undefined`
178
+ * means no coalescing, so block and probe rows are never deduped; it also
179
+ * means the pair never enters the tracking map, so the eviction sweep below
180
+ * only ever sees coalesced pairs. A failed insert does not stamp the pair, so
181
+ * the next attempt within the window is still tried.
182
+ *
183
+ * Swallow contract: this never throws to dispatch (observability writes are
184
+ * non-fatal); failures bump `pool_cooldown_record_failed` and are logged.
185
+ */
186
+ export async function appendPoolProviderEgressAuditRow(
187
+ db: CartethyiaDatabase,
188
+ opts: {
189
+ poolId: string;
190
+ providerId: string;
191
+ reason: string;
192
+ blocked: boolean;
193
+ codeSuffix?: string;
194
+ coalesceWindowMs?: number;
195
+ },
196
+ ): Promise<void> {
197
+ const { poolId, providerId, reason, blocked, codeSuffix, coalesceWindowMs } = opts;
198
+ let key: string | undefined;
199
+ let now = 0;
200
+ if (coalesceWindowMs !== undefined) {
201
+ key = `${poolId}::${providerId.toLowerCase()}`;
202
+ now = Date.now();
203
+ const last = lastRecordedByPoolProvider.get(key);
204
+ if (last !== undefined && now - last < coalesceWindowMs) return;
205
+ }
206
+ try {
207
+ await db.insert(healthEvents).values({
208
+ id: randomUUID(),
209
+ entityKind: "pool",
210
+ networkPoolId: poolId,
211
+ fromStatus: null,
212
+ toStatus: "cooldown",
213
+ reason: `provider ${providerId}: ${reason}${codeSuffix ?? ""}`,
214
+ errorCategory: blocked ? "egress_blocked" : "rate_limit_transient",
215
+ createdAt: new Date(),
216
+ });
217
+ } catch (error) {
218
+ metrics.pool_cooldown_record_failed.inc(1);
219
+ log.warn("pool provider egress audit row failed", {
220
+ networkPoolId: poolId,
221
+ providerId,
222
+ error: String(error),
223
+ });
224
+ return;
225
+ }
226
+ if (key !== undefined && coalesceWindowMs !== undefined) {
227
+ lastRecordedByPoolProvider.set(key, now);
228
+ if (lastRecordedByPoolProvider.size > MAX_TRACKED_PAIRS) {
229
+ for (const [trackedKey, recordedAt] of lastRecordedByPoolProvider) {
230
+ if (now - recordedAt >= coalesceWindowMs) {
231
+ lastRecordedByPoolProvider.delete(trackedKey);
232
+ }
233
+ }
234
+ }
235
+ }
236
+ }
237
+
155
238
  /**
156
239
  * Writes a probe-measured verdict the same way a dispatch-side event would be
157
240
  * recorded: one mirror marker and one audit row.
@@ -174,20 +257,12 @@ export async function recordProbeVerdict(
174
257
  marker: { until: number; reason: string; blocked?: boolean },
175
258
  ): Promise<void> {
176
259
  await upsertProviderCooldownMarker(db, poolId, providerId, marker);
177
- try {
178
- await db.insert(healthEvents).values({
179
- id: randomUUID(),
180
- entityKind: "pool",
181
- networkPoolId: poolId,
182
- fromStatus: null,
183
- toStatus: "cooldown",
184
- reason: `provider ${providerId}: ${marker.reason}`,
185
- errorCategory: marker.blocked === true ? "egress_blocked" : "rate_limit_transient",
186
- createdAt: new Date(),
187
- });
188
- } catch (error) {
189
- log.warn("pool probe event failed", { networkPoolId: poolId, providerId, error: String(error) });
190
- }
260
+ await appendPoolProviderEgressAuditRow(db, {
261
+ poolId,
262
+ providerId,
263
+ reason: marker.reason,
264
+ blocked: marker.blocked === true,
265
+ });
191
266
  }
192
267
 
193
268
  /**
@@ -1,11 +1,210 @@
1
+ /**
2
+ * Pool health — flag helpers + state machine.
3
+ *
4
+ * Two halves of one domain, kept in one home:
5
+ *
6
+ * - **Flag helpers** (`flagPoolBlocked`, `flagPoolCooldown`,
7
+ * `flagPoolOnEgressFailure`): durable per-(pool, provider) 429/block
8
+ * history. The volatile flags live in `NetworkPoolSelector` (fast, consulted
9
+ * on every dispatch); these mirror each flag into two durable homes — the
10
+ * append-only `health_events` table (`entity_kind: "pool"`) for audit, and
11
+ * the `provider_cooldowns` column as the state boot restores. They never
12
+ * touch `network_pools.status`: a single provider's 429 must not flip the
13
+ * whole pool to `cooldown`.
14
+ *
15
+ * - **State machine** (`recordPoolDispatchOutcome`, `disablePoolForProxyHttpStatus`,
16
+ * `recoverNetworkPool`, `sweepExpiredPoolCooldowns`): the pool row's health
17
+ * transitions, subscriber feed, and recovery sweep.
18
+ */
1
19
  import { randomUUID } from "node:crypto";
2
- import { healthEventBuffer } from "../persistence/health-event-flush";
3
20
  import { and, desc, eq, inArray, isNotNull, lte, sql } from "drizzle-orm";
4
- import type { CartethyiaDatabase } from "../persistence/db-handle";
5
- import { healthEvents, networkPools } from "../persistence/schema";
6
- import { drainPoolByteDelta } from "./pool/byte-accounting";
7
- import { resolvePoolCooldownMs } from "../config/cooldowns";
8
- import { dropProviderCooldownMarker, pruneExpiredProviderCooldownMarkers } from "./pool-cooldown-store";
21
+ import type { CartethyiaDatabase } from "../../persistence/db-handle";
22
+ import { healthEventBuffer } from "../../persistence/health-event-flush";
23
+ import { healthEvents, networkPools } from "../../persistence/schema";
24
+ import { GatewayError } from "../../protocol/gateway-error";
25
+ import { metrics } from "../../observability/metrics";
26
+ import { log } from "../../observability/logger";
27
+ import { resolvePoolProviderRateLimitCooldownMs, resolvePoolCooldownMs } from "../../config/cooldowns";
28
+ import type { NetworkPoolSelector } from "./selector";
29
+ import { appendPoolProviderEgressAuditRow, dropProviderCooldownMarker, pruneExpiredProviderCooldownMarkers, upsertProviderCooldownMarker } from "./cooldown-store";
30
+ import { drainPoolByteDelta } from "./accounting";
31
+ import type { EgressVerdict } from "../verdict";
32
+
33
+ /**
34
+ * Coalescing window for repeated per-(pool, provider) 429 events. Dispatch
35
+ * flags the same pair on every throttled request, but the durable audit log
36
+ * needs one row per cooldown episode — not one per throttled request.
37
+ */
38
+ const POOL_COOLDOWN_EVENT_WINDOW_MS = 15 * 60 * 1000;
39
+
40
+ /** Durable half of `flagPoolCooldown`; not a second public entry point. */
41
+ async function recordPoolCooldownEvent(
42
+ db: CartethyiaDatabase,
43
+ poolId: string,
44
+ providerId: string,
45
+ reason: string,
46
+ ): Promise<void> {
47
+ await appendPoolProviderEgressAuditRow(db, {
48
+ poolId,
49
+ providerId,
50
+ reason,
51
+ blocked: false,
52
+ coalesceWindowMs: POOL_COOLDOWN_EVENT_WINDOW_MS,
53
+ });
54
+ }
55
+
56
+ /**
57
+ * Retires a pool for one provider because the upstream flagged its egress.
58
+ *
59
+ * Separate from `flagPoolCooldown` on purpose: the two facts are not
60
+ * interchangeable. A 429 is an allowance that refills, so the pool returns to
61
+ * rotation when the window ends. A free-tier refusal means the address itself
62
+ * is retired upstream, and putting it back would spend a real request to learn
63
+ * the same answer. The audit row therefore carries `errorCategory:
64
+ * "egress_blocked"` rather than `rate_limit_transient`, which is what the
65
+ * console reads to draw it as a block instead of a cooldown.
66
+ */
67
+ export function flagPoolBlocked(
68
+ poolSelector: NetworkPoolSelector,
69
+ db: CartethyiaDatabase,
70
+ poolId: string,
71
+ providerId: string,
72
+ error: GatewayError,
73
+ ): void {
74
+ const reason = `Provider ${providerId} refused this egress`;
75
+ void poolSelector
76
+ .flagProviderBlocked(poolId, providerId, reason)
77
+ .then((entry) =>
78
+ // Persisted even when the flag was refused (a credential-keyed
79
+ // provider): the selector returned an inert entry, so the mirror
80
+ // records nothing a reader would act on.
81
+ upsertProviderCooldownMarker(db, poolId, providerId, entry),
82
+ )
83
+ .catch((cause: unknown) => {
84
+ metrics.pool_cooldown_record_failed.inc(1);
85
+ log.warn("pool block flag failed", {
86
+ networkPoolId: poolId,
87
+ providerId,
88
+ error: String(cause),
89
+ });
90
+ });
91
+ void recordPoolBlockEvent(db, poolId, providerId, reason, error);
92
+ }
93
+
94
+ /**
95
+ * Durable half of `flagPoolBlocked`.
96
+ *
97
+ * Not coalesced the way a cooldown is: a block happens once per (pool,
98
+ * provider) for the life of the deployment, so there is no burst to collapse.
99
+ * A repeated event means the operator re-enabled the pool and it was refused
100
+ * again, which is worth a second row.
101
+ */
102
+ async function recordPoolBlockEvent(
103
+ db: CartethyiaDatabase,
104
+ poolId: string,
105
+ providerId: string,
106
+ reason: string,
107
+ error: GatewayError,
108
+ ): Promise<void> {
109
+ await appendPoolProviderEgressAuditRow(db, {
110
+ poolId,
111
+ providerId,
112
+ reason,
113
+ blocked: true,
114
+ codeSuffix: providerCodeSuffix(error),
115
+ });
116
+ }
117
+
118
+ /** The upstream's own code, when it named one, so the audit row is actionable. */
119
+ function providerCodeSuffix(error: GatewayError): string {
120
+ const code = error.details.providerCode;
121
+ return typeof code === "string" && code.length > 0 ? ` (${code})` : "";
122
+ }
123
+
124
+ /** Cooldown applied when an upstream 429 carries no `retryAfterMs`. */
125
+ const DEFAULT_PROVIDER_COOLDOWN_MS = 15 * 60 * 1000;
126
+
127
+ /**
128
+ * The fallback window for a provider-scoped egress cooldown. Upstream `Retry-After`
129
+ * always wins; the configured default exists because a request-counted free
130
+ * tier — the one provider that flags at all — stays refused for hours after
131
+ * the 15-minute guess this function used before.
132
+ */
133
+ function providerCooldownFallbackMs(): number {
134
+ return Math.max(resolvePoolProviderRateLimitCooldownMs(), DEFAULT_PROVIDER_COOLDOWN_MS);
135
+ }
136
+
137
+ /**
138
+ * Records a provider-scoped 429 cooldown: the volatile per-(pool, provider)
139
+ * flag consulted on every dispatch, its durable audit row, and the durable
140
+ * state boot restores from.
141
+ *
142
+ * Both paths that reach this decision — the non-streaming attempt loop and
143
+ * the streaming failure path — call this one function, because each write
144
+ * reports its own failure through the same metric and warn log. Two copies
145
+ * would have to be edited together, and a divergence would surface only as
146
+ * telemetry that silently differs by response mode.
147
+ *
148
+ * Callers gate on `shouldCooldownPool`, which narrows the error to the
149
+ * upstream/provider-scoped 429 this function is written for.
150
+ */
151
+ export function flagPoolCooldown(
152
+ poolSelector: NetworkPoolSelector,
153
+ db: CartethyiaDatabase,
154
+ poolId: string,
155
+ providerId: string,
156
+ error: GatewayError,
157
+ ): void {
158
+ const reason = `Provider ${providerId} rate limit`;
159
+ const retryAfterMs =
160
+ typeof error.details.retryAfterMs === "number"
161
+ ? error.details.retryAfterMs
162
+ : providerCooldownFallbackMs();
163
+ void poolSelector
164
+ .flagProviderCooldown(poolId, providerId, retryAfterMs, reason)
165
+ .then((entry) => upsertProviderCooldownMarker(db, poolId, providerId, entry))
166
+ .catch((cause: unknown) => {
167
+ metrics.pool_cooldown_record_failed.inc(1);
168
+ log.warn("pool cooldown flag failed", {
169
+ networkPoolId: poolId,
170
+ providerId,
171
+ error: String(cause),
172
+ });
173
+ });
174
+ void recordPoolCooldownEvent(db, poolId, providerId, reason);
175
+ }
176
+
177
+ /**
178
+ * One egress-failure verdict → pool-flag dispatch, shared by the attempt loop
179
+ * and the streaming finalizer.
180
+ *
181
+ * Both paths classify the same failure with `classifyEgressFailure`, then must
182
+ * retire the same pool the same way; a policy change — which verdict maps to a
183
+ * cooldown vs a block, or a new verdict — belongs here so the two response
184
+ * modes cannot drift in telemetry.
185
+ *
186
+ * The call sites compute the verdict (the attempt loop also needs it for its
187
+ * retry decision) and pass it in; the gating is identical to what both
188
+ * previously applied by hand: only a real pool id + selector pair, only a
189
+ * `GatewayError`, and never a `"none"` verdict. The flags themselves are
190
+ * fire-and-forget advisory writes that never fail the request.
191
+ */
192
+ export function flagPoolOnEgressFailure(
193
+ poolSelector: NetworkPoolSelector | undefined,
194
+ db: CartethyiaDatabase,
195
+ poolId: string | undefined,
196
+ providerId: string,
197
+ error: unknown,
198
+ verdict: EgressVerdict,
199
+ ): void {
200
+ if (poolId === undefined || poolSelector === undefined || !(error instanceof GatewayError)) return;
201
+ if (verdict.kind === "cooldown") {
202
+ flagPoolCooldown(poolSelector, db, poolId, providerId, error);
203
+ } else if (verdict.kind === "blocked") {
204
+ flagPoolBlocked(poolSelector, db, poolId, providerId, error);
205
+ }
206
+ }
207
+
9
208
  export interface PoolHealthEvent {
10
209
  readonly id: string;
11
210
  readonly networkPoolId: string;