cartethyia 2.7.9 → 2.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (496) hide show
  1. package/.env.example +6 -5
  2. package/dist/dashboard/assets/{BannedPage-BxoG1MrL.js → BannedPage-Dq530awD.js} +1 -1
  3. package/dist/dashboard/assets/BannedPage-Dq530awD.js.gz +0 -0
  4. package/dist/dashboard/assets/{CliToolDetailPage-ixwgKk53.js → CliToolDetailPage-DZzaS-7S.js} +1 -1
  5. package/dist/dashboard/assets/CliToolDetailPage-DZzaS-7S.js.gz +0 -0
  6. package/dist/dashboard/assets/{CliToolsPage-BmttNV3U.js → CliToolsPage-DaZZ3cqP.js} +1 -1
  7. package/dist/dashboard/assets/CliToolsPage-DaZZ3cqP.js.gz +0 -0
  8. package/dist/dashboard/assets/{CombosPage-BE3qwwfC.js → CombosPage-BlC_xdkX.js} +1 -1
  9. package/dist/dashboard/assets/CombosPage-BlC_xdkX.js.gz +0 -0
  10. package/dist/dashboard/assets/{ConsoleLogPage-CRZdmMIX.js → ConsoleLogPage-D-rqeVtG.js} +1 -1
  11. package/dist/dashboard/assets/ConsoleLogPage-D-rqeVtG.js.gz +0 -0
  12. package/dist/dashboard/assets/{CustomizationPage-Dv_FJ6Er.js → CustomizationPage-7xT5yg2E.js} +1 -1
  13. package/dist/dashboard/assets/CustomizationPage-7xT5yg2E.js.gz +0 -0
  14. package/dist/dashboard/assets/HealthEventsModal-Clq8E9cW.js +1 -0
  15. package/dist/dashboard/assets/HealthEventsModal-Clq8E9cW.js.gz +0 -0
  16. package/dist/dashboard/assets/{InjectSystemPromptPanel-BOoO8rpz.js → InjectSystemPromptPanel-6Teoq9Se.js} +2 -2
  17. package/dist/dashboard/assets/InjectSystemPromptPanel-6Teoq9Se.js.gz +0 -0
  18. package/dist/dashboard/assets/{LoginPage-ChJuNNVN.js → LoginPage-CZuI0MAR.js} +1 -1
  19. package/dist/dashboard/assets/LoginPage-CZuI0MAR.js.gz +0 -0
  20. package/dist/dashboard/assets/{ModelPicker-BZAJtPg1.js → ModelPicker-C1hNTlkr.js} +1 -1
  21. package/dist/dashboard/assets/ModelPicker-C1hNTlkr.js.gz +0 -0
  22. package/dist/dashboard/assets/{OverviewPage-BE40s3u5.js → OverviewPage-0yfhSI1C.js} +1 -1
  23. package/dist/dashboard/assets/OverviewPage-0yfhSI1C.js.gz +0 -0
  24. package/dist/dashboard/assets/ProviderDetailPage-xqTxdQ4c.js +8 -0
  25. package/dist/dashboard/assets/ProviderDetailPage-xqTxdQ4c.js.gz +0 -0
  26. package/dist/dashboard/assets/{ProviderIcon-CQ6bPUf_.js → ProviderIcon-Ddhm1InL.js} +1 -1
  27. package/dist/dashboard/assets/ProviderIcon-Ddhm1InL.js.gz +0 -0
  28. package/dist/dashboard/assets/ProvidersPage-C3fYuu6i.js +1 -0
  29. package/dist/dashboard/assets/ProvidersPage-C3fYuu6i.js.gz +0 -0
  30. package/dist/dashboard/assets/ProxyPage-CHtJtZ3u.js +9 -0
  31. package/dist/dashboard/assets/ProxyPage-CHtJtZ3u.js.gz +0 -0
  32. package/dist/dashboard/assets/QuotaPage-D1RV_2BX.js +1 -0
  33. package/dist/dashboard/assets/QuotaPage-D1RV_2BX.js.gz +0 -0
  34. package/dist/dashboard/assets/{SettingsPage-C5HPWqEv.js → SettingsPage-BzZAvYSX.js} +1 -1
  35. package/dist/dashboard/assets/SettingsPage-BzZAvYSX.js.gz +0 -0
  36. package/dist/dashboard/assets/{SetupPage-CFNcATJJ.js → SetupPage--s-EfEIL.js} +1 -1
  37. package/dist/dashboard/assets/SetupPage--s-EfEIL.js.gz +0 -0
  38. package/dist/dashboard/assets/{StudioPage-YoD4tY5l.js → StudioPage-BjXFGSK4.js} +1 -1
  39. package/dist/dashboard/assets/StudioPage-BjXFGSK4.js.gz +0 -0
  40. package/dist/dashboard/assets/{UsagePage-DxjaAhb_.js → UsagePage-CP8F8BN7.js} +1 -1
  41. package/dist/dashboard/assets/UsagePage-CP8F8BN7.js.gz +0 -0
  42. package/dist/dashboard/assets/{api-keys-BTlkq_Uk.js → api-keys-2hc12Uz2.js} +1 -1
  43. package/dist/dashboard/assets/api-keys-2hc12Uz2.js.gz +0 -0
  44. package/dist/dashboard/assets/app-DGXbNfAR.css +2 -0
  45. package/dist/dashboard/assets/app-DGXbNfAR.css.gz +0 -0
  46. package/dist/dashboard/assets/assert-BMcj1xZE.js +1 -0
  47. package/dist/dashboard/assets/assert-BMcj1xZE.js.gz +0 -0
  48. package/dist/dashboard/assets/{badge-DdbT_7Am.js → badge-DkRUuVfz.js} +1 -1
  49. package/dist/dashboard/assets/badge-DkRUuVfz.js.gz +0 -0
  50. package/dist/dashboard/assets/{banner-CsGN-nbm.js → banner-CDjW841Q.js} +1 -1
  51. package/dist/dashboard/assets/banner-CDjW841Q.js.gz +0 -0
  52. package/dist/dashboard/assets/{card-mgZ_TLLZ.js → card-Bto5UlL9.js} +1 -1
  53. package/dist/dashboard/assets/card-Bto5UlL9.js.gz +0 -0
  54. package/dist/dashboard/assets/{cli-tools-A1-v8-03.js → cli-tools-DaH6g9hL.js} +1 -1
  55. package/dist/dashboard/assets/cli-tools-DaH6g9hL.js.gz +0 -0
  56. package/dist/dashboard/assets/{clipboard-button-CJ4Zty89.js → clipboard-button-BYKBCzjk.js} +1 -1
  57. package/dist/dashboard/assets/clipboard-button-BYKBCzjk.js.gz +0 -0
  58. package/dist/dashboard/assets/{dialog-B5L2jPQ_.js → dialog-DL0Zftwv.js} +1 -1
  59. package/dist/dashboard/assets/dialog-DL0Zftwv.js.gz +0 -0
  60. package/dist/dashboard/assets/{drawer-DkjUouAj.js → drawer-Dpx_UXaS.js} +1 -1
  61. package/dist/dashboard/assets/drawer-Dpx_UXaS.js.gz +0 -0
  62. package/dist/dashboard/assets/{entry-B-LVLV6o.js → entry-BV7-1NoM.js} +1 -1
  63. package/dist/dashboard/assets/entry-BV7-1NoM.js.gz +0 -0
  64. package/dist/dashboard/assets/{entry-CAhdYl3B.js → entry-CGvdNU6r.js} +1 -1
  65. package/dist/dashboard/assets/entry-CGvdNU6r.js.gz +0 -0
  66. package/dist/dashboard/assets/entry-DScKYvZj.js +4 -0
  67. package/dist/dashboard/assets/entry-DScKYvZj.js.gz +0 -0
  68. package/dist/dashboard/assets/{formatters-CrsdLVsn.js → formatters-BS3DZlZj.js} +1 -1
  69. package/dist/dashboard/assets/formatters-BS3DZlZj.js.gz +0 -0
  70. package/dist/dashboard/assets/{index-BU64Lqvp.js → index-SQnvvTWX.js} +2 -2
  71. package/dist/dashboard/assets/index-SQnvvTWX.js.gz +0 -0
  72. package/dist/dashboard/assets/{inline-B2aDLyyg.js → inline-Dl-cuLK0.js} +1 -1
  73. package/dist/dashboard/assets/inline-Dl-cuLK0.js.gz +0 -0
  74. package/dist/dashboard/assets/{input-BCVkdR9d.js → input-Bfx0EzRc.js} +1 -1
  75. package/dist/dashboard/assets/input-Bfx0EzRc.js.gz +0 -0
  76. package/dist/dashboard/assets/{layout-DNcAdMK-.js → layout-BxhbQ_JS.js} +1 -1
  77. package/dist/dashboard/assets/layout-BxhbQ_JS.js.gz +0 -0
  78. package/dist/dashboard/assets/{list-row-DgrIH0hw.js → list-row-D49Y4ptn.js} +1 -1
  79. package/dist/dashboard/assets/list-row-D49Y4ptn.js.gz +0 -0
  80. package/dist/dashboard/assets/{live-Cae01iu8.js → live-CbVTdX9V.js} +1 -1
  81. package/dist/dashboard/assets/live-CbVTdX9V.js.gz +0 -0
  82. package/dist/dashboard/assets/{network-CRib7H_2.js → network-JGvDveof.js} +1 -1
  83. package/dist/dashboard/assets/network-JGvDveof.js.gz +0 -0
  84. package/dist/dashboard/assets/{numbers-BKQE8Xpz.js → numbers-Dw6kiew-.js} +1 -1
  85. package/dist/dashboard/assets/numbers-Dw6kiew-.js.gz +0 -0
  86. package/dist/dashboard/assets/{pagination-ecPvG_8g.js → pagination-iUoI8GeE.js} +1 -1
  87. package/dist/dashboard/assets/pagination-iUoI8GeE.js.gz +0 -0
  88. package/dist/dashboard/assets/providers/sensenova-intl.png +0 -0
  89. package/dist/dashboard/assets/providers/sensenova.png +0 -0
  90. package/dist/dashboard/assets/providers/zro.svg +3 -0
  91. package/dist/dashboard/assets/providers/zro.svg.gz +0 -0
  92. package/dist/dashboard/assets/quota-DiPD6UKH.js +1 -0
  93. package/dist/dashboard/assets/quota-DiPD6UKH.js.gz +0 -0
  94. package/dist/dashboard/assets/{routing-Ckm1UXuC.js → routing-8jZr-3WE.js} +1 -1
  95. package/dist/dashboard/assets/routing-8jZr-3WE.js.gz +0 -0
  96. package/dist/dashboard/assets/{select-ChoFWyKz.js → select-oO1FxMVb.js} +2 -2
  97. package/dist/dashboard/assets/select-oO1FxMVb.js.gz +0 -0
  98. package/dist/dashboard/assets/{settings-CKjFPlKW.js → settings-CLr3kdIx.js} +1 -1
  99. package/dist/dashboard/assets/settings-CLr3kdIx.js.gz +0 -0
  100. package/dist/dashboard/assets/{stack-DWCwU3fh.js → stack-Y3pDteZr.js} +1 -1
  101. package/dist/dashboard/assets/stack-Y3pDteZr.js.gz +0 -0
  102. package/dist/dashboard/assets/{state-Cg3VL5J_.js → state-CArX_Iak.js} +1 -1
  103. package/dist/dashboard/assets/state-CArX_Iak.js.gz +0 -0
  104. package/dist/dashboard/assets/{switch-Db70BMix.js → switch-DwlwmDO-.js} +1 -1
  105. package/dist/dashboard/assets/switch-DwlwmDO-.js.gz +0 -0
  106. package/dist/dashboard/assets/{useMutation-BqdE0LuM.js → useMutation-DJ9GYEgf.js} +1 -1
  107. package/dist/dashboard/assets/useMutation-DJ9GYEgf.js.gz +0 -0
  108. package/dist/dashboard/index.html +2 -2
  109. package/dist/dashboard/index.html.gz +0 -0
  110. package/dist/dashboard/precompressed-assets.json +54 -53
  111. package/migrations/full-postgres/0046_add_accounts_quota_plan_fetched_at.sql +7 -0
  112. package/migrations/full-postgres/0047_add_provider_device_flow_proxy.sql +17 -0
  113. package/migrations/lite-sqlite3/0046_add_accounts_quota_plan_fetched_at.sql +4 -0
  114. package/migrations/lite-sqlite3/0047_add_provider_device_flow_proxy.sql +19 -0
  115. package/package.json +1 -1
  116. package/src/boot/app.ts +11 -14
  117. package/src/boot/dependencies.ts +5 -7
  118. package/src/boot/flush.ts +20 -0
  119. package/src/boot/lifecycle.ts +7 -5
  120. package/src/config/providers.ts +6 -5
  121. package/src/config/routing.ts +26 -0
  122. package/src/config/spec-metadata.ts +72 -0
  123. package/src/config/spec.ts +28 -8
  124. package/src/console/api-keys/contracts.ts +12 -1
  125. package/src/console/api-keys/routes.ts +40 -28
  126. package/src/console/auth/account-reset.ts +7 -9
  127. package/src/console/backup/contracts.ts +4 -0
  128. package/src/console/backup/export-endpoint.ts +7 -5
  129. package/src/console/backup/nine-router.ts +71 -10
  130. package/src/console/backup/service.ts +8 -1
  131. package/src/console/cli-tools/service.ts +6 -12
  132. package/src/console/console-router-guards.ts +23 -16
  133. package/src/console/console-router.ts +1 -1
  134. package/src/console/domain-registration.ts +7 -4
  135. package/src/console/inferhub/client.ts +1 -1
  136. package/src/console/inferhub/normalize.ts +24 -28
  137. package/src/console/inferhub/types.ts +3 -0
  138. package/src/console/ip-bans/routes.ts +7 -13
  139. package/src/console/observability/live.ts +2 -2
  140. package/src/console/observability/store.ts +0 -53
  141. package/src/console/providers/catalog/contracts.ts +9 -3
  142. package/src/console/providers/catalog/model-operations.ts +2 -3
  143. package/src/console/providers/catalog/provider-operations.ts +1 -1
  144. package/src/console/providers/catalog/routes.ts +3 -2
  145. package/src/console/providers/catalog/store.ts +1914 -1902
  146. package/src/console/providers/detail/routes.ts +1 -0
  147. package/src/console/providers/detail/store.ts +4 -0
  148. package/src/console/providers/oauth/callback-listener.ts +30 -11
  149. package/src/console/providers/oauth/device-egress.ts +123 -0
  150. package/src/console/providers/oauth/routes.ts +123 -16
  151. package/src/console/quota/account-quota-global.ts +14 -24
  152. package/src/console/quota/account-quota-shared.ts +49 -0
  153. package/src/console/quota/account-quota-tenant.ts +11 -23
  154. package/src/console/quota/account-test-egress.ts +2 -2
  155. package/src/console/quota/daily-growth-job.ts +1 -1
  156. package/src/console/quota/quota-view.ts +19 -3
  157. package/src/console/routing/pools/contracts.ts +1 -1
  158. package/src/console/routing/pools/operations.ts +32 -47
  159. package/src/console/routing/pools/probe-verdict.ts +2 -2
  160. package/src/console/routing/pools/relay-deploy.ts +1 -1
  161. package/src/console/routing/pools/store.ts +4 -4
  162. package/src/console/shared/errors.ts +33 -6
  163. package/src/main.ts +2 -3
  164. package/src/network/client.ts +67 -0
  165. package/src/network/egress/bidi-session.ts +322 -0
  166. package/src/network/egress/dial.ts +201 -0
  167. package/src/network/egress/factory.ts +191 -0
  168. package/src/network/{outbound-fetch.ts → egress/fetch.ts} +195 -29
  169. package/src/network/egress/tls-profile.ts +123 -0
  170. package/src/network/egress/ws.ts +175 -0
  171. package/src/network/pool/{byte-accounting.ts → accounting.ts} +3 -2
  172. package/src/network/{types.ts → pool/agent-config.ts} +1 -1
  173. package/src/network/pool/agent.ts +2 -2
  174. package/src/network/{pool-cooldown-store.ts → pool/cooldown-store.ts} +95 -20
  175. package/src/network/{pool-health-machine.ts → pool/health.ts} +205 -6
  176. package/src/network/pool/resolver.ts +10 -849
  177. package/src/network/pool/selector.ts +184 -41
  178. package/src/network/ssrf.ts +7 -7
  179. package/src/network/{retry.ts → util/retry.ts} +1 -1
  180. package/src/network/{rate-limit-verdict.ts → verdict.ts} +1 -1
  181. package/src/observability/event-log.ts +7 -361
  182. package/src/observability/performance-metrics.ts +9 -0
  183. package/src/observability/runtime-metrics.ts +14 -9
  184. package/src/observability/telemetry-buffer.ts +16 -26
  185. package/src/observability/telemetry-reconciliation.ts +9 -37
  186. package/src/observability/telemetry-row.ts +125 -0
  187. package/src/persistence/db-mode.ts +4 -1
  188. package/src/persistence/dialect.ts +18 -0
  189. package/src/persistence/migrate.ts +6 -3
  190. package/src/persistence/schema.ts +17 -0
  191. package/src/persistence/ttl-cache.ts +21 -3
  192. package/src/protocol/codecs.ts +59 -2
  193. package/src/protocol/dsml.ts +34 -86
  194. package/src/protocol/markup-stream-parser.ts +195 -0
  195. package/src/protocol/reasoning-text.ts +19 -0
  196. package/src/protocol/request/chat.ts +10 -10
  197. package/src/protocol/request/gemini.ts +8 -2
  198. package/src/protocol/request/messages.ts +34 -22
  199. package/src/protocol/response/chat.ts +12 -20
  200. package/src/protocol/response/messages.ts +34 -7
  201. package/src/protocol/response/omp-tool-call-markup.ts +43 -86
  202. package/src/protocol/response/responses.ts +18 -2
  203. package/src/protocol/stop-reason.ts +23 -1
  204. package/src/protocol/think-split.ts +112 -0
  205. package/src/protocol/translation/dialect/qwen.ts +26 -58
  206. package/src/protocol/translation/quirks.ts +15 -0
  207. package/src/protocol/upstream/send-claude-messages.ts +2 -2
  208. package/src/providers/adapters/compatible-adapter.ts +76 -12
  209. package/src/providers/authentication/oauth-client.ts +40 -6
  210. package/src/providers/authentication/oauth-device-flow.ts +105 -11
  211. package/src/providers/authentication/oauth-flow-store.ts +82 -1
  212. package/src/providers/authentication/oauth-refresh-service.ts +766 -758
  213. package/src/providers/configured-provider.ts +148 -0
  214. package/src/providers/default-registry.ts +137 -36
  215. package/src/providers/discovery/discovery-types.ts +7 -0
  216. package/src/providers/discovery/probe-phases.ts +20 -6
  217. package/src/providers/discovery/probing-service.ts +22 -2
  218. package/src/providers/integrations/alibabacp/alibabacp-models.ts +1 -2
  219. package/src/providers/integrations/{dashscope-models.ts → alibabacp/dashscope-models.ts} +2 -2
  220. package/src/providers/integrations/antigravity/antigravity-protocol.ts +16 -1
  221. package/src/providers/integrations/buddy/account.ts +969 -0
  222. package/src/providers/integrations/buddy/capabilities.ts +12 -12
  223. package/src/providers/integrations/buddy/{buddy-catalog-shared.ts → catalog.ts} +198 -136
  224. package/src/providers/integrations/buddy/{buddy-chat-shared.ts → chat.ts} +176 -20
  225. package/src/providers/integrations/buddy/codebuddy/index.ts +429 -0
  226. package/src/providers/integrations/buddy/codebuddy/shared.ts +41 -26
  227. package/src/providers/integrations/buddy/headers.ts +311 -0
  228. package/src/providers/integrations/buddy/lifecycle.ts +1018 -0
  229. package/src/providers/integrations/buddy/{buddy-media.ts → media.ts} +1 -1
  230. package/src/providers/integrations/claude/claude-quota.ts +14 -17
  231. package/src/providers/integrations/cline/cline-oauth.ts +10 -5
  232. package/src/providers/integrations/cline/cline.ts +5 -6
  233. package/src/providers/integrations/clixad/clixad-oauth.ts +98 -0
  234. package/src/providers/integrations/clixad/clixad-quota.ts +132 -0
  235. package/src/providers/integrations/clixad/provider.ts +145 -0
  236. package/src/providers/integrations/codex/codex-oauth.ts +8 -5
  237. package/src/providers/integrations/commandcode/quota.ts +22 -28
  238. package/src/providers/integrations/cursor/cursor-auth.ts +1 -9
  239. package/src/providers/integrations/cursor/cursor-exec.ts +9 -2
  240. package/src/providers/integrations/cursor/cursor-hardening.ts +279 -0
  241. package/src/providers/integrations/cursor/cursor-http2.ts +605 -6
  242. package/src/providers/integrations/cursor/cursor-request.ts +26 -12
  243. package/src/providers/integrations/cursor/cursor-shared.ts +8 -5
  244. package/src/providers/integrations/cursor/cursor.ts +574 -110
  245. package/src/providers/integrations/cursor/protocol/messages.ts +22 -29
  246. package/src/providers/integrations/cursor/protocol/protobuf-schema.ts +37 -0
  247. package/src/providers/integrations/deepai/deepai-catalog.ts +149 -0
  248. package/src/providers/integrations/deepai/deepai-common.ts +145 -0
  249. package/src/providers/integrations/deepai/deepai-oauth.ts +129 -0
  250. package/src/providers/integrations/deepai/deepai-quota.ts +97 -0
  251. package/src/providers/integrations/deepai/provider.ts +42 -0
  252. package/src/providers/integrations/devin/devin-auth.ts +7 -0
  253. package/src/providers/integrations/devin/protocol/devin-proto.ts +1 -27
  254. package/src/providers/integrations/factory/factory-oauth.ts +33 -51
  255. package/src/providers/integrations/freebuff/assets/official-system-manager.txt +88 -0
  256. package/src/providers/integrations/freebuff/assets/official-system-worker.txt +57 -0
  257. package/src/providers/integrations/freebuff/assets/official-tools.json +1070 -0
  258. package/src/providers/integrations/freebuff/freebuff-catalog.ts +120 -0
  259. package/src/providers/integrations/freebuff/freebuff-constants.ts +1233 -0
  260. package/src/providers/integrations/freebuff/freebuff-discovery.ts +108 -0
  261. package/src/providers/integrations/freebuff/freebuff-oauth.ts +240 -0
  262. package/src/providers/integrations/freebuff/freebuff-quota.ts +260 -0
  263. package/src/providers/integrations/freebuff/freebuff-virtual.ts +89 -0
  264. package/src/providers/integrations/freebuff/freebuff.ts +682 -0
  265. package/src/providers/integrations/freepi/freepi-catalog.ts +5 -0
  266. package/src/providers/integrations/freepi/freepi-discovery.ts +2 -2
  267. package/src/providers/integrations/freepi/freepi-headers.ts +2 -2
  268. package/src/providers/integrations/freepi/freepi-protocol.ts +11 -2
  269. package/src/providers/integrations/freepi/freepi-realm.ts +0 -3
  270. package/src/providers/integrations/freepi/freepi-session.ts +158 -0
  271. package/src/providers/integrations/freepi/freepi.ts +80 -54
  272. package/src/providers/integrations/github/github-discovery.ts +30 -13
  273. package/src/providers/integrations/github/github-oauth.ts +217 -44
  274. package/src/providers/integrations/github/github-quota.ts +22 -5
  275. package/src/providers/integrations/github/github.ts +40 -32
  276. package/src/providers/integrations/grok/grok-oauth.ts +52 -49
  277. package/src/providers/integrations/grok/grok-ratelimit-store.ts +10 -17
  278. package/src/providers/integrations/grok/grok-turn-index.ts +11 -16
  279. package/src/providers/integrations/hermes/hermes-oauth.ts +3 -3
  280. package/src/providers/integrations/{hermes.ts → hermes/hermes.ts} +42 -13
  281. package/src/providers/integrations/hermes/index.ts +2 -0
  282. package/src/providers/integrations/kilo/kilo-oauth.ts +4 -4
  283. package/src/providers/integrations/kilo/kilo.ts +1 -1
  284. package/src/providers/integrations/kimi/kimi-oauth.ts +3 -3
  285. package/src/providers/integrations/kiro/kiro-catalog.ts +17 -1
  286. package/src/providers/integrations/kiro/kiro-discovery.ts +10 -12
  287. package/src/providers/integrations/kiro/kiro-oauth.ts +2 -2
  288. package/src/providers/integrations/kiro/kiro-quota.ts +8 -4
  289. package/src/providers/integrations/kiro/kiro.ts +15 -19
  290. package/src/providers/integrations/muse/muse-oauth.ts +5 -5
  291. package/src/providers/integrations/ollamacloud/index.ts +2 -0
  292. package/src/providers/integrations/{ollamacloud-models.ts → ollamacloud/models.ts} +2 -2
  293. package/src/providers/integrations/{ollamacloud-quota.ts → ollamacloud/quota.ts} +3 -3
  294. package/src/providers/integrations/opencode/fingerprint.ts +1 -1
  295. package/src/providers/integrations/opencode/opencode-go-quota.ts +2 -8
  296. package/src/providers/integrations/openrouter/provider.ts +1 -1
  297. package/src/providers/integrations/perch/perch-credential.ts +25 -0
  298. package/src/providers/integrations/perch/perch-protocol.ts +0 -6
  299. package/src/providers/integrations/perch/perch-quota.ts +2 -16
  300. package/src/providers/integrations/perch/perch.ts +6 -16
  301. package/src/providers/integrations/qoder/claim-qoder-credits.ts +19 -60
  302. package/src/providers/integrations/qoder/qoder-adapter.ts +20 -125
  303. package/src/providers/integrations/qoder/qoder-auth.ts +109 -30
  304. package/src/providers/integrations/qoder/qoder-catalog.ts +21 -6
  305. package/src/providers/integrations/qoder/qoder-chat.ts +32 -29
  306. package/src/providers/integrations/qoder/qoder-checkin.ts +5 -5
  307. package/src/providers/integrations/qoder/qoder-cosy.ts +37 -9
  308. package/src/providers/integrations/qoder/qoder-oauth.ts +9 -2
  309. package/src/providers/integrations/qoder/qoder-realm.ts +13 -36
  310. package/src/providers/integrations/qoder/qoder-sse.ts +447 -68
  311. package/src/providers/integrations/qoder/qoder-types.ts +9 -0
  312. package/src/providers/integrations/qoder/qoder.ts +3 -26
  313. package/src/providers/integrations/sensenova/provider.ts +144 -0
  314. package/src/providers/integrations/trae/trae-protocol.ts +9 -5
  315. package/src/providers/integrations/trae/trae-quota.ts +21 -29
  316. package/src/providers/integrations/trae/trae-realm.ts +38 -0
  317. package/src/providers/integrations/trae/trae.ts +7 -24
  318. package/src/providers/integrations/vercel/provider.ts +117 -0
  319. package/src/providers/integrations/vercel/vercel-quota.ts +96 -0
  320. package/src/providers/integrations/xai/xai-oauth.ts +5 -5
  321. package/src/providers/integrations/zcode-start-plan/gifts.ts +1 -1
  322. package/src/providers/integrations/zcode-start-plan/oauth.ts +3 -3
  323. package/src/providers/integrations/zcode-start-plan/provider.ts +226 -23
  324. package/src/providers/integrations/zcode-start-plan/quota.ts +19 -2
  325. package/src/providers/integrations/zro/zro.ts +205 -0
  326. package/src/providers/operations/account-error-classification.ts +199 -307
  327. package/src/providers/operations/account-health-service.ts +82 -1
  328. package/src/providers/operations/account-refresh-status.ts +36 -37
  329. package/src/providers/operations/account-tier.ts +15 -16
  330. package/src/providers/operations/client-version-resolver.ts +1 -1
  331. package/src/providers/operations/client-versions.ts +100 -81
  332. package/src/providers/operations/cooldown-rules.ts +11 -16
  333. package/src/providers/operations/oauth-refresh-lead.ts +46 -22
  334. package/src/providers/operations/{model-catalog-cache.ts → provider-caches.ts} +68 -16
  335. package/src/providers/operations/provider-catalog-service.ts +1 -1
  336. package/src/providers/operations/provider-credential-service.ts +118 -51
  337. package/src/providers/operations/provider-rules/github.ts +48 -0
  338. package/src/providers/operations/provider-rules/grok.ts +33 -0
  339. package/src/providers/operations/provider-rules/inception.ts +49 -0
  340. package/src/providers/operations/provider-rules/index.ts +32 -0
  341. package/src/providers/operations/provider-rules/perch.ts +37 -0
  342. package/src/providers/operations/provider-rules/qoder.ts +70 -0
  343. package/src/providers/operations/provider-rules/trae.ts +81 -0
  344. package/src/providers/operations/session-resolution.ts +17 -1
  345. package/src/providers/provider-contracts.ts +6 -4
  346. package/src/providers/{provider-credential-resolver.ts → provider-credentials.ts} +40 -3
  347. package/src/providers/provider-metadata.ts +70 -10
  348. package/src/providers/provider-probe.ts +15 -1
  349. package/src/providers/provider-registry.ts +42 -33
  350. package/src/providers/quota/quota-contracts.ts +2 -0
  351. package/src/providers/quota/quota-limit-windows.ts +16 -5
  352. package/src/providers/quota/quota-support.ts +1 -1
  353. package/src/providers/quota/quota-window-parser.ts +90 -18
  354. package/src/providers/quota/refresh.ts +100 -21
  355. package/src/security/api-key-auth-cache.ts +1 -1
  356. package/src/security/api-key-auth.ts +6 -2
  357. package/src/security/ip-boundary.ts +1 -1
  358. package/src/transport/dispatch/attempt-finalize.ts +15 -14
  359. package/src/transport/dispatch/attempt-loop.ts +73 -19
  360. package/src/transport/dispatch/generation.ts +2 -8
  361. package/src/transport/dispatch/handler-deps.ts +1 -1
  362. package/src/transport/dispatch/leases.ts +1 -1
  363. package/src/transport/dispatch/native-shared.ts +25 -0
  364. package/src/transport/dispatch/proxy-request.ts +8 -46
  365. package/src/transport/dispatch/responses-compact.ts +2 -8
  366. package/src/transport/dispatch/retry-policy.ts +1 -1
  367. package/src/transport/dispatch/streaming/finalize.ts +10 -7
  368. package/src/transport/dispatch/streaming/stream.ts +9 -37
  369. package/src/transport/dispatch/systemone.ts +2 -8
  370. package/src/transport/dispatch/token-count.ts +2 -6
  371. package/src/transport/dispatch/upstream.ts +63 -0
  372. package/src/transport/drain-endpoint.ts +31 -4
  373. package/src/transport/failure-policy.ts +16 -30
  374. package/src/transport/guards/body-policy.ts +7 -1
  375. package/src/transport/guards/error-lifecycle.ts +13 -1
  376. package/src/transport/guards/request-context.ts +22 -14
  377. package/src/transport/request/preparer.ts +81 -71
  378. package/src/transport/routing/bounded-lru.ts +14 -0
  379. package/src/transport/routing/drain-reserve.ts +129 -0
  380. package/src/transport/routing/plan-gate.ts +7 -1
  381. package/src/transport/routing/round-robin.ts +60 -0
  382. package/src/transport/routing/route-catalog.ts +123 -111
  383. package/src/transport/routing/route-model.ts +51 -9
  384. package/src/transport/routing/router.ts +393 -32
  385. package/src/transport/shutdown-notice.ts +1 -1
  386. package/src/transport/surface/adapters.ts +11 -2
  387. package/src/transport/surface/chat/adapter.ts +2 -2
  388. package/src/transport/surface/chat/encode.ts +47 -4
  389. package/src/transport/surface/chat/parse.ts +2 -12
  390. package/src/transport/surface/completion.ts +44 -33
  391. package/src/transport/surface/content-parts.ts +21 -0
  392. package/src/transport/surface/messages/adapter.ts +7 -1
  393. package/src/transport/surface/messages/encode.ts +11 -3
  394. package/src/transport/surface/messages/parse.ts +2 -1
  395. package/src/transport/surface/messages/stream.ts +33 -17
  396. package/src/transport/surface/responses/encode.ts +22 -5
  397. package/src/transport/surface/responses/parse.ts +3 -11
  398. package/src/workers/buddy-growth-tasks.ts +8 -8
  399. package/src/workers/daily-checkin.ts +4 -4
  400. package/src/workers/quota-refresh-sweep.ts +1 -1
  401. package/src/workers/zcode-gift-claim.ts +1 -1
  402. package/dist/dashboard/assets/BannedPage-BxoG1MrL.js.gz +0 -0
  403. package/dist/dashboard/assets/CliToolDetailPage-ixwgKk53.js.gz +0 -0
  404. package/dist/dashboard/assets/CliToolsPage-BmttNV3U.js.gz +0 -0
  405. package/dist/dashboard/assets/CombosPage-BE3qwwfC.js.gz +0 -0
  406. package/dist/dashboard/assets/ConsoleLogPage-CRZdmMIX.js.gz +0 -0
  407. package/dist/dashboard/assets/CustomizationPage-Dv_FJ6Er.js.gz +0 -0
  408. package/dist/dashboard/assets/HealthEventsModal-CoVrw2sM.js +0 -1
  409. package/dist/dashboard/assets/HealthEventsModal-CoVrw2sM.js.gz +0 -0
  410. package/dist/dashboard/assets/InjectSystemPromptPanel-BOoO8rpz.js.gz +0 -0
  411. package/dist/dashboard/assets/LoginPage-ChJuNNVN.js.gz +0 -0
  412. package/dist/dashboard/assets/ModelPicker-BZAJtPg1.js.gz +0 -0
  413. package/dist/dashboard/assets/OverviewPage-BE40s3u5.js.gz +0 -0
  414. package/dist/dashboard/assets/ProviderDetailPage-sfLoQtPV.js +0 -8
  415. package/dist/dashboard/assets/ProviderDetailPage-sfLoQtPV.js.gz +0 -0
  416. package/dist/dashboard/assets/ProviderIcon-CQ6bPUf_.js.gz +0 -0
  417. package/dist/dashboard/assets/ProvidersPage-DcepmPWo.js +0 -1
  418. package/dist/dashboard/assets/ProvidersPage-DcepmPWo.js.gz +0 -0
  419. package/dist/dashboard/assets/ProxyPage-BJ87Pp0U.js +0 -9
  420. package/dist/dashboard/assets/ProxyPage-BJ87Pp0U.js.gz +0 -0
  421. package/dist/dashboard/assets/QuotaPage-gkSyRAKa.js +0 -1
  422. package/dist/dashboard/assets/QuotaPage-gkSyRAKa.js.gz +0 -0
  423. package/dist/dashboard/assets/SettingsPage-C5HPWqEv.js.gz +0 -0
  424. package/dist/dashboard/assets/SetupPage-CFNcATJJ.js.gz +0 -0
  425. package/dist/dashboard/assets/StudioPage-YoD4tY5l.js.gz +0 -0
  426. package/dist/dashboard/assets/UsagePage-DxjaAhb_.js.gz +0 -0
  427. package/dist/dashboard/assets/api-keys-BTlkq_Uk.js.gz +0 -0
  428. package/dist/dashboard/assets/app-BX2nnKqs.css +0 -2
  429. package/dist/dashboard/assets/app-BX2nnKqs.css.gz +0 -0
  430. package/dist/dashboard/assets/assert-CndthXFQ.js +0 -1
  431. package/dist/dashboard/assets/assert-CndthXFQ.js.gz +0 -0
  432. package/dist/dashboard/assets/badge-DdbT_7Am.js.gz +0 -0
  433. package/dist/dashboard/assets/banner-CsGN-nbm.js.gz +0 -0
  434. package/dist/dashboard/assets/card-mgZ_TLLZ.js.gz +0 -0
  435. package/dist/dashboard/assets/cli-tools-A1-v8-03.js.gz +0 -0
  436. package/dist/dashboard/assets/clipboard-button-CJ4Zty89.js.gz +0 -0
  437. package/dist/dashboard/assets/dialog-B5L2jPQ_.js.gz +0 -0
  438. package/dist/dashboard/assets/drawer-DkjUouAj.js.gz +0 -0
  439. package/dist/dashboard/assets/entry-B-LVLV6o.js.gz +0 -0
  440. package/dist/dashboard/assets/entry-CAhdYl3B.js.gz +0 -0
  441. package/dist/dashboard/assets/entry-D2xD0Tmd.js +0 -4
  442. package/dist/dashboard/assets/entry-D2xD0Tmd.js.gz +0 -0
  443. package/dist/dashboard/assets/formatters-CrsdLVsn.js.gz +0 -0
  444. package/dist/dashboard/assets/index-BU64Lqvp.js.gz +0 -0
  445. package/dist/dashboard/assets/inline-B2aDLyyg.js.gz +0 -0
  446. package/dist/dashboard/assets/input-BCVkdR9d.js.gz +0 -0
  447. package/dist/dashboard/assets/layout-DNcAdMK-.js.gz +0 -0
  448. package/dist/dashboard/assets/list-row-DgrIH0hw.js.gz +0 -0
  449. package/dist/dashboard/assets/live-Cae01iu8.js.gz +0 -0
  450. package/dist/dashboard/assets/network-CRib7H_2.js.gz +0 -0
  451. package/dist/dashboard/assets/numbers-BKQE8Xpz.js.gz +0 -0
  452. package/dist/dashboard/assets/pagination-ecPvG_8g.js.gz +0 -0
  453. package/dist/dashboard/assets/quota-DJ1QlAOq.js +0 -1
  454. package/dist/dashboard/assets/quota-DJ1QlAOq.js.gz +0 -0
  455. package/dist/dashboard/assets/routing-Ckm1UXuC.js.gz +0 -0
  456. package/dist/dashboard/assets/select-ChoFWyKz.js.gz +0 -0
  457. package/dist/dashboard/assets/settings-CKjFPlKW.js.gz +0 -0
  458. package/dist/dashboard/assets/stack-DWCwU3fh.js.gz +0 -0
  459. package/dist/dashboard/assets/state-Cg3VL5J_.js.gz +0 -0
  460. package/dist/dashboard/assets/switch-Db70BMix.js.gz +0 -0
  461. package/dist/dashboard/assets/useMutation-BqdE0LuM.js.gz +0 -0
  462. package/src/network/pool-health.ts +0 -210
  463. package/src/providers/bundled-discovery-providers.ts +0 -98
  464. package/src/providers/configured-openai-providers.ts +0 -120
  465. package/src/providers/integrations/buddy/buddy-checkin.ts +0 -521
  466. package/src/providers/integrations/buddy/buddy-cli-headers.ts +0 -76
  467. package/src/providers/integrations/buddy/buddy-conversation-headers.ts +0 -130
  468. package/src/providers/integrations/buddy/buddy-discovery-shared.ts +0 -234
  469. package/src/providers/integrations/buddy/buddy-growth-shared.ts +0 -496
  470. package/src/providers/integrations/buddy/buddy-identity.ts +0 -300
  471. package/src/providers/integrations/buddy/buddy-oauth-shared.ts +0 -341
  472. package/src/providers/integrations/buddy/buddy-quota-shared.ts +0 -417
  473. package/src/providers/integrations/buddy/buddy-server-tools.ts +0 -240
  474. package/src/providers/integrations/buddy/buddy-usage-shared.ts +0 -221
  475. package/src/providers/integrations/buddy/buddy-usage-store.ts +0 -181
  476. package/src/providers/integrations/buddy/codebuddy/cn-catalog.ts +0 -37
  477. package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +0 -185
  478. package/src/providers/integrations/buddy/codebuddy/oauth.ts +0 -47
  479. package/src/providers/integrations/buddy/codebuddy/provider.ts +0 -175
  480. package/src/providers/integrations/buddy/codebuddy/quota.ts +0 -154
  481. package/src/providers/integrations/qoder/chat-proto.ts +0 -328
  482. package/src/providers/integrations/qoder/qoder-grpc.ts +0 -871
  483. package/src/providers/integrations/qoder/qoder-ide.ts +0 -588
  484. package/src/providers/operations/model-discovery-cache.ts +0 -34
  485. package/src/providers/operations/probe-fetch.ts +0 -15
  486. package/src/providers/operations/provider-version-cache.ts +0 -30
  487. package/src/providers/provider-capabilities.ts +0 -33
  488. package/src/providers/provider-credential-envelope.ts +0 -43
  489. /package/dist/dashboard/assets/{app-B7kIWFm5.js → app-C0gTXEoN.js} +0 -0
  490. /package/dist/dashboard/assets/{app-B7kIWFm5.js.gz → app-C0gTXEoN.js.gz} +0 -0
  491. /package/src/network/{http2-bidi.ts → bidi.ts} +0 -0
  492. /package/src/network/pool/{pool-scripts.ts → scripts.ts} +0 -0
  493. /package/src/network/pool/{pool-state.ts → state.ts} +0 -0
  494. /package/src/network/{backoff.ts → util/backoff.ts} +0 -0
  495. /package/src/network/{deduplication.ts → util/dedupe.ts} +0 -0
  496. /package/src/network/{ip-utils.ts → util/ip.ts} +0 -0
