cartethyia 2.7.4 → 2.7.6

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 (595) hide show
  1. package/.env.example +5 -0
  2. package/README.md +18 -6
  3. package/dist/dashboard/assets/BannedPage-B1N603cM.js +1 -0
  4. package/dist/dashboard/assets/BannedPage-B1N603cM.js.gz +0 -0
  5. package/dist/dashboard/assets/CliToolDetailPage-9q4_4NoK.js +3 -0
  6. package/dist/dashboard/assets/CliToolDetailPage-9q4_4NoK.js.gz +0 -0
  7. package/dist/dashboard/assets/CliToolsPage-DwshGtK-.js +1 -0
  8. package/dist/dashboard/assets/CliToolsPage-DwshGtK-.js.gz +0 -0
  9. package/dist/dashboard/assets/CombosPage-D762MIkp.js +1 -0
  10. package/dist/dashboard/assets/CombosPage-D762MIkp.js.gz +0 -0
  11. package/dist/dashboard/assets/ConsoleLogPage-KRYjHZsX.js +1 -0
  12. package/dist/dashboard/assets/ConsoleLogPage-KRYjHZsX.js.gz +0 -0
  13. package/dist/dashboard/assets/CustomizationPage-DXsxk5uM.js +1 -0
  14. package/dist/dashboard/assets/CustomizationPage-DXsxk5uM.js.gz +0 -0
  15. package/dist/dashboard/assets/HealthEventsModal-BEjAjD4F.js +1 -0
  16. package/dist/dashboard/assets/HealthEventsModal-BEjAjD4F.js.gz +0 -0
  17. package/dist/dashboard/assets/InjectSystemPromptPanel-d5v3K_Yc.js +899 -0
  18. package/dist/dashboard/assets/InjectSystemPromptPanel-d5v3K_Yc.js.gz +0 -0
  19. package/dist/dashboard/assets/LoginPage-Ccv4tjNS.js +1 -0
  20. package/dist/dashboard/assets/LoginPage-Ccv4tjNS.js.gz +0 -0
  21. package/dist/dashboard/assets/{ModelPicker-BMS2t8BA.js → ModelPicker-C6WlpU5E.js} +1 -1
  22. package/dist/dashboard/assets/ModelPicker-C6WlpU5E.js.gz +0 -0
  23. package/dist/dashboard/assets/OverviewPage-DN9fYNWA.js +1 -0
  24. package/dist/dashboard/assets/OverviewPage-DN9fYNWA.js.gz +0 -0
  25. package/dist/dashboard/assets/ProviderDetailPage-BY0ieUgz.js +5 -0
  26. package/dist/dashboard/assets/ProviderDetailPage-BY0ieUgz.js.gz +0 -0
  27. package/dist/dashboard/assets/ProviderIcon-jjqXJDA3.js +1 -0
  28. package/dist/dashboard/assets/ProviderIcon-jjqXJDA3.js.gz +0 -0
  29. package/dist/dashboard/assets/ProvidersPage-DNsbfea2.js +1 -0
  30. package/dist/dashboard/assets/ProvidersPage-DNsbfea2.js.gz +0 -0
  31. package/dist/dashboard/assets/{ProxyPage-lsrx7NYg.js → ProxyPage-DxjvUzSD.js} +1 -1
  32. package/dist/dashboard/assets/ProxyPage-DxjvUzSD.js.gz +0 -0
  33. package/dist/dashboard/assets/QuotaPage-OaHScjpX.js +1 -0
  34. package/dist/dashboard/assets/QuotaPage-OaHScjpX.js.gz +0 -0
  35. package/dist/dashboard/assets/SettingsPage-BW4AKj2H.js +1 -0
  36. package/dist/dashboard/assets/SettingsPage-BW4AKj2H.js.gz +0 -0
  37. package/dist/dashboard/assets/SetupPage-44BZrxFC.js +1 -0
  38. package/dist/dashboard/assets/SetupPage-44BZrxFC.js.gz +0 -0
  39. package/dist/dashboard/assets/{StudioPage-Cq5tW7hW.js → StudioPage-DRPavCdU.js} +4 -4
  40. package/dist/dashboard/assets/StudioPage-DRPavCdU.js.gz +0 -0
  41. package/dist/dashboard/assets/ToolIcon-CfeLA-Z_.js +1 -0
  42. package/dist/dashboard/assets/ToolIcon-CfeLA-Z_.js.gz +0 -0
  43. package/dist/dashboard/assets/UsagePage-B7M7LvMM.js +56 -0
  44. package/dist/dashboard/assets/UsagePage-B7M7LvMM.js.gz +0 -0
  45. package/dist/dashboard/assets/api-keys-KvX83mRe.js +1 -0
  46. package/dist/dashboard/assets/api-keys-KvX83mRe.js.gz +0 -0
  47. package/dist/dashboard/assets/app-DRnQLhsc.js +9 -0
  48. package/dist/dashboard/assets/app-DRnQLhsc.js.gz +0 -0
  49. package/dist/dashboard/assets/app-mweRcsKp.css +2 -0
  50. package/dist/dashboard/assets/app-mweRcsKp.css.gz +0 -0
  51. package/dist/dashboard/assets/assert-CidQVoNo.js +1 -0
  52. package/dist/dashboard/assets/assert-CidQVoNo.js.gz +0 -0
  53. package/dist/dashboard/assets/{badge-D9CS6dZk.js → badge-BaX2GTrn.js} +1 -1
  54. package/dist/dashboard/assets/badge-BaX2GTrn.js.gz +0 -0
  55. package/dist/dashboard/assets/{banner-DympBNq4.js → banner-DFH4Bkte.js} +1 -1
  56. package/dist/dashboard/assets/banner-DFH4Bkte.js.gz +0 -0
  57. package/dist/dashboard/assets/brand/login_register.jpg +0 -0
  58. package/dist/dashboard/assets/brand/login_register_initial.jpg +0 -0
  59. package/dist/dashboard/assets/brand/og_bansos.jpg +0 -0
  60. package/dist/dashboard/assets/brand/og_image.jpg +0 -0
  61. package/dist/dashboard/assets/{card-ibRWU_uT.js → card-CxDw0lK6.js} +1 -1
  62. package/dist/dashboard/assets/card-CxDw0lK6.js.gz +0 -0
  63. package/dist/dashboard/assets/chevron-left-Bf6CaTSJ.js +1 -0
  64. package/dist/dashboard/assets/chevron-left-Bf6CaTSJ.js.gz +0 -0
  65. package/dist/dashboard/assets/circle-question-mark-Cz4un8sH.js +1 -0
  66. package/dist/dashboard/assets/circle-question-mark-Cz4un8sH.js.gz +0 -0
  67. package/dist/dashboard/assets/cli-tools-Bwv5-zGZ.js +1 -0
  68. package/dist/dashboard/assets/cli-tools-Bwv5-zGZ.js.gz +0 -0
  69. package/dist/dashboard/assets/{clipboard-button-DJm-9pz7.js → clipboard-button-DfS1ntiq.js} +1 -1
  70. package/dist/dashboard/assets/clipboard-button-DfS1ntiq.js.gz +0 -0
  71. package/dist/dashboard/assets/dialog-BK-NRr5u.js +1 -0
  72. package/dist/dashboard/assets/dialog-BK-NRr5u.js.gz +0 -0
  73. package/dist/dashboard/assets/{drawer-CqGYeOrd.js → drawer-QqPul6sC.js} +1 -1
  74. package/dist/dashboard/assets/drawer-QqPul6sC.js.gz +0 -0
  75. package/dist/dashboard/assets/entry-BMlYJVFj.js +1 -0
  76. package/dist/dashboard/assets/entry-BMlYJVFj.js.gz +0 -0
  77. package/dist/dashboard/assets/entry-D6WvojeD.js +4 -0
  78. package/dist/dashboard/assets/entry-D6WvojeD.js.gz +0 -0
  79. package/dist/dashboard/assets/entry-Dmx7gBiN.js +1 -0
  80. package/dist/dashboard/assets/entry-Dmx7gBiN.js.gz +0 -0
  81. package/dist/dashboard/assets/{formatters-D-MTJxMr.js → formatters-u7JPB5BX.js} +1 -1
  82. package/dist/dashboard/assets/formatters-u7JPB5BX.js.gz +0 -0
  83. package/dist/dashboard/assets/house-h9AZlJuT.js +1 -0
  84. package/dist/dashboard/assets/house-h9AZlJuT.js.gz +0 -0
  85. package/dist/dashboard/assets/{index-DBNPOfhV.js → index-Ko4dLRnV.js} +2 -2
  86. package/dist/dashboard/assets/index-Ko4dLRnV.js.gz +0 -0
  87. package/dist/dashboard/assets/{inline-BT0zntEr.js → inline-CHJQwxLa.js} +1 -1
  88. package/dist/dashboard/assets/inline-CHJQwxLa.js.gz +0 -0
  89. package/dist/dashboard/assets/{input-Da2ZViWg.js → input-BlmlizVV.js} +1 -1
  90. package/dist/dashboard/assets/input-BlmlizVV.js.gz +0 -0
  91. package/dist/dashboard/assets/{layout-BXNeLw6M.js → layout-Bz93I-yE.js} +1 -1
  92. package/dist/dashboard/assets/layout-Bz93I-yE.js.gz +0 -0
  93. package/dist/dashboard/assets/{list-row-BycOrrIV.js → list-row-Ml16KCfu.js} +1 -1
  94. package/dist/dashboard/assets/list-row-Ml16KCfu.js.gz +0 -0
  95. package/dist/dashboard/assets/live-7_QpnYGu.js +1 -0
  96. package/dist/dashboard/assets/live-7_QpnYGu.js.gz +0 -0
  97. package/dist/dashboard/assets/{mutation-Rr0TIvcj.js → mutation-GU7XdEjz.js} +1 -1
  98. package/dist/dashboard/assets/mutation-GU7XdEjz.js.gz +0 -0
  99. package/dist/dashboard/assets/network-CuMDaTzT.js +1 -0
  100. package/dist/dashboard/assets/network-CuMDaTzT.js.gz +0 -0
  101. package/dist/dashboard/assets/numbers-CoqpaDMx.js +2 -0
  102. package/dist/dashboard/assets/numbers-CoqpaDMx.js.gz +0 -0
  103. package/dist/dashboard/assets/pagination-B5aOoweZ.js +1 -0
  104. package/dist/dashboard/assets/pagination-B5aOoweZ.js.gz +0 -0
  105. package/dist/dashboard/assets/providers/zai.webp +0 -0
  106. package/dist/dashboard/assets/providers/zcode.webp +0 -0
  107. package/dist/dashboard/assets/{query-keys-BloKtaXJ.js → query-keys-YSoQOUh6.js} +1 -1
  108. package/dist/dashboard/assets/{query-keys-BloKtaXJ.js.gz → query-keys-YSoQOUh6.js.gz} +0 -0
  109. package/dist/dashboard/assets/quota-DJ1QlAOq.js +1 -0
  110. package/dist/dashboard/assets/quota-DJ1QlAOq.js.gz +0 -0
  111. package/dist/dashboard/assets/routing-fnCsxgNO.js +1 -0
  112. package/dist/dashboard/assets/routing-fnCsxgNO.js.gz +0 -0
  113. package/dist/dashboard/assets/{select-BIYIBkMU.js → select-BHqpd-bg.js} +2 -2
  114. package/dist/dashboard/assets/select-BHqpd-bg.js.gz +0 -0
  115. package/dist/dashboard/assets/settings-BxslsErn.js +1 -0
  116. package/dist/dashboard/assets/settings-BxslsErn.js.gz +0 -0
  117. package/dist/dashboard/assets/shield-BMQNxORh.js +1 -0
  118. package/dist/dashboard/assets/shield-BMQNxORh.js.gz +0 -0
  119. package/dist/dashboard/assets/{stack-DfgnFDWY.js → stack-j7cXjgYX.js} +1 -1
  120. package/dist/dashboard/assets/stack-j7cXjgYX.js.gz +0 -0
  121. package/dist/dashboard/assets/{state-CkfdYWel.js → state-DHmEnLa2.js} +1 -1
  122. package/dist/dashboard/assets/state-DHmEnLa2.js.gz +0 -0
  123. package/dist/dashboard/assets/{switch-SYm237HG.js → switch-DmZcyTIC.js} +1 -1
  124. package/dist/dashboard/assets/switch-DmZcyTIC.js.gz +0 -0
  125. package/dist/dashboard/assets/{useMutation-BQqCRNIi.js → useMutation-CbCP4w1o.js} +1 -1
  126. package/dist/dashboard/assets/useMutation-CbCP4w1o.js.gz +0 -0
  127. package/dist/dashboard/console-backgrounds.jpg +0 -0
  128. package/dist/dashboard/console-backgrounds_initial.jpg +0 -0
  129. package/dist/dashboard/index.html +2 -2
  130. package/dist/dashboard/index.html.gz +0 -0
  131. package/dist/dashboard/precompressed-assets.json +60 -59
  132. package/migrations/full-postgres/0036_system_prompt_presets_refs.sql +10 -0
  133. package/migrations/full-postgres/0037_share_concurrency_scope.sql +3 -0
  134. package/migrations/full-postgres/0038_add_accounts_quota_snapshot.sql +7 -0
  135. package/migrations/full-postgres/0039_add_telemetry_provider_model_indexes.sql +13 -0
  136. package/migrations/full-postgres/0040_add_models_dialect.sql +3 -0
  137. package/migrations/full-postgres/0041_add_daily_checkin_ledger_account_fk.sql +7 -0
  138. package/migrations/full-postgres/0042_add_telemetry_degenerate_stop.sql +14 -0
  139. package/migrations/lite-sqlite3/0036_system_prompt_presets_refs.sql +14 -0
  140. package/migrations/lite-sqlite3/0037_share_concurrency_scope.sql +3 -0
  141. package/migrations/lite-sqlite3/0038_add_accounts_quota_snapshot.sql +4 -0
  142. package/migrations/lite-sqlite3/0039_add_telemetry_provider_model_indexes.sql +6 -0
  143. package/migrations/lite-sqlite3/0040_add_models_dialect.sql +3 -0
  144. package/migrations/lite-sqlite3/0041_add_daily_checkin_ledger_account_fk.sql +26 -0
  145. package/migrations/lite-sqlite3/0042_add_telemetry_degenerate_stop.sql +12 -0
  146. package/package.json +4 -6
  147. package/scripts/cli/update.ts +15 -41
  148. package/src/boot/app.ts +58 -2
  149. package/src/boot/dependencies.ts +794 -732
  150. package/src/boot/lifecycle.ts +11 -12
  151. package/src/config/cooldowns.ts +34 -55
  152. package/src/config/network.ts +1 -1
  153. package/src/config/package-paths.ts +1 -1
  154. package/src/config/providers.ts +19 -14
  155. package/src/config/resources.ts +0 -5
  156. package/src/config/runtime.ts +2 -5
  157. package/src/config/spec.ts +33 -6
  158. package/src/config/usage.ts +27 -0
  159. package/src/console/api-keys/ban-constants.ts +38 -0
  160. package/src/console/api-keys/contracts.ts +71 -62
  161. package/src/console/api-keys/routes.ts +56 -24
  162. package/src/console/auth/access.ts +4 -4
  163. package/src/console/auth/account-reset.ts +1 -1
  164. package/src/console/auth/audit-recorder.ts +1 -1
  165. package/src/console/auth/lockout.ts +7 -3
  166. package/src/console/auth/row-coercion.ts +57 -12
  167. package/src/console/auth/session-resolver.ts +1 -1
  168. package/src/console/auth/session-service.ts +8 -2
  169. package/src/console/auth/session.ts +37 -31
  170. package/src/console/auth/setup-service.ts +4 -3
  171. package/src/console/auth/user-store.ts +1 -1
  172. package/src/console/backup/9router-map.ts +0 -3
  173. package/src/console/backup/contracts.ts +9 -9
  174. package/src/console/backup/export-endpoint.ts +5 -6
  175. package/src/console/backup/nine-router.ts +1 -7
  176. package/src/console/backup/routes.ts +29 -33
  177. package/src/console/backup/service.ts +468 -468
  178. package/src/console/backup/store.ts +47 -29
  179. package/src/console/backup/validate.ts +11 -1
  180. package/src/console/cli-tools/contracts.ts +43 -299
  181. package/src/console/cli-tools/fs-ops.ts +29 -0
  182. package/src/console/cli-tools/injectors/claude.ts +3 -3
  183. package/src/console/cli-tools/injectors/codex.ts +181 -181
  184. package/src/console/cli-tools/injectors/driver.ts +150 -247
  185. package/src/console/cli-tools/injectors/kilo.ts +146 -21
  186. package/src/console/cli-tools/injectors/opencode.ts +176 -153
  187. package/src/console/cli-tools/routes.ts +7 -1
  188. package/src/console/cli-tools/service.ts +8 -7
  189. package/src/console/cli-tools/store.ts +2 -2
  190. package/src/console/console-router.ts +32 -0
  191. package/src/console/dashboard-assets.ts +9 -9
  192. package/src/console/domain-registration.ts +16 -2
  193. package/src/console/inferhub/client.ts +0 -7
  194. package/src/console/inferhub/key-store.ts +1 -1
  195. package/src/console/inferhub/normalize.ts +2 -2
  196. package/src/console/inferhub/routes.ts +1 -1
  197. package/src/console/inferhub/types.ts +1 -1
  198. package/src/console/observability/contracts.ts +7 -1
  199. package/src/console/observability/live.ts +31 -18
  200. package/src/console/observability/logs.ts +1 -1
  201. package/src/console/observability/sse.ts +62 -8
  202. package/src/console/observability/store.ts +23 -4
  203. package/src/console/providers/catalog/catalog-projections.ts +1 -1
  204. package/src/console/providers/catalog/contracts.ts +39 -0
  205. package/src/console/providers/catalog/model-operations.ts +1 -1
  206. package/src/console/providers/catalog/provider-operations.ts +95 -17
  207. package/src/console/providers/catalog/public-model-store.ts +0 -0
  208. package/src/console/providers/catalog/routes.ts +27 -3
  209. package/src/console/providers/catalog/store.ts +147 -8
  210. package/src/console/providers/detail/routes.ts +1 -1
  211. package/src/console/providers/oauth/callback-listener.ts +100 -18
  212. package/src/console/providers/oauth/routes.ts +92 -13
  213. package/src/console/quota/account-quota-global.ts +41 -3
  214. package/src/console/quota/account-quota-shared.ts +10 -3
  215. package/src/console/quota/account-quota-tenant.ts +259 -66
  216. package/src/console/quota/daily-growth-job.ts +308 -0
  217. package/src/console/quota/native-identity.ts +2 -2
  218. package/src/console/quota/quota-view.ts +1 -1
  219. package/src/console/remote-support/routes.ts +1 -1
  220. package/src/console/remote-support/service.ts +2 -2
  221. package/src/console/routing/model/contracts.ts +1 -1
  222. package/src/console/routing/pools/contracts.ts +2 -2
  223. package/src/console/routing/pools/import-parse.ts +3 -3
  224. package/src/console/routing/pools/relay-deploy.ts +3 -3
  225. package/src/console/settings/contracts.ts +29 -2
  226. package/src/console/settings/store.ts +20 -0
  227. package/src/console/share/share-router.ts +100 -32
  228. package/src/console/share/share-stats.ts +11 -13
  229. package/src/console/share/share-usage.ts +1 -1
  230. package/src/console/shared/errors.ts +2 -2
  231. package/src/console/shared/object.ts +18 -0
  232. package/src/console/shared/read-through-cache.ts +4 -4
  233. package/src/console/shared/transaction.ts +14 -0
  234. package/src/console/system-prompt/contracts.ts +89 -12
  235. package/src/console/system-prompt/routes.ts +98 -45
  236. package/src/console/updates/apply-update.ts +244 -0
  237. package/src/console/updates/routes.ts +252 -97
  238. package/src/main.ts +11 -5
  239. package/src/network/http2-bidi.ts +1 -1
  240. package/src/network/ip-utils.ts +298 -0
  241. package/src/network/outbound-fetch.ts +1 -1
  242. package/src/network/pool/agent.ts +7 -6
  243. package/src/network/pool/byte-accounting.ts +1 -1
  244. package/src/network/pool/resolver.ts +16 -12
  245. package/src/network/pool/selector.ts +150 -136
  246. package/src/network/pool-health-machine.ts +1 -1
  247. package/src/network/rate-limit-verdict.ts +1 -1
  248. package/src/network/retry.ts +1 -1
  249. package/src/network/ssrf.ts +20 -71
  250. package/src/observability/event-log.ts +9 -2
  251. package/src/observability/event-query.ts +61 -291
  252. package/src/observability/log-sanitize.ts +1 -1
  253. package/src/observability/metrics.ts +4 -4
  254. package/src/observability/payload-capture.ts +6 -5
  255. package/src/observability/payload-store.ts +1 -1
  256. package/src/observability/runtime-metrics.ts +1 -1
  257. package/src/observability/telemetry-buffer.ts +19 -3
  258. package/src/observability/telemetry-reconciliation.ts +3 -3
  259. package/src/observability/token-speed.ts +1 -1
  260. package/src/persistence/connection-url.ts +1 -1
  261. package/src/persistence/db-handle.ts +1 -1
  262. package/src/persistence/db-pg.ts +2 -2
  263. package/src/persistence/db-sqlite.ts +3 -3
  264. package/src/persistence/dialect.ts +5 -1
  265. package/src/persistence/readiness.ts +1 -1
  266. package/src/persistence/redis.ts +1 -1
  267. package/src/persistence/schema.ts +121 -40
  268. package/src/persistence/shared-cache.ts +2 -2
  269. package/src/persistence/sort-order.ts +1 -1
  270. package/src/persistence/stores/api-key-store.ts +29 -2
  271. package/src/persistence/stores/share-store.ts +20 -8
  272. package/src/persistence/system-prompt-store.ts +63 -18
  273. package/src/persistence/tenant-preferences.ts +1 -1
  274. package/src/persistence/ttl-cache.ts +3 -3
  275. package/src/persistence/usage-flush.ts +7 -2
  276. package/src/protocol/canonical-model.ts +1 -1
  277. package/src/protocol/codecs.ts +6 -10
  278. package/src/protocol/dsml.ts +8 -125
  279. package/src/protocol/image-clamp.ts +2 -2
  280. package/src/protocol/reasoning-text.ts +1 -1
  281. package/src/protocol/reasoning.ts +7 -52
  282. package/src/protocol/registry.ts +50 -15
  283. package/src/protocol/request/chat.ts +46 -19
  284. package/src/protocol/request/codex.ts +1 -1
  285. package/src/protocol/request/messages.ts +46 -14
  286. package/src/protocol/request/responses.ts +19 -10
  287. package/src/protocol/response/chat.ts +25 -10
  288. package/src/protocol/response/codex.ts +1 -1
  289. package/src/protocol/response/messages.ts +1 -1
  290. package/src/protocol/response/omp-tool-call-markup.ts +1 -1
  291. package/src/protocol/response/responses.ts +1 -1
  292. package/src/protocol/sse-decoder.ts +50 -1
  293. package/src/protocol/stop-reason.ts +2 -2
  294. package/src/protocol/think-split.ts +347 -0
  295. package/src/protocol/translation/attachment-integrity.ts +3 -3
  296. package/src/protocol/translation/cache-controls.ts +8 -20
  297. package/src/protocol/translation/capabilities.ts +6 -6
  298. package/src/protocol/translation/dialect/deepseek.ts +10 -0
  299. package/src/protocol/translation/dialect/inband.ts +39 -0
  300. package/src/protocol/translation/dialect/model-thinking.ts +89 -0
  301. package/src/protocol/translation/dialect/qwen.ts +114 -0
  302. package/src/protocol/translation/dialect/resolver.ts +83 -0
  303. package/src/protocol/translation/dialect/rules.ts +93 -0
  304. package/src/protocol/translation/dialect/thinking-ladder.ts +245 -0
  305. package/src/protocol/translation/dialect/types.ts +58 -0
  306. package/src/protocol/translation/quirks.ts +18 -4
  307. package/src/protocol/translation/thinking.ts +12 -252
  308. package/src/protocol/translation/tool-id.ts +3 -3
  309. package/src/protocol/translation/tool-repair.ts +1 -1
  310. package/src/protocol/upstream/post-json.ts +6 -1
  311. package/src/providers/adapters/compatible-adapter.ts +71 -4
  312. package/src/providers/authentication/jwt-validator.ts +0 -5
  313. package/src/providers/authentication/oauth-client.ts +254 -253
  314. package/src/providers/authentication/oauth-flow-store.ts +42 -7
  315. package/src/providers/bundled-discovery-providers.ts +6 -30
  316. package/src/providers/configured-openai-providers.ts +82 -97
  317. package/src/providers/default-registry.ts +35 -37
  318. package/src/providers/discovery/catalog/catalog-format.ts +13 -2
  319. package/src/providers/discovery/discovery-types.ts +0 -1
  320. package/src/providers/discovery/openai-model-discovery.ts +2 -2
  321. package/src/providers/discovery/probe-phases.ts +48 -13
  322. package/src/providers/discovery/probing-service.ts +6 -8
  323. package/src/providers/integrations/agentrouter/agentrouter.ts +12 -2
  324. package/src/providers/integrations/antigravity/antigravity-oauth.ts +0 -1
  325. package/src/providers/integrations/antigravity/antigravity-protocol.ts +88 -28
  326. package/src/providers/integrations/antigravity/antigravity-quota.ts +18 -6
  327. package/src/providers/integrations/antigravity/antigravity.ts +22 -13
  328. package/src/providers/integrations/buddy/buddy-catalog-shared.ts +1 -1
  329. package/src/providers/integrations/buddy/buddy-chat-shared.ts +20 -4
  330. package/src/providers/integrations/buddy/buddy-cli-headers.ts +19 -52
  331. package/src/providers/integrations/buddy/buddy-identity.ts +86 -0
  332. package/src/providers/integrations/buddy/buddy-oauth-shared.ts +1 -1
  333. package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +1 -2
  334. package/src/providers/integrations/buddy/codebuddy/provider.ts +4 -15
  335. package/src/providers/integrations/buddy/codebuddy/shared.ts +30 -9
  336. package/src/providers/integrations/buddy/workbuddy/provider.ts +5 -9
  337. package/src/providers/integrations/buddy/workbuddy/shared.ts +21 -3
  338. package/src/providers/integrations/claude/claude-cch.ts +148 -148
  339. package/src/providers/integrations/claude/fallback-credit.ts +3 -7
  340. package/src/providers/integrations/cline/cline.ts +1 -1
  341. package/src/providers/integrations/codex/codex-device-code.ts +101 -101
  342. package/src/providers/integrations/codex/codex-oauth.ts +0 -1
  343. package/src/providers/integrations/commandcode/commandcode.ts +75 -115
  344. package/src/providers/integrations/commandcode/quota.ts +131 -51
  345. package/src/providers/integrations/cursor/cursor-http2.ts +0 -3
  346. package/src/providers/integrations/cursor/cursor-quota.ts +2 -6
  347. package/src/providers/integrations/cursor/cursor-request.ts +0 -87
  348. package/src/providers/integrations/cursor/cursor-shared.ts +0 -4
  349. package/src/providers/integrations/cursor/cursor.ts +35 -3
  350. package/src/providers/integrations/cursor/protocol/framing.ts +0 -56
  351. package/src/providers/integrations/cursor/protocol/messages.ts +0 -96
  352. package/src/providers/integrations/factory/factory.ts +9 -9
  353. package/src/providers/integrations/github/github-discovery.ts +70 -12
  354. package/src/providers/integrations/github/github-oauth.ts +184 -14
  355. package/src/providers/integrations/github/github.ts +134 -26
  356. package/src/providers/integrations/grok/grok-oauth.ts +261 -261
  357. package/src/providers/integrations/grok/grok-quota-frame.ts +1 -1
  358. package/src/providers/integrations/grok/grok-quota.ts +16 -7
  359. package/src/providers/integrations/grok/grok-ratelimit-store.ts +0 -5
  360. package/src/providers/integrations/grok/grok-ratelimit.ts +3 -0
  361. package/src/providers/integrations/grok/grok.ts +86 -36
  362. package/src/providers/integrations/kilo/kilo-oauth.ts +1 -1
  363. package/src/providers/integrations/kiro/kiro-cache.ts +31 -0
  364. package/src/providers/integrations/kiro/kiro-catalog.ts +31 -5
  365. package/src/providers/integrations/kiro/kiro-discovery.ts +47 -43
  366. package/src/providers/integrations/kiro/kiro-machine-id.ts +6 -3
  367. package/src/providers/integrations/kiro/kiro-oauth.ts +523 -255
  368. package/src/providers/integrations/kiro/kiro-quota.ts +35 -47
  369. package/src/providers/integrations/kiro/kiro-request.ts +8 -11
  370. package/src/providers/integrations/kiro/kiro-stream.ts +13 -0
  371. package/src/providers/integrations/kiro/kiro.ts +80 -68
  372. package/src/providers/integrations/muse/muse-oauth.ts +0 -1
  373. package/src/providers/integrations/muse/muse-quota.ts +0 -4
  374. package/src/providers/integrations/opencode/fingerprint.ts +7 -5
  375. package/src/providers/integrations/opencode/provider.ts +17 -24
  376. package/src/providers/integrations/qoder/qoder-catalog.ts +94 -11
  377. package/src/providers/integrations/qoder/qoder-oauth.ts +165 -25
  378. package/src/providers/integrations/qoder/qoder-quota.ts +48 -6
  379. package/src/providers/integrations/qoder/qoder-realm.ts +10 -31
  380. package/src/providers/integrations/qoder/qoder-sse.ts +159 -0
  381. package/src/providers/integrations/qoder/qoder-umid.ts +21 -25
  382. package/src/providers/integrations/qoder/qoder-virtual.ts +12 -0
  383. package/src/providers/integrations/qoder/qoder.ts +422 -71
  384. package/src/providers/integrations/shared/connect.ts +0 -54
  385. package/src/providers/integrations/shared/hash.ts +15 -0
  386. package/src/providers/integrations/shared/session-cache.ts +136 -0
  387. package/src/providers/integrations/trae/trae-cache.ts +7 -87
  388. package/src/providers/integrations/trae/trae-catalog.ts +1 -6
  389. package/src/providers/integrations/trae/trae-protocol.ts +17 -8
  390. package/src/providers/integrations/trae/trae-realm.ts +0 -7
  391. package/src/providers/integrations/trae/trae.ts +51 -8
  392. package/src/providers/integrations/xai/xai-oauth.ts +5 -5
  393. package/src/providers/integrations/zcode-start-plan/gifts.ts +265 -0
  394. package/src/providers/integrations/zcode-start-plan/oauth.ts +154 -0
  395. package/src/providers/integrations/zcode-start-plan/provider.ts +131 -0
  396. package/src/providers/integrations/zcode-start-plan/quota.ts +152 -0
  397. package/src/providers/integrations/zcode-start-plan/zcode_prompt.json +10 -0
  398. package/src/providers/operations/account-error-classification.ts +198 -133
  399. package/src/providers/operations/account-health-service.ts +31 -39
  400. package/src/providers/operations/account-reset-service.ts +58 -1
  401. package/src/providers/operations/byok-wire-profile.ts +19 -15
  402. package/src/providers/operations/cli-platform.ts +31 -5
  403. package/src/providers/operations/client-version-resolver.ts +4 -6
  404. package/src/providers/operations/client-versions.ts +10 -7
  405. package/src/providers/operations/provider-catalog-seeder.ts +2 -0
  406. package/src/providers/operations/provider-catalog-service.ts +18 -2
  407. package/src/providers/operations/provider-credential-service.ts +19 -2
  408. package/src/providers/provider-contracts.ts +18 -7
  409. package/src/providers/provider-metadata.ts +17 -2
  410. package/src/providers/provider-model-definition.ts +22 -1
  411. package/src/providers/provider-probe.ts +4 -2
  412. package/src/providers/provider-usage.ts +50 -19
  413. package/src/providers/quota/cache.ts +29 -0
  414. package/src/providers/quota/quota-contracts.ts +4 -0
  415. package/src/providers/quota/refresh.ts +27 -4
  416. package/src/providers/quota/snapshot.ts +94 -0
  417. package/src/providers/search/search-provider.ts +1 -3
  418. package/src/providers/search/search-providers.ts +2 -1
  419. package/src/security/abuse.ts +4 -4
  420. package/src/security/admission/service.ts +13 -6
  421. package/src/security/admission/ttl.ts +61 -61
  422. package/src/security/api-key-auth.ts +17 -3
  423. package/src/security/client-router-fingerprint.ts +4 -4
  424. package/src/security/credential-codec.ts +3 -3
  425. package/src/security/csrf.ts +2 -2
  426. package/src/security/ip-boundary.ts +16 -189
  427. package/src/security/model-access-rule.ts +1 -1
  428. package/src/security/outbound-headers.ts +18 -0
  429. package/src/shared/invoke-guard.ts +47 -0
  430. package/src/transport/contracts.ts +13 -4
  431. package/src/transport/dispatch/attempt-finalize.ts +70 -22
  432. package/src/transport/dispatch/attempt-loop.ts +22 -0
  433. package/src/transport/dispatch/capture-policy.ts +34 -37
  434. package/src/transport/dispatch/empty-stream.ts +23 -20
  435. package/src/transport/dispatch/generation.ts +25 -45
  436. package/src/transport/dispatch/native-shared.ts +127 -0
  437. package/src/transport/dispatch/proxy-request.ts +3 -1
  438. package/src/transport/dispatch/responses-compact.ts +25 -46
  439. package/src/transport/dispatch/streaming/encoder.ts +18 -72
  440. package/src/transport/dispatch/streaming/finalize.ts +2 -2
  441. package/src/transport/dispatch/streaming/stream.ts +7 -3
  442. package/src/transport/dispatch/streaming/thinking-loop.ts +1 -1
  443. package/src/transport/dispatch/systemone.ts +20 -48
  444. package/src/transport/dispatch/token-count.ts +41 -38
  445. package/src/transport/dispatch/websearch.ts +19 -45
  446. package/src/transport/drain-endpoint.ts +1 -1
  447. package/src/transport/failure-policy.ts +21 -1
  448. package/src/transport/guards/auth-guard.ts +0 -6
  449. package/src/transport/guards/body-policy.ts +3 -3
  450. package/src/transport/guards/error-lifecycle.ts +4 -0
  451. package/src/transport/guards/headroom-guard.ts +1 -1
  452. package/src/transport/guards/pipeline.ts +1 -1
  453. package/src/transport/guards/request-context.ts +12 -17
  454. package/src/transport/request/preparer.ts +286 -7
  455. package/src/transport/request/rtk/compress-request.ts +3 -3
  456. package/src/transport/request/rtk/rtk-constants.ts +2 -2
  457. package/src/transport/request/rtk/rtk-filters.ts +8 -8
  458. package/src/transport/request/state.ts +7 -13
  459. package/src/transport/request/system-prompt-injection.ts +74 -11
  460. package/src/transport/request/tenant-preferences.ts +22 -0
  461. package/src/transport/routing/candidate-index.ts +2 -2
  462. package/src/transport/routing/round-robin.ts +1 -1
  463. package/src/transport/routing/route-catalog.ts +17 -18
  464. package/src/transport/routing/route-model.ts +126 -33
  465. package/src/transport/routing/router.ts +159 -37
  466. package/src/transport/shutdown-notice.ts +2 -2
  467. package/src/transport/surface/adapters.ts +3 -3
  468. package/src/transport/surface/chat/encode.ts +7 -7
  469. package/src/transport/surface/chat/parse.ts +7 -7
  470. package/src/transport/surface/completion.ts +2 -2
  471. package/src/transport/surface/content-parts.ts +1 -1
  472. package/src/transport/surface/dialects.ts +2 -2
  473. package/src/transport/surface/messages/parse.ts +3 -3
  474. package/src/transport/surface/reasoning-summary.ts +9 -14
  475. package/src/transport/surface/responses/adapter.ts +1 -1
  476. package/src/transport/surface/responses/errors.ts +0 -9
  477. package/src/transport/vision-handoff-cache.ts +58 -0
  478. package/src/transport/vision-handoff.ts +431 -0
  479. package/src/workers/account-identity-sweep.ts +2 -2
  480. package/src/workers/buddy-growth-tasks.ts +392 -346
  481. package/src/workers/daily-checkin.ts +102 -49
  482. package/src/workers/maintenance-lease.ts +2 -35
  483. package/src/workers/oauth-refresh-sweep.ts +8 -1
  484. package/src/workers/quota-refresh-sweep.ts +120 -82
  485. package/src/workers/scheduler.ts +67 -4
  486. package/src/workers/sweep-runner.ts +24 -5
  487. package/src/workers/zcode-gift-claim.ts +108 -0
  488. package/dist/dashboard/assets/AccountCooldown-Bjo92Qat.js +0 -1
  489. package/dist/dashboard/assets/AccountCooldown-Bjo92Qat.js.gz +0 -0
  490. package/dist/dashboard/assets/BannedPage-ChG5svFb.js +0 -1
  491. package/dist/dashboard/assets/BannedPage-ChG5svFb.js.gz +0 -0
  492. package/dist/dashboard/assets/CliToolDetailPage-BIcWQun7.js +0 -1
  493. package/dist/dashboard/assets/CliToolDetailPage-BIcWQun7.js.gz +0 -0
  494. package/dist/dashboard/assets/CliToolsPage-Bf2VelDe.js +0 -1
  495. package/dist/dashboard/assets/CliToolsPage-Bf2VelDe.js.gz +0 -0
  496. package/dist/dashboard/assets/CombosPage-DCgm5g6g.js +0 -1
  497. package/dist/dashboard/assets/CombosPage-DCgm5g6g.js.gz +0 -0
  498. package/dist/dashboard/assets/ConsoleLogPage-a8mD0QF1.js +0 -1
  499. package/dist/dashboard/assets/ConsoleLogPage-a8mD0QF1.js.gz +0 -0
  500. package/dist/dashboard/assets/CustomizationPage-ggeMSjte.js +0 -1
  501. package/dist/dashboard/assets/CustomizationPage-ggeMSjte.js.gz +0 -0
  502. package/dist/dashboard/assets/HealthEventsModal-DzSWzE_j.js +0 -1
  503. package/dist/dashboard/assets/HealthEventsModal-DzSWzE_j.js.gz +0 -0
  504. package/dist/dashboard/assets/LoginPage-U6AKaJcF.js +0 -1
  505. package/dist/dashboard/assets/LoginPage-U6AKaJcF.js.gz +0 -0
  506. package/dist/dashboard/assets/ModelPicker-BMS2t8BA.js.gz +0 -0
  507. package/dist/dashboard/assets/OverviewPage-D2D4IiHl.js +0 -1
  508. package/dist/dashboard/assets/OverviewPage-D2D4IiHl.js.gz +0 -0
  509. package/dist/dashboard/assets/ProviderDetailPage-CrNf7j_X.js +0 -5
  510. package/dist/dashboard/assets/ProviderDetailPage-CrNf7j_X.js.gz +0 -0
  511. package/dist/dashboard/assets/ProviderIcon-FB6yS4v1.js +0 -1
  512. package/dist/dashboard/assets/ProviderIcon-FB6yS4v1.js.gz +0 -0
  513. package/dist/dashboard/assets/ProvidersPage-BSDbquc1.js +0 -1
  514. package/dist/dashboard/assets/ProvidersPage-BSDbquc1.js.gz +0 -0
  515. package/dist/dashboard/assets/ProxyPage-lsrx7NYg.js.gz +0 -0
  516. package/dist/dashboard/assets/QuotaPage-L0skzML8.js +0 -1
  517. package/dist/dashboard/assets/QuotaPage-L0skzML8.js.gz +0 -0
  518. package/dist/dashboard/assets/SettingsPage-CmAbjRHW.js +0 -1
  519. package/dist/dashboard/assets/SettingsPage-CmAbjRHW.js.gz +0 -0
  520. package/dist/dashboard/assets/SetupPage-f0DE1h0-.js +0 -1
  521. package/dist/dashboard/assets/SetupPage-f0DE1h0-.js.gz +0 -0
  522. package/dist/dashboard/assets/SortableList-ClzpwyET.js +0 -1
  523. package/dist/dashboard/assets/SortableList-ClzpwyET.js.gz +0 -0
  524. package/dist/dashboard/assets/StudioPage-Cq5tW7hW.js.gz +0 -0
  525. package/dist/dashboard/assets/ToolIcon-K1ckcDKg.js +0 -1
  526. package/dist/dashboard/assets/ToolIcon-K1ckcDKg.js.gz +0 -0
  527. package/dist/dashboard/assets/UsagePage-Dfc9rWQn.js +0 -56
  528. package/dist/dashboard/assets/UsagePage-Dfc9rWQn.js.gz +0 -0
  529. package/dist/dashboard/assets/api-keys-C38iXoO4.js +0 -1
  530. package/dist/dashboard/assets/api-keys-C38iXoO4.js.gz +0 -0
  531. package/dist/dashboard/assets/app-BayTTy01.js +0 -9
  532. package/dist/dashboard/assets/app-BayTTy01.js.gz +0 -0
  533. package/dist/dashboard/assets/app-a159k0ub.css +0 -2
  534. package/dist/dashboard/assets/app-a159k0ub.css.gz +0 -0
  535. package/dist/dashboard/assets/assert-DbRA8EG2.js +0 -15
  536. package/dist/dashboard/assets/assert-DbRA8EG2.js.gz +0 -0
  537. package/dist/dashboard/assets/badge-D9CS6dZk.js.gz +0 -0
  538. package/dist/dashboard/assets/banner-DympBNq4.js.gz +0 -0
  539. package/dist/dashboard/assets/card-ibRWU_uT.js.gz +0 -0
  540. package/dist/dashboard/assets/cli-tools-63qBGaRw.js +0 -1
  541. package/dist/dashboard/assets/cli-tools-63qBGaRw.js.gz +0 -0
  542. package/dist/dashboard/assets/clipboard-button-DJm-9pz7.js.gz +0 -0
  543. package/dist/dashboard/assets/dialog-8fiE6tec.js +0 -1
  544. package/dist/dashboard/assets/dialog-8fiE6tec.js.gz +0 -0
  545. package/dist/dashboard/assets/drawer-CqGYeOrd.js.gz +0 -0
  546. package/dist/dashboard/assets/entry-BSAsYsmV.js +0 -1
  547. package/dist/dashboard/assets/entry-BSAsYsmV.js.gz +0 -0
  548. package/dist/dashboard/assets/entry-CiIQULZD.js +0 -4
  549. package/dist/dashboard/assets/entry-CiIQULZD.js.gz +0 -0
  550. package/dist/dashboard/assets/entry-Daz0lJ-W.js +0 -1
  551. package/dist/dashboard/assets/entry-Daz0lJ-W.js.gz +0 -0
  552. package/dist/dashboard/assets/formatters-D-MTJxMr.js.gz +0 -0
  553. package/dist/dashboard/assets/github-badge-CPK47xQx.js +0 -1
  554. package/dist/dashboard/assets/github-badge-CPK47xQx.js.gz +0 -0
  555. package/dist/dashboard/assets/index-DBNPOfhV.js.gz +0 -0
  556. package/dist/dashboard/assets/inline-BT0zntEr.js.gz +0 -0
  557. package/dist/dashboard/assets/input-Da2ZViWg.js.gz +0 -0
  558. package/dist/dashboard/assets/layout-BXNeLw6M.js.gz +0 -0
  559. package/dist/dashboard/assets/list-row-BycOrrIV.js.gz +0 -0
  560. package/dist/dashboard/assets/live-DCQNZpF7.js +0 -1
  561. package/dist/dashboard/assets/live-DCQNZpF7.js.gz +0 -0
  562. package/dist/dashboard/assets/mutation-Rr0TIvcj.js.gz +0 -0
  563. package/dist/dashboard/assets/network-9Yj2ZyLo.js +0 -1
  564. package/dist/dashboard/assets/network-9Yj2ZyLo.js.gz +0 -0
  565. package/dist/dashboard/assets/numbers-CEagj6ZZ.js +0 -2
  566. package/dist/dashboard/assets/numbers-CEagj6ZZ.js.gz +0 -0
  567. package/dist/dashboard/assets/providers/zai-coding-plan.svg +0 -6
  568. package/dist/dashboard/assets/providers/zai-coding-plan.svg.gz +0 -0
  569. package/dist/dashboard/assets/quota-Caw7wqci.js +0 -1
  570. package/dist/dashboard/assets/quota-Caw7wqci.js.gz +0 -0
  571. package/dist/dashboard/assets/routing-CO9zCUus.js +0 -1
  572. package/dist/dashboard/assets/routing-CO9zCUus.js.gz +0 -0
  573. package/dist/dashboard/assets/select-BIYIBkMU.js.gz +0 -0
  574. package/dist/dashboard/assets/settings-jS3-PNSA.js +0 -1
  575. package/dist/dashboard/assets/settings-jS3-PNSA.js.gz +0 -0
  576. package/dist/dashboard/assets/stack-DfgnFDWY.js.gz +0 -0
  577. package/dist/dashboard/assets/state-CkfdYWel.js.gz +0 -0
  578. package/dist/dashboard/assets/stream-retry-5QnVuSSo.js +0 -1
  579. package/dist/dashboard/assets/stream-retry-5QnVuSSo.js.gz +0 -0
  580. package/dist/dashboard/assets/switch-SYm237HG.js.gz +0 -0
  581. package/dist/dashboard/assets/useMutation-BQqCRNIi.js.gz +0 -0
  582. package/src/console/cli-tools/injectors/cline.ts +0 -110
  583. package/src/console/cli-tools/injectors/copilot.ts +0 -108
  584. package/src/console/cli-tools/injectors/cowork.ts +0 -64
  585. package/src/console/cli-tools/injectors/deepseek-tui.ts +0 -85
  586. package/src/console/cli-tools/injectors/droid.ts +0 -96
  587. package/src/console/cli-tools/injectors/grok-build.ts +0 -70
  588. package/src/console/cli-tools/injectors/hermes.ts +0 -101
  589. package/src/console/cli-tools/injectors/jcode.ts +0 -115
  590. package/src/console/cli-tools/injectors/openclaw.ts +0 -161
  591. package/src/network/response-headers.ts +0 -39
  592. package/src/protocol/response/dsml.ts +0 -38
  593. package/src/providers/integrations/xiaomi-mimo/think-stream.ts +0 -229
  594. /package/dist/dashboard/assets/brand/{og_bansos.webp → og_bansos_initial.webp} +0 -0
  595. /package/dist/dashboard/assets/brand/{og_image.webp → og_image_initial.webp} +0 -0
