cartethyia 2.7.9 → 2.8.0

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 (375) hide show
  1. package/.env.example +3 -2
  2. package/dist/dashboard/assets/{BannedPage-BxoG1MrL.js → BannedPage-Cc0vKpMG.js} +1 -1
  3. package/dist/dashboard/assets/BannedPage-Cc0vKpMG.js.gz +0 -0
  4. package/dist/dashboard/assets/{CliToolDetailPage-ixwgKk53.js → CliToolDetailPage-Bqj2aa1T.js} +1 -1
  5. package/dist/dashboard/assets/CliToolDetailPage-Bqj2aa1T.js.gz +0 -0
  6. package/dist/dashboard/assets/{CliToolsPage-BmttNV3U.js → CliToolsPage-CXsaOAO2.js} +1 -1
  7. package/dist/dashboard/assets/CliToolsPage-CXsaOAO2.js.gz +0 -0
  8. package/dist/dashboard/assets/{CombosPage-BE3qwwfC.js → CombosPage-BeLpJ4ma.js} +1 -1
  9. package/dist/dashboard/assets/CombosPage-BeLpJ4ma.js.gz +0 -0
  10. package/dist/dashboard/assets/{ConsoleLogPage-CRZdmMIX.js → ConsoleLogPage-DnV0uZ3m.js} +1 -1
  11. package/dist/dashboard/assets/ConsoleLogPage-DnV0uZ3m.js.gz +0 -0
  12. package/dist/dashboard/assets/{CustomizationPage-Dv_FJ6Er.js → CustomizationPage-CBxAjo63.js} +1 -1
  13. package/dist/dashboard/assets/CustomizationPage-CBxAjo63.js.gz +0 -0
  14. package/dist/dashboard/assets/HealthEventsModal-B5yEE0k6.js +1 -0
  15. package/dist/dashboard/assets/HealthEventsModal-B5yEE0k6.js.gz +0 -0
  16. package/dist/dashboard/assets/{InjectSystemPromptPanel-BOoO8rpz.js → InjectSystemPromptPanel-BWFLECmz.js} +2 -2
  17. package/dist/dashboard/assets/InjectSystemPromptPanel-BWFLECmz.js.gz +0 -0
  18. package/dist/dashboard/assets/{LoginPage-ChJuNNVN.js → LoginPage-CgoPc5B9.js} +1 -1
  19. package/dist/dashboard/assets/LoginPage-CgoPc5B9.js.gz +0 -0
  20. package/dist/dashboard/assets/{ModelPicker-BZAJtPg1.js → ModelPicker-D9bFQern.js} +1 -1
  21. package/dist/dashboard/assets/ModelPicker-D9bFQern.js.gz +0 -0
  22. package/dist/dashboard/assets/{OverviewPage-BE40s3u5.js → OverviewPage-B_p4tL8k.js} +1 -1
  23. package/dist/dashboard/assets/OverviewPage-B_p4tL8k.js.gz +0 -0
  24. package/dist/dashboard/assets/ProviderDetailPage-rs5E2rxv.js +8 -0
  25. package/dist/dashboard/assets/ProviderDetailPage-rs5E2rxv.js.gz +0 -0
  26. package/dist/dashboard/assets/{ProviderIcon-CQ6bPUf_.js → ProviderIcon-D_0PiZtg.js} +1 -1
  27. package/dist/dashboard/assets/ProviderIcon-D_0PiZtg.js.gz +0 -0
  28. package/dist/dashboard/assets/ProvidersPage-CLI2bJTK.js +1 -0
  29. package/dist/dashboard/assets/ProvidersPage-CLI2bJTK.js.gz +0 -0
  30. package/dist/dashboard/assets/{ProxyPage-BJ87Pp0U.js → ProxyPage-BwNC_-Cx.js} +1 -1
  31. package/dist/dashboard/assets/ProxyPage-BwNC_-Cx.js.gz +0 -0
  32. package/dist/dashboard/assets/QuotaPage-DA-a1iSN.js +1 -0
  33. package/dist/dashboard/assets/QuotaPage-DA-a1iSN.js.gz +0 -0
  34. package/dist/dashboard/assets/{SettingsPage-C5HPWqEv.js → SettingsPage-CctTga2C.js} +1 -1
  35. package/dist/dashboard/assets/SettingsPage-CctTga2C.js.gz +0 -0
  36. package/dist/dashboard/assets/{SetupPage-CFNcATJJ.js → SetupPage-febLU4r1.js} +1 -1
  37. package/dist/dashboard/assets/SetupPage-febLU4r1.js.gz +0 -0
  38. package/dist/dashboard/assets/{StudioPage-YoD4tY5l.js → StudioPage-M6fvR5Wz.js} +1 -1
  39. package/dist/dashboard/assets/StudioPage-M6fvR5Wz.js.gz +0 -0
  40. package/dist/dashboard/assets/{UsagePage-DxjaAhb_.js → UsagePage-nyeuRqz9.js} +1 -1
  41. package/dist/dashboard/assets/UsagePage-nyeuRqz9.js.gz +0 -0
  42. package/dist/dashboard/assets/{api-keys-BTlkq_Uk.js → api-keys-BFOlvrJC.js} +1 -1
  43. package/dist/dashboard/assets/api-keys-BFOlvrJC.js.gz +0 -0
  44. package/dist/dashboard/assets/app-Dx2QgsNj.css +2 -0
  45. package/dist/dashboard/assets/app-Dx2QgsNj.css.gz +0 -0
  46. package/dist/dashboard/assets/assert-CqTLw5e7.js +1 -0
  47. package/dist/dashboard/assets/assert-CqTLw5e7.js.gz +0 -0
  48. package/dist/dashboard/assets/{badge-DdbT_7Am.js → badge-C83EqLB0.js} +1 -1
  49. package/dist/dashboard/assets/badge-C83EqLB0.js.gz +0 -0
  50. package/dist/dashboard/assets/{banner-CsGN-nbm.js → banner-D1xn7pIe.js} +1 -1
  51. package/dist/dashboard/assets/banner-D1xn7pIe.js.gz +0 -0
  52. package/dist/dashboard/assets/{card-mgZ_TLLZ.js → card-DU3Lq5Lm.js} +1 -1
  53. package/dist/dashboard/assets/card-DU3Lq5Lm.js.gz +0 -0
  54. package/dist/dashboard/assets/{cli-tools-A1-v8-03.js → cli-tools-Dw1U_drK.js} +1 -1
  55. package/dist/dashboard/assets/cli-tools-Dw1U_drK.js.gz +0 -0
  56. package/dist/dashboard/assets/{clipboard-button-CJ4Zty89.js → clipboard-button-CEL5Sg_J.js} +1 -1
  57. package/dist/dashboard/assets/clipboard-button-CEL5Sg_J.js.gz +0 -0
  58. package/dist/dashboard/assets/{dialog-B5L2jPQ_.js → dialog-BjFR5UHy.js} +1 -1
  59. package/dist/dashboard/assets/dialog-BjFR5UHy.js.gz +0 -0
  60. package/dist/dashboard/assets/{drawer-DkjUouAj.js → drawer-HvBW0AjI.js} +1 -1
  61. package/dist/dashboard/assets/drawer-HvBW0AjI.js.gz +0 -0
  62. package/dist/dashboard/assets/entry-BSu3gFlt.js +4 -0
  63. package/dist/dashboard/assets/entry-BSu3gFlt.js.gz +0 -0
  64. package/dist/dashboard/assets/{entry-B-LVLV6o.js → entry-CECVELyy.js} +1 -1
  65. package/dist/dashboard/assets/entry-CECVELyy.js.gz +0 -0
  66. package/dist/dashboard/assets/{entry-CAhdYl3B.js → entry-DYYofQ7W.js} +1 -1
  67. package/dist/dashboard/assets/entry-DYYofQ7W.js.gz +0 -0
  68. package/dist/dashboard/assets/{formatters-CrsdLVsn.js → formatters-DLW4i8zK.js} +1 -1
  69. package/dist/dashboard/assets/formatters-DLW4i8zK.js.gz +0 -0
  70. package/dist/dashboard/assets/{index-BU64Lqvp.js → index-1-fCqe64.js} +2 -2
  71. package/dist/dashboard/assets/index-1-fCqe64.js.gz +0 -0
  72. package/dist/dashboard/assets/{inline-B2aDLyyg.js → inline-DGvU2oLK.js} +1 -1
  73. package/dist/dashboard/assets/inline-DGvU2oLK.js.gz +0 -0
  74. package/dist/dashboard/assets/{input-BCVkdR9d.js → input-iNnO1X2K.js} +1 -1
  75. package/dist/dashboard/assets/input-iNnO1X2K.js.gz +0 -0
  76. package/dist/dashboard/assets/{layout-DNcAdMK-.js → layout-0q8UGypb.js} +1 -1
  77. package/dist/dashboard/assets/layout-0q8UGypb.js.gz +0 -0
  78. package/dist/dashboard/assets/{list-row-DgrIH0hw.js → list-row-C0w2bQjJ.js} +1 -1
  79. package/dist/dashboard/assets/list-row-C0w2bQjJ.js.gz +0 -0
  80. package/dist/dashboard/assets/{live-Cae01iu8.js → live-M5aJ_xCa.js} +1 -1
  81. package/dist/dashboard/assets/live-M5aJ_xCa.js.gz +0 -0
  82. package/dist/dashboard/assets/{network-CRib7H_2.js → network-BKeQGT3K.js} +1 -1
  83. package/dist/dashboard/assets/network-BKeQGT3K.js.gz +0 -0
  84. package/dist/dashboard/assets/{numbers-BKQE8Xpz.js → numbers-CivAk3da.js} +1 -1
  85. package/dist/dashboard/assets/numbers-CivAk3da.js.gz +0 -0
  86. package/dist/dashboard/assets/{pagination-ecPvG_8g.js → pagination-CpzvI5ep.js} +1 -1
  87. package/dist/dashboard/assets/pagination-CpzvI5ep.js.gz +0 -0
  88. package/dist/dashboard/assets/providers/zro.svg +3 -0
  89. package/dist/dashboard/assets/providers/zro.svg.gz +0 -0
  90. package/dist/dashboard/assets/quota-DiPD6UKH.js +1 -0
  91. package/dist/dashboard/assets/quota-DiPD6UKH.js.gz +0 -0
  92. package/dist/dashboard/assets/{routing-Ckm1UXuC.js → routing-BFQX2VBc.js} +1 -1
  93. package/dist/dashboard/assets/routing-BFQX2VBc.js.gz +0 -0
  94. package/dist/dashboard/assets/{select-ChoFWyKz.js → select-CXbck0jG.js} +2 -2
  95. package/dist/dashboard/assets/select-CXbck0jG.js.gz +0 -0
  96. package/dist/dashboard/assets/{settings-CKjFPlKW.js → settings-C_XvMHh9.js} +1 -1
  97. package/dist/dashboard/assets/settings-C_XvMHh9.js.gz +0 -0
  98. package/dist/dashboard/assets/{stack-DWCwU3fh.js → stack-DggRiCxE.js} +1 -1
  99. package/dist/dashboard/assets/stack-DggRiCxE.js.gz +0 -0
  100. package/dist/dashboard/assets/{state-Cg3VL5J_.js → state-ClFzfbYp.js} +1 -1
  101. package/dist/dashboard/assets/state-ClFzfbYp.js.gz +0 -0
  102. package/dist/dashboard/assets/{switch-Db70BMix.js → switch-CTIFxKqS.js} +1 -1
  103. package/dist/dashboard/assets/switch-CTIFxKqS.js.gz +0 -0
  104. package/dist/dashboard/assets/{useMutation-BqdE0LuM.js → useMutation-BClG3yWj.js} +1 -1
  105. package/dist/dashboard/assets/useMutation-BClG3yWj.js.gz +0 -0
  106. package/dist/dashboard/index.html +2 -2
  107. package/dist/dashboard/index.html.gz +0 -0
  108. package/dist/dashboard/precompressed-assets.json +54 -53
  109. package/migrations/full-postgres/0046_add_accounts_quota_plan_fetched_at.sql +7 -0
  110. package/migrations/lite-sqlite3/0046_add_accounts_quota_plan_fetched_at.sql +4 -0
  111. package/package.json +1 -1
  112. package/src/boot/app.ts +10 -13
  113. package/src/boot/dependencies.ts +1 -1
  114. package/src/boot/flush.ts +20 -0
  115. package/src/boot/lifecycle.ts +7 -5
  116. package/src/config/routing.ts +26 -0
  117. package/src/config/spec-metadata.ts +72 -0
  118. package/src/config/spec.ts +20 -0
  119. package/src/console/api-keys/contracts.ts +12 -1
  120. package/src/console/api-keys/routes.ts +40 -28
  121. package/src/console/auth/account-reset.ts +7 -9
  122. package/src/console/backup/contracts.ts +4 -0
  123. package/src/console/backup/export-endpoint.ts +7 -5
  124. package/src/console/cli-tools/service.ts +6 -12
  125. package/src/console/console-router-guards.ts +23 -16
  126. package/src/console/domain-registration.ts +0 -2
  127. package/src/console/inferhub/normalize.ts +24 -28
  128. package/src/console/inferhub/types.ts +3 -0
  129. package/src/console/ip-bans/routes.ts +7 -13
  130. package/src/console/observability/store.ts +0 -53
  131. package/src/console/providers/catalog/contracts.ts +2 -2
  132. package/src/console/providers/catalog/discover-constants.ts +6 -0
  133. package/src/console/providers/catalog/routes.ts +1 -0
  134. package/src/console/providers/catalog/store.ts +21 -16
  135. package/src/console/quota/account-quota-global.ts +14 -24
  136. package/src/console/quota/account-quota-shared.ts +49 -0
  137. package/src/console/quota/account-quota-tenant.ts +6 -18
  138. package/src/console/quota/quota-view.ts +19 -3
  139. package/src/console/routing/pools/operations.ts +31 -46
  140. package/src/console/shared/errors.ts +33 -6
  141. package/src/main.ts +2 -3
  142. package/src/network/pool/byte-accounting.ts +3 -2
  143. package/src/network/pool/selector.ts +182 -39
  144. package/src/network/pool-cooldown-store.ts +89 -14
  145. package/src/network/pool-health.ts +49 -61
  146. package/src/network/rate-limit-verdict.ts +1 -1
  147. package/src/network/ssrf.ts +6 -6
  148. package/src/observability/event-log.ts +7 -361
  149. package/src/observability/performance-metrics.ts +9 -0
  150. package/src/observability/runtime-metrics.ts +14 -9
  151. package/src/observability/telemetry-buffer.ts +15 -25
  152. package/src/observability/telemetry-reconciliation.ts +9 -37
  153. package/src/observability/telemetry-row.ts +125 -0
  154. package/src/persistence/db-mode.ts +4 -1
  155. package/src/persistence/dialect.ts +18 -0
  156. package/src/persistence/migrate.ts +6 -3
  157. package/src/persistence/schema.ts +8 -0
  158. package/src/persistence/ttl-cache.ts +20 -2
  159. package/src/protocol/codecs.ts +1 -1
  160. package/src/protocol/dsml.ts +34 -86
  161. package/src/protocol/markup-stream-parser.ts +195 -0
  162. package/src/protocol/reasoning-text.ts +19 -0
  163. package/src/protocol/request/chat.ts +10 -10
  164. package/src/protocol/request/gemini.ts +8 -2
  165. package/src/protocol/request/messages.ts +3 -4
  166. package/src/protocol/response/chat.ts +12 -20
  167. package/src/protocol/response/omp-tool-call-markup.ts +43 -86
  168. package/src/protocol/response/responses.ts +18 -2
  169. package/src/protocol/stop-reason.ts +23 -1
  170. package/src/protocol/think-split.ts +112 -0
  171. package/src/protocol/translation/dialect/qwen.ts +26 -58
  172. package/src/providers/adapters/compatible-adapter.ts +71 -12
  173. package/src/providers/authentication/oauth-client.ts +1 -1
  174. package/src/providers/authentication/oauth-device-flow.ts +98 -8
  175. package/src/providers/authentication/oauth-refresh-service.ts +30 -49
  176. package/src/providers/configured-provider.ts +148 -0
  177. package/src/providers/default-registry.ts +61 -36
  178. package/src/providers/discovery/probe-phases.ts +14 -4
  179. package/src/providers/discovery/probing-service.ts +15 -2
  180. package/src/providers/integrations/alibabacp/alibabacp-models.ts +1 -2
  181. package/src/providers/integrations/{dashscope-models.ts → alibabacp/dashscope-models.ts} +2 -2
  182. package/src/providers/integrations/antigravity/antigravity-protocol.ts +5 -1
  183. package/src/providers/integrations/buddy/buddy-chat-shared.ts +173 -17
  184. package/src/providers/integrations/buddy/buddy-identity.ts +19 -6
  185. package/src/providers/integrations/buddy/capabilities.ts +2 -2
  186. package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +3 -0
  187. package/src/providers/integrations/buddy/codebuddy/provider.ts +4 -0
  188. package/src/providers/integrations/buddy/codebuddy/shared.ts +37 -22
  189. package/src/providers/integrations/claude/claude-quota.ts +14 -17
  190. package/src/providers/integrations/cline/cline.ts +2 -2
  191. package/src/providers/integrations/commandcode/quota.ts +22 -28
  192. package/src/providers/integrations/cursor/cursor-auth.ts +0 -8
  193. package/src/providers/integrations/cursor/cursor-exec.ts +9 -2
  194. package/src/providers/integrations/cursor/cursor-request.ts +26 -12
  195. package/src/providers/integrations/cursor/cursor-shared.ts +7 -4
  196. package/src/providers/integrations/cursor/cursor.ts +89 -40
  197. package/src/providers/integrations/cursor/protocol/messages.ts +22 -29
  198. package/src/providers/integrations/cursor/protocol/protobuf-schema.ts +37 -0
  199. package/src/providers/integrations/devin/protocol/devin-proto.ts +1 -27
  200. package/src/providers/integrations/factory/factory-oauth.ts +17 -44
  201. package/src/providers/integrations/freebuff/assets/official-system-manager.txt +88 -0
  202. package/src/providers/integrations/freebuff/assets/official-system-worker.txt +57 -0
  203. package/src/providers/integrations/freebuff/assets/official-tools.json +1070 -0
  204. package/src/providers/integrations/freebuff/freebuff-catalog.ts +120 -0
  205. package/src/providers/integrations/freebuff/freebuff-constants.ts +1233 -0
  206. package/src/providers/integrations/freebuff/freebuff-discovery.ts +108 -0
  207. package/src/providers/integrations/freebuff/freebuff-oauth.ts +240 -0
  208. package/src/providers/integrations/freebuff/freebuff-quota.ts +260 -0
  209. package/src/providers/integrations/freebuff/freebuff-virtual.ts +89 -0
  210. package/src/providers/integrations/freebuff/freebuff.ts +682 -0
  211. package/src/providers/integrations/grok/grok-oauth.ts +48 -45
  212. package/src/providers/integrations/grok/grok-ratelimit-store.ts +10 -17
  213. package/src/providers/integrations/grok/grok-turn-index.ts +11 -16
  214. package/src/providers/integrations/{hermes.ts → hermes/hermes.ts} +9 -9
  215. package/src/providers/integrations/hermes/index.ts +2 -0
  216. package/src/providers/integrations/kiro/kiro-catalog.ts +17 -1
  217. package/src/providers/integrations/kiro/kiro-discovery.ts +10 -12
  218. package/src/providers/integrations/kiro/kiro-quota.ts +8 -4
  219. package/src/providers/integrations/kiro/kiro.ts +15 -19
  220. package/src/providers/integrations/ollamacloud/index.ts +2 -0
  221. package/src/providers/integrations/{ollamacloud-models.ts → ollamacloud/models.ts} +2 -2
  222. package/src/providers/integrations/{ollamacloud-quota.ts → ollamacloud/quota.ts} +3 -3
  223. package/src/providers/integrations/opencode/opencode-go-quota.ts +2 -8
  224. package/src/providers/integrations/perch/perch-credential.ts +25 -0
  225. package/src/providers/integrations/perch/perch-protocol.ts +0 -3
  226. package/src/providers/integrations/perch/perch-quota.ts +2 -16
  227. package/src/providers/integrations/perch/perch.ts +3 -13
  228. package/src/providers/integrations/qoder/claim-qoder-credits.ts +18 -59
  229. package/src/providers/integrations/qoder/qoder-adapter.ts +1 -12
  230. package/src/providers/integrations/qoder/qoder-auth.ts +101 -29
  231. package/src/providers/integrations/qoder/qoder-catalog.ts +20 -5
  232. package/src/providers/integrations/qoder/qoder-chat.ts +7 -21
  233. package/src/providers/integrations/qoder/qoder-checkin.ts +4 -4
  234. package/src/providers/integrations/qoder/qoder-grpc.ts +0 -1
  235. package/src/providers/integrations/qoder/qoder-ide.ts +0 -107
  236. package/src/providers/integrations/qoder/qoder.ts +0 -1
  237. package/src/providers/integrations/trae/trae-protocol.ts +9 -5
  238. package/src/providers/integrations/trae/trae-quota.ts +6 -28
  239. package/src/providers/integrations/trae/trae-realm.ts +38 -0
  240. package/src/providers/integrations/trae/trae.ts +7 -24
  241. package/src/providers/integrations/zcode-start-plan/provider.ts +226 -23
  242. package/src/providers/integrations/zcode-start-plan/quota.ts +19 -2
  243. package/src/providers/integrations/zro/zro.ts +204 -0
  244. package/src/providers/operations/account-error-classification.ts +199 -307
  245. package/src/providers/operations/account-health-service.ts +82 -1
  246. package/src/providers/operations/account-refresh-status.ts +22 -37
  247. package/src/providers/operations/account-tier.ts +15 -16
  248. package/src/providers/operations/client-version-resolver.ts +1 -1
  249. package/src/providers/operations/client-versions.ts +5 -51
  250. package/src/providers/operations/cooldown-rules.ts +11 -16
  251. package/src/providers/operations/{model-catalog-cache.ts → provider-caches.ts} +68 -16
  252. package/src/providers/operations/provider-catalog-service.ts +1 -1
  253. package/src/providers/operations/provider-credential-service.ts +102 -32
  254. package/src/providers/operations/provider-rules/github.ts +48 -0
  255. package/src/providers/operations/provider-rules/grok.ts +33 -0
  256. package/src/providers/operations/provider-rules/inception.ts +49 -0
  257. package/src/providers/operations/provider-rules/index.ts +32 -0
  258. package/src/providers/operations/provider-rules/perch.ts +37 -0
  259. package/src/providers/operations/provider-rules/qoder.ts +70 -0
  260. package/src/providers/operations/provider-rules/trae.ts +81 -0
  261. package/src/providers/provider-contracts.ts +4 -2
  262. package/src/providers/{provider-credential-resolver.ts → provider-credentials.ts} +40 -3
  263. package/src/providers/provider-metadata.ts +40 -10
  264. package/src/providers/provider-probe.ts +15 -1
  265. package/src/providers/provider-registry.ts +42 -33
  266. package/src/providers/quota/quota-limit-windows.ts +16 -5
  267. package/src/providers/quota/quota-support.ts +1 -1
  268. package/src/providers/quota/quota-window-parser.ts +90 -18
  269. package/src/providers/quota/refresh.ts +75 -18
  270. package/src/security/api-key-auth-cache.ts +1 -1
  271. package/src/security/api-key-auth.ts +6 -2
  272. package/src/transport/dispatch/attempt-finalize.ts +14 -13
  273. package/src/transport/dispatch/attempt-loop.ts +71 -17
  274. package/src/transport/dispatch/generation.ts +2 -8
  275. package/src/transport/dispatch/native-shared.ts +25 -0
  276. package/src/transport/dispatch/proxy-request.ts +8 -46
  277. package/src/transport/dispatch/responses-compact.ts +2 -8
  278. package/src/transport/dispatch/streaming/finalize.ts +9 -6
  279. package/src/transport/dispatch/streaming/stream.ts +8 -36
  280. package/src/transport/dispatch/systemone.ts +2 -8
  281. package/src/transport/dispatch/token-count.ts +2 -6
  282. package/src/transport/dispatch/upstream.ts +63 -0
  283. package/src/transport/drain-endpoint.ts +31 -4
  284. package/src/transport/failure-policy.ts +16 -30
  285. package/src/transport/guards/body-policy.ts +7 -1
  286. package/src/transport/guards/error-lifecycle.ts +13 -1
  287. package/src/transport/guards/request-context.ts +22 -14
  288. package/src/transport/request/preparer.ts +81 -71
  289. package/src/transport/routing/bounded-lru.ts +14 -0
  290. package/src/transport/routing/drain-reserve.ts +129 -0
  291. package/src/transport/routing/plan-gate.ts +7 -1
  292. package/src/transport/routing/round-robin.ts +60 -0
  293. package/src/transport/routing/route-catalog.ts +123 -111
  294. package/src/transport/routing/route-model.ts +51 -9
  295. package/src/transport/routing/router.ts +393 -32
  296. package/src/transport/surface/adapters.ts +11 -2
  297. package/src/transport/surface/chat/adapter.ts +2 -2
  298. package/src/transport/surface/chat/encode.ts +47 -4
  299. package/src/transport/surface/chat/parse.ts +2 -12
  300. package/src/transport/surface/completion.ts +44 -33
  301. package/src/transport/surface/content-parts.ts +21 -0
  302. package/src/transport/surface/messages/adapter.ts +7 -1
  303. package/src/transport/surface/messages/encode.ts +11 -3
  304. package/src/transport/surface/messages/parse.ts +2 -1
  305. package/src/transport/surface/messages/stream.ts +33 -17
  306. package/src/transport/surface/responses/encode.ts +22 -5
  307. package/src/transport/surface/responses/parse.ts +3 -11
  308. package/dist/dashboard/assets/BannedPage-BxoG1MrL.js.gz +0 -0
  309. package/dist/dashboard/assets/CliToolDetailPage-ixwgKk53.js.gz +0 -0
  310. package/dist/dashboard/assets/CliToolsPage-BmttNV3U.js.gz +0 -0
  311. package/dist/dashboard/assets/CombosPage-BE3qwwfC.js.gz +0 -0
  312. package/dist/dashboard/assets/ConsoleLogPage-CRZdmMIX.js.gz +0 -0
  313. package/dist/dashboard/assets/CustomizationPage-Dv_FJ6Er.js.gz +0 -0
  314. package/dist/dashboard/assets/HealthEventsModal-CoVrw2sM.js +0 -1
  315. package/dist/dashboard/assets/HealthEventsModal-CoVrw2sM.js.gz +0 -0
  316. package/dist/dashboard/assets/InjectSystemPromptPanel-BOoO8rpz.js.gz +0 -0
  317. package/dist/dashboard/assets/LoginPage-ChJuNNVN.js.gz +0 -0
  318. package/dist/dashboard/assets/ModelPicker-BZAJtPg1.js.gz +0 -0
  319. package/dist/dashboard/assets/OverviewPage-BE40s3u5.js.gz +0 -0
  320. package/dist/dashboard/assets/ProviderDetailPage-sfLoQtPV.js +0 -8
  321. package/dist/dashboard/assets/ProviderDetailPage-sfLoQtPV.js.gz +0 -0
  322. package/dist/dashboard/assets/ProviderIcon-CQ6bPUf_.js.gz +0 -0
  323. package/dist/dashboard/assets/ProvidersPage-DcepmPWo.js +0 -1
  324. package/dist/dashboard/assets/ProvidersPage-DcepmPWo.js.gz +0 -0
  325. package/dist/dashboard/assets/ProxyPage-BJ87Pp0U.js.gz +0 -0
  326. package/dist/dashboard/assets/QuotaPage-gkSyRAKa.js +0 -1
  327. package/dist/dashboard/assets/QuotaPage-gkSyRAKa.js.gz +0 -0
  328. package/dist/dashboard/assets/SettingsPage-C5HPWqEv.js.gz +0 -0
  329. package/dist/dashboard/assets/SetupPage-CFNcATJJ.js.gz +0 -0
  330. package/dist/dashboard/assets/StudioPage-YoD4tY5l.js.gz +0 -0
  331. package/dist/dashboard/assets/UsagePage-DxjaAhb_.js.gz +0 -0
  332. package/dist/dashboard/assets/api-keys-BTlkq_Uk.js.gz +0 -0
  333. package/dist/dashboard/assets/app-BX2nnKqs.css +0 -2
  334. package/dist/dashboard/assets/app-BX2nnKqs.css.gz +0 -0
  335. package/dist/dashboard/assets/assert-CndthXFQ.js +0 -1
  336. package/dist/dashboard/assets/assert-CndthXFQ.js.gz +0 -0
  337. package/dist/dashboard/assets/badge-DdbT_7Am.js.gz +0 -0
  338. package/dist/dashboard/assets/banner-CsGN-nbm.js.gz +0 -0
  339. package/dist/dashboard/assets/card-mgZ_TLLZ.js.gz +0 -0
  340. package/dist/dashboard/assets/cli-tools-A1-v8-03.js.gz +0 -0
  341. package/dist/dashboard/assets/clipboard-button-CJ4Zty89.js.gz +0 -0
  342. package/dist/dashboard/assets/dialog-B5L2jPQ_.js.gz +0 -0
  343. package/dist/dashboard/assets/drawer-DkjUouAj.js.gz +0 -0
  344. package/dist/dashboard/assets/entry-B-LVLV6o.js.gz +0 -0
  345. package/dist/dashboard/assets/entry-CAhdYl3B.js.gz +0 -0
  346. package/dist/dashboard/assets/entry-D2xD0Tmd.js +0 -4
  347. package/dist/dashboard/assets/entry-D2xD0Tmd.js.gz +0 -0
  348. package/dist/dashboard/assets/formatters-CrsdLVsn.js.gz +0 -0
  349. package/dist/dashboard/assets/index-BU64Lqvp.js.gz +0 -0
  350. package/dist/dashboard/assets/inline-B2aDLyyg.js.gz +0 -0
  351. package/dist/dashboard/assets/input-BCVkdR9d.js.gz +0 -0
  352. package/dist/dashboard/assets/layout-DNcAdMK-.js.gz +0 -0
  353. package/dist/dashboard/assets/list-row-DgrIH0hw.js.gz +0 -0
  354. package/dist/dashboard/assets/live-Cae01iu8.js.gz +0 -0
  355. package/dist/dashboard/assets/network-CRib7H_2.js.gz +0 -0
  356. package/dist/dashboard/assets/numbers-BKQE8Xpz.js.gz +0 -0
  357. package/dist/dashboard/assets/pagination-ecPvG_8g.js.gz +0 -0
  358. package/dist/dashboard/assets/quota-DJ1QlAOq.js +0 -1
  359. package/dist/dashboard/assets/quota-DJ1QlAOq.js.gz +0 -0
  360. package/dist/dashboard/assets/routing-Ckm1UXuC.js.gz +0 -0
  361. package/dist/dashboard/assets/select-ChoFWyKz.js.gz +0 -0
  362. package/dist/dashboard/assets/settings-CKjFPlKW.js.gz +0 -0
  363. package/dist/dashboard/assets/stack-DWCwU3fh.js.gz +0 -0
  364. package/dist/dashboard/assets/state-Cg3VL5J_.js.gz +0 -0
  365. package/dist/dashboard/assets/switch-Db70BMix.js.gz +0 -0
  366. package/dist/dashboard/assets/useMutation-BqdE0LuM.js.gz +0 -0
  367. package/src/providers/bundled-discovery-providers.ts +0 -98
  368. package/src/providers/configured-openai-providers.ts +0 -120
  369. package/src/providers/operations/model-discovery-cache.ts +0 -34
  370. package/src/providers/operations/probe-fetch.ts +0 -15
  371. package/src/providers/operations/provider-version-cache.ts +0 -30
  372. package/src/providers/provider-capabilities.ts +0 -33
  373. package/src/providers/provider-credential-envelope.ts +0 -43
  374. /package/dist/dashboard/assets/{app-B7kIWFm5.js → app-DIXLjojg.js} +0 -0
  375. /package/dist/dashboard/assets/{app-B7kIWFm5.js.gz → app-DIXLjojg.js.gz} +0 -0
