cartethyia 2.8.0 → 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 (348) hide show
  1. package/.env.example +3 -3
  2. package/dist/dashboard/assets/{BannedPage-Cc0vKpMG.js → BannedPage-Dq530awD.js} +1 -1
  3. package/dist/dashboard/assets/BannedPage-Dq530awD.js.gz +0 -0
  4. package/dist/dashboard/assets/{CliToolDetailPage-Bqj2aa1T.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-CXsaOAO2.js → CliToolsPage-DaZZ3cqP.js} +1 -1
  7. package/dist/dashboard/assets/CliToolsPage-DaZZ3cqP.js.gz +0 -0
  8. package/dist/dashboard/assets/{CombosPage-BeLpJ4ma.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-DnV0uZ3m.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-CBxAjo63.js → CustomizationPage-7xT5yg2E.js} +1 -1
  13. package/dist/dashboard/assets/CustomizationPage-7xT5yg2E.js.gz +0 -0
  14. package/dist/dashboard/assets/{HealthEventsModal-B5yEE0k6.js → HealthEventsModal-Clq8E9cW.js} +1 -1
  15. package/dist/dashboard/assets/HealthEventsModal-Clq8E9cW.js.gz +0 -0
  16. package/dist/dashboard/assets/{InjectSystemPromptPanel-BWFLECmz.js → InjectSystemPromptPanel-6Teoq9Se.js} +1 -1
  17. package/dist/dashboard/assets/InjectSystemPromptPanel-6Teoq9Se.js.gz +0 -0
  18. package/dist/dashboard/assets/{LoginPage-CgoPc5B9.js → LoginPage-CZuI0MAR.js} +1 -1
  19. package/dist/dashboard/assets/LoginPage-CZuI0MAR.js.gz +0 -0
  20. package/dist/dashboard/assets/{ModelPicker-D9bFQern.js → ModelPicker-C1hNTlkr.js} +1 -1
  21. package/dist/dashboard/assets/ModelPicker-C1hNTlkr.js.gz +0 -0
  22. package/dist/dashboard/assets/{OverviewPage-B_p4tL8k.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-D_0PiZtg.js → ProviderIcon-Ddhm1InL.js} +1 -1
  27. package/dist/dashboard/assets/ProviderIcon-Ddhm1InL.js.gz +0 -0
  28. package/dist/dashboard/assets/{ProvidersPage-CLI2bJTK.js → ProvidersPage-C3fYuu6i.js} +1 -1
  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-DA-a1iSN.js → QuotaPage-D1RV_2BX.js} +1 -1
  33. package/dist/dashboard/assets/QuotaPage-D1RV_2BX.js.gz +0 -0
  34. package/dist/dashboard/assets/{SettingsPage-CctTga2C.js → SettingsPage-BzZAvYSX.js} +1 -1
  35. package/dist/dashboard/assets/SettingsPage-BzZAvYSX.js.gz +0 -0
  36. package/dist/dashboard/assets/{SetupPage-febLU4r1.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-M6fvR5Wz.js → StudioPage-BjXFGSK4.js} +1 -1
  39. package/dist/dashboard/assets/StudioPage-BjXFGSK4.js.gz +0 -0
  40. package/dist/dashboard/assets/{UsagePage-nyeuRqz9.js → UsagePage-CP8F8BN7.js} +1 -1
  41. package/dist/dashboard/assets/UsagePage-CP8F8BN7.js.gz +0 -0
  42. package/dist/dashboard/assets/{api-keys-BFOlvrJC.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-CqTLw5e7.js → assert-BMcj1xZE.js} +1 -1
  47. package/dist/dashboard/assets/assert-BMcj1xZE.js.gz +0 -0
  48. package/dist/dashboard/assets/{badge-C83EqLB0.js → badge-DkRUuVfz.js} +1 -1
  49. package/dist/dashboard/assets/badge-DkRUuVfz.js.gz +0 -0
  50. package/dist/dashboard/assets/{banner-D1xn7pIe.js → banner-CDjW841Q.js} +1 -1
  51. package/dist/dashboard/assets/banner-CDjW841Q.js.gz +0 -0
  52. package/dist/dashboard/assets/{card-DU3Lq5Lm.js → card-Bto5UlL9.js} +1 -1
  53. package/dist/dashboard/assets/card-Bto5UlL9.js.gz +0 -0
  54. package/dist/dashboard/assets/{cli-tools-Dw1U_drK.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-CEL5Sg_J.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-BjFR5UHy.js → dialog-DL0Zftwv.js} +1 -1
  59. package/dist/dashboard/assets/dialog-DL0Zftwv.js.gz +0 -0
  60. package/dist/dashboard/assets/{drawer-HvBW0AjI.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-CECVELyy.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-DYYofQ7W.js → entry-CGvdNU6r.js} +1 -1
  65. package/dist/dashboard/assets/entry-CGvdNU6r.js.gz +0 -0
  66. package/dist/dashboard/assets/{entry-BSu3gFlt.js → entry-DScKYvZj.js} +3 -3
  67. package/dist/dashboard/assets/entry-DScKYvZj.js.gz +0 -0
  68. package/dist/dashboard/assets/{formatters-DLW4i8zK.js → formatters-BS3DZlZj.js} +1 -1
  69. package/dist/dashboard/assets/formatters-BS3DZlZj.js.gz +0 -0
  70. package/dist/dashboard/assets/{index-1-fCqe64.js → index-SQnvvTWX.js} +2 -2
  71. package/dist/dashboard/assets/index-SQnvvTWX.js.gz +0 -0
  72. package/dist/dashboard/assets/{inline-DGvU2oLK.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-iNnO1X2K.js → input-Bfx0EzRc.js} +1 -1
  75. package/dist/dashboard/assets/input-Bfx0EzRc.js.gz +0 -0
  76. package/dist/dashboard/assets/{layout-0q8UGypb.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-C0w2bQjJ.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-M5aJ_xCa.js → live-CbVTdX9V.js} +1 -1
  81. package/dist/dashboard/assets/live-CbVTdX9V.js.gz +0 -0
  82. package/dist/dashboard/assets/{network-BKeQGT3K.js → network-JGvDveof.js} +1 -1
  83. package/dist/dashboard/assets/network-JGvDveof.js.gz +0 -0
  84. package/dist/dashboard/assets/{numbers-CivAk3da.js → numbers-Dw6kiew-.js} +1 -1
  85. package/dist/dashboard/assets/numbers-Dw6kiew-.js.gz +0 -0
  86. package/dist/dashboard/assets/{pagination-CpzvI5ep.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/{routing-BFQX2VBc.js → routing-8jZr-3WE.js} +1 -1
  91. package/dist/dashboard/assets/routing-8jZr-3WE.js.gz +0 -0
  92. package/dist/dashboard/assets/{select-CXbck0jG.js → select-oO1FxMVb.js} +1 -1
  93. package/dist/dashboard/assets/select-oO1FxMVb.js.gz +0 -0
  94. package/dist/dashboard/assets/{settings-C_XvMHh9.js → settings-CLr3kdIx.js} +1 -1
  95. package/dist/dashboard/assets/settings-CLr3kdIx.js.gz +0 -0
  96. package/dist/dashboard/assets/{stack-DggRiCxE.js → stack-Y3pDteZr.js} +1 -1
  97. package/dist/dashboard/assets/stack-Y3pDteZr.js.gz +0 -0
  98. package/dist/dashboard/assets/{state-ClFzfbYp.js → state-CArX_Iak.js} +1 -1
  99. package/dist/dashboard/assets/state-CArX_Iak.js.gz +0 -0
  100. package/dist/dashboard/assets/{switch-CTIFxKqS.js → switch-DwlwmDO-.js} +1 -1
  101. package/dist/dashboard/assets/switch-DwlwmDO-.js.gz +0 -0
  102. package/dist/dashboard/assets/{useMutation-BClG3yWj.js → useMutation-DJ9GYEgf.js} +1 -1
  103. package/dist/dashboard/assets/useMutation-DJ9GYEgf.js.gz +0 -0
  104. package/dist/dashboard/index.html +2 -2
  105. package/dist/dashboard/index.html.gz +0 -0
  106. package/dist/dashboard/precompressed-assets.json +51 -51
  107. package/migrations/full-postgres/0047_add_provider_device_flow_proxy.sql +17 -0
  108. package/migrations/lite-sqlite3/0047_add_provider_device_flow_proxy.sql +19 -0
  109. package/package.json +1 -1
  110. package/src/boot/app.ts +1 -1
  111. package/src/boot/dependencies.ts +4 -6
  112. package/src/config/providers.ts +6 -5
  113. package/src/config/spec.ts +8 -8
  114. package/src/console/backup/nine-router.ts +71 -10
  115. package/src/console/backup/service.ts +8 -1
  116. package/src/console/console-router.ts +1 -1
  117. package/src/console/domain-registration.ts +7 -2
  118. package/src/console/inferhub/client.ts +1 -1
  119. package/src/console/observability/live.ts +2 -2
  120. package/src/console/providers/catalog/contracts.ts +9 -3
  121. package/src/console/providers/catalog/model-operations.ts +2 -3
  122. package/src/console/providers/catalog/provider-operations.ts +1 -1
  123. package/src/console/providers/catalog/routes.ts +2 -2
  124. package/src/console/providers/catalog/store.ts +1914 -1907
  125. package/src/console/providers/detail/routes.ts +1 -0
  126. package/src/console/providers/detail/store.ts +4 -0
  127. package/src/console/providers/oauth/callback-listener.ts +30 -11
  128. package/src/console/providers/oauth/device-egress.ts +123 -0
  129. package/src/console/providers/oauth/routes.ts +123 -16
  130. package/src/console/quota/account-quota-tenant.ts +5 -5
  131. package/src/console/quota/account-test-egress.ts +2 -2
  132. package/src/console/quota/daily-growth-job.ts +1 -1
  133. package/src/console/routing/pools/contracts.ts +1 -1
  134. package/src/console/routing/pools/operations.ts +1 -1
  135. package/src/console/routing/pools/probe-verdict.ts +2 -2
  136. package/src/console/routing/pools/relay-deploy.ts +1 -1
  137. package/src/console/routing/pools/store.ts +4 -4
  138. package/src/network/client.ts +67 -0
  139. package/src/network/egress/bidi-session.ts +322 -0
  140. package/src/network/egress/dial.ts +201 -0
  141. package/src/network/egress/factory.ts +191 -0
  142. package/src/network/{outbound-fetch.ts → egress/fetch.ts} +195 -29
  143. package/src/network/egress/tls-profile.ts +123 -0
  144. package/src/network/egress/ws.ts +175 -0
  145. package/src/network/{types.ts → pool/agent-config.ts} +1 -1
  146. package/src/network/pool/agent.ts +2 -2
  147. package/src/network/{pool-cooldown-store.ts → pool/cooldown-store.ts} +7 -7
  148. package/src/network/{pool-health-machine.ts → pool/health.ts} +205 -6
  149. package/src/network/pool/resolver.ts +10 -849
  150. package/src/network/pool/selector.ts +2 -2
  151. package/src/network/ssrf.ts +1 -1
  152. package/src/network/{retry.ts → util/retry.ts} +1 -1
  153. package/src/observability/telemetry-buffer.ts +1 -1
  154. package/src/persistence/schema.ts +9 -0
  155. package/src/persistence/ttl-cache.ts +1 -1
  156. package/src/protocol/codecs.ts +58 -1
  157. package/src/protocol/request/messages.ts +31 -18
  158. package/src/protocol/response/messages.ts +34 -7
  159. package/src/protocol/translation/quirks.ts +15 -0
  160. package/src/protocol/upstream/send-claude-messages.ts +2 -2
  161. package/src/providers/adapters/compatible-adapter.ts +5 -0
  162. package/src/providers/authentication/oauth-client.ts +40 -6
  163. package/src/providers/authentication/oauth-device-flow.ts +7 -3
  164. package/src/providers/authentication/oauth-flow-store.ts +82 -1
  165. package/src/providers/authentication/oauth-refresh-service.ts +766 -739
  166. package/src/providers/default-registry.ts +76 -0
  167. package/src/providers/discovery/discovery-types.ts +7 -0
  168. package/src/providers/discovery/probe-phases.ts +6 -2
  169. package/src/providers/discovery/probing-service.ts +7 -0
  170. package/src/providers/integrations/antigravity/antigravity-protocol.ts +11 -0
  171. package/src/providers/integrations/buddy/account.ts +969 -0
  172. package/src/providers/integrations/buddy/capabilities.ts +10 -10
  173. package/src/providers/integrations/buddy/{buddy-catalog-shared.ts → catalog.ts} +198 -136
  174. package/src/providers/integrations/buddy/{buddy-chat-shared.ts → chat.ts} +5 -5
  175. package/src/providers/integrations/buddy/codebuddy/index.ts +429 -0
  176. package/src/providers/integrations/buddy/codebuddy/shared.ts +4 -4
  177. package/src/providers/integrations/buddy/{buddy-identity.ts → headers.ts} +138 -140
  178. package/src/providers/integrations/buddy/lifecycle.ts +1018 -0
  179. package/src/providers/integrations/buddy/{buddy-media.ts → media.ts} +1 -1
  180. package/src/providers/integrations/cline/cline-oauth.ts +10 -5
  181. package/src/providers/integrations/cline/cline.ts +3 -4
  182. package/src/providers/integrations/clixad/clixad-oauth.ts +98 -0
  183. package/src/providers/integrations/clixad/clixad-quota.ts +132 -0
  184. package/src/providers/integrations/clixad/provider.ts +145 -0
  185. package/src/providers/integrations/codex/codex-oauth.ts +8 -5
  186. package/src/providers/integrations/cursor/cursor-auth.ts +1 -1
  187. package/src/providers/integrations/cursor/cursor-hardening.ts +279 -0
  188. package/src/providers/integrations/cursor/cursor-http2.ts +605 -6
  189. package/src/providers/integrations/cursor/cursor-shared.ts +1 -1
  190. package/src/providers/integrations/cursor/cursor.ts +485 -70
  191. package/src/providers/integrations/deepai/deepai-catalog.ts +149 -0
  192. package/src/providers/integrations/deepai/deepai-common.ts +145 -0
  193. package/src/providers/integrations/deepai/deepai-oauth.ts +129 -0
  194. package/src/providers/integrations/deepai/deepai-quota.ts +97 -0
  195. package/src/providers/integrations/deepai/provider.ts +42 -0
  196. package/src/providers/integrations/devin/devin-auth.ts +7 -0
  197. package/src/providers/integrations/factory/factory-oauth.ts +16 -7
  198. package/src/providers/integrations/freebuff/freebuff.ts +2 -2
  199. package/src/providers/integrations/freepi/freepi-catalog.ts +5 -0
  200. package/src/providers/integrations/freepi/freepi-discovery.ts +2 -2
  201. package/src/providers/integrations/freepi/freepi-headers.ts +2 -2
  202. package/src/providers/integrations/freepi/freepi-protocol.ts +11 -2
  203. package/src/providers/integrations/freepi/freepi-realm.ts +0 -3
  204. package/src/providers/integrations/freepi/freepi-session.ts +158 -0
  205. package/src/providers/integrations/freepi/freepi.ts +80 -54
  206. package/src/providers/integrations/github/github-discovery.ts +30 -13
  207. package/src/providers/integrations/github/github-oauth.ts +217 -44
  208. package/src/providers/integrations/github/github-quota.ts +22 -5
  209. package/src/providers/integrations/github/github.ts +40 -32
  210. package/src/providers/integrations/grok/grok-oauth.ts +4 -4
  211. package/src/providers/integrations/hermes/hermes-oauth.ts +3 -3
  212. package/src/providers/integrations/hermes/hermes.ts +34 -5
  213. package/src/providers/integrations/kilo/kilo-oauth.ts +4 -4
  214. package/src/providers/integrations/kilo/kilo.ts +1 -1
  215. package/src/providers/integrations/kimi/kimi-oauth.ts +3 -3
  216. package/src/providers/integrations/kiro/kiro-oauth.ts +2 -2
  217. package/src/providers/integrations/muse/muse-oauth.ts +5 -5
  218. package/src/providers/integrations/opencode/fingerprint.ts +1 -1
  219. package/src/providers/integrations/openrouter/provider.ts +1 -1
  220. package/src/providers/integrations/perch/perch-protocol.ts +0 -3
  221. package/src/providers/integrations/perch/perch.ts +3 -3
  222. package/src/providers/integrations/qoder/claim-qoder-credits.ts +1 -1
  223. package/src/providers/integrations/qoder/qoder-adapter.ts +19 -113
  224. package/src/providers/integrations/qoder/qoder-auth.ts +8 -1
  225. package/src/providers/integrations/qoder/qoder-catalog.ts +1 -1
  226. package/src/providers/integrations/qoder/qoder-chat.ts +25 -8
  227. package/src/providers/integrations/qoder/qoder-checkin.ts +1 -1
  228. package/src/providers/integrations/qoder/qoder-cosy.ts +37 -9
  229. package/src/providers/integrations/qoder/qoder-oauth.ts +9 -2
  230. package/src/providers/integrations/qoder/qoder-realm.ts +13 -36
  231. package/src/providers/integrations/qoder/qoder-sse.ts +447 -68
  232. package/src/providers/integrations/qoder/qoder-types.ts +9 -0
  233. package/src/providers/integrations/qoder/qoder.ts +3 -25
  234. package/src/providers/integrations/sensenova/provider.ts +144 -0
  235. package/src/providers/integrations/trae/trae-quota.ts +15 -1
  236. package/src/providers/integrations/vercel/provider.ts +117 -0
  237. package/src/providers/integrations/vercel/vercel-quota.ts +96 -0
  238. package/src/providers/integrations/xai/xai-oauth.ts +5 -5
  239. package/src/providers/integrations/zcode-start-plan/gifts.ts +1 -1
  240. package/src/providers/integrations/zcode-start-plan/oauth.ts +3 -3
  241. package/src/providers/integrations/zro/zro.ts +2 -1
  242. package/src/providers/operations/account-refresh-status.ts +14 -0
  243. package/src/providers/operations/client-versions.ts +95 -30
  244. package/src/providers/operations/oauth-refresh-lead.ts +46 -22
  245. package/src/providers/operations/provider-caches.ts +1 -1
  246. package/src/providers/operations/provider-credential-service.ts +22 -25
  247. package/src/providers/operations/session-resolution.ts +17 -1
  248. package/src/providers/provider-contracts.ts +2 -2
  249. package/src/providers/provider-metadata.ts +30 -0
  250. package/src/providers/quota/quota-contracts.ts +2 -0
  251. package/src/providers/quota/refresh.ts +25 -3
  252. package/src/security/ip-boundary.ts +1 -1
  253. package/src/transport/dispatch/attempt-finalize.ts +1 -1
  254. package/src/transport/dispatch/attempt-loop.ts +3 -3
  255. package/src/transport/dispatch/handler-deps.ts +1 -1
  256. package/src/transport/dispatch/leases.ts +1 -1
  257. package/src/transport/dispatch/retry-policy.ts +1 -1
  258. package/src/transport/dispatch/streaming/finalize.ts +2 -2
  259. package/src/transport/dispatch/streaming/stream.ts +1 -1
  260. package/src/transport/dispatch/upstream.ts +1 -1
  261. package/src/transport/shutdown-notice.ts +1 -1
  262. package/src/workers/buddy-growth-tasks.ts +8 -8
  263. package/src/workers/daily-checkin.ts +4 -4
  264. package/src/workers/quota-refresh-sweep.ts +1 -1
  265. package/src/workers/zcode-gift-claim.ts +1 -1
  266. package/dist/dashboard/assets/BannedPage-Cc0vKpMG.js.gz +0 -0
  267. package/dist/dashboard/assets/CliToolDetailPage-Bqj2aa1T.js.gz +0 -0
  268. package/dist/dashboard/assets/CliToolsPage-CXsaOAO2.js.gz +0 -0
  269. package/dist/dashboard/assets/CombosPage-BeLpJ4ma.js.gz +0 -0
  270. package/dist/dashboard/assets/ConsoleLogPage-DnV0uZ3m.js.gz +0 -0
  271. package/dist/dashboard/assets/CustomizationPage-CBxAjo63.js.gz +0 -0
  272. package/dist/dashboard/assets/HealthEventsModal-B5yEE0k6.js.gz +0 -0
  273. package/dist/dashboard/assets/InjectSystemPromptPanel-BWFLECmz.js.gz +0 -0
  274. package/dist/dashboard/assets/LoginPage-CgoPc5B9.js.gz +0 -0
  275. package/dist/dashboard/assets/ModelPicker-D9bFQern.js.gz +0 -0
  276. package/dist/dashboard/assets/OverviewPage-B_p4tL8k.js.gz +0 -0
  277. package/dist/dashboard/assets/ProviderDetailPage-rs5E2rxv.js +0 -8
  278. package/dist/dashboard/assets/ProviderDetailPage-rs5E2rxv.js.gz +0 -0
  279. package/dist/dashboard/assets/ProviderIcon-D_0PiZtg.js.gz +0 -0
  280. package/dist/dashboard/assets/ProvidersPage-CLI2bJTK.js.gz +0 -0
  281. package/dist/dashboard/assets/ProxyPage-BwNC_-Cx.js +0 -9
  282. package/dist/dashboard/assets/ProxyPage-BwNC_-Cx.js.gz +0 -0
  283. package/dist/dashboard/assets/QuotaPage-DA-a1iSN.js.gz +0 -0
  284. package/dist/dashboard/assets/SettingsPage-CctTga2C.js.gz +0 -0
  285. package/dist/dashboard/assets/SetupPage-febLU4r1.js.gz +0 -0
  286. package/dist/dashboard/assets/StudioPage-M6fvR5Wz.js.gz +0 -0
  287. package/dist/dashboard/assets/UsagePage-nyeuRqz9.js.gz +0 -0
  288. package/dist/dashboard/assets/api-keys-BFOlvrJC.js.gz +0 -0
  289. package/dist/dashboard/assets/app-Dx2QgsNj.css +0 -2
  290. package/dist/dashboard/assets/app-Dx2QgsNj.css.gz +0 -0
  291. package/dist/dashboard/assets/assert-CqTLw5e7.js.gz +0 -0
  292. package/dist/dashboard/assets/badge-C83EqLB0.js.gz +0 -0
  293. package/dist/dashboard/assets/banner-D1xn7pIe.js.gz +0 -0
  294. package/dist/dashboard/assets/card-DU3Lq5Lm.js.gz +0 -0
  295. package/dist/dashboard/assets/cli-tools-Dw1U_drK.js.gz +0 -0
  296. package/dist/dashboard/assets/clipboard-button-CEL5Sg_J.js.gz +0 -0
  297. package/dist/dashboard/assets/dialog-BjFR5UHy.js.gz +0 -0
  298. package/dist/dashboard/assets/drawer-HvBW0AjI.js.gz +0 -0
  299. package/dist/dashboard/assets/entry-BSu3gFlt.js.gz +0 -0
  300. package/dist/dashboard/assets/entry-CECVELyy.js.gz +0 -0
  301. package/dist/dashboard/assets/entry-DYYofQ7W.js.gz +0 -0
  302. package/dist/dashboard/assets/formatters-DLW4i8zK.js.gz +0 -0
  303. package/dist/dashboard/assets/index-1-fCqe64.js.gz +0 -0
  304. package/dist/dashboard/assets/inline-DGvU2oLK.js.gz +0 -0
  305. package/dist/dashboard/assets/input-iNnO1X2K.js.gz +0 -0
  306. package/dist/dashboard/assets/layout-0q8UGypb.js.gz +0 -0
  307. package/dist/dashboard/assets/list-row-C0w2bQjJ.js.gz +0 -0
  308. package/dist/dashboard/assets/live-M5aJ_xCa.js.gz +0 -0
  309. package/dist/dashboard/assets/network-BKeQGT3K.js.gz +0 -0
  310. package/dist/dashboard/assets/numbers-CivAk3da.js.gz +0 -0
  311. package/dist/dashboard/assets/pagination-CpzvI5ep.js.gz +0 -0
  312. package/dist/dashboard/assets/routing-BFQX2VBc.js.gz +0 -0
  313. package/dist/dashboard/assets/select-CXbck0jG.js.gz +0 -0
  314. package/dist/dashboard/assets/settings-C_XvMHh9.js.gz +0 -0
  315. package/dist/dashboard/assets/stack-DggRiCxE.js.gz +0 -0
  316. package/dist/dashboard/assets/state-ClFzfbYp.js.gz +0 -0
  317. package/dist/dashboard/assets/switch-CTIFxKqS.js.gz +0 -0
  318. package/dist/dashboard/assets/useMutation-BClG3yWj.js.gz +0 -0
  319. package/src/console/providers/catalog/discover-constants.ts +0 -6
  320. package/src/network/pool-health.ts +0 -198
  321. package/src/providers/integrations/buddy/buddy-checkin.ts +0 -521
  322. package/src/providers/integrations/buddy/buddy-cli-headers.ts +0 -76
  323. package/src/providers/integrations/buddy/buddy-conversation-headers.ts +0 -130
  324. package/src/providers/integrations/buddy/buddy-discovery-shared.ts +0 -234
  325. package/src/providers/integrations/buddy/buddy-growth-shared.ts +0 -496
  326. package/src/providers/integrations/buddy/buddy-oauth-shared.ts +0 -341
  327. package/src/providers/integrations/buddy/buddy-quota-shared.ts +0 -417
  328. package/src/providers/integrations/buddy/buddy-server-tools.ts +0 -240
  329. package/src/providers/integrations/buddy/buddy-usage-shared.ts +0 -221
  330. package/src/providers/integrations/buddy/buddy-usage-store.ts +0 -181
  331. package/src/providers/integrations/buddy/codebuddy/cn-catalog.ts +0 -37
  332. package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +0 -188
  333. package/src/providers/integrations/buddy/codebuddy/oauth.ts +0 -47
  334. package/src/providers/integrations/buddy/codebuddy/provider.ts +0 -179
  335. package/src/providers/integrations/buddy/codebuddy/quota.ts +0 -154
  336. package/src/providers/integrations/qoder/chat-proto.ts +0 -328
  337. package/src/providers/integrations/qoder/qoder-grpc.ts +0 -870
  338. package/src/providers/integrations/qoder/qoder-ide.ts +0 -481
  339. /package/dist/dashboard/assets/{app-DIXLjojg.js → app-C0gTXEoN.js} +0 -0
  340. /package/dist/dashboard/assets/{app-DIXLjojg.js.gz → app-C0gTXEoN.js.gz} +0 -0
  341. /package/src/network/{http2-bidi.ts → bidi.ts} +0 -0
  342. /package/src/network/pool/{byte-accounting.ts → accounting.ts} +0 -0
  343. /package/src/network/pool/{pool-scripts.ts → scripts.ts} +0 -0
  344. /package/src/network/pool/{pool-state.ts → state.ts} +0 -0
  345. /package/src/network/{backoff.ts → util/backoff.ts} +0 -0
  346. /package/src/network/{deduplication.ts → util/dedupe.ts} +0 -0
  347. /package/src/network/{ip-utils.ts → util/ip.ts} +0 -0
  348. /package/src/network/{rate-limit-verdict.ts → verdict.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 };
@@ -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,13 +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 { 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 "./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";
24
24
 
25
25
  type PoolProviderCooldownMap = NonNullable<
26
26
  (typeof networkPools.$inferSelect)["providerCooldowns"]
@@ -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;