@@ -8,8 +8,9 @@
8
8
  * injection (Claude identity/billing prepend, buddy fixed system turn,
9
9
  * antigravity identity) happens later, in the adapters, and is untouched.
10
10
  *
11
- * Resolution is most-specific-wins: model > provider > global, where a
12
- * provider either follows the tenant's global prompt or carries a custom one.
11
+ * Resolution is most-specific-wins: alias/combo (when the caller's requested
12
+ * name matches one) > model > provider > global. A scope either references a
13
+ * named preset, carries a custom setting, or follows the global prompt.
13
14
  * Failure is non-fatal — an unusable configuration must not fail a
14
15
  * dispatchable request.
15
16
  */
@@ -17,6 +18,7 @@ import type { CanonicalRequest } from "../../protocol/canonical-model";
17
18
  import type { ServiceKind } from "../../protocol/canonical-model";
18
19
  import type {
19
20
  PromptSetting,
21
+ ScopePromptRef,
20
22
  TenantSystemPrompts,
21
23
  } from "../../console/system-prompt/contracts";
22
24
  import {
@@ -28,28 +30,89 @@ export interface SystemPromptRoute {
28
30
  readonly providerId: string;
29
31
  readonly modelId: string;
30
32
  readonly serviceKind: ServiceKind;
33
+ /** The model name the client asked for (pre-alias/combo resolution), when
34
+ * it differs from the resolved `modelId` — lets a combo/alias carry its
35
+ * own injection reference. */
36
+ readonly requestedModel?: string;
37
+ }
38
+
39
+ /**
40
+ * Resolves one scope reference to a concrete prompt setting. Returns
41
+ * `undefined` when the scope has nothing to say (no entry, preset
42
+ * missing/disabled, custom disabled). `undefined` here means "skip this
43
+ * scope" — the caller decides the final global fallback.
44
+ *
45
+ * `global` is itself a `ScopePromptRef | null`, so the `{ kind: "global" }`
46
+ * marker resolves through the global ref — which may be a preset, a custom
47
+ * setting, or `null` (nothing). An explicit `{ kind: "off" }` is handled by
48
+ * the caller before this function runs.
49
+ */
50
+ function resolveRef(
51
+ prompts: TenantSystemPrompts,
52
+ ref: ScopePromptRef | undefined,
53
+ ): PromptSetting | undefined {
54
+ if (ref === undefined || ref.kind === "off" || ref.kind === "global") return undefined;
55
+ if (ref.kind === "preset") {
56
+ const preset = prompts.presets[ref.presetId];
57
+ if (preset === undefined || !preset.enabled) return undefined;
58
+ return { enabled: true, mode: preset.mode, text: preset.text };
59
+ }
60
+ return ref.setting.enabled ? ref.setting : undefined;
61
+ }
62
+
63
+ /**
64
+ * Whether a scope entry explicitly stops injection. An explicit `off` is
65
+ * terminal — it does NOT fall through to the global prompt.
66
+ */
67
+ function isExplicitOff(ref: ScopePromptRef | undefined): boolean {
68
+ return ref?.kind === "off";
31
69
  }
32
70
 
33
71
  /**
34
72
  * Resolves the effective prompt for a route: the most-specific enabled scope
35
- * wins. A provider scope honours its `followGlobal` toggle; model scope is
36
- * always explicit. Returns `undefined` when nothing applies.
73
+ * wins. Alias/combo scopes (keyed by the requested name) beat the model scope;
74
+ * a provider scope honours its reference; model scope is always explicit. An
75
+ * explicit `off` at any scope stops injection for that request — it does not
76
+ * fall through to the global prompt. Returns `undefined` when nothing applies.
37
77
  */
38
78
  export function resolveTenantSystemPrompt(
39
79
  prompts: TenantSystemPrompts | undefined,
40
- route: Pick<SystemPromptRoute, "providerId" | "modelId">,
80
+ route: Pick<SystemPromptRoute, "providerId" | "modelId" | "requestedModel">,
41
81
  ): PromptSetting | undefined {
42
82
  if (prompts === undefined) return undefined;
83
+ const global = prompts.global;
84
+
85
+ const requested = route.requestedModel;
86
+ if (requested !== undefined && requested.length > 0 && requested !== route.modelId) {
87
+ const aliasRef = prompts.aliases[requested];
88
+ if (aliasRef !== undefined) {
89
+ if (isExplicitOff(aliasRef)) return undefined;
90
+ const aliasSetting = resolveRef(prompts, aliasRef);
91
+ if (aliasSetting !== undefined) return aliasSetting;
92
+ }
93
+ const comboRef = prompts.combos[requested];
94
+ if (comboRef !== undefined) {
95
+ if (isExplicitOff(comboRef)) return undefined;
96
+ const comboSetting = resolveRef(prompts, comboRef);
97
+ if (comboSetting !== undefined) return comboSetting;
98
+ }
99
+ }
100
+
43
101
  const model = prompts.models[systemPromptModelKey(route.providerId, route.modelId)];
44
- if (model !== undefined && model.enabled) return model;
102
+ if (model !== undefined) {
103
+ if (isExplicitOff(model)) return undefined;
104
+ const modelSetting = resolveRef(prompts, model);
105
+ if (modelSetting !== undefined) return modelSetting;
106
+ }
107
+
45
108
  const provider = prompts.providers[route.providerId];
46
109
  if (provider !== undefined) {
47
- if (provider.followGlobal) {
48
- return prompts.global !== null && prompts.global.enabled ? prompts.global : undefined;
49
- }
50
- if (provider.setting !== undefined && provider.setting.enabled) return provider.setting;
110
+ if (isExplicitOff(provider)) return undefined;
111
+ const providerSetting = resolveRef(prompts, provider);
112
+ if (providerSetting !== undefined) return providerSetting;
51
113
  }
52
- return prompts.global !== null && prompts.global.enabled ? prompts.global : undefined;
114
+
115
+ return resolveRef(prompts, global ?? undefined);
53
116
  }
54
117
 
55
118
  /** Marker text guard so retries never double-inject. */
@@ -107,6 +107,27 @@ export async function resolveTenantImageClampMaxBytes(
107
107
  }
108
108
  }