@@ -490,6 +490,31 @@ const RAW_BUNDLED_PROVIDER_METADATA = [
490
490
  // rows are served by `/v1/search`, never by the canonical chat pipeline.
491
491
  { id: "exa", displayName: "Exa", baseUrl: "https://mcp.exa.ai", serviceKinds: ["websearch"], requiresAccount: false, credentialHint: "No API key. Search goes through the public MCP." },
492
492
  { id: "parallel", displayName: "Parallel", baseUrl: "https://search.parallel.ai", serviceKinds: ["websearch"], requiresAccount: false, credentialUrl: "https://platform.parallel.ai/settings?tab=api-keys", credentialHint: "Optional. Leave empty for the public MCP, or paste an API key for the higher-limit search API." },
493
+ {
494
+ // Freebuff (Codebuff Desktop). OAuth code flow → bearer token; every
495
+ // dispatch carries the desktop identity group + Ed25519 device signature
496
+ // (see documentation/research/freebuff/). The chat surface is
497
+ // OpenAI-compatible on `/api/v1/chat/completions` but requires a stateful
498
+ // session admission + agent-runs preflight — handled by the custom adapter.
499
+ id: "freebuff",
500
+ displayName: "Freebuff",
501
+ baseUrl: "https://www.codebuff.com",
502
+ hasAdapterUserAgent: true,
503
+ credentialUrl: "https://www.codebuff.com/login",
504
+ credentialHint: "Sign in with your Freebuff account (Desktop/CLI code flow).",
505
+ },
506
+ {
507
+ // Zro (moonmath). Fast, private, multi-region inference for open models.
508
+ // Claude Code connects via the Messages wire with a plain bearer key; the
509
+ // provider's own Claude Code docs configure `ANTHROPIC_BASE_URL` +
510
+ // `ANTHROPIC_AUTH_TOKEN`. No quota endpoint exists — entitlement is the
511
+ // `/v1/models` list itself (a key only sees the models it may call).
512
+ id: "zro",
513
+ displayName: "Zro",
514
+ baseUrl: "https://zro.moonmath.ai",
515
+ credentialUrl: "https://zro.moonmath.ai/docs/key-controls",
516
+ credentialHint: "Create or copy an API key (sk-...) from the Zro key controls page (docs/key-controls).",
517
+ },
493
518
  ] as const;
