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
@@ -2,6 +2,8 @@
2
2
  import { GatewayError } from "../../protocol/gateway-error";
3
3
  import { type ServiceKind, type WireFamily } from "../../protocol/canonical-model";
4
4
  import type { CallerIdentity } from "../../security/access-control";
5
+ import type { ProviderQuotaWindow } from "../../providers/quota/quota-contracts";
6
+ import { log } from "../../observability/logger";
5
7
 
6
8
  export type { CallerIdentity };
7
9
 
@@ -14,6 +16,8 @@ export interface RouteCandidate {
14
16
  readonly provider_id: string;
15
17
  readonly model_id: string;
16
18
  readonly wire_family: WireFamily;
19
+ /** Explicit wire dialect (from `models.dialect`), when set. Absent = resolver fallback. */
20
+ readonly dialect?: string;
17
21
  /**
18
22
  * Protocol shape of the model. `llm` (the default) flows through the
19
23
  * canonical surface pipeline; a non-`llm` model is dispatched by its native
@@ -84,6 +88,12 @@ export interface RouteCandidate {
84
88
  * refused.
85
89
  */
86
90
  readonly pro_only?: boolean;
91
+ /**
92
+ * True when the provider publishes this model as free (its own directory
93
+ * statement, e.g. Qoder's `is_free`). A free model may be routed to any
94
+ * account — the credit-preference ordering skips it entirely.
95
+ */
96
+ readonly free_tier?: boolean;
87
97
  /**
88
98
  * The plan the provider last reported for `provider_account_id`.
89
99
  *
@@ -95,9 +105,44 @@ export interface RouteCandidate {
95
105
  readonly usage_floor?: number;
96
106
  readonly usage_floor_unit?: "credit" | "percent";
97
107
  /** Live quota windows (cache) the floor check evaluates. */
98
- readonly quota_windows?: readonly unknown[];
108
+ readonly quota_windows?: readonly ProviderQuotaWindow[];
109
+ /** A successful Start Plan balance authoritatively lists its model grants. */
110
+ readonly quota_entitlements_known?: boolean;
99
111
  }
100
112
 