109
109
 
110
+ /**
111
+ * The tenant's ordered vision-handoff model list, or `[]` when none is
112
+ * configured (legacy `[image]` placeholder behavior). Read through the shared
113
+ * revision-keyed preference reader; a settings outage yields an empty list,
114
+ * which is the safe direction (handoff is an upgrade, never a prerequisite).
115
+ */
116
+ export async function resolveTenantVisionHandoffModels(
117
+ db: CartethyiaDatabase,
118
+ tenantId: string | undefined,
119
+ ): Promise<readonly string[]> {
120
+ if (tenantId === undefined) return [];
121
+ try {
122
+ const prefs = await preferencesReaderFor(db).readPreferences(tenantId);
123
+ const models = prefs?.visionHandoffModels;
124
+ if (!Array.isArray(models)) return [];
125
+ return models.filter((model): model is string => typeof model === "string" && model.trim().length > 0);
126
+ } catch {
127
+ return [];
128
+ }
129
+ }
130
+
110
131
  /**
111
132
  * Applies request shaping owned by tenant preferences and deployment config:
112
133
  * thinking normalization and the Responses reasoning summary override.
@@ -133,6 +154,7 @@ export async function applyTenantPreferences(
133
154
  providerId: candidate.provider_id,
134
155
  modelId: candidate.model_id,
135
156
  serviceKind: candidate.service_kind ?? "llm",
157
+ requestedModel: prepared.plan?.requested_model ?? candidate.model_id,
136
158
  });
137
159
  if (prefs) {
138
160
  // When the routed model is Responses (codex/openai/muse-spark) and the
@@ -2,7 +2,7 @@
2
2
  import type { RouteCandidate, RouteSnapshot } from "./route-model";
3
3
 
4
4
  /** One parsed model reference: `provider/model` or a bare `model`. */