@@ -78,6 +78,38 @@ function parseSharedChildrenQuery(query: URLSearchParams): SharedChildrenPageOpt
78
78
  return { limit, offset, sortBy, order, status };
79
79
  }
80
80
 
81
+ /**
82
+ * Drops the auth-cache snapshot for a key family: the parent first, then
83
+ * every child.
84
+ *
85
+ * Parent policy is live-inherited by children at auth time, so their cached
86
+ * snapshots must die with the parent — otherwise a denylist / budget edit
87
+ * looks like a no-op for up to AUTH_CACHE_TTL_MS per child.
88
+ */
89
+ function invalidateFamilyCaches(keyId: string, children: readonly ApiKeyRecord[]): void {
90
+ invalidateApiKeyCache(keyId);
91
+ for (const child of children) invalidateApiKeyCache(child.id);
92
+ }
93
+
94
+ /**
95
+ * Purges admission counters for a key family: the parent first, then every
96
+ * child.
97
+ *
98
+ * Share children admit under the parent id, so the parent's counter must die
99
+ * with a revoked/regenerated parent too — otherwise a family budget can be
100
+ * bypassed by a counter seeded under the old credential.
101
+ */
102
+ async function purgeFamilyAdmission(
103
+ admissionService: { purgeKey(apiKeyId: string): Promise<void> },
104
+ keyId: string,
105
+ children: readonly ApiKeyRecord[],
106
+ ): Promise<void> {
107
+ await Promise.all([
108
+ admissionService.purgeKey(keyId),
109
+ ...children.map((child) => admissionService.purgeKey(child.id)),
110
+ ]);
111
+ }
112
+
81
113
  export function createApiKeyOperations(config: ApiKeyConfig) {
82
114
  const operations = {
83
115
  async listKeys(access: AccessDecision | undefined, includeChildren = false): Promise<ApiKeyResponse[]> {
@@ -92,17 +124,7 @@ export function createApiKeyOperations(config: ApiKeyConfig) {
92
124
  const visible = includeChildren
93
125
  ? records.filter((record) => record.revokedAt === undefined)
94
126
  : topLevel;
95
- // A share template's own counter is the live family accumulator now:
96
- // every recipient commit adds to it (see ApiKeyAdmissionService
97
- // commitUsage), so reporting it raw gives the enforced family total.
98
- // No telemetry add-on is needed, and adding it here would double-count.
99
- return Promise.all(
100
- visible.map(async (record) => {
101
- const response = sanitizeApiKeyResponse(record);
102
- if (record.keyMode !== "share") return response;
103
- return response;
104
- }),
105
- );
127
+ return visible.map(sanitizeApiKeyResponse);
106
128
  },
107
129
  async listActiveBannedKeys(access: AccessDecision | undefined): Promise<ApiKeyResponse[]> {
108
130
  const authorized = requireTenantScope(access, "dashboard:read");
@@ -343,11 +365,7 @@ export function createApiKeyOperations(config: ApiKeyConfig) {
343
365
  });
344
366
  if (!updated) throw new ConsoleDomainError("key_not_found", 404, "Key not found");
345
367
  const children = await config.store.listChildren(authorized.tenantId, keyId);
346
- // Parent policy is live-inherited by children at auth time, so their
347
- // cached snapshots must die with the parent — otherwise a denylist /
348
- // budget edit looks like a no-op for up to AUTH_CACHE_TTL_MS per child.
349
- invalidateApiKeyCache(keyId);
350
- for (const child of children) invalidateApiKeyCache(child.id);
368
+ invalidateFamilyCaches(keyId, children);
351
369
  // The allow/deny list and prefix decide which models `/v1/models`
352
370
  // returns, which is a different cache from the auth snapshot: the
353
371
  // listing is cached per (tenant, key, mode, list, prefix), so editing
@@ -397,10 +415,7 @@ export function createApiKeyOperations(config: ApiKeyConfig) {
397
415
  // Drop stale admission counters so a newly lowered one-time/recurring
398
416
  // budget cannot be bypassed by a counter seeded under the looser limit.
399
417
  // Share children admit under the parent id, so purge the parent too.
400
- await Promise.all([
401
- config.admissionService.purgeKey(keyId),
402
- ...children.map((child) => config.admissionService.purgeKey(child.id)),
403
- ]);
418
+ await purgeFamilyAdmission(config.admissionService, keyId, children);
404
419
  }
405
420
  // Adding a daily/monthly ceiling to a key that already spent in the
406
421
  // current bucket must not hand it a full fresh budget: seed the counter
@@ -447,12 +462,8 @@ export function createApiKeyOperations(config: ApiKeyConfig) {
447
462
  const children = await config.store.listChildren(authorized.tenantId, keyId);
448
463
  const removed = await config.store.remove(authorized.tenantId, keyId);
449
464
  if (!removed) throw new ConsoleDomainError("key_not_found", 404, "Key not found");
450
- invalidateApiKeyCache(keyId);
451
- for (const child of children) invalidateApiKeyCache(child.id);
452
- await Promise.all([
453
- config.admissionService.purgeKey(keyId),
454
- ...children.map((child) => config.admissionService.purgeKey(child.id)),
455
- ]);
465
+ invalidateFamilyCaches(keyId, children);
466
+ await purgeFamilyAdmission(config.admissionService, keyId, children);
456
467
  await config.auditSink?.record({
457
468
  access: authorized,
458
469
  action: "api_key.deleted",
@@ -626,8 +637,9 @@ export function createApiKeyOperations(config: ApiKeyConfig) {
626
637
  keyPrefix: generated.prefix,
627
638
  });
628
639
  if (!updated) throw new ConsoleDomainError("key_not_found", 404, "Key not found");
629
- invalidateApiKeyCache(keyId);
630
- await config.admissionService.purgeKey(keyId);
640
+ // Regeneration is personal-key only, so the family is the key itself.
641
+ invalidateFamilyCaches(keyId, []);
642
+ await purgeFamilyAdmission(config.admissionService, keyId, []);
631
643
  const shareStore = config.shareStore;
632
644
  let share: ShareKeyResponse | null = null;
633
645
  if (shareStore) {
@@ -23,7 +23,7 @@
23
23
  * the operator to type it rather than press Enter on a menu row.
24
24
  */
25
25
  import { GatewayError } from "../../protocol/gateway-error";
26
- import { isLoopbackPeer, tokenMatches } from "../../transport/drain-endpoint";
26
+ import { requireLoopbackAdmin } from "../../transport/drain-endpoint";
27
27
 
28
28
  interface ConsoleAccountResetDeps {
29
29
  /** Shared secret; the route is registered only when this is set. */
@@ -40,14 +40,12 @@ export function createConsoleAccountResetHandler(
40
40
  deps: ConsoleAccountResetDeps,
41
41
  ): (context: { request: Request; body: unknown }) => Promise<Response> {
42
42
  return async ({ request, body }) => {
43
- if (!isLoopbackPeer(deps.resolvePeerAddress(request)))
44
- throw new GatewayError(
45
- "policy_rejected",
46
- 403,
47
- "console account reset is only available from loopback (request {requestId}). This operation is only allowed from a trusted loopback connection — no hard feelings 🫂",
48
- );
49
- if (!tokenMatches(request.headers.get("x-drain-token"), deps.token))
50
- throw new GatewayError("authentication_failed", 401, "invalid reset token (request {requestId}). Wrong key, right door — check the token 🔑");
43
+ requireLoopbackAdmin(request, {
44
+ peer: deps.resolvePeerAddress(request),
45
+ token: deps.token,
46
+ policyRejectedMessage: "console account reset is only available from loopback (request {requestId}). This operation is only allowed from a trusted loopback connection — no hard feelings 🫂",
47
+ authenticationFailedMessage: "invalid reset token (request {requestId}). Wrong key, right door — check the token 🔑",
48
+ });
51
49
  const confirm =
52
50
  body !== null && typeof body === "object" && "confirm" in body
53
51
  ? body.confirm
@@ -20,6 +20,8 @@
20
20
  * as sensitive as the database.
21
21
  */
22
22
  import { getTableColumns, getTableName, type Column, type Table } from "drizzle-orm";
23
+ import type { BackupInspection } from "./service";
24
+ import type { DeleteAllScope } from "./store";
23
25
  import { resolveMaxBodyBytes } from "../../config/runtime";
24
26
  import {
25
27
  apiKeys,
@@ -393,6 +395,8 @@ export type BackupSection = "config" | "telemetry";
393
395
 
394
396
  export const BACKUP_SECTIONS: readonly BackupSection[] = ["config", "telemetry"];
395
397
 
398
+ export type { BackupInspection, DeleteAllScope };
399
+
396
400
  export interface BackupPayload {
397
401
  readonly app: typeof BACKUP_APP;
398
402
  readonly version: number;
@@ -22,7 +22,7 @@ import { mkdirSync, writeFileSync } from "node:fs";
22
22
  import { homedir } from "node:os";
23
23
  import { join } from "node:path";
24
24
  import { GatewayError } from "../../protocol/gateway-error";
25
- import { isLoopbackPeer, tokenMatches } from "../../transport/drain-endpoint";
25
+ import { requireLoopbackAdmin } from "../../transport/drain-endpoint";
26
26
  import { tablesForSection } from "./contracts";
27
27
  import { compressBackupStream, exportBackupStream } from "./store";
28
28
  import type { CartethyiaDatabase } from "../../persistence/db-handle";
@@ -52,10 +52,12 @@ export function createBackupExportHandler(
52
52
  // both: neither refusal is about the body. The first is where the request
53
53
  // came from, the second is the credential it carried — and `invalid_request`
54
54
  // told an operator to fix a body that was never the problem.
55
- if (!isLoopbackPeer(deps.resolvePeerAddress(request)))
56
- throw new GatewayError("policy_rejected", 403, "backup export is only available from loopback (request {requestId}). This operation is only allowed from a trusted loopback connection — no hard feelings 🫂");
57
- if (!tokenMatches(request.headers.get("x-drain-token"), deps.token))
58
- throw new GatewayError("authentication_failed", 401, "invalid export token (request {requestId}). Wrong key, right door — check the token 🔑");
55
+ requireLoopbackAdmin(request, {
56
+ peer: deps.resolvePeerAddress(request),
57
+ token: deps.token,
58
+ policyRejectedMessage: "backup export is only available from loopback (request {requestId}). This operation is only allowed from a trusted loopback connection — no hard feelings 🫂",
59
+ authenticationFailedMessage: "invalid export token (request {requestId}). Wrong key, right door — check the token 🔑",
60
+ });
59
61
 
60
62
  const directory =
61
63
  body !== null && typeof body === "object" && "directory" in body
@@ -23,6 +23,7 @@ import type { BackupPayload, BackupRow } from "./contracts";
23
23
  import { BACKUP_APP, BACKUP_VERSION } from "./contracts";
24
24
  import { isRecord } from "../../protocol/codecs";
25
25
  import { hashSecret } from "../../security/crypto";
26
+ import { resolveGithubCopilotMode, resolveGithubEnterpriseDomain } from "../../config/providers";
26
27
  import {
27
28
  AUTH_STATE_KEYS,
28
29
  COMBO_STRATEGIES,
@@ -100,6 +101,24 @@ interface ResolvedConnectionCredential {
100
101
  readonly value: string;
101
102
  readonly kind: "api_key" | "oauth";
102
103
  }
104
+
105
+ /** The provider-specific rebinding a router export may need before it becomes our envelope. */
106
+ export interface ImportedProviderCredential {
107
+ readonly credential: string;
108
+ readonly profile: "opencode-copilot" | "vscode-client-real";
109
+ readonly refreshToken?: string;
110
+ readonly expiresAt: Date;
111
+ readonly requiresRefresh: boolean;
112
+ readonly warnings: readonly string[];
113
+ }
114
+
115
+ export type ImportedProviderCredentialBinder = (
116
+ value: string,
117
+ refreshValue: string | null | undefined,
118
+ profile: "opencode-copilot" | "vscode-client-real",
119
+ domain: string,
120
+ expiresAt?: Date,
121
+ ) => ImportedProviderCredential;
103
122
  function credentialOf(entry: Row, provider: string): ResolvedConnectionCredential | null {
104
123
  const apiKey = text(entry.apiKey);
105
124
  const accessToken = text(entry.accessToken);
@@ -128,7 +147,7 @@ function credentialOf(entry: Row, provider: string): ResolvedConnectionCredentia
128
147
  * we have no such provider — a set entry with no provider behind it would
129
148
  * classify a row that can never import.
130
149
  */
131
- const OAUTH_PROVIDERS = new Set(["claude", "codex", "antigravity", "kimi", "cline"]);
150
+ const OAUTH_PROVIDERS = new Set(["claude", "codex", "antigravity", "kimi", "cline", "github"]);
132
151
 
133
152
  /** The non-secret per-account config, as our `auth_state` bag. */
134
153
  function authStateOf(entry: Row): Record<string, unknown> {
@@ -223,7 +242,11 @@ function modelReference(
223
242
  * leaving them null would make them global, i.e. visible to every tenant, which
224
243
  * an import must never do implicitly.
225
244
  */
226
- export function convert9RouterBackup(input: unknown, tenantId: string): ConversionResult {
245
+ export function convert9RouterBackup(
246
+ input: unknown,
247
+ tenantId: string,
248
+ importBinders: Readonly<Record<string, ImportedProviderCredentialBinder>> = {},
249
+ ): ConversionResult {
227
250
  if (typeof input !== "object" || input === null || Array.isArray(input)) {
228
251
  throw new Error("router backup must be a JSON object");
229
252
  }
@@ -273,6 +296,32 @@ export function convert9RouterBackup(input: unknown, tenantId: string): Conversi
273
296
  }
274
297
  const accountId = crypto.randomUUID();
275
298
  const kind = credential.kind;
299
+ const specific = isRecord(entry.providerSpecificData) ? entry.providerSpecificData : {};
300
+ const importedRefresh = text(entry.refreshToken);
301
+ const sourceExpiry = typeof entry.expiresAt === "string" && Number.isFinite(Date.parse(entry.expiresAt))
302
+ ? new Date(entry.expiresAt)
303
+ : undefined;
304
+ const importBinder = provider === "github" && kind === "oauth" ? importBinders.github : undefined;
305
+ let githubBinding: ImportedProviderCredential | undefined;
306
+ if (importBinder !== undefined) {
307
+ try {
308
+ githubBinding = importBinder(
309
+ credential.value,
310
+ importedRefresh,
311
+ resolveGithubCopilotMode(),
312
+ resolveGithubEnterpriseDomain() || "github.com",
313
+ sourceExpiry,
314
+ );
315
+ } catch (error) {
316
+ // One malformed row must not take the rest of the backup down with it.
317
+ // The row is reported and left out; the operator can re-authenticate it.
318
+ skipped.push(`${label}: GitHub Copilot credential could not be bound: ${error instanceof Error ? error.message : String(error)}`);
319
+ return;
320
+ }
321
+ }
322
+ const storedCredential = githubBinding?.credential ?? credential.value;
323
+ const storedRefresh = githubBinding?.refreshToken ?? importedRefresh;
324
+ const storedAuthState = authStateOf(entry).auth_state;
276
325
  accounts.push({
277
326
  id: accountId,
278
327
  provider_id: provider,
@@ -282,7 +331,7 @@ export function convert9RouterBackup(input: unknown, tenantId: string): Conversi
282
331
  // `SEALED_COLUMNS`. The store re-seals it under this instance's key on the
283
332
  // way in; sealing it here would put ciphertext in a payload whose only
284
333
  // portable form is the plaintext, and the restore would drop it.
285
- access_token: credential.value,
334
+ access_token: storedCredential,
286
335
  credential_kind: kind,
287
336
  status: bool(entry.isActive, true) ? "active" : "disabled",
288
337
  consecutive_failures: 0,
@@ -292,29 +341,41 @@ export function convert9RouterBackup(input: unknown, tenantId: string): Conversi
292
341
  // rather than invented — a lower number means the operator wants it used
293
342
  // first, and the column means the same thing.
294
343
  sort_index: typeof entry.priority === "number" && Number.isFinite(entry.priority) ? entry.priority : 0,
295
- ...(authStateOf(entry)),
344
+ ...(githubBinding === undefined
345
+ ? (storedAuthState === undefined ? {} : { auth_state: storedAuthState })
346
+ : {
347
+ auth_state: {
348
+ ...(isRecord(storedAuthState) ? storedAuthState : {}),
349
+ github_copilot_profile: githubBinding.profile,
350
+ },
351
+ }),
296
352
  ...(emailOf(entry)),
297
353
  created_at: { __date: isoDate(entry.createdAt, now) },
298
354
  });
299
355
  connectedProviders.add(provider);
300
356
  // The refresh token is a second secret on the same account, so it gets its
301
357
  // own sealed row rather than being folded into the credential.
302
- const specific = isRecord(entry.providerSpecificData) ? entry.providerSpecificData : {};
303
- const refresh = text(entry.refreshToken);
304
358
  const clientSecret = text(specific.clientSecret);
305
- if (refresh !== null || clientSecret !== null) {
359
+ for (const warning of githubBinding?.warnings ?? []) {
360
+ warnings.push(`account "${label}": ${warning}`);
361
+ }
362
+ if (storedRefresh !== null || clientSecret !== null || githubBinding?.requiresRefresh === true) {
306
363
  // A companion secret authorizes a refresh, so on its own it has nothing to
307
364
  // authorize. It is still carried rather than dropped, and the report says
308
365
  // why it is inert — a secret lost without a word is how an operator
309
366
  // discovers the loss from a failed refresh days later.
310
- if (clientSecret !== null && refresh === null) {
367
+ if (clientSecret !== null && storedRefresh === null) {
311
368
  warnings.push(`account "${label}": carried a client secret with no refresh token; it cannot authorize a refresh`);
312
369
  }
313
370
  oauthStates.push({
314
371
  provider_account_id: accountId,
315
- ...(refresh === null ? {} : { refresh_token: refresh }),
372
+ ...(storedRefresh === null ? {} : { refresh_token: storedRefresh }),
316
373
  ...(clientSecret === null ? {} : { client_secret: clientSecret }),
317
- ...(typeof entry.expiresAt === "string" ? { expires_at: { __date: isoDate(entry.expiresAt, now) } } : {}),
374
+ ...(githubBinding !== undefined
375
+ ? { expires_at: { __date: githubBinding.expiresAt.toISOString() } }
376
+ : typeof entry.expiresAt === "string"
377
+ ? { expires_at: { __date: isoDate(entry.expiresAt, now) } }
378
+ : {}),
318
379
  });
319
380
  }
320
381
  });
@@ -311,7 +311,14 @@ export class BackupService {
311
311
  }
312
312
 
313
313
  if (detected.kind === "nine_router") {
314
- const { payload: converted, report } = convert9RouterBackup(detected.payload, tenantId);
314
+ // dynamic-import: lazy OAuth graph — pulling the Github OAuth client into
315
+ // the console boot graph would make every console route pay for it.
316
+ const { bindImportedGithubCredential } = await import(
317
+ "../../providers/integrations/github/github-oauth"
318
+ );
319
+ const { payload: converted, report } = convert9RouterBackup(detected.payload, tenantId, {
320
+ github: bindImportedGithubCredential,
321
+ });
315
322
  const result = await this.apply(converted, tenantId, selection);
316
323
  return { ...result, report, format: "nine_router" };
317
324
  }
@@ -8,7 +8,6 @@ import type {
8
8
  CliMappingSettings,
9
9
  DownloadResult,
10
10
  ToolDef,
11
- ToolInjector,
12
11
  ToolStatus,
13
12
  } from "./contracts";
14
13
  import { TOOL_IDS, TOOL_REGISTRY, type ToolId, type ToolRegistryEntry } from "./contracts";
@@ -47,11 +46,6 @@ function toToolRegistryEntry(def: ToolDef): ToolRegistryEntry {
47
46
  };
48
47
  }
49
48
 
50
- function injectorFor(toolId: string): ToolInjector | null {
51
- const injector = INJECTORS[toolId as ToolId];
52
- return injector ?? null;
53
- }
54
-
55
49
  /** Operator-facing summary of what an apply actually did. */
56
50
  function applyMessage(outcome: ApplyOutcome, toolId: string, fileMessage: string): string {
57
51
  switch (outcome) {
@@ -139,8 +133,8 @@ export class CliToolService {
139
133
  }
140
134
 
141
135
  async getStatus(toolId: string): Promise<ToolStatus | null> {
142
- const injector = injectorFor(toolId);
143
- if (injector === null) return null;
136
+ const injector = INJECTORS[toolId as ToolId];
137
+ if (injector === undefined) return null;
144
138
  return injector.getStatus();
145
139
  }
146
140
 
@@ -249,8 +243,8 @@ export class CliToolService {
249
243
  }
250
244
 
251
245
  async downloadConfig(toolId: string, input: ApplyInput): Promise<DownloadResult | null> {
252
- const injector = injectorFor(toolId);
253
- if (injector === null) return null;
246
+ const injector = INJECTORS[toolId as ToolId];
247
+ if (injector === undefined) return null;
254
248
  if (!input.endpoint || !input.apiKey) return null;
255
249
  try {
256
250
  return await injector.download(input);
@@ -281,8 +275,8 @@ export class CliToolService {
281
275
  apiKeyId: string,
282
276
  input: ApplyInput & { mode?: "file" | "remote" | "both" },
283
277
  ): Promise<ApplyConfigResult | null> {
284
- const injector = injectorFor(toolId);
285
- if (injector === null) return null;
278
+ const injector = INJECTORS[toolId as ToolId];
279
+ if (injector === undefined) return null;
286
280
  const mode = input.mode ?? "both";
287
281
  const wantsFile = mode === "file" || mode === "both";
288
282
  const wantsRemote = mode === "remote" || mode === "both";
@@ -11,6 +11,27 @@ import { GatewayError } from "../protocol/gateway-error";
11
11
  import { fastPathname } from "../transport/pathname";
12
12
  import { parseCookieValue, SESSION_COOKIE_NAME, isCsrfValid } from "../security/csrf";
13
13
 
14
+ /**
15
+ * Console auth routes are excluded from CSRF and mutation-rate limiting: they
16
+ * run before a session exists or are the very requests that mint the session,
17
+ * so they must not require a CSRF token or count against the session's budget.
18
+ */
19
+ const AUTH_ROUTE_SUFFIXES = ["/auth/login", "/auth/setup", "/auth/setup/restore", "/auth/session"] as const;
20
+
21
+ /**
22
+ * Whether `request` is a protected console mutation: a `/console/api/*` route
23
+ * in an unsafe method, excluding the auth-route suffixes. Shared by the CSRF
24
+ * and mutation-limiter guards so a change to the protected set lands in both.
25
+ */
26
+ function isProtectedConsoleMutation(request: Request): boolean {
27
+ const path = fastPathname(request.url);
28
+ return (
29
+ path.startsWith("/console/api/") &&
30
+ ["POST", "PUT", "PATCH", "DELETE"].includes(request.method) &&
31
+ !AUTH_ROUTE_SUFFIXES.some((suffix) => path.endsWith(suffix))
32
+ );
33
+ }
34
+
14
35
  /**
15
36
  * Stateless double-submit CSRF guard for unsafe `/console/api/*` mutations.
16
37
  * Compares the `x-csrf-token` header against the readable `csrf_token`
@@ -19,16 +40,9 @@ import { parseCookieValue, SESSION_COOKIE_NAME, isCsrfValid } from "../security/
19
40
  * store whose outage could masquerade as an auth failure.
20
41
  */
21
42
  export function createConsoleCsrfMiddleware(): Elysia {
22
- const excluded = ["/auth/login", "/auth/setup", "/auth/setup/restore", "/auth/session"];
23
43
  return new Elysia()
24
44
  .beforeHandle(async ({ request }) => {
25
- const path = fastPathname(request.url);
26
- if (
27
- !path.startsWith("/console/api/") ||
28
- !["POST", "PUT", "PATCH", "DELETE"].includes(request.method) ||
29
- excluded.some((suffix) => path.endsWith(suffix))
30
- )
31
- return;
45
+ if (!isProtectedConsoleMutation(request)) return;
32
46
  const token = parseCookieValue(request, SESSION_COOKIE_NAME);
33
47
  // No session cookie means no ambient authority to forge: logout and
34
48
  // other mutations without a session fail authentication downstream.
@@ -75,16 +89,9 @@ function pruneConsoleMutationHits(now: number): void {
75
89
  }
76
90
 
77
91
  export function createConsoleMutationLimiterMiddleware(): Elysia {
78
- const excluded = ["/auth/login", "/auth/setup", "/auth/setup/restore", "/auth/session"];
79
92
  return new Elysia()
80
93
  .beforeHandle(async ({ request, set }) => {
81
- const path = fastPathname(request.url);
82
- if (
83
- !path.startsWith("/console/api/") ||
84
- !["POST", "PUT", "PATCH", "DELETE"].includes(request.method) ||
85
- excluded.some((suffix) => path.endsWith(suffix))
86
- )
87
- return;
94
+ if (!isProtectedConsoleMutation(request)) return;
88
95
  const key = consoleMutationKey(request);
89
96
  // No session cookie means no ambient authority to abuse: the request
90
97
  // fails authentication downstream. Only tracked sessions are throttled.
@@ -33,7 +33,7 @@ import { DrizzleCliToolSecretSource } from "./cli-tools/secret-source";
33
33
  import type { ShareLinkStore } from "../persistence/stores/share-store";
34
34
  import type { ProviderRegistry } from "../providers/provider-registry";
35
35
  import type { BundledProviderCatalog } from "../providers/operations/provider-catalog-service";
36
- import type { ValidatedNetworkBindingFactory } from "../network/pool/resolver";
36
+ import type { ValidatedNetworkBindingFactory } from "../network/egress/factory";
37
37
  import type { TrustedProxyBoundary } from "../config/network";
38
38
  import type { IpAbuseStore } from "../security/abuse";
39
39
  import { gzipResponse } from "./response-compression";
@@ -38,7 +38,6 @@ import { createAuditRoutes } from "./audit/routes";
38
38
  import { DrizzleAuditReadStore } from "./audit/store";
39
39
  import { createIpBanRoutes } from "./ip-bans/routes";
40
40
  import type { IpAbuseStore } from "../security/abuse";
41
- import { createPerformanceRoutes } from "./observability/performance";
42
41
  import { createUpdateRoutes } from "./updates/routes";
43
42
  import { createRemoteSupportRoutes } from "./remote-support/routes";
44
43
  import { createRemoteSupportService } from "./remote-support/service";
@@ -66,7 +65,7 @@ import {
66
65
  } from "../persistence/stores/share-store";
67
66
  import { createShareUsagePort } from "./share/share-usage";
68
67
  import { resolveSsrfPolicy } from "../config/network";
69
- import { createValidatedFetch } from "../network/outbound-fetch";
68
+ import { createValidatedFetch } from "../network/egress/fetch";
70
69
  import type { AuditRecorder } from "./auth/audit-recorder";
71
70
  import type { CliToolService } from "./cli-tools/service";
72
71
  import type { NetworkPoolSelector } from "../network/pool/selector";
@@ -78,7 +77,7 @@ import { resolveRedisBackend } from "../persistence/redis";
78
77
  import type { RedisClient } from "../persistence/redis";
79
78
  import type { ProviderRegistry } from "../providers/provider-registry";
80
79
  import type { BundledProviderCatalog } from "../providers/operations/provider-catalog-service";
81
- import type { ValidatedNetworkBindingFactory } from "../network/pool/resolver";
80
+ import type { ValidatedNetworkBindingFactory } from "../network/egress/factory";
82
81
  import type { CartethyiaDatabase } from "../persistence/db-handle";
83
82
  import type { ConsoleAccessResolver } from "./auth/access";
84
83
 
@@ -278,7 +277,6 @@ export function registerConsoleDomains(
278
277
  ...(ctx.stateStore ? { stateStore: ctx.stateStore } : {}),
279
278
  }));
280
279
  console.use(createLogRoutes({ accessResolver: ctx.accessResolver, auditSink: ctx.auditRecorder }));
281
- console.use(createPerformanceRoutes({ accessResolver: ctx.accessResolver }));
282
280
  console.use(createUpdateRoutes({
283
281
  accessResolver: ctx.accessResolver,
284
282
  observabilityStore,
@@ -436,6 +434,11 @@ export function registerConsoleDomains(
436
434
  accessResolver: ctx.accessResolver,
437
435
  snapshotInvalidator: ctx.routeSnapshotService,
438
436
  callbackListener: oauthCallbackListener,
437
+ deviceEgress: {
438
+ db: ctx.db,
439
+ poolSelector: ctx.poolSelector,
440
+ networkBindingFactory: ctx.networkBindingFactory,
441
+ },
439
442
  };
440
443
  console.use(createOAuthLoginRoutes(oauthConfig));
441
444
  }
@@ -13,7 +13,7 @@
13
13
  * and a failure returns `undefined` — never a half-parsed object that a route
14
14
  * would render as if it were good.
15
15
  */
16
- import { createValidatedFetch, type ValidatedFetch } from "../../network/outbound-fetch";
16
+ import { createValidatedFetch, type ValidatedFetch } from "../../network/egress/fetch";
17
17
 
18
18
  export const INFERHUB_API_BASE = "https://inferhub.dev/api";
19
19
 
@@ -35,19 +35,12 @@ const FAMILY_STATES: readonly FamilyState[] = [
35
35
  "low_traffic",
36
36
  ];
37
37
 
38
- /** Reads a finite number, or null. Never coerces a missing metric to zero. */
39
- function num(value: unknown): number | null {
40
- if (typeof value === "number" && Number.isFinite(value)) return value;
41
- // Catalog and budgets carry decimal strings; market carries numbers. Both
42
- // are read here so one helper covers the wire's own inconsistency.
43
- if (typeof value === "string" && value.trim().length > 0) {
44
- const parsed = Number(value);
45
- if (Number.isFinite(parsed)) return parsed;
46
- }
47
- return null;
48
- }
49
-
50
- /** Reads a finite number, or null. Never coerces a missing metric to zero. */
38
+ /**
39
+ * Reads a finite number, or null. Never coerces a missing metric to zero.
40
+ *
41
+ * Catalog and budgets carry decimal strings; market carries numbers. Both
42
+ * are read here so one helper covers the wire's own inconsistency.
43
+ */
51
44
  export function numOrNull(value: unknown): number | null {
52
45
  if (typeof value === "number" && Number.isFinite(value)) return value;
53
46
  if (typeof value === "string" && value.trim().length > 0) {
@@ -122,8 +115,8 @@ function pricePointsToTiers(
122
115
  const seen = new Map<number, Tier>();
123
116
  for (const point of points) {
124
117
  if (!Array.isArray(point) || point.length < 2) continue;
125
- const price = num(point[0]);
126
- const avail = num(point[1]);
118
+ const price = numOrNull(point[0]);
119
+ const avail = numOrNull(point[1]);
127
120
  if (price === null || avail === null) continue;
128
121
  const count = Math.max(0, Math.trunc(avail));
129
122
  const key = Math.round(price * 10 ** PRICE_PRECISION) / 10 ** PRICE_PRECISION;
@@ -208,8 +201,8 @@ export function normalizeCatalogModel(
208
201
  const prefix = str(upstream.prefix) ?? "";
209
202
  if (modelSlug === null || prefix.length === 0) return null;
210
203
  const id = `${prefix}/${modelSlug}`;
211
- const officialIn = num(raw.officialIn);
212
- const officialOut = num(raw.officialOut);
204
+ const officialIn = numOrNull(raw.officialIn);
205
+ const officialOut = numOrNull(raw.officialOut);
213
206
  const tiersIn = pricePointsToTiers(raw.pricePointsIn, officialIn);
214
207
  const tiersOut = pricePointsToTiers(raw.pricePointsOut, officialOut);
215
208
  // Live providers is the capacity behind the cheapest tier, not a row count:
@@ -234,10 +227,13 @@ export function normalizeCatalogModel(
234
227
  minAskOut: tiersOut[0]?.price ?? null,
235
228
  maxAskIn: tiersIn.at(-1)?.price ?? null,
236
229
  maxAskOut: tiersOut.at(-1)?.price ?? null,
230
+ // Computed by the route layer before sending; starts null in internal models.
231
+ capacityPriceIn: null,
232
+ capacityPriceOut: null,
237
233
  providersLive,
238
234
  // Not carried by the catalog; merged from `/api/market` by the store.
239
235
  lastRate: null,
240
- cacheHitRate: num(raw.cacheHitRate),
236
+ cacheHitRate: numOrNull(raw.cacheHitRate),
241
237
  health: {
242
238
  successRatePct: null,
243
239
  avgTtftMs: null,
@@ -245,8 +241,8 @@ export function normalizeCatalogModel(
245
241
  familyState: null,
246
242
  },
247
243
  capabilities: {
248
- contextWindow: num(raw.contextWindow),
249
- maxOutputTokens: num(raw.maxOutputTokens),
244
+ contextWindow: numOrNull(raw.contextWindow),
245
+ maxOutputTokens: numOrNull(raw.maxOutputTokens),
250
246
  modalities: stringArray(raw.modalities),
251
247
  outputModality:
252
248
  outputModality === "image" || outputModality === "video" ? outputModality : "text",
@@ -274,13 +270,13 @@ export function mergeMarketPrices(
274
270
  if (entry === undefined) return model;
275
271
  return {
276
272
  ...model,
277
- lastRate: num(entry.lastRate),
273
+ lastRate: numOrNull(entry.lastRate),
278
274
  // Market is the cheaper source for the extremes, but the catalog's tier
279
275
  // grouping is what gives them capacity, so only the nulls are filled.
280
- minAskIn: model.minAskIn ?? num(entry.minAskIn),
281
- minAskOut: model.minAskOut ?? num(entry.minAskOut),
282
- maxAskIn: model.maxAskIn ?? num(entry.maxAskIn),
283
- maxAskOut: model.maxAskOut ?? num(entry.maxAskOut),
276
+ minAskIn: model.minAskIn ?? numOrNull(entry.minAskIn),
277
+ minAskOut: model.minAskOut ?? numOrNull(entry.minAskOut),
278
+ maxAskIn: model.maxAskIn ?? numOrNull(entry.maxAskIn),
279
+ maxAskOut: model.maxAskOut ?? numOrNull(entry.maxAskOut),
284
280
  };
285
281
  });
286
282
  }
@@ -306,9 +302,9 @@ export function mergeModelHealth(
306
302
  ...model,
307
303
  health: {
308
304
  ...model.health,
309
- successRatePct: num(entry.successRatePct),
310
- avgTtftMs: num(entry.avgTtftMs),
311
- avgTps: num(entry.avgTps),
305
+ successRatePct: numOrNull(entry.successRatePct),
306
+ avgTtftMs: numOrNull(entry.avgTtftMs),
307
+ avgTps: numOrNull(entry.avgTps),
312
308
  },
313
309
  };
314
310
  });
@@ -61,6 +61,9 @@ export interface ModelMarket {
61
61
  readonly minAskOut: number | null;
62
62
  readonly maxAskIn: number | null;
63
63
  readonly maxAskOut: number | null;
64
+ /** Capacity-weighted price computed by the route layer; null when no tiers. */
65
+ readonly capacityPriceIn: number | null;
66
+ readonly capacityPriceOut: number | null;
64
67
  /** Zero means no live provider — listed, but never routed to. */
65
68
  readonly providersLive: number;
66
69
  readonly lastRate: number | null;