494
519
 
495
520
  /**
@@ -617,6 +642,11 @@ const ADAPTER_USER_AGENT_PROVIDER_IDS: ReadonlySet<string> = new Set(
617
642
  BUNDLED_PROVIDER_METADATA.filter((provider) => provider.hasAdapterUserAgent).map((provider) => provider.id),
618
643
  );
619
644
 
645
+ /** Bundled metadata keyed by provider id for O(1) lookups. */
646
+ const BUNDLED_PROVIDER_BY_ID: ReadonlyMap<BundledProviderId, BundledProviderMetadata> = new Map(
647
+ BUNDLED_PROVIDER_METADATA.map((definition) => [definition.id, definition]),
648
+ );
649
+
620
650
  /** Whether the bundled adapter provides its own upstream User-Agent. */
621
651
  export function providerHasAdapterUserAgent(providerId: string): boolean {
622
652
  return ADAPTER_USER_AGENT_PROVIDER_IDS.has(providerId);
@@ -628,7 +658,7 @@ export function providerHasAdapterUserAgent(providerId: string): boolean {
628
658
  * minted inside a device/browser flow with no dedicated key page).
629
659
  */
630
660
  export function providerCredentialUrl(providerId: string): string | undefined {
631
- return BUNDLED_PROVIDER_METADATA.find((candidate) => candidate.id === providerId)?.credentialUrl;
661
+ return BUNDLED_PROVIDER_BY_ID.get(providerId as BundledProviderId)?.credentialUrl;
632
662
  }
633
663
 
634
664
  /**
@@ -636,18 +666,18 @@ export function providerCredentialUrl(providerId: string): string | undefined {
636
666
  * `undefined` where the link alone is self-explanatory.
637
667
  */
638
668
  export function providerCredentialHint(providerId: string): string | undefined {
639
- return BUNDLED_PROVIDER_METADATA.find((candidate) => candidate.id === providerId)?.credentialHint;
669
+ return BUNDLED_PROVIDER_BY_ID.get(providerId as BundledProviderId)?.credentialHint;
640
670
  }
641
671
 
642
672
  /** Presentation-only limitation for the provider detail page. */
643
673
  export function providerOperatorNotice(providerId: string): string | undefined {
644
- return BUNDLED_PROVIDER_METADATA.find((candidate) => candidate.id === providerId)?.operatorNotice;
674
+ return BUNDLED_PROVIDER_BY_ID.get(providerId as BundledProviderId)?.operatorNotice;
645
675
  }
646
676
 
647
677
  /** Canonical builtin display name; falls back to the provider id for unknowns. */
648
678
  export function providerDisplayName(providerId: string): string {
649
679
  return (
650
- BUNDLED_PROVIDER_METADATA.find((candidate) => candidate.id === providerId)?.displayName ??
680
+ BUNDLED_PROVIDER_BY_ID.get(providerId as BundledProviderId)?.displayName ??
651
681
  providerId
652
682
  );
653
683
  }
@@ -679,7 +709,7 @@ export function providerDisplayName(providerId: string): string {
679
709
  * gateway guessing it will not.
680
710
  */
681
711
  export function providerServiceKinds(providerId: string): readonly ServiceKind[] {
682
- const declared = BUNDLED_PROVIDER_METADATA.find((candidate) => candidate.id === providerId)
712
+ const declared = BUNDLED_PROVIDER_BY_ID.get(providerId as BundledProviderId)
683
713
  ?.serviceKinds;
684
714
  // Bundled providers are explicit: they say which surfaces they serve, and a
685
715
  // bundled provider without the declaration serves LLM only.
@@ -708,7 +738,7 @@ export function customProviderServiceKinds(
708
738
 
709
739
  /** Canonical builtin base URL used by adapters, discovery, and quota clients. */
710
740
  export function providerBaseUrl(providerId: string): string {
711
- const entry = BUNDLED_PROVIDER_METADATA.find((candidate) => candidate.id === providerId);
741
+ const entry = BUNDLED_PROVIDER_BY_ID.get(providerId as BundledProviderId);
712
742
  if (!entry) throw new Error(`Unknown builtin provider "${providerId}"`);
713
743
  return entry.baseUrl.replace(/\/+$/, "");
714
744
  }
@@ -744,7 +774,7 @@ export const PROVIDER_COMPATIBILITY_PROFILES: Record<string, CompatibilityProfil
744
774
 
745
775
  /** Default wire family for a builtin provider. */
746
776
  export function providerDefaultWireFamily(providerId: string): WireFamily {
747
- return BUNDLED_PROVIDER_METADATA.find((candidate) => candidate.id === providerId)?.wireFamilyDefault ?? "chat";
777
+ return BUNDLED_PROVIDER_BY_ID.get(providerId as BundledProviderId)?.wireFamilyDefault ?? "chat";
748
778
  }
749
779
 
750
780
  /**
@@ -754,12 +784,12 @@ export function providerDefaultWireFamily(providerId: string): WireFamily {
754
784
  * OpenAI-compatible adapter.
755
785
  */
756
786
  export function providerUsesBespokeWire(providerId: string): boolean {
757
- return BUNDLED_PROVIDER_METADATA.find((candidate) => candidate.id === providerId)?.bespokeWire ?? false;
787
+ return BUNDLED_PROVIDER_BY_ID.get(providerId as BundledProviderId)?.bespokeWire ?? false;
758
788
  }
759
789
 
760
790
  /** Whether a builtin provider requires a persisted account row. */
761
791
  export function providerRequiresAccount(providerId: string): boolean {
762
- return BUNDLED_PROVIDER_METADATA.find((candidate) => candidate.id === providerId)?.requiresAccount ?? true;
792
+ return BUNDLED_PROVIDER_BY_ID.get(providerId as BundledProviderId)?.requiresAccount ?? true;
763
793
  }
764
794
 
765
795
  /**
@@ -769,7 +799,7 @@ export function providerRequiresAccount(providerId: string): boolean {
769
799
  */
770
800
  export function providerJwtVerification(providerId: string): ProviderJwtVerification {
771
801
  return (
772
- BUNDLED_PROVIDER_METADATA.find((candidate) => candidate.id === providerId)?.jwtVerification ?? {}
802
+ BUNDLED_PROVIDER_BY_ID.get(providerId as BundledProviderId)?.jwtVerification ?? {}
773
803
  );
774
804
  }
775
805
 
@@ -16,7 +16,21 @@ import { resolveDialect } from "../protocol/translation/dialect/resolver";
16
16
  import type { CanonicalRequest } from "../protocol/canonical-model";
17
17
  import type { ProviderDispatchContext } from "./provider-contracts";
18
18
  import { providerBaseUrl, providerRateLimitIsIpScoped } from "./provider-metadata";
19
- import type { ProviderDispatchTarget, ProviderId } from "./provider-contracts";
19
+ import type { ProviderDispatchTarget, ProviderId, ValidatedOutboundFetch } from "./provider-contracts";
20
+
21
+ /**
22
+ * Probe fetch wrapper: merges `Request`-carried headers with per-call init
23
+ * headers before delegating to the validated fetch. Required wherever the
24
+ * fetch is handed to adapters as `outbound_fetch`, because adapters may call
25
+ * it with a `Request` whose headers would otherwise be dropped.
26
+ */
27
+ export function createProbeFetch(fetcher: ValidatedOutboundFetch): ValidatedOutboundFetch {
28
+ return (input, init) => {
29
+ const headers = new Headers(input instanceof Request ? input.headers : undefined);
30
+ new Headers(init?.headers).forEach((value, name) => headers.set(name, value));
31
+ return fetcher(input, { ...init, headers });
32
+ };
33
+ }
20
34
 
21
35
 
22
36
  /**
@@ -58,6 +58,36 @@ export interface ProviderAuthentication {
58
58
  readonly refresher?: OAuthTokenRefresher;
59
59
  }
60
60
 
61
+ /** Provider implementation fields composed with canonical metadata. */
62
+ export type ProviderModuleCapabilities = Partial<Omit<ProviderModule, "loadAdapter">> &
63
+ Pick<ProviderModule, "loadAdapter">;
64
+
65
+ /**
66
+ * Creates a lazy authentication loader from a dynamic import and client export.
67
+ * Most OAuth clients also refresh their own tokens.
68
+ */
69
+ export function oauthCapability<M, K extends keyof M>(
70
+ loader: () => Promise<M>,
71
+ exportName: K,
72
+ options: { readonly withRefresher?: boolean } = {},
73
+ ): () => Promise<ProviderAuthentication> {
74
+ return async () => {
75
+ const module = await loader();
76
+ const client = (module as Record<string, unknown>)[exportName as string] as OAuthLoginClient & { refresh: unknown };
77
+ return options.withRefresher
78
+ ? ({ client, refresher: client } as ProviderAuthentication)
79
+ : ({ client } as ProviderAuthentication);
80
+ };
81
+ }
82
+
83
+ /** Creates a lazy quota collector loader from a dynamic import and export name. */
84
+ export function quotaCapability<M, K extends keyof M>(
85
+ loader: () => Promise<M>,
86
+ exportName: K,
87
+ ): () => Promise<QuotaFetcher> {
88
+ return () => loader().then((module) => (module as Record<string, unknown>)[exportName as string] as QuotaFetcher);
89
+ }
90
+
61
91
  /**
62
92
  * Derives a registry registration from a fully populated provider module.
63
93
  * Keeps the registration shape in one place so the default registry and any
@@ -95,12 +125,6 @@ function assertAdapterIdentity(expected: ProviderId, adapter: ProviderAdapter):
95
125
  });
96
126
  }
97
127
 
98
- /** Result of loading the configured provider registrations. */
99
- export interface ProviderRegistrySnapshot {
100
- readonly revision: number;
101
- readonly adapters: ReadonlyMap<ProviderId, ProviderAdapter>;
102
- }
103
-
104
128
  /**
105
129
  * Single provider-registration authority. Every registration loads on
106
130
  * demand through its own loader; registrations that are not added are
@@ -116,7 +140,6 @@ export class ProviderRegistry {
116
140
  #quotaLoading = new Map<ProviderId, Promise<QuotaFetcher>>();
117
141
  #modelDiscovery = new Map<ProviderId, ProviderModelDiscovery>();
118
142
  #modelDiscoveryLoading = new Map<ProviderId, Promise<ProviderModelDiscovery>>();
119
- #revision = 0;
120
143
 
121
144
  /** Adds one registration and rejects duplicate canonical IDs. */
122
145
  register(registration: ProviderRegistration): void {
@@ -132,7 +155,6 @@ export class ProviderRegistry {
132
155
  });
133
156
  }
134
157
  this.#registrations.set(providerId, registration);
135
- this.#revision += 1;
136
158
  }
137
159
  /** Removes one registration and any cached adapter/auth/quota/discovery state. */
138
160
  unregister(providerId: string): boolean {
@@ -142,7 +164,6 @@ export class ProviderRegistry {
142
164
  this.#authentication.delete(normalized);
143
165
  this.#quotaCollectors.delete(normalized);
144
166
  this.#modelDiscovery.delete(normalized);
145
- if (removed) this.#revision += 1;
146
167
  return removed;
147
168
  }
148
169
 
@@ -170,28 +191,25 @@ export class ProviderRegistry {
170
191
  this.#authentication.delete(providerId);
171
192
  this.#quotaCollectors.delete(providerId);
172
193
  this.#modelDiscovery.delete(providerId);
173
- this.#revision += 1;
174
194
  return replaced;
175
195
  }
176
196
 
177
- /** Loads one registered provider, or `undefined` when it is not registered. */
178
- async loadOne(providerId: string): Promise<ProviderAdapter | undefined> {
179
- const normalized = parseProviderId(providerId);
180
- const registration = this.#registrations.get(normalized);
181
- if (registration === undefined) return undefined;
182
- return this.#loadRegistration(normalized, registration);
183
- }
184
-
185
197
  /**
186
198
  * Resolves one adapter on demand, returning the cached instance when it has
187
- * already loaded. This never evaluates a provider's module graph until that
188
- * provider is actually dispatched to, so heavy bespoke adapters
189
- * (protobuf-based providers) stay out of the startup path.
199
+ * already loaded — unless `force` is set, which rebuilds the adapter from
200
+ * the registration (single-flight, and the fresh instance replaces the
201
+ * cache). This never evaluates a provider's module graph until that provider
202
+ * is actually dispatched to, so heavy bespoke adapters (protobuf-based
203
+ * providers) stay out of the startup path. `force` is used by the probe
204
+ * path, which wants the adapter rebuilt against the latest registration
205
+ * rather than a cached instance.
190
206
  */
191
- async resolve(providerId: string): Promise<ProviderAdapter | undefined> {
207
+ async resolve(providerId: string, opts: { readonly force?: boolean } = {}): Promise<ProviderAdapter | undefined> {
192
208
  const normalized = parseProviderId(providerId);
193
- const cached = this.#loaded.get(normalized);
194
- if (cached !== undefined) return cached;
209
+ if (!opts.force) {
210
+ const cached = this.#loaded.get(normalized);
211
+ if (cached !== undefined) return cached;
212
+ }
195
213
  const registration = this.#registrations.get(normalized);
196
214
  if (registration === undefined) return undefined;
197
215
  return this.#loadRegistration(normalized, registration);
@@ -306,7 +324,6 @@ export class ProviderRegistry {
306
324
  metrics.proxy_provider_adapter_load_ms.observe(durationMs);
307
325
  trackAdapterLoad(providerId, durationMs);
308
326
  this.#loaded.set(providerId, adapter);
309
- this.#revision += 1;
310
327
  return adapter;
311
328
  })().finally(() => {
312
329
  this.#loading.delete(providerId);
@@ -315,14 +332,6 @@ export class ProviderRegistry {
315
332
  return task;
316
333
  }
317
334
 
318
- /** Returns the immutable registry snapshot; disabled optional providers are absent. */
319
- snapshot(): ProviderRegistrySnapshot {
320
- return {
321
- revision: this.#revision,
322
- adapters: new Map(this.#loaded),
323
- };
324
- }
325
-
326
335
  /** Returns registration metadata without loading optional code. */
327
336
  registrations(): readonly ProviderRegistration[] {
328
337
  return [...this.#registrations.values()];
@@ -10,6 +10,7 @@
10
10
  */
11
11
  import type { ProviderQuotaWindow } from "./quota-contracts";
12
12
  import { isoDate, number, percentWindow, record, text } from "./quota-contracts";
13
+ import { resolveWindowEmission } from "./quota-window-parser";
13
14
 
14
15
  /** Where one provider's limit entries and their fields live. */
15
16
  export interface LimitWindowShape {
@@ -77,14 +78,24 @@ export function parseLimitWindows(
77
78
  const kind = text(readFirst(limit, shape.kindPaths)) ?? `window-${index + 1}`;
78
79
  const percent = number(readFirst(limit, shape.percentPaths));
79
80
  const reset = isoDate(readFirst(limit, shape.resetPaths));
80
- if (percent === null && reset === null) continue;
81
+ const rawUsed = shape.usedPaths ? number(readFirst(limit, shape.usedPaths)) : null;
81
82
  const limitValue = shape.limitPaths
82
83
  ? number(readFirst(limit, shape.limitPaths))
83
84
  : null;
84
- const usedValue =
85
- (shape.usedPaths ? number(readFirst(limit, shape.usedPaths)) : null) ??
86
- shape.deriveUsed?.(limit, percent, limitValue) ??
87
- null;
85
+ const { emit } = resolveWindowEmission({
86
+ explicitPercent: null,
87
+ usedPercent: percent,
88
+ remainingPercent: null,
89
+ used: rawUsed,
90
+ limit: limitValue,
91
+ reset,
92
+ deriveFromUsedLimit: false,
93
+ // The array walker has always emitted a reset-only window (percent null,
94
+ // reset present); only an entry with neither is dropped.
95
+ emitWithoutPercent: true,
96
+ });
97
+ if (!emit) continue;
98
+ const usedValue = rawUsed ?? shape.deriveUsed?.(limit, percent, limitValue) ?? null;
88
99
  const label =
89
100
  shape.labelFor?.(kind, limit) ?? shape.fallbackLabel?.(kind) ?? humanize(kind);
90
101
  windows.push(percentWindow(kind, label, percent, reset, usedValue, limitValue, "independent"));
@@ -3,7 +3,7 @@ import { cleanError, unsupportedQuota } from "./quota-contracts";
3
3
  import type { ProviderRegistry } from "../provider-registry";
4
4
  import { parseProviderId } from "../provider-contracts";
5
5
  import { providerBaseUrl } from "../provider-metadata";
6
- import { createProbeFetch } from "../operations/probe-fetch";
6
+ import { createProbeFetch } from "../provider-probe";
7
7
  /**
8
8
  * Verifies an API key against the provider's OpenAI-compatible `/models`
9
9
  * endpoint. For providers that expose no billing/quota surface, a reachable
@@ -98,6 +98,75 @@ function findReset(root: unknown, mapping: QuotaWindowMapping): string | null {
98
98
  return mapping.resetFallback ?? null;
99
99
  }
100
100
 
101
+ /**
102
+ * The raw per-window candidates the shared emission core resolves.
103
+ *
104
+ * Both quota walkers (`parseQuotaWindows` and `parseLimitWindows`) feed their
105
+ * independently-read fields into one resolution so the percent fallback chain
106
+ * and the skip rule cannot drift between them.
107
+ */
108
+ export interface WindowFieldCandidates {
109
+ /** Provider-preferred percent (e.g. a `derivePercent` result). */
110
+ readonly explicitPercent: number | null;
111
+ /** Raw used-percent read from the payload. */
112
+ readonly usedPercent: number | null;
113
+ /** Raw remaining-percent read from the payload. */
114
+ readonly remainingPercent: number | null;
115
+ /** Absolute used value. */
116
+ readonly used: number | null;
117
+ /** Absolute limit value. */
118
+ readonly limit: number | null;
119
+ /** Reset timestamp; required to emit a reset-only window. */
120
+ readonly reset: string | null;
121
+ /**
122
+ * Whether percent may be derived from absolute used/limit when no percentage
123
+ * was reported. Only `parseQuotaWindows` does this — `parseLimitWindows`
124
+ * reports percentages and resets the upstream gave it, never ones it
125
+ * invented, so Cline entries stay reset-only instead of gaining a derived
126
+ * percent.
127
+ */
128
+ readonly deriveFromUsedLimit: boolean;
129
+ /**
130
+ * When true, a reset-only window (percent null, reset present) is still
131
+ * emitted. When false/absent, an entry with neither percent nor reset is
132
+ * dropped — never rendered as 0% — and a reset-only window is dropped too.
133
+ */
134
+ readonly emitWithoutPercent?: boolean;
135
+ }
136
+
137
+ /**
138
+ * Shared per-window field resolution: the percent fallback chain plus the
139
+ * "emit or skip" decision.
140
+ *
141
+ * Percent resolution order is exact: explicit percent, then used-percent, then
142
+ * remaining-percent inverted (100 − remaining), then derived from absolute
143
+ * used/limit (only when the limit is positive and `deriveFromUsedLimit` is
144
+ * set). The result is never coerced to 0: a missing percentage stays `null`,
145
+ * so a window can be omitted or rendered as a countdown depending on
146
+ * `emitWithoutPercent`.
147
+ */
148
+ export function resolveWindowEmission(candidates: WindowFieldCandidates): {
149
+ readonly emit: boolean;
150
+ readonly percent: number | null;
151
+ } {
152
+ let percent = candidates.explicitPercent;
153
+ if (percent === null && candidates.usedPercent !== null) percent = candidates.usedPercent;
154
+ if (percent === null && candidates.remainingPercent !== null) {
155
+ percent = Math.max(0, 100 - candidates.remainingPercent);
156
+ }
157
+ if (
158
+ percent === null &&
159
+ candidates.deriveFromUsedLimit &&
160
+ candidates.used !== null &&
161
+ candidates.limit !== null &&
162
+ candidates.limit > 0
163
+ ) {
164
+ percent = (candidates.used / candidates.limit) * 100;
165
+ }
166
+ const emit = percent !== null || (candidates.emitWithoutPercent === true && candidates.reset !== null);
167
+ return { emit, percent };
168
+ }
169
+
101
170
  /** Parses a generic quota payload using the provided window mappings. */
102
171
  export function parseQuotaWindows(
103
172
  body: unknown,
@@ -110,27 +179,30 @@ export function parseQuotaWindows(
110
179
  for (const mapping of windowMappings) {
111
180
  const used = findNumber(payload, mapping.usedPaths);
112
181
  const limit = findNumber(payload, mapping.limitPaths);
182
+ const reset = findReset(payload, mapping);
113
183
 
114
- let percent: number | null = mapping.derivePercent ? mapping.derivePercent(payload) : null;
115
- if (percent === null) {
116
- const usedPercent = findNumber(payload, mapping.usedPercentPaths);
117
- const remainingPercent = findNumber(payload, mapping.remainingPercentPaths);
118
- if (usedPercent !== null) {
119
- percent = usedPercent;
120
- } else if (remainingPercent !== null) {
121
- percent = Math.max(0, 100 - remainingPercent);
122
- } else if (used !== null && limit !== null && limit > 0) {
123
- percent = (used / limit) * 100;
124
- }
125
- }
126
- if (percent !== null && mapping.valueMultiplier !== undefined && mapping.valueMultiplier !== 0) {
127
- percent = Math.min(100, Math.max(0, percent * mapping.valueMultiplier));
128
- }
184
+ const { emit, percent } = resolveWindowEmission({
185
+ explicitPercent: mapping.derivePercent ? mapping.derivePercent(payload) : null,
186
+ usedPercent: findNumber(payload, mapping.usedPercentPaths),
187
+ remainingPercent: findNumber(payload, mapping.remainingPercentPaths),
188
+ used,
189
+ limit,
190
+ reset,
191
+ deriveFromUsedLimit: true,
192
+ ...(mapping.emitWithoutPercent === undefined ? {} : { emitWithoutPercent: mapping.emitWithoutPercent }),
193
+ });
194
+ if (!emit) continue;
129
195
 
130
- const reset = findReset(payload, mapping);
131
- if (percent === null && !(mapping.emitWithoutPercent === true && reset !== null)) continue;
196
+ let resolvedPercent = percent;
197
+ if (
198
+ resolvedPercent !== null &&
199
+ mapping.valueMultiplier !== undefined &&
200
+ mapping.valueMultiplier !== 0
201
+ ) {
202
+ resolvedPercent = Math.min(100, Math.max(0, resolvedPercent * mapping.valueMultiplier));
203
+ }
132
204
 
133
- windows.push(percentWindow(mapping.kind, mapping.label, percent, reset, used, limit, "independent"));
205
+ windows.push(percentWindow(mapping.kind, mapping.label, resolvedPercent, reset, used, limit, "independent"));
134
206
  }
135
207
 
136
208
  if (options.extraWindows) windows.push(...options.extraWindows(payload));
@@ -18,6 +18,7 @@ import type { ProviderId } from "../provider-contracts";
18
18
  import type { ProviderRegistry } from "../provider-registry";
19
19
  import { quotaLens, setCachedQuota } from "./cache";
20
20
  import { persistQuotaSnapshot } from "./snapshot";
21
+ import { applyAccountVerdict } from "../operations/account-health-service";
21
22
  import { asJsonbRecord } from "../../shared/coerce";
22
23
  import { KIRO_DEFAULT_LABELS } from "../integrations/kiro/kiro-oauth";
23
24
 
@@ -150,12 +151,52 @@ export async function recordAccountCheck(
150
151
  db: CartethyiaDatabase,
151
152
  accountId: string,
152
153
  outcome: { ok: boolean; error?: string | null; category?: string | null },
154
+ snapshotInvalidator?: { invalidate(): unknown },
153
155
  ): Promise<void> {
154
- const activeOnly = and(
155
- eq(providerAccounts.id, accountId),
156
- eq(providerAccounts.status, "active"),
157
- );
158
156
  if (outcome.ok) {
157
+ // A successful quota refresh is the freshest evidence about the account's
158
+ // health: heal a cooldown that the refresh shows is no longer justified, so
159
+ // the account returns to rotation immediately instead of waiting out the
160
+ // remainder of a stale fallback window. Only healable scopes heal — an
161
+ // auth/policy cooldown must never be lifted by a quota report (the
162
+ // credential is still refused). `lastErrorCategory` carries the category
163
+ // that parked the account, so the heal is scoped by the same verdict the
164
+ // health machine wrote.
165
+ const healableCategories = new Set([
166
+ "quota_exhausted",
167
+ "rate_limit_transient",
168
+ "model_capacity",
169
+ "model_unreachable",
170
+ "unknown",
171
+ "check_failed",
172
+ ]);
173
+ const rows = await db
174
+ .select({ status: providerAccounts.status, lastErrorCategory: providerAccounts.lastErrorCategory })
175
+ .from(providerAccounts)
176
+ .where(eq(providerAccounts.id, accountId))
177
+ .limit(1);
178
+ const account = rows[0];
179
+ const healable =
180
+ account?.status === "cooldown" &&
181
+ (account.lastErrorCategory === null || healableCategories.has(account.lastErrorCategory));
182
+ if (healable) {
183
+ const healed = await db
184
+ .update(providerAccounts)
185
+ .set({
186
+ status: "active",
187
+ lastSuccessAt: new Date(),
188
+ lastError: null,
189
+ lastErrorCategory: null,
190
+ lastErrorAt: null,
191
+ consecutiveFailures: 0,
192
+ cooldownUntil: null,
193
+ lastRecoveredAt: new Date(),
194
+ })
195
+ .where(eq(providerAccounts.id, accountId))
196
+ .returning({ id: providerAccounts.id });
197
+ if (healed.length > 0) await snapshotInvalidator?.invalidate();
198
+ return;
199
+ }
159
200
  await db
160
201
  .update(providerAccounts)
161
202
  .set({
@@ -165,7 +206,7 @@ export async function recordAccountCheck(
165
206
  lastErrorAt: null,
166
207
  consecutiveFailures: 0,
167
208
  })
168
- .where(activeOnly);
209
+ .where(and(eq(providerAccounts.id, accountId), eq(providerAccounts.status, "active")));
169
210
  return;
170
211
  }
171
212
  const transient = isTransientTimeout(outcome.error);
@@ -186,7 +227,7 @@ export async function recordAccountCheck(
186
227
  lastErrorAt: new Date(),
187
228
  ...(transient ? {} : { consecutiveFailures: sql`${providerAccounts.consecutiveFailures} + 1` }),
188
229
  })
189
- .where(activeOnly)
230
+ .where(and(eq(providerAccounts.id, accountId), eq(providerAccounts.status, "active")))
190
231
  .returning({ consecutiveFailures: providerAccounts.consecutiveFailures });
191
232
  if (transient) return;
192
233
  const failures = failing[0]?.consecutiveFailures ?? 0;
@@ -196,13 +237,18 @@ export async function recordAccountCheck(
196
237
  // pass. The cooldown clears itself; a success before it elapses resets the
197
238
  // count above.
198
239
  if (failures >= QUOTA_FAILURE_PARK_THRESHOLD) {
199
- await db
200
- .update(providerAccounts)
201
- .set({
202
- status: "cooldown",
203
- cooldownUntil: new Date(Date.now() + QUOTA_FAILURE_COOLDOWN_MS),
204
- })
205
- .where(activeOnly);
240
+ // Converge on the health application seam (proposal §3.5): quota refresh
241
+ // parking is a producer into `applyAccountVerdict`, not a separate state
242
+ // machine.
243
+ const parked = await applyAccountVerdict(db, {
244
+ accountId,
245
+ status: "cooldown",
246
+ reason: outcome.error ?? "Account check failed",
247
+ category: outcome.category ?? "quota_fetch_failed",
248
+ cooldownUntil: new Date(Date.now() + QUOTA_FAILURE_COOLDOWN_MS),
249
+ fromStatus: "active",
250
+ });
251
+ if (parked) await snapshotInvalidator?.invalidate();
206
252
  }
207
253
  }
208
254
 
@@ -427,7 +473,13 @@ async function runQuotaRefresh(
427
473
  try {
428
474
  await deps.db
429
475
  .update(providerAccounts)
430
- .set({ quotaPlan: quota.plan })
476
+ .set({
477
+ quotaPlan: quota.plan,
478
+ // Freshness stamp for the plan column: routing uses it to downgrade
479
+ // a plan older than the freshness horizon to `unknown` instead of
480
+ // trusting a weeks-old string.
481
+ quotaPlanFetchedAt: new Date(),
482
+ })
431
483
  .where(eq(providerAccounts.id, target.accountId));
432
484
  } catch (error) {
433
485
  log.warn(`[quota] failed to record plan for account=${target.accountId}`, error as Error);
@@ -469,10 +521,15 @@ async function runQuotaRefresh(
469
521
  }
470
522
  }
471
523
  try {
472
- await recordAccountCheck(deps.db, target.accountId, {
473
- ok: quota.error === null,
474
- ...(quota.error === null ? {} : { error: quota.error, category: failure ?? "quota_fetch_failed" }),
475
- });
524
+ await recordAccountCheck(
525
+ deps.db,
526
+ target.accountId,
527
+ {
528
+ ok: quota.error === null,
529
+ ...(quota.error === null ? {} : { error: quota.error, category: failure ?? "quota_fetch_failed" }),
530
+ },
531
+ deps.snapshotInvalidator,
532
+ );
476
533
  } catch (error) {
477
534
  // A stamp failure must not lose the freshly fetched quota.
478
535
  log.warn(`[quota] failed to stamp check outcome for account=${target.accountId}`, error as Error);
@@ -1,7 +1,7 @@
1
1
  /** Bounded process-local authorization cache; shared-cache I/O stays in the auth service. */
2
2
  export const AUTH_CACHE_TTL_MS = 3_000;
3
3
  const AUTH_CACHE_MAX_ENTRIES = 10_000;
4
- const AUTH_NEGATIVE_CACHE_TTL_MS = 1_500;
4
+ export const AUTH_NEGATIVE_CACHE_TTL_MS = 1_500;
5
5
  const AUTH_FAILURE_COOLDOWN_MS = 1_000;
6
6
 
7
7
  interface CacheEntry<T> {
@@ -21,8 +21,12 @@ import type { ApiKeyModelAccessMode } from "../persistence/schema";
21
21
  * snapshot's consumed value is a reference point, so a brief stale window
22
22
  * cannot meaningfully over-consume a budget.
23
23
  *
24
- * Negative results (unknown/revoked tokens) are deliberately NOT cached, so
25
- * a newly minted key is usable immediately.
24
+ * Unknown or revoked tokens are negative-cached for
25
+ * `AUTH_NEGATIVE_CACHE_TTL_MS` (1.5s): a token rejected within that window
26
+ * stays refused even if its key is minted or un-revoked in between. A newly
27
+ * minted key is usable immediately only because `invalidateApiKeyCache`
28
+ * clears the negative entries when a mutation lands — not because negatives
29
+ * are absent.
26
30
  */
27
31
  const authorizationCache = createApiKeyAuthorizationCache<ResolvedApiKey>();
28
32