5
- export interface ParsedModel {
5
+ interface ParsedModel {
6
6
  readonly providerId: string | null;
7
7
  readonly modelId: string;
8
8
  }
@@ -24,7 +24,7 @@ export function parseModelReference(model: string): ParsedModel {
24
24
  * candidates cost K×C `candidateMatches` calls (each re-parsing the model
25
25
  * string), repeated on every request.
26
26
  */
27
- export interface RouteCandidateIndex {
27
+ interface RouteCandidateIndex {
28
28
  /** Candidates keyed by `provider_id/model_id`, snapshot order preserved. */
29
29
  readonly qualified: ReadonlyMap<string, readonly RouteCandidate[]>;
30
30
  /** Candidates keyed by bare `model_id`, snapshot order preserved. */
@@ -23,7 +23,7 @@ export class RoundRobinState {
23
23
  * combinations would retain one entry forever. 1000 is far above the working
24
24
  * set of any real catalog, so eviction only ever targets genuinely cold keys.
25
25
  */
26
- export const MAX_ROUND_ROBIN_ENTRIES = 1000;
26
+ const MAX_ROUND_ROBIN_ENTRIES = 1000;
27
27
 
28
28
  /**
29
29
  * Creates the bounded LRU map the router keeps round-robin state in.
@@ -20,12 +20,12 @@ import {
20
20
  import { DEFAULT_PROVIDER_MAX_INFLIGHT } from "../../persistence/schema";
21
21
  import { resolveTenantOverride } from "../../persistence/tenant-scope";
22
22
  import {
23
- type RouteCandidate,
24
23
  type ComboDefinition,
25
24
  type PoolRoutingSetting,
26
25
  type ProviderRoutingMap,
27
26
  type RouteSnapshot,
28
27
  type SnapshotBuilder,
28
+ type RoutedCandidate,
29
29
  } from "./route-model";
30
30
  import { DEFAULT_PROXY_SKIP_PROVIDER_IDS } from "../../providers/provider-metadata";
31
31
  import { buildClaudeCliUserAgent } from "../../providers/operations/custom-cli-headers";
@@ -84,18 +84,6 @@ export function cliMappingSourceKeys(toolId: string, sourceModel: string): reado
84
84
  * — `RoutingEngine.plan()` filters candidates by the requesting tenant.
85
85
  */
86
86
 
87
- type RouteCandidateWithHealth = RouteCandidate & {
88
- health_status?: "cooldown" | "model_cooldown" | "disabled";
89
- cooldown_kind?: "hard" | "soft";
90
- /**
91
- * Epoch milliseconds at which this candidate's cooling window ends, when one
92
- * is in force. Routing-internal: it is what lets `plan()` report a real reset
93
- * on the all-accounts-cooling answer, and it never reaches a client verbatim —
94
- * only the derived `retry-after` does.
95
- */
96
- cooldown_until?: number;
97
- };
98
-
99
87
  const HARD_COOLDOWN_CATEGORIES: Readonly<Record<string, true>> = {
100
88
  quota_exhausted: true,
101
89
  policy_blocked: true,
@@ -246,6 +234,7 @@ interface MergedModelRow {
246
234
  providerId: string;
247
235
  modelId: string;
248
236
  wireFamily: WireFamily;
237
+ dialect?: string | null;
249
238
  serviceKind: ServiceKind;
250
239
  endpointPath: string;
251
240
  modalities: unknown;
@@ -270,6 +259,7 @@ function mergeModelCatalog(
270
259
  providerId: row.providerId,
271
260
  modelId: row.modelId,
272
261
  wireFamily: row.wireFamily as WireFamily,
262
+ ...(row.dialect === null ? {} : { dialect: row.dialect }),
273
263
  serviceKind: row.serviceKind as ServiceKind,
274
264
  endpointPath: row.endpointPath,
275
265
  modalities: row.modalities,
@@ -309,6 +299,7 @@ const MODEL_COLUMNS = {
309
299
  providerId: models.providerId,
310
300
  modelId: models.modelId,
311
301
  wireFamily: models.wireFamily,
302
+ dialect: models.dialect,
312
303
  serviceKind: models.serviceKind,
313
304
  endpointPath: models.endpointPath,
314
305
  modalities: models.modalities,
@@ -678,7 +669,7 @@ class RouteCatalogRepository {
678
669
  accountsByProvider.set(account.providerId, list);
679
670
  }
680
671
 
681
- const candidates: RouteCandidateWithHealth[] = [];
672
+ const candidates: RoutedCandidate[] = [];
682
673
  for (const model of mergedModelRows) {
683
674
  const providerTenantId = providerTenantById.get(model.providerId);
684
675
  if (providerTenantId === undefined) continue;
@@ -717,15 +708,17 @@ class RouteCatalogRepository {
717
708
  const tenantPools = resolveNetworkPools(model.providerId, tenantOwnerId);
718
709
  if (!tenantPools) continue;
719
710
  emittedPoolAccount = true;
720
- const tenantCandidate: RouteCandidateWithHealth = {
711
+ const tenantCandidate: RoutedCandidate = {
721
712
  provider_id: model.providerId,
722
713
  model_id: model.modelId,
723
714
  wire_family: model.wireFamily as WireFamily,
715
+ ...(model.dialect == null ? {} : { dialect: model.dialect }),
724
716
  service_kind: model.serviceKind,
725
717
  endpoint: model.endpointPath,
726
718
  ...(model.outputLimit == null ? {} : { output_limit: model.outputLimit }),
727
719
  capability_profile: capabilityProfile,
728
720
  tenant_id: tenantOwnerId,
721
+ ...(model.freeTier === true ? { free_tier: true } : {}),
729
722
  // Public credential-less candidates have no account to own them.
730
723
  owner_tenant_id: null,
731
724
  requires_account: false as const,
@@ -756,15 +749,17 @@ class RouteCatalogRepository {
756
749
  for (const tId of configuredPoolTenants) {
757
750
  const tenantPools = resolveNetworkPools(model.providerId, tId);
758
751
  if (tenantPools) {
759
- const tenantCandidate: RouteCandidateWithHealth = {
752
+ const tenantCandidate: RoutedCandidate = {
760
753
  provider_id: model.providerId,
761
754
  model_id: model.modelId,
762
755
  wire_family: model.wireFamily as WireFamily,
756
+ ...(model.dialect == null ? {} : { dialect: model.dialect }),
763
757
  service_kind: model.serviceKind,
764
758
  endpoint: model.endpointPath,
765
759
  ...(model.outputLimit == null ? {} : { output_limit: model.outputLimit }),
766
760
  capability_profile: capabilityProfile,
767
761
  tenant_id: tId,
762
+ ...(model.freeTier === true ? { free_tier: true } : {}),
768
763
  // Public credential-less candidates have no account to own them.
769
764
  owner_tenant_id: null,
770
765
  requires_account: false as const,
@@ -784,15 +779,17 @@ class RouteCatalogRepository {
784
779
  const networkPools = providerRequiresAccount
785
780
  ? undefined
786
781
  : resolveNetworkPools(model.providerId, providerTenantId);
787
- const candidate: RouteCandidateWithHealth = {
782
+ const candidate: RoutedCandidate = {
788
783
  provider_id: model.providerId,
789
784
  model_id: model.modelId,
790
785
  wire_family: model.wireFamily as WireFamily,
786
+ ...(model.dialect == null ? {} : { dialect: model.dialect }),
791
787
  service_kind: model.serviceKind,
792
788
  endpoint: model.endpointPath,
793
789
  ...(model.outputLimit == null ? {} : { output_limit: model.outputLimit }),
794
790
  capability_profile: capabilityProfile,
795
791
  tenant_id: providerTenantId,
792
+ ...(model.freeTier === true ? { free_tier: true } : {}),
796
793
  // Public credential-less candidates have no account to own them.
797
794
  owner_tenant_id: null,
798
795
  ...(providerRequiresAccount
@@ -826,15 +823,17 @@ class RouteCatalogRepository {
826
823
  }
827
824
  const networkPools = resolveNetworkPools(model.providerId, rowTenantId);
828
825
  const routeUserAgent = resolveUserAgent(model.providerId, rowTenantId);
829
- const candidate: RouteCandidateWithHealth = {
826
+ const candidate: RoutedCandidate = {
830
827
  provider_id: model.providerId,
831
828
  model_id: model.modelId,
832
829
  wire_family: model.wireFamily as WireFamily,
830
+ ...(model.dialect == null ? {} : { dialect: model.dialect }),
833
831
  service_kind: model.serviceKind,
834
832
  endpoint: model.endpointPath,
835
833
  ...(model.outputLimit == null ? {} : { output_limit: model.outputLimit }),
836
834
  capability_profile: capabilityProfile,
837
835
  ...(routeUserAgent === undefined ? {} : { user_agent: routeUserAgent }),
836
+ ...(model.freeTier === true ? { free_tier: true } : {}),
838
837
  tenant_id: rowTenantId,
839
838
  provider_account_id: account.id,
840
839
  // The account's owning tenant (null = shared pool-wide) — the bucket
@@ -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 {
@@ -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} (request {requestId}). Drop the ${capability} part of the request, or route to a model that accepts it. Know your tools — this one just isn't in the box 🧰`,
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.
@@ -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 (request {requestId}) (${explainEligibilityReasons(distinct)}). Add an account, or wait for an operator to re-enable one. Every account is on a coffee break — try later ☕️`,
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
  }
@@ -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