cartethyia 2.7.3 → 2.7.5

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 (625) hide show
  1. package/.env.example +2 -0
  2. package/README.md +17 -5
  3. package/dist/dashboard/assets/{BannedPage-2TJD37Hb.js → BannedPage-Cj15GsMp.js} +1 -1
  4. package/dist/dashboard/assets/BannedPage-Cj15GsMp.js.gz +0 -0
  5. package/dist/dashboard/assets/ClaimGiftsButton-D83pBJ6J.js +1 -0
  6. package/dist/dashboard/assets/ClaimGiftsButton-D83pBJ6J.js.gz +0 -0
  7. package/dist/dashboard/assets/{CliToolDetailPage-xefJNRmJ.js → CliToolDetailPage-DZKmVuJN.js} +1 -1
  8. package/dist/dashboard/assets/CliToolDetailPage-DZKmVuJN.js.gz +0 -0
  9. package/dist/dashboard/assets/{CliToolsPage-Rj9iHQ3F.js → CliToolsPage-BIccZmeL.js} +1 -1
  10. package/dist/dashboard/assets/CliToolsPage-BIccZmeL.js.gz +0 -0
  11. package/dist/dashboard/assets/CombosPage-DEEKkYD-.js +1 -0
  12. package/dist/dashboard/assets/CombosPage-DEEKkYD-.js.gz +0 -0
  13. package/dist/dashboard/assets/ConsoleLogPage-D5rol2j2.js +1 -0
  14. package/dist/dashboard/assets/ConsoleLogPage-D5rol2j2.js.gz +0 -0
  15. package/dist/dashboard/assets/CustomizationPage-BKj_r0rp.js +1 -0
  16. package/dist/dashboard/assets/CustomizationPage-BKj_r0rp.js.gz +0 -0
  17. package/dist/dashboard/assets/InjectSystemPromptPanel-B5nZvmf0.js +656 -0
  18. package/dist/dashboard/assets/InjectSystemPromptPanel-B5nZvmf0.js.gz +0 -0
  19. package/dist/dashboard/assets/{LoginPage-C4w7vea-.js → LoginPage-DkXb6dx-.js} +1 -1
  20. package/dist/dashboard/assets/LoginPage-DkXb6dx-.js.gz +0 -0
  21. package/dist/dashboard/assets/{ModelPicker-BtPcAfot.js → ModelPicker-DnQPSIA8.js} +1 -1
  22. package/dist/dashboard/assets/ModelPicker-DnQPSIA8.js.gz +0 -0
  23. package/dist/dashboard/assets/OverviewPage-Ci2wQKUz.js +1 -0
  24. package/dist/dashboard/assets/OverviewPage-Ci2wQKUz.js.gz +0 -0
  25. package/dist/dashboard/assets/ProviderDetailPage-odlgYrOg.js +5 -0
  26. package/dist/dashboard/assets/ProviderDetailPage-odlgYrOg.js.gz +0 -0
  27. package/dist/dashboard/assets/ProviderIcon-EfeloBn4.js +1 -0
  28. package/dist/dashboard/assets/ProviderIcon-EfeloBn4.js.gz +0 -0
  29. package/dist/dashboard/assets/ProvidersPage-CqFBQEB8.js +1 -0
  30. package/dist/dashboard/assets/ProvidersPage-CqFBQEB8.js.gz +0 -0
  31. package/dist/dashboard/assets/ProxyPage-5SnUSy3j.js +9 -0
  32. package/dist/dashboard/assets/ProxyPage-5SnUSy3j.js.gz +0 -0
  33. package/dist/dashboard/assets/QuotaPage-CmnMDzhP.js +1 -0
  34. package/dist/dashboard/assets/QuotaPage-CmnMDzhP.js.gz +0 -0
  35. package/dist/dashboard/assets/SettingsPage-BJtRZ4qD.js +1 -0
  36. package/dist/dashboard/assets/SettingsPage-BJtRZ4qD.js.gz +0 -0
  37. package/dist/dashboard/assets/{SetupPage-BQS0Rx_u.js → SetupPage-WTa0sktC.js} +1 -1
  38. package/dist/dashboard/assets/SetupPage-WTa0sktC.js.gz +0 -0
  39. package/dist/dashboard/assets/{StudioPage-TP7voAQ0.js → StudioPage-BfAb5-o_.js} +3 -3
  40. package/dist/dashboard/assets/StudioPage-BfAb5-o_.js.gz +0 -0
  41. package/dist/dashboard/assets/UsagePage-BNzeZ2Vk.js +56 -0
  42. package/dist/dashboard/assets/UsagePage-BNzeZ2Vk.js.gz +0 -0
  43. package/dist/dashboard/assets/api-keys-l29JROPv.js +1 -0
  44. package/dist/dashboard/assets/api-keys-l29JROPv.js.gz +0 -0
  45. package/dist/dashboard/assets/app--PJWq8_C.css +2 -0
  46. package/dist/dashboard/assets/app--PJWq8_C.css.gz +0 -0
  47. package/dist/dashboard/assets/{assert-C0YYuZJ4.js → assert-C0K-c4eo.js} +1 -1
  48. package/dist/dashboard/assets/assert-C0K-c4eo.js.gz +0 -0
  49. package/dist/dashboard/assets/{badge-B_DMpMJN.js → badge-DfbhQU4k.js} +1 -1
  50. package/dist/dashboard/assets/badge-DfbhQU4k.js.gz +0 -0
  51. package/dist/dashboard/assets/{banner-DMrMPmth.js → banner-BllT4LcF.js} +1 -1
  52. package/dist/dashboard/assets/banner-BllT4LcF.js.gz +0 -0
  53. package/dist/dashboard/assets/brand/login_register.jpg +0 -0
  54. package/dist/dashboard/assets/brand/login_register_initial.jpg +0 -0
  55. package/dist/dashboard/assets/brand/og_bansos.jpg +0 -0
  56. package/dist/dashboard/assets/brand/og_image.jpg +0 -0
  57. package/dist/dashboard/assets/{card-Be5Uzamx.js → card-CtkX66Wd.js} +1 -1
  58. package/dist/dashboard/assets/card-CtkX66Wd.js.gz +0 -0
  59. package/dist/dashboard/assets/chevron-left-Bf6CaTSJ.js +1 -0
  60. package/dist/dashboard/assets/chevron-left-Bf6CaTSJ.js.gz +0 -0
  61. package/dist/dashboard/assets/{cli-tools-muFRphDu.js → cli-tools-CQXVMn4P.js} +1 -1
  62. package/dist/dashboard/assets/cli-tools-CQXVMn4P.js.gz +0 -0
  63. package/dist/dashboard/assets/{clipboard-button-DOfrVmsi.js → clipboard-button-BQqBQd2n.js} +1 -1
  64. package/dist/dashboard/assets/clipboard-button-BQqBQd2n.js.gz +0 -0
  65. package/dist/dashboard/assets/{dialog-C9omdjQt.js → dialog-BEK7KP0H.js} +1 -1
  66. package/dist/dashboard/assets/dialog-BEK7KP0H.js.gz +0 -0
  67. package/dist/dashboard/assets/{drawer-Bm8WKaGY.js → drawer-BrTEZ8zu.js} +1 -1
  68. package/dist/dashboard/assets/drawer-BrTEZ8zu.js.gz +0 -0
  69. package/dist/dashboard/assets/entry-CHWxbS56.js +1 -0
  70. package/dist/dashboard/assets/entry-CHWxbS56.js.gz +0 -0
  71. package/dist/dashboard/assets/{entry-q5HwZ8nN.js → entry-EBHJ3DrE.js} +1 -1
  72. package/dist/dashboard/assets/entry-EBHJ3DrE.js.gz +0 -0
  73. package/dist/dashboard/assets/entry-LCS85hiq.js +4 -0
  74. package/dist/dashboard/assets/entry-LCS85hiq.js.gz +0 -0
  75. package/dist/dashboard/assets/{formatters-BTScrQpe.js → formatters-BAACA727.js} +1 -1
  76. package/dist/dashboard/assets/formatters-BAACA727.js.gz +0 -0
  77. package/dist/dashboard/assets/{github-badge-Orjuzemr.js → github-badge-YSCE4UC2.js} +1 -1
  78. package/dist/dashboard/assets/github-badge-YSCE4UC2.js.gz +0 -0
  79. package/dist/dashboard/assets/{index-cPrX5vn6.js → index-FQlj0HUT.js} +2 -2
  80. package/dist/dashboard/assets/index-FQlj0HUT.js.gz +0 -0
  81. package/dist/dashboard/assets/{inline-Dvux8w-0.js → inline-C4gU5iOL.js} +1 -1
  82. package/dist/dashboard/assets/inline-C4gU5iOL.js.gz +0 -0
  83. package/dist/dashboard/assets/{input-CU1Rgf3p.js → input-BHHX_Tmm.js} +1 -1
  84. package/dist/dashboard/assets/input-BHHX_Tmm.js.gz +0 -0
  85. package/dist/dashboard/assets/{layout-C_7A6H55.js → layout-CajrYUnD.js} +1 -1
  86. package/dist/dashboard/assets/{layout-C_7A6H55.js.gz → layout-CajrYUnD.js.gz} +0 -0
  87. package/dist/dashboard/assets/{list-row-CzFvogbj.js → list-row-Bx2I7XUe.js} +1 -1
  88. package/dist/dashboard/assets/list-row-Bx2I7XUe.js.gz +0 -0
  89. package/dist/dashboard/assets/live-DNNqRbj2.js +1 -0
  90. package/dist/dashboard/assets/live-DNNqRbj2.js.gz +0 -0
  91. package/dist/dashboard/assets/minimize-2-BTmMTK4o.js +1 -0
  92. package/dist/dashboard/assets/minimize-2-BTmMTK4o.js.gz +0 -0
  93. package/dist/dashboard/assets/{mutation-JJGQm8TD.js → mutation-Rr0TIvcj.js} +1 -1
  94. package/dist/dashboard/assets/mutation-Rr0TIvcj.js.gz +0 -0
  95. package/dist/dashboard/assets/network-C5UBACrJ.js +1 -0
  96. package/dist/dashboard/assets/network-C5UBACrJ.js.gz +0 -0
  97. package/dist/dashboard/assets/numbers-CUWAZK4J.js +2 -0
  98. package/dist/dashboard/assets/numbers-CUWAZK4J.js.gz +0 -0
  99. package/dist/dashboard/assets/pagination-Xsi9Qx04.js +1 -0
  100. package/dist/dashboard/assets/pagination-Xsi9Qx04.js.gz +0 -0
  101. package/dist/dashboard/assets/{query-keys-CLaNz8Xz.js → query-keys-BloKtaXJ.js} +1 -1
  102. package/dist/dashboard/assets/query-keys-BloKtaXJ.js.gz +0 -0
  103. package/dist/dashboard/assets/quota-DvQbD_sr.js +1 -0
  104. package/dist/dashboard/assets/quota-DvQbD_sr.js.gz +0 -0
  105. package/dist/dashboard/assets/{routing-BEH32QOC.js → routing-in0f_IfK.js} +1 -1
  106. package/dist/dashboard/assets/routing-in0f_IfK.js.gz +0 -0
  107. package/dist/dashboard/assets/{select-B3iTetHN.js → select-BLULNqTp.js} +2 -2
  108. package/dist/dashboard/assets/select-BLULNqTp.js.gz +0 -0
  109. package/dist/dashboard/assets/settings-CcGczj6I.js +1 -0
  110. package/dist/dashboard/assets/settings-CcGczj6I.js.gz +0 -0
  111. package/dist/dashboard/assets/shield-BMQNxORh.js +1 -0
  112. package/dist/dashboard/assets/shield-BMQNxORh.js.gz +0 -0
  113. package/dist/dashboard/assets/{stack-KrOrm7JZ.js → stack-D2lIR0tg.js} +1 -1
  114. package/dist/dashboard/assets/stack-D2lIR0tg.js.gz +0 -0
  115. package/dist/dashboard/assets/{state-DLS7IDMy.js → state-DTARjfiW.js} +1 -1
  116. package/dist/dashboard/assets/state-DTARjfiW.js.gz +0 -0
  117. package/dist/dashboard/assets/{switch-42ItvbAs.js → switch-c0oPSnqW.js} +1 -1
  118. package/dist/dashboard/assets/switch-c0oPSnqW.js.gz +0 -0
  119. package/dist/dashboard/assets/{useMutation-Dmnfh1Ue.js → useMutation-m8WGNPrz.js} +1 -1
  120. package/dist/dashboard/assets/useMutation-m8WGNPrz.js.gz +0 -0
  121. package/dist/dashboard/console-backgrounds.jpg +0 -0
  122. package/dist/dashboard/console-backgrounds_initial.jpg +0 -0
  123. package/dist/dashboard/index.html +2 -2
  124. package/dist/dashboard/index.html.gz +0 -0
  125. package/dist/dashboard/precompressed-assets.json +59 -57
  126. package/migrations/full-postgres/0027_qoder_identity_genuine_seed.sql +9 -10
  127. package/migrations/full-postgres/0032_add_provider_pool_accounts.sql +53 -0
  128. package/migrations/full-postgres/0033_add_api_key_ban.sql +9 -0
  129. package/migrations/full-postgres/0034_add_api_key_ban_until.sql +7 -0
  130. package/migrations/full-postgres/0035_align_pool_account_status_enum.sql +21 -0
  131. package/migrations/full-postgres/0036_system_prompt_presets_refs.sql +10 -0
  132. package/migrations/full-postgres/0037_share_concurrency_scope.sql +3 -0
  133. package/migrations/full-postgres/0038_add_accounts_quota_snapshot.sql +7 -0
  134. package/migrations/full-postgres/0039_add_telemetry_provider_model_indexes.sql +13 -0
  135. package/migrations/full-postgres/0040_add_models_dialect.sql +3 -0
  136. package/migrations/full-postgres/0041_add_daily_checkin_ledger_account_fk.sql +7 -0
  137. package/migrations/lite-sqlite3/0027_qoder_identity_genuine_seed.sql +9 -10
  138. package/migrations/lite-sqlite3/0032_add_provider_pool_accounts.sql +44 -0
  139. package/migrations/lite-sqlite3/0033_add_api_key_ban.sql +8 -0
  140. package/migrations/lite-sqlite3/0034_add_api_key_ban_until.sql +5 -0
  141. package/migrations/lite-sqlite3/0035_align_pool_account_status_enum.sql +6 -0
  142. package/migrations/lite-sqlite3/0036_system_prompt_presets_refs.sql +14 -0
  143. package/migrations/lite-sqlite3/0037_share_concurrency_scope.sql +3 -0
  144. package/migrations/lite-sqlite3/0038_add_accounts_quota_snapshot.sql +4 -0
  145. package/migrations/lite-sqlite3/0039_add_telemetry_provider_model_indexes.sql +6 -0
  146. package/migrations/lite-sqlite3/0040_add_models_dialect.sql +3 -0
  147. package/migrations/lite-sqlite3/0041_add_daily_checkin_ledger_account_fk.sql +26 -0
  148. package/package.json +4 -5
  149. package/scripts/cli/update.ts +15 -41
  150. package/src/boot/app.ts +67 -10
  151. package/src/boot/dependencies.ts +786 -732
  152. package/src/boot/lifecycle.ts +11 -12
  153. package/src/config/cooldowns.ts +34 -55
  154. package/src/config/network.ts +8 -4
  155. package/src/config/package-paths.ts +1 -1
  156. package/src/config/providers.ts +13 -14
  157. package/src/config/resources.ts +0 -5
  158. package/src/config/runtime.ts +2 -5
  159. package/src/config/spec.ts +26 -6
  160. package/src/console/api-keys/ban-constants.ts +38 -0
  161. package/src/console/api-keys/contracts.ts +92 -31
  162. package/src/console/api-keys/routes.ts +131 -6
  163. package/src/console/audit/routes.ts +3 -3
  164. package/src/console/auth/access.ts +4 -4
  165. package/src/console/auth/account-reset.ts +5 -5
  166. package/src/console/auth/audit-recorder.ts +1 -1
  167. package/src/console/auth/lockout.ts +7 -3
  168. package/src/console/auth/row-coercion.ts +57 -12
  169. package/src/console/auth/session-resolver.ts +1 -1
  170. package/src/console/auth/session-service.ts +8 -2
  171. package/src/console/auth/session.ts +40 -34
  172. package/src/console/auth/setup-service.ts +4 -3
  173. package/src/console/auth/user-store.ts +1 -1
  174. package/src/console/backup/9router-map.ts +0 -3
  175. package/src/console/backup/contracts.ts +11 -9
  176. package/src/console/backup/export-endpoint.ts +11 -12
  177. package/src/console/backup/nine-router.ts +1 -7
  178. package/src/console/backup/routes.ts +32 -36
  179. package/src/console/backup/service.ts +468 -468
  180. package/src/console/backup/store.ts +47 -29
  181. package/src/console/backup/validate.ts +11 -1
  182. package/src/console/cli-tools/contracts.ts +7 -7
  183. package/src/console/cli-tools/injectors/cline.ts +110 -110
  184. package/src/console/cli-tools/injectors/codex.ts +181 -181
  185. package/src/console/cli-tools/injectors/copilot.ts +108 -108
  186. package/src/console/cli-tools/injectors/driver.ts +247 -247
  187. package/src/console/cli-tools/injectors/jcode.ts +115 -115
  188. package/src/console/cli-tools/injectors/openclaw.ts +161 -161
  189. package/src/console/cli-tools/injectors/opencode.ts +153 -153
  190. package/src/console/cli-tools/routes.ts +4 -4
  191. package/src/console/cli-tools/service.ts +1 -1
  192. package/src/console/cli-tools/store.ts +2 -2
  193. package/src/console/console-router-guards.ts +2 -2
  194. package/src/console/console-router.ts +36 -4
  195. package/src/console/dashboard-assets.ts +9 -9
  196. package/src/console/data-management/routes.ts +3 -3
  197. package/src/console/domain-registration.ts +18 -4
  198. package/src/console/inferhub/client.ts +0 -7
  199. package/src/console/inferhub/key-store.ts +1 -1
  200. package/src/console/inferhub/normalize.ts +2 -2
  201. package/src/console/inferhub/routes.ts +1 -1
  202. package/src/console/inferhub/types.ts +1 -1
  203. package/src/console/observability/contracts.ts +5 -1
  204. package/src/console/observability/live.ts +34 -21
  205. package/src/console/observability/logs.ts +4 -4
  206. package/src/console/observability/performance.ts +3 -3
  207. package/src/console/observability/routes.ts +3 -3
  208. package/src/console/observability/sse.ts +62 -8
  209. package/src/console/observability/store.ts +36 -20
  210. package/src/console/providers/catalog/catalog-projections.ts +1 -1
  211. package/src/console/providers/catalog/contracts.ts +82 -22
  212. package/src/console/providers/catalog/model-operations.ts +1 -1
  213. package/src/console/providers/catalog/provider-operations.ts +110 -17
  214. package/src/console/providers/catalog/public-model-store.ts +0 -0
  215. package/src/console/providers/catalog/routes.ts +30 -6
  216. package/src/console/providers/catalog/store-helpers.ts +349 -326
  217. package/src/console/providers/catalog/store.ts +1662 -1292
  218. package/src/console/providers/detail/routes.ts +4 -4
  219. package/src/console/providers/oauth/callback-listener.ts +100 -18
  220. package/src/console/providers/oauth/routes.ts +95 -16
  221. package/src/console/quota/account-quota-global.ts +41 -3
  222. package/src/console/quota/account-quota-shared.ts +10 -3
  223. package/src/console/quota/account-quota-tenant.ts +285 -67
  224. package/src/console/quota/account-test-egress.ts +4 -4
  225. package/src/console/quota/daily-growth-job.ts +308 -0
  226. package/src/console/quota/native-identity.ts +2 -2
  227. package/src/console/quota/quota-view.ts +1 -1
  228. package/src/console/remote-support/routes.ts +1 -1
  229. package/src/console/remote-support/service.ts +2 -2
  230. package/src/console/routing/model/contracts.ts +1 -1
  231. package/src/console/routing/model/routes.ts +3 -3
  232. package/src/console/routing/pools/contracts.ts +13 -10
  233. package/src/console/routing/pools/import-parse.ts +3 -3
  234. package/src/console/routing/pools/operations.ts +14 -7
  235. package/src/console/routing/pools/probe-verdict.ts +8 -3
  236. package/src/console/routing/pools/relay-deploy.ts +12 -12
  237. package/src/console/routing/pools/routes.ts +3 -3
  238. package/src/console/routing/pools/store.ts +21 -1
  239. package/src/console/settings/contracts.ts +29 -2
  240. package/src/console/settings/routes.ts +3 -3
  241. package/src/console/settings/store.ts +20 -0
  242. package/src/console/share/share-router.ts +112 -44
  243. package/src/console/share/share-stats.ts +109 -39
  244. package/src/console/share/share-usage.ts +1 -1
  245. package/src/console/shared/errors.ts +2 -2
  246. package/src/console/shared/object.ts +18 -0
  247. package/src/console/shared/read-through-cache.ts +42 -6
  248. package/src/console/shared/transaction.ts +14 -0
  249. package/src/console/system-prompt/contracts.ts +89 -12
  250. package/src/console/system-prompt/routes.ts +98 -45
  251. package/src/console/updates/apply-update.ts +244 -0
  252. package/src/console/updates/routes.ts +256 -98
  253. package/src/main.ts +11 -5
  254. package/src/network/http2-bidi.ts +1 -1
  255. package/src/network/ip-utils.ts +298 -0
  256. package/src/network/outbound-fetch.ts +76 -5
  257. package/src/network/pool/agent.ts +7 -6
  258. package/src/network/pool/byte-accounting.ts +1 -1
  259. package/src/network/pool/resolver.ts +55 -51
  260. package/src/network/pool/selector.ts +150 -136
  261. package/src/network/pool-cooldown-store.ts +71 -16
  262. package/src/network/pool-health-machine.ts +22 -2
  263. package/src/network/rate-limit-verdict.ts +68 -0
  264. package/src/network/retry.ts +1 -1
  265. package/src/network/ssrf.ts +24 -75
  266. package/src/observability/event-log.ts +2 -2
  267. package/src/observability/event-query.ts +286 -214
  268. package/src/observability/log-sanitize.ts +1 -1
  269. package/src/observability/metrics.ts +4 -4
  270. package/src/observability/payload-capture.ts +6 -5
  271. package/src/observability/payload-store.ts +1 -1
  272. package/src/observability/runtime-metrics.ts +1 -1
  273. package/src/observability/telemetry-buffer.ts +12 -3
  274. package/src/observability/telemetry-reconciliation.ts +3 -3
  275. package/src/observability/telemetry-status.ts +1 -1
  276. package/src/observability/token-speed.ts +1 -1
  277. package/src/persistence/connection-url.ts +1 -1
  278. package/src/persistence/db-handle.ts +1 -1
  279. package/src/persistence/db-pg.ts +2 -2
  280. package/src/persistence/db-sqlite.ts +10 -5
  281. package/src/persistence/dialect.ts +5 -1
  282. package/src/persistence/readiness.ts +19 -7
  283. package/src/persistence/redis.ts +1 -1
  284. package/src/persistence/schema.ts +177 -45
  285. package/src/persistence/shared-cache.ts +2 -2
  286. package/src/persistence/sort-order.ts +1 -1
  287. package/src/persistence/stores/api-key-store.ts +109 -4
  288. package/src/persistence/stores/share-store.ts +20 -8
  289. package/src/persistence/stores/telemetry-store.ts +1 -1
  290. package/src/persistence/system-prompt-store.ts +63 -18
  291. package/src/persistence/tenant-preferences.ts +1 -1
  292. package/src/persistence/ttl-cache.ts +3 -3
  293. package/src/persistence/usage-flush.ts +7 -2
  294. package/src/protocol/canonical-model.ts +1 -1
  295. package/src/protocol/codecs.ts +12 -16
  296. package/src/protocol/dsml.ts +3 -49
  297. package/src/protocol/gateway-error.ts +6 -3
  298. package/src/protocol/image-clamp.ts +2 -2
  299. package/src/protocol/image-generation.ts +2 -2
  300. package/src/protocol/reasoning-text.ts +1 -1
  301. package/src/protocol/reasoning.ts +7 -52
  302. package/src/protocol/registry.ts +50 -15
  303. package/src/protocol/request/chat.ts +46 -19
  304. package/src/protocol/request/codex.ts +1 -1
  305. package/src/protocol/request/messages.ts +46 -14
  306. package/src/protocol/request/responses.ts +19 -10
  307. package/src/protocol/response/chat.ts +27 -12
  308. package/src/protocol/response/codex.ts +2 -2
  309. package/src/protocol/response/gemini.ts +1 -1
  310. package/src/protocol/response/messages.ts +6 -6
  311. package/src/protocol/response/omp-tool-call-markup.ts +1 -1
  312. package/src/protocol/response/responses.ts +3 -3
  313. package/src/protocol/sse-decoder.ts +64 -4
  314. package/src/protocol/stop-reason.ts +2 -2
  315. package/src/protocol/think-split.ts +347 -0
  316. package/src/protocol/translation/attachment-integrity.ts +3 -3
  317. package/src/protocol/translation/cache-controls.ts +8 -20
  318. package/src/protocol/translation/capabilities.ts +8 -8
  319. package/src/protocol/translation/dialect/deepseek.ts +10 -0
  320. package/src/protocol/translation/dialect/inband.ts +39 -0
  321. package/src/protocol/translation/dialect/model-thinking.ts +89 -0
  322. package/src/protocol/translation/dialect/qwen.ts +114 -0
  323. package/src/protocol/translation/dialect/resolver.ts +83 -0
  324. package/src/protocol/translation/dialect/rules.ts +93 -0
  325. package/src/protocol/translation/dialect/thinking-ladder.ts +245 -0
  326. package/src/protocol/translation/dialect/types.ts +58 -0
  327. package/src/protocol/translation/quirks.ts +18 -4
  328. package/src/protocol/translation/thinking.ts +12 -252
  329. package/src/protocol/translation/tool-id.ts +3 -3
  330. package/src/protocol/translation/tool-repair.ts +1 -1
  331. package/src/protocol/upstream/post-json.ts +6 -1
  332. package/src/protocol/upstream/send-claude-messages.ts +3 -3
  333. package/src/providers/adapters/base-adapter.ts +1 -1
  334. package/src/providers/adapters/compatible-adapter.ts +44 -5
  335. package/src/providers/authentication/jwt-validator.ts +0 -5
  336. package/src/providers/authentication/oauth-client.ts +254 -253
  337. package/src/providers/authentication/oauth-flow-store.ts +42 -7
  338. package/src/providers/authentication/oauth-refresh-service.ts +2 -2
  339. package/src/providers/bundled-discovery-providers.ts +6 -30
  340. package/src/providers/configured-openai-providers.ts +82 -97
  341. package/src/providers/configured-provider.ts +1 -1
  342. package/src/providers/default-registry.ts +23 -35
  343. package/src/providers/discovery/catalog/catalog-format.ts +13 -2
  344. package/src/providers/discovery/discovery-types.ts +0 -1
  345. package/src/providers/discovery/openai-model-discovery.ts +2 -2
  346. package/src/providers/discovery/probe-phases.ts +50 -15
  347. package/src/providers/discovery/probing-service.ts +18 -20
  348. package/src/providers/integrations/agentrouter/agentrouter.ts +14 -5
  349. package/src/providers/integrations/alibabacp/alibabacp.ts +2 -2
  350. package/src/providers/integrations/anthropic.ts +4 -11
  351. package/src/providers/integrations/antigravity/antigravity-oauth.ts +0 -1
  352. package/src/providers/integrations/antigravity/antigravity-protocol.ts +0 -14
  353. package/src/providers/integrations/antigravity/antigravity.ts +15 -26
  354. package/src/providers/integrations/buddy/buddy-catalog-shared.ts +2 -2
  355. package/src/providers/integrations/buddy/buddy-chat-shared.ts +20 -4
  356. package/src/providers/integrations/buddy/buddy-cli-headers.ts +19 -52
  357. package/src/providers/integrations/buddy/buddy-discovery-shared.ts +37 -6
  358. package/src/providers/integrations/buddy/buddy-identity.ts +108 -1
  359. package/src/providers/integrations/buddy/buddy-media.ts +16 -16
  360. package/src/providers/integrations/buddy/buddy-oauth-shared.ts +1 -1
  361. package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +1 -2
  362. package/src/providers/integrations/buddy/codebuddy/provider.ts +5 -16
  363. package/src/providers/integrations/buddy/codebuddy/shared.ts +30 -9
  364. package/src/providers/integrations/buddy/workbuddy/provider.ts +6 -10
  365. package/src/providers/integrations/buddy/workbuddy/shared.ts +21 -3
  366. package/src/providers/integrations/claude/claude-betas.ts +1 -1
  367. package/src/providers/integrations/claude/claude-cch.ts +148 -148
  368. package/src/providers/integrations/claude/claude-credentials.ts +1 -1
  369. package/src/providers/integrations/claude/claude-slow-lane-retry.ts +1 -1
  370. package/src/providers/integrations/claude/claude.ts +3 -10
  371. package/src/providers/integrations/claude/fallback-credit.ts +3 -7
  372. package/src/providers/integrations/cline/cline.ts +2 -2
  373. package/src/providers/integrations/cloudflareai/cloudflareai.ts +4 -7
  374. package/src/providers/integrations/codex/codex-device-code.ts +101 -101
  375. package/src/providers/integrations/codex/codex-errors.ts +1 -1
  376. package/src/providers/integrations/codex/codex-oauth.ts +0 -1
  377. package/src/providers/integrations/codex/codex.ts +14 -15
  378. package/src/providers/integrations/commandcode/commandcode.ts +76 -116
  379. package/src/providers/integrations/commandcode/quota.ts +132 -48
  380. package/src/providers/integrations/cursor/cursor-http2.ts +11 -14
  381. package/src/providers/integrations/cursor/cursor-quota.ts +2 -6
  382. package/src/providers/integrations/cursor/cursor-request.ts +0 -87
  383. package/src/providers/integrations/cursor/cursor-shared.ts +0 -4
  384. package/src/providers/integrations/cursor/cursor.ts +53 -20
  385. package/src/providers/integrations/cursor/protocol/framing.ts +0 -56
  386. package/src/providers/integrations/cursor/protocol/messages.ts +0 -96
  387. package/src/providers/integrations/factory/factory.ts +13 -13
  388. package/src/providers/integrations/gemini.ts +11 -11
  389. package/src/providers/integrations/github/github-discovery.ts +55 -10
  390. package/src/providers/integrations/github/github-oauth.ts +6 -5
  391. package/src/providers/integrations/github/github.ts +47 -20
  392. package/src/providers/integrations/grok/grok-oauth.ts +261 -261
  393. package/src/providers/integrations/grok/grok-quota-frame.ts +1 -1
  394. package/src/providers/integrations/grok/grok-quota.ts +16 -7
  395. package/src/providers/integrations/grok/grok-ratelimit-store.ts +0 -5
  396. package/src/providers/integrations/grok/grok-ratelimit.ts +3 -0
  397. package/src/providers/integrations/grok/grok.ts +88 -42
  398. package/src/providers/integrations/inferhub.ts +4 -11
  399. package/src/providers/integrations/kilo/kilo-oauth.ts +1 -1
  400. package/src/providers/integrations/kimi/kimi.ts +4 -11
  401. package/src/providers/integrations/kiro/kiro-cache.ts +31 -0
  402. package/src/providers/integrations/kiro/kiro-catalog.ts +31 -5
  403. package/src/providers/integrations/kiro/kiro-discovery.ts +47 -43
  404. package/src/providers/integrations/kiro/kiro-machine-id.ts +6 -3
  405. package/src/providers/integrations/kiro/kiro-oauth.ts +523 -255
  406. package/src/providers/integrations/kiro/kiro-quota.ts +35 -47
  407. package/src/providers/integrations/kiro/kiro-request.ts +8 -11
  408. package/src/providers/integrations/kiro/kiro-stream.ts +13 -0
  409. package/src/providers/integrations/kiro/kiro.ts +91 -78
  410. package/src/providers/integrations/muse/muse-oauth.ts +0 -1
  411. package/src/providers/integrations/muse/muse-quota.ts +0 -4
  412. package/src/providers/integrations/muse/muse.ts +6 -9
  413. package/src/providers/integrations/opencode/fingerprint.ts +7 -5
  414. package/src/providers/integrations/opencode/provider.ts +17 -24
  415. package/src/providers/integrations/perplexity.ts +2 -2
  416. package/src/providers/integrations/qoder/qoder-catalog.ts +132 -17
  417. package/src/providers/integrations/qoder/qoder-checkin.ts +26 -8
  418. package/src/providers/integrations/qoder/qoder-native-identity-store.ts +9 -8
  419. package/src/providers/integrations/qoder/qoder-oauth.ts +165 -25
  420. package/src/providers/integrations/qoder/qoder-quota.ts +48 -6
  421. package/src/providers/integrations/qoder/qoder-realm.ts +22 -28
  422. package/src/providers/integrations/qoder/qoder-sse.ts +159 -0
  423. package/src/providers/integrations/qoder/qoder-umid.ts +33 -38
  424. package/src/providers/integrations/qoder/qoder-virtual.ts +14 -2
  425. package/src/providers/integrations/qoder/qoder.ts +521 -63
  426. package/src/providers/integrations/shared/connect.ts +0 -54
  427. package/src/providers/integrations/shared/hash.ts +15 -0
  428. package/src/providers/integrations/shared/session-cache.ts +136 -0
  429. package/src/providers/integrations/tokenharbor.ts +2 -2
  430. package/src/providers/integrations/trae/trae-cache.ts +7 -87
  431. package/src/providers/integrations/trae/trae-catalog.ts +1 -6
  432. package/src/providers/integrations/trae/trae-protocol.ts +17 -8
  433. package/src/providers/integrations/trae/trae-realm.ts +0 -7
  434. package/src/providers/integrations/trae/trae.ts +59 -19
  435. package/src/providers/integrations/xai/xai-oauth.ts +5 -5
  436. package/src/providers/integrations/zcode/oauth.ts +1 -1
  437. package/src/providers/integrations/zcode-start-plan/gifts.ts +265 -0
  438. package/src/providers/integrations/zcode-start-plan/oauth.ts +154 -0
  439. package/src/providers/integrations/zcode-start-plan/provider.ts +131 -0
  440. package/src/providers/integrations/zcode-start-plan/quota.ts +152 -0
  441. package/src/providers/integrations/zcode-start-plan/zcode_prompt.json +10 -0
  442. package/src/providers/operations/account-error-classification.ts +131 -116
  443. package/src/providers/operations/account-health-service.ts +31 -39
  444. package/src/providers/operations/account-reset-service.ts +58 -1
  445. package/src/providers/operations/cli-platform.ts +31 -5
  446. package/src/providers/operations/client-version-resolver.ts +4 -6
  447. package/src/providers/operations/client-versions.ts +10 -7
  448. package/src/providers/operations/provider-catalog-seeder.ts +2 -0
  449. package/src/providers/operations/provider-catalog-service.ts +22 -6
  450. package/src/providers/operations/provider-credential-service.ts +35 -6
  451. package/src/providers/operations/upstream-deadline.ts +3 -3
  452. package/src/providers/provider-contracts.ts +65 -10
  453. package/src/providers/provider-credential-resolver.ts +1 -1
  454. package/src/providers/provider-metadata.ts +17 -2
  455. package/src/providers/provider-model-definition.ts +22 -1
  456. package/src/providers/provider-probe.ts +4 -2
  457. package/src/providers/provider-registry.ts +4 -4
  458. package/src/providers/provider-usage.ts +81 -16
  459. package/src/providers/quota/cache.ts +29 -0
  460. package/src/providers/quota/quota-contracts.ts +28 -10
  461. package/src/providers/quota/refresh.ts +29 -6
  462. package/src/providers/quota/snapshot.ts +94 -0
  463. package/src/providers/search/search-provider.ts +6 -8
  464. package/src/providers/search/search-providers.ts +2 -1
  465. package/src/security/abuse.ts +8 -8
  466. package/src/security/admission/reasons.ts +17 -17
  467. package/src/security/admission/service.ts +2 -2
  468. package/src/security/admission/ttl.ts +61 -61
  469. package/src/security/api-key-auth.ts +20 -8
  470. package/src/security/client-router-fingerprint.ts +4 -4
  471. package/src/security/credential-codec.ts +3 -3
  472. package/src/security/csrf.ts +2 -2
  473. package/src/security/ip-boundary.ts +16 -189
  474. package/src/security/model-access-rule.ts +1 -1
  475. package/src/security/outbound-headers.ts +18 -0
  476. package/src/shared/coerce.ts +19 -0
  477. package/src/shared/invoke-guard.ts +47 -0
  478. package/src/transport/contracts.ts +13 -4
  479. package/src/transport/dispatch/attempt-finalize.ts +5 -4
  480. package/src/transport/dispatch/attempt-loop.ts +10 -7
  481. package/src/transport/dispatch/capture-policy.ts +34 -37
  482. package/src/transport/dispatch/empty-stream.ts +28 -25
  483. package/src/transport/dispatch/generation.ts +33 -53
  484. package/src/transport/dispatch/leases.ts +8 -8
  485. package/src/transport/dispatch/native-shared.ts +127 -0
  486. package/src/transport/dispatch/proxy-request.ts +10 -8
  487. package/src/transport/dispatch/responses-compact.ts +35 -56
  488. package/src/transport/dispatch/retry-policy.ts +4 -67
  489. package/src/transport/dispatch/stream-bridge.ts +1 -1
  490. package/src/transport/dispatch/streaming/encoder.ts +18 -72
  491. package/src/transport/dispatch/streaming/finalize.ts +11 -10
  492. package/src/transport/dispatch/streaming/stream.ts +7 -3
  493. package/src/transport/dispatch/streaming/thinking-loop.ts +1 -1
  494. package/src/transport/dispatch/systemone.ts +27 -55
  495. package/src/transport/dispatch/token-count.ts +49 -46
  496. package/src/transport/dispatch/websearch.ts +24 -50
  497. package/src/transport/drain-endpoint.ts +3 -3
  498. package/src/transport/failure-policy.ts +19 -6
  499. package/src/transport/guards/auth-guard.ts +6 -11
  500. package/src/transport/guards/body-policy.ts +7 -7
  501. package/src/transport/guards/error-lifecycle.ts +18 -3
  502. package/src/transport/guards/guard-errors.ts +7 -3
  503. package/src/transport/guards/headroom-guard.ts +2 -2
  504. package/src/transport/guards/pipeline.ts +7 -7
  505. package/src/transport/guards/readiness-guard.ts +16 -2
  506. package/src/transport/guards/request-context.ts +15 -20
  507. package/src/transport/request/preparer.ts +306 -27
  508. package/src/transport/request/rtk/compress-request.ts +3 -3
  509. package/src/transport/request/rtk/rtk-constants.ts +2 -2
  510. package/src/transport/request/rtk/rtk-filters.ts +8 -8
  511. package/src/transport/request/state.ts +5 -20
  512. package/src/transport/request/system-prompt-injection.ts +74 -11
  513. package/src/transport/request/tenant-preferences.ts +22 -0
  514. package/src/transport/routing/candidate-index.ts +2 -2
  515. package/src/transport/routing/round-robin.ts +1 -1
  516. package/src/transport/routing/route-catalog.ts +113 -19
  517. package/src/transport/routing/route-model.ts +131 -38
  518. package/src/transport/routing/router.ts +154 -39
  519. package/src/transport/shutdown-notice.ts +21 -9
  520. package/src/transport/surface/adapters.ts +3 -3
  521. package/src/transport/surface/chat/encode.ts +7 -7
  522. package/src/transport/surface/chat/parse.ts +7 -7
  523. package/src/transport/surface/completion.ts +3 -3
  524. package/src/transport/surface/content-parts.ts +1 -1
  525. package/src/transport/surface/dialects.ts +2 -2
  526. package/src/transport/surface/messages/encode.ts +1 -1
  527. package/src/transport/surface/messages/parse.ts +3 -3
  528. package/src/transport/surface/responses/adapter.ts +1 -1
  529. package/src/transport/surface/responses/errors.ts +0 -9
  530. package/src/transport/vision-handoff-cache.ts +58 -0
  531. package/src/transport/vision-handoff.ts +376 -0
  532. package/src/workers/account-identity-sweep.ts +2 -2
  533. package/src/workers/buddy-growth-tasks.ts +392 -346
  534. package/src/workers/daily-checkin.ts +102 -49
  535. package/src/workers/maintenance-lease.ts +2 -35
  536. package/src/workers/oauth-refresh-sweep.ts +8 -1
  537. package/src/workers/quota-refresh-sweep.ts +120 -82
  538. package/src/workers/scheduler.ts +67 -4
  539. package/src/workers/sweep-runner.ts +24 -5
  540. package/src/workers/zcode-gift-claim.ts +108 -0
  541. package/dist/dashboard/assets/AccountCooldown-Bjo92Qat.js +0 -1
  542. package/dist/dashboard/assets/AccountCooldown-Bjo92Qat.js.gz +0 -0
  543. package/dist/dashboard/assets/BannedPage-2TJD37Hb.js.gz +0 -0
  544. package/dist/dashboard/assets/CliToolDetailPage-xefJNRmJ.js.gz +0 -0
  545. package/dist/dashboard/assets/CliToolsPage-Rj9iHQ3F.js.gz +0 -0
  546. package/dist/dashboard/assets/CombosPage-JEbYJFxw.js +0 -1
  547. package/dist/dashboard/assets/CombosPage-JEbYJFxw.js.gz +0 -0
  548. package/dist/dashboard/assets/ConsoleLogPage-C3xx_inM.js +0 -1
  549. package/dist/dashboard/assets/ConsoleLogPage-C3xx_inM.js.gz +0 -0
  550. package/dist/dashboard/assets/CustomizationPage-Dw306gJU.js +0 -1
  551. package/dist/dashboard/assets/CustomizationPage-Dw306gJU.js.gz +0 -0
  552. package/dist/dashboard/assets/HealthEventsModal-jaBo1TWV.js +0 -1
  553. package/dist/dashboard/assets/HealthEventsModal-jaBo1TWV.js.gz +0 -0
  554. package/dist/dashboard/assets/LoginPage-C4w7vea-.js.gz +0 -0
  555. package/dist/dashboard/assets/ModelPicker-BtPcAfot.js.gz +0 -0
  556. package/dist/dashboard/assets/OverviewPage-CGC5gNs1.js +0 -1
  557. package/dist/dashboard/assets/OverviewPage-CGC5gNs1.js.gz +0 -0
  558. package/dist/dashboard/assets/ProviderDetailPage-DQcthNGt.js +0 -4
  559. package/dist/dashboard/assets/ProviderDetailPage-DQcthNGt.js.gz +0 -0
  560. package/dist/dashboard/assets/ProviderIcon-FB6yS4v1.js +0 -1
  561. package/dist/dashboard/assets/ProviderIcon-FB6yS4v1.js.gz +0 -0
  562. package/dist/dashboard/assets/ProvidersPage-0OsXRGXe.js +0 -1
  563. package/dist/dashboard/assets/ProvidersPage-0OsXRGXe.js.gz +0 -0
  564. package/dist/dashboard/assets/ProxyPage-DdPc8slb.js +0 -9
  565. package/dist/dashboard/assets/ProxyPage-DdPc8slb.js.gz +0 -0
  566. package/dist/dashboard/assets/QuotaPage-Ci5O4BnH.js +0 -1
  567. package/dist/dashboard/assets/QuotaPage-Ci5O4BnH.js.gz +0 -0
  568. package/dist/dashboard/assets/SettingsPage-CDZjTt87.js +0 -1
  569. package/dist/dashboard/assets/SettingsPage-CDZjTt87.js.gz +0 -0
  570. package/dist/dashboard/assets/SetupPage-BQS0Rx_u.js.gz +0 -0
  571. package/dist/dashboard/assets/SortableList-BAL5npBC.js +0 -1
  572. package/dist/dashboard/assets/SortableList-BAL5npBC.js.gz +0 -0
  573. package/dist/dashboard/assets/StudioPage-TP7voAQ0.js.gz +0 -0
  574. package/dist/dashboard/assets/UsagePage-Cj_fEzhg.js +0 -56
  575. package/dist/dashboard/assets/UsagePage-Cj_fEzhg.js.gz +0 -0
  576. package/dist/dashboard/assets/api-keys-MbI7e7rt.js +0 -1
  577. package/dist/dashboard/assets/api-keys-MbI7e7rt.js.gz +0 -0
  578. package/dist/dashboard/assets/app-B2vyPoII.css +0 -2
  579. package/dist/dashboard/assets/app-B2vyPoII.css.gz +0 -0
  580. package/dist/dashboard/assets/assert-C0YYuZJ4.js.gz +0 -0
  581. package/dist/dashboard/assets/badge-B_DMpMJN.js.gz +0 -0
  582. package/dist/dashboard/assets/banner-DMrMPmth.js.gz +0 -0
  583. package/dist/dashboard/assets/card-Be5Uzamx.js.gz +0 -0
  584. package/dist/dashboard/assets/cli-tools-muFRphDu.js.gz +0 -0
  585. package/dist/dashboard/assets/clipboard-button-DOfrVmsi.js.gz +0 -0
  586. package/dist/dashboard/assets/dialog-C9omdjQt.js.gz +0 -0
  587. package/dist/dashboard/assets/drawer-Bm8WKaGY.js.gz +0 -0
  588. package/dist/dashboard/assets/entry-DS90gUks.js +0 -1
  589. package/dist/dashboard/assets/entry-DS90gUks.js.gz +0 -0
  590. package/dist/dashboard/assets/entry-j40LJotO.js +0 -4
  591. package/dist/dashboard/assets/entry-j40LJotO.js.gz +0 -0
  592. package/dist/dashboard/assets/entry-q5HwZ8nN.js.gz +0 -0
  593. package/dist/dashboard/assets/formatters-BTScrQpe.js.gz +0 -0
  594. package/dist/dashboard/assets/github-badge-Orjuzemr.js.gz +0 -0
  595. package/dist/dashboard/assets/index-cPrX5vn6.js.gz +0 -0
  596. package/dist/dashboard/assets/inline-Dvux8w-0.js.gz +0 -0
  597. package/dist/dashboard/assets/input-CU1Rgf3p.js.gz +0 -0
  598. package/dist/dashboard/assets/list-row-CzFvogbj.js.gz +0 -0
  599. package/dist/dashboard/assets/live-KaG15HLQ.js +0 -1
  600. package/dist/dashboard/assets/live-KaG15HLQ.js.gz +0 -0
  601. package/dist/dashboard/assets/minimize-2-D-MN8W8a.js +0 -1
  602. package/dist/dashboard/assets/minimize-2-D-MN8W8a.js.gz +0 -0
  603. package/dist/dashboard/assets/mutation-JJGQm8TD.js.gz +0 -0
  604. package/dist/dashboard/assets/numbers-BNDZnsUE.js +0 -2
  605. package/dist/dashboard/assets/numbers-BNDZnsUE.js.gz +0 -0
  606. package/dist/dashboard/assets/query-keys-CLaNz8Xz.js.gz +0 -0
  607. package/dist/dashboard/assets/quota-DZGyp0N5.js +0 -1
  608. package/dist/dashboard/assets/quota-DZGyp0N5.js.gz +0 -0
  609. package/dist/dashboard/assets/routing-BEH32QOC.js.gz +0 -0
  610. package/dist/dashboard/assets/select-B3iTetHN.js.gz +0 -0
  611. package/dist/dashboard/assets/settings-Dt3Eqbnq.js +0 -1
  612. package/dist/dashboard/assets/settings-Dt3Eqbnq.js.gz +0 -0
  613. package/dist/dashboard/assets/stack-KrOrm7JZ.js.gz +0 -0
  614. package/dist/dashboard/assets/state-DLS7IDMy.js.gz +0 -0
  615. package/dist/dashboard/assets/stream-retry-5QnVuSSo.js +0 -1
  616. package/dist/dashboard/assets/stream-retry-5QnVuSSo.js.gz +0 -0
  617. package/dist/dashboard/assets/switch-42ItvbAs.js.gz +0 -0
  618. package/dist/dashboard/assets/useMutation-Dmnfh1Ue.js.gz +0 -0
  619. package/src/network/response-headers.ts +0 -39
  620. package/src/protocol/response/dsml.ts +0 -38
  621. package/src/providers/integrations/xiaomi-mimo/think-stream.ts +0 -229
  622. /package/dist/dashboard/assets/{app-wvwCi4Eh.js → app-Bgd3zANQ.js} +0 -0
  623. /package/dist/dashboard/assets/{app-wvwCi4Eh.js.gz → app-Bgd3zANQ.js.gz} +0 -0
  624. /package/dist/dashboard/assets/brand/{og_bansos.webp → og_bansos_initial.webp} +0 -0
  625. /package/dist/dashboard/assets/brand/{og_image.webp → og_image_initial.webp} +0 -0