113
+ /**
114
+ * A candidate as the routing engine actually evaluates it: the catalog fields
115
+ * above plus the health/cooling stamp `route-catalog.ts` projects onto it and
116
+ * the plan-time slow-lane marker.
117
+ *
118
+ * Declared here — not as a route-catalog-local intersection — so
119
+ * `EligibilityEvaluator.evaluate` can take the routed shape without a cast,
120
+ * and so every stamping site agrees on the same field set by construction.
121
+ */
122
+ export type RoutedCandidate = RouteCandidate & {
123
+ /**
124
+ * Health marker the catalog projects from `provider_accounts.status` and
125
+ * unexpired `model_cooldowns` entries. Absent = no health exclusion known.
126
+ * Mutable by declaration: the catalog builds the base candidate first and
127
+ * stamps health/cooling onto it as account rows are walked.
128
+ */
129
+ health_status?: "cooldown" | "model_cooldown" | "disabled";
130
+ /** Cooldown class the catalog derives from the account's last error category. */
131
+ cooldown_kind?: "hard" | "soft";
132
+ /**
133
+ * Epoch milliseconds at which this candidate's cooling window ends, when one
134
+ * is in force. Routing-internal: it is what lets `plan()` report a real reset
135
+ * on the all-accounts-cooling answer, and it never reaches a client verbatim —
136
+ * only the derived `retry-after` does.
137
+ */
138
+ cooldown_until?: number;
139
+ /**
140
+ * Slow-lane stamp applied at plan time (test-supported; runtime slow-lane
141
+ * activity is read live via `sharedClaudeSlowLanes.isActive`).
142
+ */
143
+ lane?: "slow";
144
+ };
145
+
101
146
  /**
102
147
  * Provider-routing strategies. The runtime tuple is the source: the console's
103
148
  * Elysia body schema builds its `t.Literal` union from these values and its
@@ -270,7 +315,6 @@ export type EligibilityReason =
270
315
  | "cooldown_hard"
271
316
  | "model_cooldown"
272
317
  | "quota_exhausted"
273
- | "locked"
274
318
  | "disabled"
275
319
 
276
320
  export interface EligibilityDecision {
@@ -280,7 +324,7 @@ export interface EligibilityDecision {
280
324
  /**
281
325
  * Epoch milliseconds at which this candidate's cooling window ends, when the
282
326
  * exclusion that produced this decision carries one. Absent for a healthy
283
- * candidate and for every reason with no deadline (`disabled`, `locked`,
327
+ * candidate and for every reason with no deadline (`disabled`,
284
328
  * `quota_exhausted` from spent windows or the usage floor). `plan()` takes the
285
329
  * minimum across the cooling set to report the earliest reset a client could
286
330
  * retry against.
@@ -328,7 +372,7 @@ export interface AdmissionController {
328
372
  * Translates the evaluator's internal reason codes into operator English.
329
373
  *
330
374
  * These names are the vocabulary the routing layers use with each other —
331
- * `model_cooldown`, `cooldown_hard`, `locked` — not something to print into the
375
+ * `model_cooldown`, `cooldown_hard` — not something to print into the
332
376
  * public message, where the client would read an enum instead of an
333
377
  * explanation. The codes stay in the envelope's `reasons` field for anyone who
334
378
  * wants them; the sentence carries what they mean.
@@ -342,9 +386,9 @@ const ELIGIBILITY_REASON_LABELS: Readonly<Record<string, string>> = {
342
386
  cooldown_hard: "every account is in a hard cooldown",
343
387
  cooldown: "accounts are cooling down",
344
388
  disabled: "all accounts are disabled",
345
- locked: "all accounts are locked by another request",
346
389
  credit_floor_reached: "credit or quota is exhausted",
347
390
  quota_exhausted: "every account has an exhausted quota window or is below its configured usage floor",
391
+ capability_unsupported: "the accounts serving this model do not support the request's capabilities (e.g. images, tools, or reasoning)",
348
392
  };
349
393
 
350
394
  function explainEligibilityReasons(reasons: readonly string[]): string {
@@ -358,7 +402,7 @@ export function ambiguousModelError(bare: string, owners: string[]): GatewayErro
358
402
  return new GatewayError(
359
403
  "ambiguous_model",
360
404
  400,
361
- `'${bare}' matches more than one provider (${owners.join(", ")}). Qualify it as '${owners[0]}/${bare}', or add an alias to pick one.`,
405
+ `'${bare}' matches more than one provider (${owners.join(", ")}). Qualify it as '${owners[0]}/${bare}', or add an alias to pick one. Too popular for its own good — qualify it 🧩`,
362
406
  {
363
407
  model: bare,
364
408
  owners,
@@ -380,7 +424,7 @@ export function modelNotFoundError(requested: string, resolved: readonly string[
380
424
  404,
381
425
  `'${requested}' is not routed${
382
426
  targets.length > 0 ? ` — it resolved to ${hint}, which no account serves` : ""
383
- }. Enable the model on an account, or point the alias at one that exists.`,
427
+ } (request {requestId}). Enable the model on an account, or point the alias at one that exists. Try another model — this one's off the menu 📚`,
384
428
  {
385
429
  model: requested,
386
430
  ...(targets.length > 0 ? { resolved_models: targets } : {}),
@@ -388,15 +432,6 @@ export function modelNotFoundError(requested: string, resolved: readonly string[
388
432
  );
389
433
  }
390
434
 
391
- export function capabilityUnsupportedError(capability: string): GatewayError {
392
- return new GatewayError(
393
- "capability_unsupported",
394
- 400,
395
- `No account serving this model supports ${capability}. Drop the ${capability} part of the request, or route to a model that accepts it.`,
396
- { capability },
397
- );
398
- }
399
-
400
435
  /**
401
436
  * Every candidate in the plan refused admission: each account is already
402
437
  * serving its configured max-inflight.
@@ -424,8 +459,8 @@ export function capacityExhaustedError(context?: {
424
459
  "provider_capacity_exhausted",
425
460
  429,
426
461
  onlyOne
427
- ? "The only account routing this model is at its concurrency limit. Retry shortly; if it persists, raise the provider's max-inflight or add another account."
428
- : "Every account is at its concurrency limit. Retry shortly; if it persists, raise the provider's max-inflight or add an account.",
462
+ ? "The only account routing this model is at its concurrency limit (request {requestId}). Retry shortly; if it persists, raise the provider's max-inflight or add another account. Even the machine needs a rest — chill 🦋"
463
+ : "Every account is at its concurrency limit (request {requestId}). Retry shortly; if it persists, raise the provider's max-inflight or add an account. Even the machine needs a rest — chill 🦋",
429
464
  {
430
465
  ...(context?.providerId !== undefined ? { providerId: context.providerId } : {}),
431
466
  ...(context?.model !== undefined ? { model: context.model } : {}),
@@ -439,7 +474,7 @@ export function capacityExhaustedError(context?: {
439
474
 
440
475
  /**
441
476
  * The model exists and is routed, but every account serving it is currently
442
- * unusable (disabled / locked). This is transient and
477
+ * unusable (disabled). This is transient and
443
478
  * retry-able, so it must NOT surface as `model_not_found` — a 404 tells the
444
479
  * client to fix its request when the real fix is to wait or add capacity.
445
480
  *
@@ -453,10 +488,18 @@ export function accountsUnavailableError(
453
488
  reasons: readonly string[],
454
489
  ): GatewayError {
455
490
  const distinct = [...new Set(reasons)].sort();
491
+ const capabilityBlocked = distinct.includes("capability_unsupported");
492
+ // When the blocker is a capability (the model exists and accounts exist, but
493
+ // none can carry the request's images/tools/reasoning), telling the client to
494
+ // "add an account" or wait out a coffee break is misleading — more accounts
495
+ // cannot conjure a capability the model itself lacks. Name the real fix.
496
+ const hint = capabilityBlocked
497
+ ? "Request a model that supports this capability (or a vision-capable model for images), or drop the unsupported part of the request. The model is the limit, not the pool 🧰"
498
+ : "Add an account, or wait for an operator to re-enable one. Every account is on a coffee break — try later ☕️";
456
499
  return new GatewayError(
457
500
  "accounts_unavailable",
458
501
  503,
459
- `No account in the pool can serve '${requested}' right now (${explainEligibilityReasons(distinct)}). Add an account, or wait for an operator to re-enable one.`,
502
+ `No account in the pool can serve '${requested}' right now (request {requestId}) (${explainEligibilityReasons(distinct)}). ${hint}`,
460
503
  { model: requested, reasons: distinct, candidate_count: reasons.length, gateway_hint: "no_healthy_account" },
461
504
  );
462
505
  }
@@ -492,7 +535,7 @@ export function accountsRateLimitedError(
492
535
  return new GatewayError(
493
536
  "accounts_rate_limited",
494
537
  429,
495
- `Every account serving '${requested}'${routeNote} is cooling down after hitting upstream limits. The request is fine — retry once the cooldown resets.`,
538
+ `Every account serving '${requested}'${routeNote} is cooling down after hitting upstream limits (request {requestId}). The request is fine — retry once the cooldown resets. They're resting up 🌸`,
496
539
  {
497
540
  model: requested,
498
541
  routed_model: routed,
@@ -536,42 +579,83 @@ export class InMemoryRouteSnapshotService implements RouteSnapshotService {
536
579
  * only populate the cache if no mutation happened while it ran.
537
580
  */