@@ -36,8 +36,7 @@ import {
36
36
  type ProxyCooldownEntry,
37
37
  } from "./pool-state";
38
38
 
39
- export type { PoolRotation, PoolUsageSnapshot, ProxyCooldownEntry };
40
- export { parseCooldownEntry };
39
+ export type { PoolRotation, ProxyCooldownEntry };
41
40
 
42
41
  // Pool selector
43
42
  /**
@@ -51,7 +50,7 @@ export { parseCooldownEntry };
51
50
  */
52
51
  export const PROVIDER_BLOCK_MS = 10 * 365 * 24 * 60 * 60 * 1000;
53
52
 
54
- export const DEFAULT_PROXY_CONCURRENCY = 10;
53
+ const DEFAULT_PROXY_CONCURRENCY = 10;
55
54
  const DEFAULT_PROXY_WEIGHT = 100;
56
55
  const MAX_PROXY_WEIGHT = 1000;
57
56
  // Crash-recovery bound, not a lease duration: normal releases DECR/DEL the
@@ -63,7 +62,7 @@ const MAX_PROXY_WEIGHT = 1000;
63
62
  function poolInflightTtlSeconds(): number {
64
63
  return resolveInflightTtlSeconds();
65
64
  }
66
- export function effectiveConcurrency(limit: number): number {
65
+ function effectiveConcurrency(limit: number): number {
67
66
  if (!Number.isInteger(limit) || limit < 1) return 0;
68
67
  return limit;
69
68
  }
@@ -100,7 +99,7 @@ function decrementLocal(counter: Map<string, number>, key: string): void {
100
99
  else counter.set(key, value - 1);
101
100
  }
102
101
 
103
- export type PoolSelectionFailureReason =
102
+ type PoolSelectionFailureReason =
104
103
  | "at_capacity"
105
104
  | "provider_at_capacity"
106
105
  | "cooldown"
@@ -109,7 +108,7 @@ export type PoolSelectionFailureReason =
109
108
  | "coordination_unavailable"
110
109
  | "no_active_pool";
111
110
 
112
- export interface PoolCapacitySnapshot {
111
+ interface PoolCapacitySnapshot {
113
112
  readonly poolId: string;
114
113
  readonly maxInflight: number;
115
114
  readonly currentInflight: number;
@@ -206,67 +205,67 @@ export class NetworkPoolSelector {
206
205
  }
207
206
 
208
207
  /**
209
- * Raise this provider's share of a pool, enforcing both the pool ceiling and
210
- * the provider ceiling. The pool half is still checked here so a caller that
211
- * already admitted the pool through `acquire` does not double-count it — use
212
- * this instead of `acquire` whenever a provider ceiling applies.
208
+ * One local slot acquisition: checks every map's ceiling, bumps each counter,
209
+ * and returns a release that decrements all of them exactly once. Shared by
210
+ * `acquire` (single pool counter) and `acquireProvider` (pool + provider
211
+ * share) so the release closure and the local bookkeeping live in one place.
213
212
  */
214
- acquireProvider(
215
- poolId: string,
216
- providerId: string,
217
- poolCapacity: number,
218
- providerCapacity: number,
213
+ private acquireLocalSlot(
214
+ increments: ReadonlyArray<{
215
+ readonly map: Map<string, number>;
216
+ readonly key: string;
217
+ readonly capacity: number;
218
+ }>,
219
219
  ): { acquired: boolean; release: () => void } {
220
- const current = this.getInflight(poolId);
221
- if (current >= poolCapacity) return { acquired: false, release: () => {} };
222
- const key = providerInflightStateKey(poolId, providerId);
223
- const providerCurrent = this.state.providerInflight.get(key) ?? 0;
224
- if (providerCurrent >= providerCapacity) return { acquired: false, release: () => {} };
225
-
226
- enforceInflightLimit(this.state.inflight);
227
- enforceInflightLimit(this.state.providerInflight);
228
- this.state.inflight.set(poolId, current + 1);
229
- this.state.providerInflight.set(key, providerCurrent + 1);
220
+ for (const increment of increments) {
221
+ const current = increment.map.get(increment.key) ?? 0;
222
+ if (current >= increment.capacity) return { acquired: false, release: () => {} };
223
+ }
224
+ for (const increment of increments) {
225
+ enforceInflightLimit(increment.map);
226
+ increment.map.set(increment.key, (increment.map.get(increment.key) ?? 0) + 1);
227
+ }
230
228
  this.notifyPoolUsage();
231
229
 
232
230
  let released = false;
233
231
  const release = () => {
234
232
  if (released) return;
235
233
  released = true;
236
- decrementLocal(this.state.inflight, poolId);
237
- decrementLocal(this.state.providerInflight, key);
234
+ for (const increment of increments) decrementLocal(increment.map, increment.key);
238
235
  this.notifyPoolUsage();
239
236
  };
240
237
  return { acquired: true, release };
241
238
  }
242
239
 
240
+ /**
241
+ * Raise this provider's share of a pool, enforcing both the pool ceiling and
242
+ * the provider ceiling. The pool half is still checked here so a caller that
243
+ * already admitted the pool through `acquire` does not double-count it — use
244
+ * this instead of `acquire` whenever a provider ceiling applies.
245
+ */
246
+ acquireProvider(
247
+ poolId: string,
248
+ providerId: string,
249
+ poolCapacity: number,
250
+ providerCapacity: number,
251
+ ): { acquired: boolean; release: () => void } {
252
+ return this.acquireLocalSlot([
253
+ { map: this.state.inflight, key: poolId, capacity: poolCapacity },
254
+ {
255
+ map: this.state.providerInflight,
256
+ key: providerInflightStateKey(poolId, providerId),
257
+ capacity: providerCapacity,
258
+ },
259
+ ]);
260
+ }
261
+
243
262
  acquire(
244
263
  poolId: string,
245
264
  maxInflight = DEFAULT_PROXY_CONCURRENCY,
246
265
  ): { acquired: boolean; release: () => void } {
247
- const current = this.getInflight(poolId);
248
- if (current >= maxInflight) {
249
- return { acquired: false, release: () => {} };
250
- }
251
-
252
- enforceInflightLimit(this.state.inflight);
253
- this.state.inflight.set(poolId, current + 1);
254
- this.notifyPoolUsage();
255
- let released = false;
256
-
257
- const release = () => {
258
- if (released) return;
259
- released = true;
260
- const val = this.state.inflight.get(poolId) ?? 1;
261
- if (val <= 1) {
262
- this.state.inflight.delete(poolId);
263
- } else {
264
- this.state.inflight.set(poolId, val - 1);
265
- }
266
- this.notifyPoolUsage();
267
- };
268
-
269
- return { acquired: true, release };
266
+ return this.acquireLocalSlot([
267
+ { map: this.state.inflight, key: poolId, capacity: maxInflight },
268
+ ]);
270
269
  }
271
270
 
272
271
  private cooldownKey(poolId: string, providerId: string): string {
@@ -577,6 +576,98 @@ export class NetworkPoolSelector {
577
576
  await this.redis.del(indexKey);
578
577
  }
579
578
 
579
+ /**
580
+ * Tries one local candidate: bump the local counters (pool, and provider
581
+ * share when a ceiling is set) and return its release. `undefined` when the
582
+ * candidate is full or the provider share is exhausted.
583
+ */
584
+ private localAcquire(
585
+ candidate: { readonly poolId: string; readonly capacity: number },
586
+ providerId: string,
587
+ providerCapacity: number | undefined,
588
+ ): { poolId: string; release: () => void } | undefined {
589
+ const slot =
590
+ providerCapacity === undefined
591
+ ? this.acquire(candidate.poolId, candidate.capacity)
592
+ : this.acquireProvider(candidate.poolId, providerId, candidate.capacity, providerCapacity);
593
+ return slot.acquired ? { poolId: candidate.poolId, release: slot.release } : undefined;
594
+ }
595
+
596
+ /**
597
+ * One atomic Redis admit (the compare-and-increment script) for a candidate:
598
+ * bumps both the remote and local counters on success and returns a release
599
+ * that decrements both. `undefined` when the pool or provider share is full
600
+ * (1 admitted; 0 pool full; 2 provider full — either full result fails over
601
+ * to the next pool, which is the whole point of the ceiling).
602
+ */
603
+ private async redisAdmit(
604
+ redis: RedisClient,
605
+ candidate: { readonly poolId: string; readonly capacity: number },
606
+ providerId: string,
607
+ providerCapacity: number | undefined,
608
+ ): Promise<{ poolId: string; release: () => void } | undefined> {
609
+ const providerKey =
610
+ providerCapacity === undefined
611
+ ? undefined
612
+ : proxyProviderInflightKey(candidate.poolId, providerId);
613
+ const result =
614
+ providerKey === undefined
615
+ ? await redisEvalNumber(
616
+ redis,
617
+ POOL_ADMIT_SCRIPT,
618
+ 1,
619
+ proxyInflightKey(candidate.poolId),
620
+ String(candidate.capacity),
621
+ String(poolInflightTtlSeconds()),
622
+ )
623
+ : await redisEvalNumber(
624
+ redis,
625
+ POOL_PROVIDER_ADMIT_SCRIPT,
626
+ 2,
627
+ proxyInflightKey(candidate.poolId),
628
+ providerKey,
629
+ String(candidate.capacity),
630
+ String(providerCapacity),
631
+ String(poolInflightTtlSeconds()),
632
+ );
633
+ if (result !== 1) return undefined;
634
+ enforceInflightLimit(this.state.inflight);
635
+ this.state.inflight.set(candidate.poolId, this.getInflight(candidate.poolId) + 1);
636
+ if (providerKey !== undefined) {
637
+ enforceInflightLimit(this.state.providerInflight);
638
+ const localProviderKey = providerInflightStateKey(candidate.poolId, providerId);
639
+ this.state.providerInflight.set(
640
+ localProviderKey,
641
+ this.getProviderInflight(candidate.poolId, providerId) + 1,
642
+ );
643
+ }
644
+ this.notifyPoolUsage();
645
+ let released = false;
646
+ return {
647
+ poolId: candidate.poolId,
648
+ release: () => {
649
+ if (released) return;
650
+ released = true;
651
+ decrementLocal(this.state.inflight, candidate.poolId);
652
+ if (providerKey !== undefined) {
653
+ decrementLocal(
654
+ this.state.providerInflight,
655
+ providerInflightStateKey(candidate.poolId, providerId),
656
+ );
657
+ }
658
+ this.notifyPoolUsage();
659
+ const script = providerKey === undefined ? POOL_RELEASE_SCRIPT : POOL_PROVIDER_RELEASE_SCRIPT;
660
+ const keys =
661
+ providerKey === undefined
662
+ ? [proxyInflightKey(candidate.poolId)]
663
+ : [proxyInflightKey(candidate.poolId), providerKey];
664
+ void redisEvalNumber(redis, script, keys.length, ...keys).catch((error: unknown) => {
665
+ log.error("[pool-selector] failed to release distributed slot", error as Error);
666
+ });
667
+ },
668
+ };
669
+ }
670
+
580
671
  /**
581
672
  * Distributed pool admission — the selection algorithm:
582
673
  *
@@ -652,17 +743,8 @@ export class NetworkPoolSelector {
652
743
  shuffleArray(scored);
653
744
  for (const candidate of scored) {
654
745
  if (this.getInflight(candidate.poolId) >= candidate.capacity) continue;
655
- const slot =
656
- providerCapacity === undefined
657
- ? this.acquire(candidate.poolId, candidate.capacity)
658
- : this.acquireProvider(
659
- candidate.poolId,
660
- providerId,
661
- candidate.capacity,
662
- providerCapacity,
663
- );
664
- if (!slot.acquired) continue;
665
- return { poolId: candidate.poolId, release: slot.release };
746
+ const slot = this.localAcquire(candidate, providerId, providerCapacity);
747
+ if (slot) return slot;
666
748
  }
667
749
  return undefined;
668
750
  }
@@ -677,18 +759,10 @@ export class NetworkPoolSelector {
677
759
  const candidate = scored[absolute];
678
760
  if (!candidate) continue;
679
761
  if (this.getInflight(candidate.poolId) >= candidate.capacity) continue;
680
- const slot =
681
- providerCapacity === undefined
682
- ? this.acquire(candidate.poolId, candidate.capacity)
683
- : this.acquireProvider(
684
- candidate.poolId,
685
- providerId,
686
- candidate.capacity,
687
- providerCapacity,
688
- );
689
- if (!slot.acquired) continue;
762
+ const slot = this.localAcquire(candidate, providerId, providerCapacity);
763
+ if (!slot) continue;
690
764
  advanceRotation(this.state.rotationCursors, rotation.key, scored.length, absolute, rotation.rotateCount);
691
- return { poolId: candidate.poolId, release: slot.release };
765
+ return slot;
692
766
  }
693
767
  return undefined;
694
768
  }
@@ -716,11 +790,7 @@ export class NetworkPoolSelector {
716
790
  }
717
791
  }
718
792
  if (!bestPool) return undefined;
719
- const slot =
720
- providerCapacity === undefined
721
- ? this.acquire(bestPool.poolId, bestPool.capacity)
722
- : this.acquireProvider(bestPool.poolId, providerId, bestPool.capacity, providerCapacity);
723
- return slot.acquired ? { poolId: bestPool.poolId, release: slot.release } : undefined;
793
+ return this.localAcquire(bestPool, providerId, providerCapacity);
724
794
  }
725
795
 
726
796
  const redis = this.redis;
@@ -795,34 +865,8 @@ export class NetworkPoolSelector {
795
865
  });
796
866
  }
797
867
  for (const candidate of withCounts) {
798
- const providerKey =
799
- providerCapacity === undefined
800
- ? undefined
801
- : proxyProviderInflightKey(candidate.poolId, providerId);
802
- const result =
803
- providerKey === undefined
804
- ? await redisEvalNumber(
805
- redis,
806
- POOL_ADMIT_SCRIPT,
807
- 1,
808
- proxyInflightKey(candidate.poolId),
809
- String(candidate.capacity),
810
- String(poolInflightTtlSeconds()),
811
- )
812
- : await redisEvalNumber(
813
- redis,
814
- POOL_PROVIDER_ADMIT_SCRIPT,
815
- 2,
816
- proxyInflightKey(candidate.poolId),
817
- providerKey,
818
- String(candidate.capacity),
819
- String(providerCapacity),
820
- String(poolInflightTtlSeconds()),
821
- );
822
- // 1 admitted; 0 pool full; 2 provider full. Either full result fails over
823
- // to the next pool, which is the whole point of the ceiling: the request
824
- // moves to an egress that still has room for this provider.
825
- if (result !== 1) continue;
868
+ const admitted = await this.redisAdmit(redis, candidate, providerId, providerCapacity);
869
+ if (admitted === undefined) continue;
826
870
  if (rotation && rotation.strategy !== "random") {
827
871
  advanceRotation(
828
872
  this.state.rotationCursors,
@@ -832,37 +876,7 @@ export class NetworkPoolSelector {
832
876
  rotation.rotateCount,
833
877
  );
834
878
  }
835
- enforceInflightLimit(this.state.inflight);
836
- this.state.inflight.set(candidate.poolId, this.getInflight(candidate.poolId) + 1);
837
- if (providerKey !== undefined) {
838
- enforceInflightLimit(this.state.providerInflight);
839
- const localProviderKey = providerInflightStateKey(candidate.poolId, providerId);
840
- this.state.providerInflight.set(
841
- localProviderKey,
842
- this.getProviderInflight(candidate.poolId, providerId) + 1,
843
- );
844
- }
845
- this.notifyPoolUsage();
846
- let released = false;
847
- return {
848
- poolId: candidate.poolId,
849
- release: () => {
850
- if (released) return;
851
- released = true;
852
- decrementLocal(this.state.inflight, candidate.poolId);
853
- if (providerKey !== undefined) {
854
- decrementLocal(this.state.providerInflight, providerInflightStateKey(candidate.poolId, providerId));
855
- }
856
- this.notifyPoolUsage();
857
- const script = providerKey === undefined ? POOL_RELEASE_SCRIPT : POOL_PROVIDER_RELEASE_SCRIPT;
858
- const keys = providerKey === undefined ? [proxyInflightKey(candidate.poolId)] : [proxyInflightKey(candidate.poolId), providerKey];
859
- void redisEvalNumber(redis, script, keys.length, ...keys).catch(
860
- (error: unknown) => {
861
- log.error("[pool-selector] failed to release distributed slot", error as Error);
862
- },
863
- );
864
- },
865
- };
879
+ return admitted;
866
880
  }
867
881
  return undefined;
868
882
  }
@@ -18,6 +18,7 @@ import type { CartethyiaDatabase } from "../persistence/db-handle";
18
18
  import { networkPools, healthEvents } from "../persistence/schema";
19
19
  import { log } from "../observability/logger";
20
20
  import { providerRateLimitIsIpScoped } from "../providers/provider-metadata";
21
+ import { resolvePoolProviderRateLimitCooldownMs } from "../config/cooldowns";
21
22
  import { PROVIDER_BLOCK_MS, type ProxyCooldownEntry } from "./pool/selector";
22
23
 
23
24
  type PoolProviderCooldownMap = NonNullable<
@@ -120,6 +121,37 @@ export async function dropAllProviderCooldownMarkers(
120
121
  }
121
122
  }
122
123
 
124
+ /**
125
+ * Removes expired provider markers from one pool row, keeping live cooldowns
126
+ * and permanent blocks intact. Returns how many markers were dropped.
127
+ *
128
+ * The selector and Redis prune expired entries on read, but the durable JSON
129
+ * mirror is only ever written, never swept — so a marker that expired while
130
+ * the process was down survives on the row forever. Harmless to routing (boot
131
+ * restores only future-dated markers), but it bloats the row and makes the
132
+ * backup/restore surface carry dead state.
133
+ */
134
+ export async function pruneExpiredProviderCooldownMarkers(
135
+ db: CartethyiaDatabase,
136
+ poolId: string,
137
+ ): Promise<number> {
138
+ try {
139
+ const map = await readMarkerMap(db, poolId);
140
+ const now = Date.now();
141
+ const expiredKeys: string[] = [];
142
+ for (const [providerKey, marker] of Object.entries(map)) {
143
+ if (marker.until <= now) expiredKeys.push(providerKey);
144
+ }
145
+ if (expiredKeys.length === 0) return 0;
146
+ for (const providerKey of expiredKeys) delete map[providerKey];
147
+ await writeMarkerMap(db, poolId, map);
148
+ return expiredKeys.length;
149
+ } catch (error) {
150
+ log.warn("pool provider cooldown mirror prune failed", { networkPoolId: poolId, error: String(error) });
151
+ return 0;
152
+ }
153
+ }
154
+
123
155
  /**
124
156
  * Writes a probe-measured verdict the same way a dispatch-side event would be
125
157
  * recorded: one mirror marker and one audit row.
@@ -201,19 +233,22 @@ function probeEndpointOf(endpointConfig: unknown): string {
201
233
  /**
202
234
  * Re-enforces the stored probe verdicts at boot (new: replay, read-ahead).
203
235
  *
204
- * A stored `blocked` verdict is a *state*, not a decoration. If the marker the
205
- * probe flagged failed to persist, or the verdict predated the marker column,
206
- * the egress would return to rotation after restart and get refused again —
207
- * dispatch spending a real request to rediscover a refusal the row already
208
- * knew. So the boot read reconciles: a fresh (endpoint still matches) `blocked`
209
- * verdict for an address-keyed provider becomes a block flag, even when the
210
- * mirror has nothing (or has an older, cooler entry — the retirement claim is
211
- * the sharper fact and wins).
236
+ * A stored verdict is a *state*, not a decoration. If the marker the probe
237
+ * flagged failed to persist, or the verdict predated the marker column, the
238
+ * egress would return to rotation after restart and get refused again — dispatch
239
+ * spending a real request to rediscover a refusal the row already knew. So the
240
+ * boot read reconciles:
212
241
  *
213
- * `throttled` is deliberately **not** replayed into a cooldown: the cooldown
214
- * the probe flagged already carries the measured window, and blind-replaying
215
- * one from a verdict that predates the restart could outlive the upstream's
216
- * actual window.
242
+ * - A fresh `blocked` verdict for an address-keyed provider becomes a block
243
+ * flag, even when the mirror has nothing (or has an older, cooler entry — the
244
+ * retirement claim is the sharper fact and wins).
245
+ * - A fresh `throttled` verdict for an address-keyed provider becomes the same
246
+ * cooldown a dispatch-side 429 would have written. The probe measured the
247
+ * rate-limit window itself (`provider_probe_at` is when it observed the 429),
248
+ * so the replayed window starts at that measurement, not at boot — without
249
+ * the monotonic clock, a restart mid-window would shorten the cool-off the
250
+ * probe proved. The configured default window is applied when the probe did
251
+ * not record a duration.
217
252
  *
218
253
  * Returns how many verdicts were replayed.
219
254
  */
@@ -221,6 +256,7 @@ export async function restorePoolProbeVerdicts(
221
256
  selector: { hydrateProviderCooldown(entry: ProxyCooldownEntry): void },
222
257
  db: CartethyiaDatabase,
223
258
  ): Promise<number> {
259
+ const probeCooldownMs = resolvePoolProviderRateLimitCooldownMs();
224
260
  const rows = await db
225
261
  .select({
226
262
  id: networkPools.id,
@@ -230,21 +266,40 @@ export async function restorePoolProbeVerdicts(
230
266
  providerProbeStatus: networkPools.providerProbeStatus,
231
267
  providerProbeReason: networkPools.providerProbeReason,
232
268
  providerProbeEndpoint: networkPools.providerProbeEndpoint,
269
+ providerProbeAt: networkPools.providerProbeAt,
233
270
  })
234
271
  .from(networkPools);
235
272
  let replayed = 0;
236
273
  for (const row of rows) {
237
274
  if (row.status !== "active") continue;
238
- if (row.providerProbeStatus !== "blocked") continue;
275
+ if (row.providerProbeStatus !== "blocked" && row.providerProbeStatus !== "throttled") continue;
239
276
  if (row.providerProbeEndpoint !== probeEndpointOf(row.endpointConfig)) continue;
240
277
  const providerId = row.providerProbeProviderId;
241
278
  if (typeof providerId !== "string" || !providerRateLimitIsIpScoped(providerId)) continue;
279
+ if (row.providerProbeStatus === "blocked") {
280
+ selector.hydrateProviderCooldown({
281
+ poolId: row.id,
282
+ providerId: providerId.toLowerCase(),
283
+ until: Date.now() + PROVIDER_BLOCK_MS,
284
+ reason: row.providerProbeReason ?? "Upstream refused this egress address",
285
+ blocked: true,
286
+ });
287
+ replayed += 1;
288
+ continue;
289
+ }
290
+ // `throttled`: replay the cooldown the probe measured. The upstream's
291
+ // window began when the probe saw the 429, so the marker expires at
292
+ // `probeAt + window`, unless that is already past — an allowance that
293
+ // refilled while the process was down must not be resurrected.
294
+ const probeAt = row.providerProbeAt?.getTime();
295
+ const until =
296
+ probeAt !== undefined ? probeAt + probeCooldownMs : Date.now() + probeCooldownMs;
297
+ if (until <= Date.now()) continue;
242
298
  selector.hydrateProviderCooldown({
243
299
  poolId: row.id,
244
300
  providerId: providerId.toLowerCase(),
245
- until: Date.now() + PROVIDER_BLOCK_MS,
246
- reason: row.providerProbeReason ?? "Upstream refused this egress address",
247
- blocked: true,
301
+ until,
302
+ reason: row.providerProbeReason ?? "Operator probe measured a provider 429",
248
303
  });
249
304
  replayed += 1;
250
305
  }
@@ -5,6 +5,7 @@ import type { CartethyiaDatabase } from "../persistence/db-handle";
5
5
  import { healthEvents, networkPools } from "../persistence/schema";
6
6
  import { drainPoolByteDelta } from "./pool/byte-accounting";
7
7
  import { resolvePoolCooldownMs } from "../config/cooldowns";
8
+ import { dropProviderCooldownMarker, pruneExpiredProviderCooldownMarkers } from "./pool-cooldown-store";
8
9
  export interface PoolHealthEvent {
9
10
  readonly id: string;
10
11
  readonly networkPoolId: string;
@@ -15,7 +16,7 @@ export interface PoolHealthEvent {
15
16
  readonly createdAt: string;
16
17
  }
17
18
 
18
- export interface PoolHealthSnapshot {
19
+ interface PoolHealthSnapshot {
19
20
  readonly poolId: string;
20
21
  readonly tenantId: string;
21
22
  readonly status: "active" | "cooldown" | "disabled";
@@ -54,7 +55,14 @@ function poolFaultCategory(error: unknown, origin: string | undefined): string |
54
55
  export async function recordPoolDispatchOutcome(
55
56
  db: CartethyiaDatabase,
56
57
  poolId: string,
57
- input: { readonly succeeded: boolean; readonly error?: unknown; readonly errorOrigin?: string; readonly snapshotInvalidator?: { invalidate(): unknown } },
58
+ input: {
59
+ readonly succeeded: boolean;
60
+ readonly error?: unknown;
61
+ readonly errorOrigin?: string;
62
+ /** The provider the attempt dialed; used to clear its stale egress marker on success. */
63
+ readonly providerId?: string;
64
+ readonly snapshotInvalidator?: { invalidate(): unknown };
65
+ },
58
66
  ): Promise<void> {
59
67
  const category = input.succeeded ? undefined : poolFaultCategory(input.error, input.errorOrigin);
60
68
  // Metered bytes are banked even when health is untouched: an upstream-origin
@@ -95,6 +103,14 @@ export async function recordPoolDispatchOutcome(
95
103
  lastErrorCategory = null;
96
104
  lastErrorAt = null;
97
105
  lastSuccessAt = now;
106
+ // A completed dispatch proves the egress is serving again for this
107
+ // provider — clear its stale rate-limit marker (the selector's local
108
+ // entry and the Redis copy) so a spent pool is not kept out of rotation
109
+ // after it demonstrably recovered. Fire-and-forget: a failed clear must
110
+ // never fail a completed request.
111
+ if (input.providerId !== undefined) {
112
+ void dropProviderCooldownMarker(db, poolId, input.providerId).catch(() => undefined);
113
+ }
98
114
  } else {
99
115
  failures += 1;
100
116
  // Every failed probe parks the pool: a pool that cannot carry traffic is
@@ -252,5 +268,9 @@ export async function sweepExpiredPoolCooldowns(db: CartethyiaDatabase): Promise
252
268
  await db.update(networkPools).set({ status: "active", consecutiveFailures: 0, cooldownUntil: null, lastRecoveredAt: now }).where(inArray(networkPools.id, expired.map((row) => row.id)));
253
269
  await db.insert(healthEvents).values(expired.map((row) => ({ id: randomUUID(), entityKind: "pool" as const, networkPoolId: row.id, fromStatus: row.status, toStatus: "active" as const, reason: "Cooldown elapsed — auto-recovered", errorCategory: null, createdAt: now })));
254
270
  for (const row of expired) publishPoolHealth({ poolId: row.id, tenantId: row.tenantId, status: "active", consecutiveFailures: 0 });
271
+ // The pool-level cooldown elapsed, so the same window's provider markers are
272
+ // expired too. Drop them from the durable JSON mirror — the selector and
273
+ // Redis prune on read, but the row's stale copy would otherwise live forever.
274
+ await Promise.all(expired.map((row) => pruneExpiredProviderCooldownMarkers(db, row.id)));
255
275
  return expired.length;
256
276
  }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Unified egress-failure classification for ip-scoped, rate-limited providers.
3
+ *
4
+ * One 429 is read in several places — dispatch (`attempt-loop`, streaming
5
+ * `finalize`), the provider probe, account health, and the pool health machine
6
+ * each had their own vocabulary. The two questions that decide whether a probe
7
+ * *pool* (not an account) is responsible for a failure are:
8
+ *
9
+ * - Did the upstream count this request against the egress address? Only a
10
+ * provider whose limits follow the IP (`ipScopedRateLimit`) qualifies — for
11
+ * everyone else the 429 belongs to the credential that was dialed, and
12
+ * cooling the pool would sideline healthy accounts.
13
+ * - Is the address itself retired (free-tier 403) rather than merely
14
+ * throttled (429)? A block is terminal; a cooldown refills.
15
+ *
16
+ * This module is the single source of truth for those two facts. Dispatch and
17
+ * the probe both read it, so the operator's "test" and gateway's own dial
18
+ * cannot disagree about which egress is spent.
19
+ */
20
+ import { GatewayError } from "../protocol/gateway-error";
21
+ import { providerRateLimitIsIpScoped } from "../providers/provider-metadata";
22
+
23
+ /** What one upstream failure means for the egress that carried it. */
24
+ type EgressVerdict =
25
+ | { kind: "none" }
26
+ | { kind: "cooldown"; retryAfterMs?: number }
27
+ | { kind: "blocked" };
28
+
29
+ /** The upstream's own wording for a retired free-tier address. */
30
+ export const FREE_TIER_REFUSAL = /freetier|free tier can only be used/i;
31
+
32
+ /**
33
+ * Classifies one upstream failure against one provider.
34
+ *
35
+ * `providerId` is the caller's own candidate, not a field read off the error:
36
+ * the in-stream error frames the protocol modules build carry no `providerId`
37
+ * detail, so reading it from `details` would silently disable the IP-scoped
38
+ * rule on exactly the streaming path it exists for.
39
+ */
40
+ export function classifyEgressFailure(error: unknown, providerId: string): EgressVerdict {
41
+ if (!(error instanceof GatewayError) || error.origin !== "upstream") {
42
+ return { kind: "none" };
43
+ }
44
+ if (!providerRateLimitIsIpScoped(providerId)) return { kind: "none" };
45
+
46
+ // A provider-scoped 429 exhausted the address's allowance: cool the pool
47
+ // until the window refills.
48
+ if (error.status === 429 && error.details.rateLimitScope === "provider") {
49
+ const retryAfterMs =
50
+ typeof error.details.retryAfterMs === "number" ? error.details.retryAfterMs : undefined;
51
+ return retryAfterMs === undefined ? { kind: "cooldown" } : { kind: "cooldown", retryAfterMs };
52
+ }
53
+
54
+ // A free-tier 403 retires the address: the request was never read, so the
55
+ // same egress cannot succeed no matter what the payload says. Deliberately
56
+ // narrower than "any 403" — a content-policy or licence refusal is also a
57
+ // 403 and must not retire a healthy proxy.
58
+ if (error.status === 403) {
59
+ const providerCode =
60
+ typeof error.details.providerCode === "string" ? error.details.providerCode : "";
61
+ const raw = String(error.details.raw ?? "");
62
+ if (/freetier|free.?tier/i.test(providerCode) || FREE_TIER_REFUSAL.test(`${error.message} ${raw}`)) {
63
+ return { kind: "blocked" };
64
+ }
65
+ }
66
+
67
+ return { kind: "none" };
68
+ }
@@ -9,7 +9,7 @@
9
9
  import { exponentialBackoff, type BackoffOptions } from "./backoff";
10
10
  import { sleep as defaultSleep } from "../shared/sleep";
11
11
 
12
- export interface RetryOptions {
12
+ interface RetryOptions {
13
13
  readonly maxRetries?: number;
14
14
  readonly baseDelayMs?: number;
15
15
  readonly maxDelayMs?: number;