538
581
  private building: { revision: RoutingRevision; promise: Promise<RouteSnapshot> } | undefined;
539
-
540
- constructor(private readonly builder: SnapshotBuilder) {
582
+ /**
583
+ * Debounced background-rebuild timer (G-02). One invalidate schedules a
584
+ * rebuild; a burst of invalidates within `rebuildDebounceMs` coalesces into
585
+ * the single latest-revision rebuild. The timer is cleared on `stop()`.
586
+ */
587
+ private rebuildTimer: ReturnType<typeof setTimeout> | undefined;
588
+ private stopped = false;
589
+
590
+ constructor(
591
+ private readonly builder: SnapshotBuilder,
592
+ options: { readonly rebuildDebounceMs?: number } = {},
593
+ ) {
594
+ this.rebuildDebounceMs = options.rebuildDebounceMs ?? 500;
541
595
  // Peers' mutations arrive once per process, not per instance: the handler
542
- // drops this instance's copy, which is the only one there is.
596
+ // schedules a background rebuild, which is the only one there is.
543
597
  this.unsubscribe = subscribeRoutingInvalidation(() => {
544
598
  this.revision += 1;
545
- this.snapshot = undefined;
599
+ this.scheduleRebuild();
546
600
  });
547
601
  }
548
602
 
549
- /** Ends the peer-invalidation subscription. */
603
+ /** Ends the peer-invalidation subscription and clears the rebuild timer. */
550
604
  readonly unsubscribe: () => void;
551
605
 
552
- async getSnapshot(): Promise<RouteSnapshot> {
553
- if (this.snapshot) return this.snapshot;
554
- if (this.building && this.building.revision === this.revision) return this.building.promise;
606
+ /** Wall-clock debounce for coalescing invalidate bursts. */
607
+ private readonly rebuildDebounceMs: number;
608
+
609
+ /**
610
+ * Schedules a background rebuild at the current revision. Debounced: while a
611
+ * timer is pending, further invalidates just bump the revision and the
612
+ * pending rebuild picks up the latest. Single-flight by construction — the
613
+ * rebuild itself reuses {@link building}, so concurrent callers share one
614
+ * promise for the same revision.
615
+ */
616
+ private scheduleRebuild(): void {
617
+ if (this.stopped || this.rebuildTimer !== undefined) return;
618
+ this.rebuildTimer = setTimeout(() => {
619
+ this.rebuildTimer = undefined;
620
+ void this.rebuildNow().catch((error: unknown) => {
621
+ // A background rebuild failure is a soft miss: requests served the
622
+ // last-completed snapshot, and the next invalidation retries.
623
+ log.warn("[route-snapshot] background rebuild failed", error as Error);
624
+ });
625
+ }, this.rebuildDebounceMs);
626
+ if (typeof this.rebuildTimer.unref === "function") this.rebuildTimer.unref();
627
+ }
628
+
629
+ /** Starts (or joins) the build for the current revision. */
630
+ private rebuildNow(): Promise<RouteSnapshot> {
555
631
  const revision = this.revision;
632
+ if (this.building && this.building.revision === revision) return this.building.promise;
556
633
  const promise = this.build(revision);
557
634
  this.building = { revision, promise };
558
- try {
559
- return await promise;
560
- } finally {
635
+ void promise.finally(() => {
561
636
  // Only the build that is still current may clear the slot; a build
562
637
  // started after a mutation must not be dropped by an older one finishing.
563
638
  if (this.building?.promise === promise) this.building = undefined;
564
- }
639
+ });
640
+ return promise;
565
641
  }
566
642
 
643
+ async getSnapshot(): Promise<RouteSnapshot> {
644
+ // G-02: serve the last-completed snapshot while a rebuild runs, so the
645
+ // page-open request path never pays the catalog build. Only a cold start
646
+ // (no snapshot at all) falls through to a synchronous build here.
647
+ if (this.snapshot) return this.snapshot;
648
+ return this.rebuildNow();
649
+ }
567
650
 
568
651
  async invalidate(): Promise<RoutingRevision> {
569
652
  this.revision += 1;
570
- this.snapshot = undefined;
571
- // Lazy: the next getSnapshot() rebuilds. Dashboard mutations only need
572
- // the new revision token — they must not pay seven Postgres round-trips
573
- // inside the mutating request while holding no pool pressure budget.
574
- //
653
+ // G-02: trigger a BACKGROUND rebuild immediately instead of clearing the
654
+ // snapshot and making the next request pay. The rebuild is debounced so a
655
+ // burst of invalidates (a recovery sweep, a cascade of console writes)
656
+ // coalesces into one rebuild, and it never blocks the mutating request —
657
+ // a console write does not wait on its own rebuild here.
658
+ this.scheduleRebuild();
575
659
  // Broadcast, because `reusePort` runs several gateway processes and each
576
660
  // holds its own snapshot: a console write handled by one of them left the
577
661
  // others serving pre-write routing until something else invalidated them.
@@ -581,6 +665,15 @@ export class InMemoryRouteSnapshotService implements RouteSnapshotService {
581
665
  return this.revision;
582
666
  }
583
667
 
668
+ /** Stops background rebuilds. Test/teardown helper. */
669
+ stop(): void {
670
+ this.stopped = true;
671
+ if (this.rebuildTimer !== undefined) {
672
+ clearTimeout(this.rebuildTimer);
673
+ this.rebuildTimer = undefined;
674
+ }
675
+ }
676
+
584
677
  private async build(revision: RoutingRevision): Promise<RouteSnapshot> {
585
678
  const built = await this.builder();
586
679
  const providerRouting: ProviderRoutingMap | undefined = built.providerRouting
@@ -12,6 +12,7 @@ import {
12
12
  type ProviderRoutingSetting,
13
13
  type Reservation,
14
14
  type RouteCandidate,
15
+ type RoutedCandidate,
15
16
  type RoutePlan,
16
17
  type EligibilityDecision,
17
18
  type RouteContinuationAffinity,
@@ -34,22 +35,100 @@ import {
34
35
  } from "./candidate-index";
35
36
  import { totalRemainingCredit, totalRemainingPercent, type ProviderQuotaWindow } from "../../providers/quota/quota-contracts";
36
37
  import { getCachedQuotaEntry, quotaLens } from "../../providers/quota/cache";
38
+ import { readQuotaSnapshot } from "../../providers/quota/snapshot";
39
+ import type { CartethyiaDatabase } from "../../persistence/db-handle";
37
40
  import type { RedisClient } from "../../persistence/redis";
38
41
  import { sharedClaudeSlowLanes } from "../../providers/integrations/claude/claude-slow-lane";
39
42
 
43
+ /**
44
+ * Filters quota windows to those relevant to the candidate model.
45
+ * Providers like Antigravity report separate quotas for distinct model families
46
+ * (Gemini vs Claude/GPT), and Grok reports per-product buckets.
47
+ * Evaluating a Claude exhaustion against a Gemini request would incorrectly
48
+ * mark the account exhausted for Gemini when Gemini capacity is still available.
49
+ */
50
+ export function filterCandidateQuotaWindows(
51
+ windows: readonly ProviderQuotaWindow[],
52
+ candidate: { readonly provider_id?: string; readonly model_id?: string },
53
+ ): readonly ProviderQuotaWindow[] {
54
+ if (windows.length === 0) return windows;
55
+ const providerId = candidate.provider_id?.toLowerCase() ?? "";
56
+ const modelId = candidate.model_id?.toLowerCase() ?? "";
57
+ windows = windows.filter((window) => window.modelIds === undefined || window.modelIds.includes(modelId));
58
+
59
+ if (providerId === "antigravity") {
60
+ const isGemini = /gemini|tab_flash/i.test(modelId);
61
+ const isClaude = /claude|opus|sonnet/i.test(modelId);
62
+ if (isGemini) {
63
+ return windows.filter((w) => {
64
+ const text = `${w.kind} ${w.label}`.toLowerCase();
65
+ if (text.includes("gemini") || text.includes("google")) return true;
66
+ return !text.includes("claude") && !text.includes("gpt");
67
+ });
68
+ }
69
+ if (isClaude) {
70
+ return windows.filter((w) => {
71
+ const text = `${w.kind} ${w.label}`.toLowerCase();
72
+ if (text.includes("claude") || text.includes("gpt")) return true;
73
+ return !text.includes("gemini") && !text.includes("google");
74
+ });
75
+ }
76
+ }
77
+
78
+ if (providerId === "grok") {
79
+ return windows.filter((w) => {
80
+ if (w.kind.startsWith("product:")) {
81
+ const product = w.kind.split(":")[1]?.toLowerCase();
82
+ return product ? modelId.includes(product) : true;
83
+ }
84
+ return true;
85
+ });
86
+ }
87
+
88
+ return windows;
89
+ }
90
+
40
91
  /**
41
92
  * True when the account's remaining allowance is at or below its provider's
42
93
  * floor. Credit mode sums the absolute remaining credit; percent mode uses the
43
94
  * aggregate credit percentage and the lowest independent percentage window.
44
95
  * No reading means unknown, which is not below the floor.
45
96
  */
46
- function belowUsageFloor(state: {
47
- readonly usage_floor?: number;
48
- readonly usage_floor_unit?: "credit" | "percent";
49
- readonly quota_windows?: readonly unknown[] | undefined;
50
- }): boolean {
51
- const windows = state.quota_windows as readonly ProviderQuotaWindow[] | undefined;
52
- if (windows === undefined || windows.length === 0) return false;
97
+ function belowUsageFloor(state: RoutedCandidate): boolean {
98
+ const rawWindows = state.quota_windows;
99
+ // A successful Start Plan balance (`quota_entitlements_known`) authoritatively
100
+ // lists its model grants: when the provider is zcode-start and no window
101
+ // matches the requested model, the account is below the floor even with an
102
+ // empty/absent window list — there is no grant to fund the model.
103
+ if (state.provider_id === "zcode-start" && state.quota_entitlements_known === true) {
104
+ if (rawWindows === undefined || rawWindows.length === 0) return true;
105
+ const knownGrants = rawWindows.some((window) => window.modelIds !== undefined);
106
+ if (!knownGrants) return true;
107
+ }
108
+ if (rawWindows === undefined || rawWindows.length === 0) return false;
109
+ const relevant = filterCandidateQuotaWindows(rawWindows, state);
110
+ // Model-scoped grants (Start Plan) are checked separately: a grant that has
111
+ // not started, has expired, or is exhausted cannot fund the requested model,
112
+ // so it must not count as credit.
113
+ const now = Date.now();
114
+ const grants = relevant.filter((window) => window.modelIds !== undefined);
115
+ if (state.provider_id === "zcode-start" && grants.length === 0 && rawWindows.some((window) => window.modelIds !== undefined)) {
116
+ return true;
117
+ }
118
+ if (
119
+ grants.length > 0 &&
120
+ !grants.some((window) => {
121
+ if (window.startsAt && Date.parse(window.startsAt) > now) return false;
122
+ if (window.resetsAt && Date.parse(window.resetsAt) <= now) return false;
123
+ if (typeof window.remaining === "number" && window.remaining <= 0) return false;
124
+ if (typeof window.remainingPercent === "number" && window.remainingPercent <= 0) return false;
125
+ const remaining = state.usage_floor_unit === "credit" ? window.remaining : window.remainingPercent;
126
+ return state.usage_floor === undefined || remaining == null || remaining > state.usage_floor;
127
+ })
128
+ ) {
129
+ return true;
130
+ }
131
+ const windows = relevant.filter((window) => window.modelIds === undefined);
53
132
  const credit = totalRemainingCredit(windows);
54
133
  const percent = totalRemainingPercent(windows);
55
134
  const exhaustedIndependentWindow = windows.some(
@@ -67,6 +146,21 @@ function belowUsageFloor(state: {
67
146
  return remaining !== null && remaining <= floor;
68
147
  }
69
148
 
149
+ /**
150
+ * Routing priority for an account's remaining allowance when two candidates sit
151
+ * in the same rank group: positive remaining credit first, then unknown, then
152
+ * known-zero last. Free-tier models skip the preference entirely — a free model
153
+ * routes to any account, as the provider's own directory stated it is free.
154
+ */
155
+ function creditPriority(candidate: RoutedCandidate): number {
156
+ if (candidate.free_tier === true) return 0;
157
+ const windows = candidate.quota_windows;
158
+ if (windows === undefined || windows.length === 0) return 0;
159
+ const credit = totalRemainingCredit(windows);
160
+ if (credit === null) return 0;
161
+ return credit > 0 ? 1 : -1;
162
+ }
163
+
70
164
  /**
71
165
  * Normalizes a requested model name before alias lookup. Coding-agent CLIs
72
166
  * version their model ids with variant suffixes (`claude-opus-5[1m]`,
@@ -233,10 +327,17 @@ function spreadDeadline(cooldownUntil: number | undefined): { cooldown_until?: n
233
327
  function earliestWindowResetMs(
234
328
  windows: readonly ProviderQuotaWindow[] | undefined,
235
329
  nowMs: number,
330
+ candidate: RouteCandidate,
236
331
  ): number | undefined {
237
332
  if (windows === undefined) return undefined;
333
+ windows = filterCandidateQuotaWindows(windows, candidate);
238
334
  let earliest: number | undefined;
239
335
  for (const window of windows) {
336
+ if (window.startsAt && Date.parse(window.startsAt) > nowMs) {
337
+ const at = Date.parse(window.startsAt);
338
+ if (earliest === undefined || at < earliest) earliest = at;
339
+ continue;
340
+ }
240
341
  if (typeof window.resetsAt !== "string" || window.resetsAt.length === 0) continue;
241
342
  const at = Date.parse(window.resetsAt);
242
343
  if (!Number.isFinite(at) || at <= nowMs) continue;
@@ -249,7 +350,7 @@ function earliestWindowResetMs(
249
350
  * The earliest instant any cooling candidate recovers, in epoch ms.
250
351
  *
251
352
  * `undefined` when no decision carries a deadline — which is the honest answer
252
- * for a set whose exclusions have no reset (`disabled`, `locked`), and keeps
353
+ * for a set whose exclusions have no reset (`disabled`), and keeps
253
354
  * `accountsRateLimitedError` from reporting a number nobody measured. The
254
355
  * minimum, not the maximum: the client can be served as soon as the *first*
255
356
  * account comes back, so quoting the slowest would hold it off longer than it
@@ -268,36 +369,22 @@ function earliestResetMs(
268
369
  }
269
370
 
270
371
  export class EligibilityEvaluator {
271
- evaluate(candidate: RouteCandidate): EligibilityDecision {
272
- const state = candidate as RouteCandidate & {
273
- readonly health_status?: string;
274
- readonly cooldown_kind?: "hard" | "soft";
275
- readonly cooldown_until?: number;
276
- readonly account_locked?: boolean;
277
- readonly locked?: boolean;
278
- readonly usage_floor?: number;
279
- readonly usage_floor_unit?: "credit" | "percent";
280
- readonly quota_windows?: readonly ProviderQuotaWindow[] | undefined;
281
- /** Slow-lane stamp applied at plan time; see `applySlowLane`. */
282
- readonly lane?: "slow";
283
- };
284
- if (state.account_locked || state.locked)
285
- return { eligible: false, reason: "locked", candidate };
286
- if (state.health_status === "disabled")
372
+ evaluate(candidate: RoutedCandidate): EligibilityDecision {
373
+ if (candidate.health_status === "disabled")
287
374
  return { eligible: false, reason: "disabled", candidate };
288
- if (state.health_status === "model_cooldown")
289
- return { eligible: false, reason: "model_cooldown", candidate, ...spreadDeadline(state.cooldown_until) };
290
- if (state.health_status === "cooldown" && state.cooldown_kind === "hard")
291
- return { eligible: false, reason: "cooldown_hard", candidate, ...spreadDeadline(state.cooldown_until) };
375
+ if (candidate.health_status === "model_cooldown")
376
+ return { eligible: false, reason: "model_cooldown", candidate, ...spreadDeadline(candidate.cooldown_until) };
377
+ if (candidate.health_status === "cooldown" && candidate.cooldown_kind === "hard")
378
+ return { eligible: false, reason: "cooldown_hard", candidate, ...spreadDeadline(candidate.cooldown_until) };
292
379
  // The floor is checked here, not after selection: an account below its
293
380
  // floor must not win the route. An account with no reading is left
294
381
  // eligible — a missing quota fetch is not proof the allowance is gone.
295
- if (belowUsageFloor(state))
382
+ if (belowUsageFloor(candidate))
296
383
  return {
297
384
  eligible: false,
298
385
  reason: "quota_exhausted",
299
386
  candidate,
300
- ...spreadDeadline(earliestWindowResetMs(state.quota_windows, Date.now())),
387
+ ...spreadDeadline(earliestWindowResetMs(candidate.quota_windows, Date.now(), candidate)),
301
388
  };
302
389
  // A slow lane is orthogonal to cooldown, never a member of it: the account
303
390
  // still serves on spare capacity when the request carries the slow usage
@@ -308,10 +395,10 @@ export class EligibilityEvaluator {
308
395
  // exclusion, not that it overrides one. An account with no usable allowance
309
396
  // is not rescued by the lane — the lane is a latency budget against spare
310
397
  // capacity, and an account below its floor has none.
311
- if (state.lane === "slow")
398
+ if (candidate.lane === "slow")
312
399
  return { eligible: true, reason: "slow", candidate };
313
- if (state.health_status === "cooldown")
314
- return { eligible: true, reason: "cooldown", candidate, ...spreadDeadline(state.cooldown_until) };
400
+ if (candidate.health_status === "cooldown")
401
+ return { eligible: true, reason: "cooldown", candidate, ...spreadDeadline(candidate.cooldown_until) };
315
402
  return { eligible: true, reason: "healthy", candidate };
316
403
  }
317
404
 
@@ -332,6 +419,7 @@ export class RoutingEngine {
332
419
  constructor(
333
420
  private readonly admission: AdmissionController = new InMemoryAdmissionController(),
334
421
  private readonly redis?: RedisClient,
422
+ private readonly db?: CartethyiaDatabase,
335
423
  ) {}
336
424
 
337
425
  private getRoundRobin(key: string): RoundRobinState {
@@ -467,13 +555,32 @@ export class RoutingEngine {
467
555
  accountId,
468
556
  this.redis,
469
557
  ).catch(() => undefined);
470
- const quotaWindows = cached?.quota.windows ?? [];
558
+ // The live cache is authoritative when warm; when it is cold (e.g. right
559
+ // after a restart, before the sweep re-reads accounts), fall back to the
560
+ // persisted quota snapshot — the durable source of truth — so routing
561
+ // still prefers accounts that actually have remaining credit. The snapshot
562
+ // is never fresher than the cache, but a stale balance beats an unknown
563
+ // one for ordering, and the sweep refreshes it on schedule.
564
+ let quota = cached?.quota;
565
+ if (quota === undefined && this.db !== undefined) {
566
+ const stored = await readQuotaSnapshot(this.db, accountId).catch(() => null);
567
+ quota = stored?.quota;
568
+ }
569
+ const rawWindows = quota?.windows;
570
+ // The cache is authoritative when it has a reading; when it does not,
571
+ // keep whatever the candidate already carried (the credit ordering reads
572
+ // these windows, and a miss must not erase a stamped value).
573
+ const quotaWindows =
574
+ rawWindows === undefined
575
+ ? undefined
576
+ : candidate.provider_id === "zcode-start" ? rawWindows : filterCandidateQuotaWindows(rawWindows, candidate);
471
577
  return {
472
578
  ...candidate,
473
579
  ...(setting?.usageFloor == null
474
580
  ? {}
475
581
  : { usage_floor: setting.usageFloor, usage_floor_unit: setting.usageFloorUnit }),
476
- quota_windows: quotaWindows,
582
+ ...(quotaWindows === undefined ? {} : { quota_windows: quotaWindows }),
583
+ ...(quota?.source === "zcode-start" && quota.error === null ? { quota_entitlements_known: true } : {}),
477
584
  };
478
585
  }
479
586
 
@@ -624,7 +731,7 @@ export class RoutingEngine {
624
731
  throw new GatewayError(
625
732
  "invalid_request",
626
733
  400,
627
- "This tool result belongs to a Cursor account that is no longer routable. Reconnect the account, or resend the complete conversation.",
734
+ "This tool result belongs to a Cursor account that is no longer routable (request {requestId}). Reconnect the account, or resend the complete conversation. The session went cold — reconnect or resend 🔌",
628
735
  {
629
736
  providerId: affinity.providerId,
630
737
  accountId: affinity.providerAccountId,
@@ -641,6 +748,11 @@ export class RoutingEngine {
641
748
  const pinnedDecisions = pinned.map((candidate) => this.eligibility.evaluate(candidate));
642
749
  const blocked = pinnedDecisions.find((decision) => !decision.eligible);
643
750
  if (blocked !== undefined) {
751
+ // A rebaseable continuation (complete conversation replay) is not owed
752
+ // to this account: the recorded exchange may travel as history to a
753
+ // fresh Run on an eligible account. Cooling/blocked pinned account must
754
+ // not turn that into a hard refusal — fall through to normal routing.
755
+ if (affinity.rebaseable) return null;
644
756
  const reason = blocked.reason;
645
757
  if (
646
758
  reason === "cooldown_hard" ||
@@ -652,7 +764,7 @@ export class RoutingEngine {
652
764
  throw new GatewayError(
653
765
  "invalid_request",
654
766
  400,
655
- "This tool result belongs to an account that is no longer routable. Reconnect the account, or resend the complete conversation.",
767
+ "This tool result belongs to an account that is no longer routable (request {requestId}). Reconnect the account, or resend the complete conversation. The session went cold — reconnect or resend 🔌",
656
768
  {
657
769
  providerId: affinity.providerId,
658
770
  accountId: affinity.providerAccountId,
@@ -810,11 +922,14 @@ export class RoutingEngine {
810
922
  );
811
923
  }
812
924
 
813
- // Single sort by rank (fresh → slow → cooling → saturated), stable.
925
+ // Single sort by rank (fresh → slow → cooling → saturated), stable. Within
926
+ // the same rank, accounts with remaining credit come first unless the model
927
+ // is free tier, which routes to any account.
814
928
  eligible = [...eligible].sort((left, right) => {
815
929
  const leftRank = decisions.get(admissionKey(left))?.rank ?? ROUTING_RANK.FRESH;
816
930
  const rightRank = decisions.get(admissionKey(right))?.rank ?? ROUTING_RANK.FRESH;
817
- return leftRank - rightRank;
931
+ if (leftRank !== rightRank) return leftRank - rightRank;
932
+ return creditPriority(right) - creditPriority(left);
818
933
  });
819
934
 
820
935
  // Provider routing rotates accounts within the same rank group only; it can