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
@@ -5,7 +5,7 @@ import type { ApiKeyAdmissionService } from "../../security/admission/service";
5
5
  import type { VisionHandoffCall, VisionHandoffResult } from "../vision-handoff";
6
6
  import { isImageOrPlaceholderPart, lastImageCarryingUserTurnIndex } from "../vision-handoff";
7
7
  import type { CallerIdentity } from "../../security/access-control";
8
- import type { RouteCandidate as RouteCandidate, InMemoryRouteSnapshotService, RoutePlan } from "../routing/route-model";
8
+ import type { RouteCandidate as RouteCandidate, InMemoryRouteSnapshotService, RoutePlan, RouteSnapshot } from "../routing/route-model";
9
9
  import type { RouteContinuationAffinity } from "../routing/route-model";
10
10
  import { resolveAliasTarget, type RoutingEngine } from "../routing/router";
11
11
  import { projectForRoute, routeCapabilitiesFor, deriveRequiredCapabilities } from "../../protocol/translation/capabilities";
@@ -80,7 +80,17 @@ function replaceImagePartsWithText(
80
80
  }
81
81
  const hasImage = message.content.some(isImageOrPlaceholderPart);
82
82
  if (!hasImage) return message;
83
- return { ...message, content: message.content.filter((part) => !isImageOrPlaceholderPart(part)) };
83
+ // Never delete images silently (proposal Option E): replace a dropped
84
+ // older image with a marker so the main model at least knows something
85
+ // was there, instead of the loss being invisible.
86
+ return {
87
+ ...message,
88
+ content: message.content.flatMap((part) =>
89
+ isImageOrPlaceholderPart(part)
90
+ ? [{ kind: "text" as const, text: "[image omitted]" }]
91
+ : [part],
92
+ ),
93
+ };
84
94
  });
85
95
  const unchangedMessages = nextMessages.every(
86
96
  (message, i) => message === request.messages[i],
@@ -350,6 +360,61 @@ export class ProxyRequestPreparer {
350
360
 
351
361
  constructor(private readonly deps: ProxyRequestPreparerDeps) {}
352
362
 
363
+ /**
364
+ * The key's model-admission gate, shared by the canonical and native
365
+ * preparation paths: CLI-mapping eligibility → alias resolution → the
366
+ * model allowlist → the model-prefix gate, in that exact order. Rejects
367
+ * with the same `model_not_allowed` 403s (same code, status, message, and
368
+ * detail fields) on both paths, so a policy change here cannot silently
369
+ * split admission between the chat and native surfaces. Returns the state
370
+ * the gate computed that the caller still needs for routing: the resolved
371
+ * alias target and whether CLI mappings were eligible.
372
+ */
373
+ #resolveAliasAndGateModel(
374
+ model: string,
375
+ authorization: ResolvedApiKey,
376
+ clientUserAgent: string | undefined,
377
+ snapshot: RouteSnapshot,
378
+ ): { readonly resolvedTarget: string; readonly allowCliMappings: boolean } {
379
+ // Scope opts the key into the mapping table; the User-Agent decides whether
380
+ // *this* request may consume it. Without the UA gate, a Claude→DeepSeek
381
+ // remap would also rewrite a non-Claude caller that named `opus`.
382
+ const allowCliMappings = allowsCliToolMappings(authorization.scopes, {
383
+ ...(clientUserAgent === undefined ? {} : { userAgent: clientUserAgent }),
384
+ });
385
+ // CLI source→target mappings are an explicit API-key capability. Ordinary
386
+ // tenant aliases remain available to every key; only selected keys may
387
+ // consume the CLI mapping table.
388
+ const resolvedTarget = resolveAliasTarget(
389
+ snapshot,
390
+ authorization.tenantId,
391
+ model,
392
+ allowCliMappings,
393
+ authorization.cliMappingOwnerId ?? authorization.id,
394
+ );
395
+ // Pass `model` as the requested name so an allowlisted alias / a CLI
396
+ // remapping can authorize the resolved target (see modelRejectionReason).
397
+ const allowedForKey = isModelAllowed(authorization.snapshot, resolvedTarget, undefined, model);
398
+ if (
399
+ authorization.modelPrefix &&
400
+ !model.startsWith(authorization.modelPrefix) &&
401
+ !resolvedTarget.startsWith(authorization.modelPrefix) &&
402
+ !allowedForKey
403
+ )
404
+ throw new GatewayError(
405
+ "model_not_allowed",
406
+ 403,
407
+ `This key may only use models starting with '${authorization.modelPrefix}' (request {requestId}). Ask for one inside that prefix, or have an operator widen the key. The door's locked for this one — check the allowlist 🔒`,
408
+ { model, required_prefix: authorization.modelPrefix },
409
+ );
410
+ if (!allowedForKey) {
411
+ throw new GatewayError("model_not_allowed", 403, "model is not allowed for this API key (request {requestId}). The door's locked for this one — check the allowlist 🔒", {
412
+ model,
413
+ });
414
+ }
415
+ return { resolvedTarget, allowCliMappings };
416
+ }
417
+
353
418
  /** Injects the vision-handoff runner (called once from boot wiring). */
354
419
  bindVisionHandoff(
355
420
  runner: (input: VisionHandoffCall) => Promise<readonly VisionHandoffResult[]>,
@@ -391,47 +456,12 @@ export class ProxyRequestPreparer {
391
456
  ? initialRequest
392
457
  : withThinkingSuffixIntent({ ...initialRequest, model: bareModel }, thinkingIntent);
393
458
  const snapshot = await this.deps.snapshotService.getSnapshot();
394
- // Scope opts the key into the mapping table; the User-Agent decides whether
395
- // *this* request may consume it. Without the UA gate, a Claude→DeepSeek
396
- // remap would also rewrite a non-Claude caller that named `opus`.
397
- const allowCliMappings = allowsCliToolMappings(authorization.scopes, {
398
- ...(input.clientUserAgent === undefined ? {} : { userAgent: input.clientUserAgent }),
399
- });
400
- // CLI source→target mappings are an explicit API-key capability. Ordinary
401
- // tenant aliases remain available to every key; only selected keys may
402
- // consume the CLI mapping table.
403
- const resolvedTarget = resolveAliasTarget(
404
- snapshot,
405
- authorization.tenantId,
406
- request.model,
407
- allowCliMappings,
408
- authorization.cliMappingOwnerId ?? authorization.id,
409
- );
410
- // Pass `request.model` as the requested name so an allowlisted alias / a
411
- // CLI remapping can authorize the resolved target (see modelRejectionReason).
412
- const allowedForKey = isModelAllowed(
413
- authorization.snapshot,
414
- resolvedTarget,
415
- undefined,
459
+ const { allowCliMappings } = this.#resolveAliasAndGateModel(
416
460
  request.model,
461
+ authorization,
462
+ input.clientUserAgent,
463
+ snapshot,
417
464
  );
418
- if (
419
- authorization.modelPrefix &&
420
- !request.model.startsWith(authorization.modelPrefix) &&
421
- !resolvedTarget.startsWith(authorization.modelPrefix) &&
422
- !allowedForKey
423
- )
424
- throw new GatewayError(
425
- "model_not_allowed",
426
- 403,
427
- `This key may only use models starting with '${authorization.modelPrefix}' (request {requestId}). Ask for one inside that prefix, or have an operator widen the key. The door's locked for this one — check the allowlist 🔒`,
428
- { model: request.model, required_prefix: authorization.modelPrefix },
429
- );
430
- if (!allowedForKey) {
431
- throw new GatewayError("model_not_allowed", 403, "model is not allowed for this API key (request {requestId}). The door's locked for this one — check the allowlist 🔒", {
432
- model: request.model,
433
- });
434
- }
435
465
  // An inline image that cannot survive the upstream is dropped here rather
436
466
  // than dispatched: a provider rejects the whole request for one corrupt
437
467
  // attachment, and the buddy family reports it with no field named, so the
@@ -474,6 +504,7 @@ export class ProxyRequestPreparer {
474
504
  allowCliMappings,
475
505
  authorization.cliMappingOwnerId ?? authorization.id,
476
506
  continuationAffinity,
507
+ request.conversation_id ?? request.conversation?.conversation_id,
477
508
  );
478
509
  } catch (error) {
479
510
  // `plan()` collapses "no candidate supports this capability" into
@@ -541,6 +572,7 @@ export class ProxyRequestPreparer {
541
572
  allowCliMappings,
542
573
  authorization.cliMappingOwnerId ?? authorization.id,
543
574
  continuationAffinity,
575
+ request.conversation_id ?? request.conversation?.conversation_id,
544
576
  );
545
577
  plan = handoffPlan;
546
578
  variantRequest = replaced;
@@ -725,42 +757,18 @@ export class ProxyRequestPreparer {
725
757
  readonly clientUserAgent?: string;
726
758
  readonly eligible: (candidate: RouteCandidate) => boolean;
727
759
  readonly emptyMessage: string;
760
+ /** Session identity for sticky routing (conversation id when present). */
761
+ readonly conversationId?: string;
728
762
  }): Promise<PreparedNativeRequest> {
729
763
  if (input.signal?.aborted)
730
764
  throw new GatewayError("transport_closed", 499, "The request was cancelled (request {requestId}). Send it again whenever ready 🙃");
731
765
  const snapshot = await this.deps.snapshotService.getSnapshot();
732
- const allowCliMappings = allowsCliToolMappings(input.authorization.scopes, {
733
- ...(input.clientUserAgent === undefined ? {} : { userAgent: input.clientUserAgent }),
734
- });
735
- const resolvedTarget = resolveAliasTarget(
736
- snapshot,
737
- input.authorization.tenantId,
738
- input.model,
739
- allowCliMappings,
740
- input.authorization.cliMappingOwnerId ?? input.authorization.id,
741
- );
742
- const allowedForKey = isModelAllowed(
743
- input.authorization.snapshot,
744
- resolvedTarget,
745
- undefined,
766
+ const { allowCliMappings } = this.#resolveAliasAndGateModel(
746
767
  input.model,
768
+ input.authorization,
769
+ input.clientUserAgent,
770
+ snapshot,
747
771
  );
748
- if (
749
- input.authorization.modelPrefix &&
750
- !input.model.startsWith(input.authorization.modelPrefix) &&
751
- !resolvedTarget.startsWith(input.authorization.modelPrefix) &&
752
- !allowedForKey
753
- )
754
- throw new GatewayError(
755
- "model_not_allowed",
756
- 403,
757
- `This key may only use models starting with '${input.authorization.modelPrefix}' (request {requestId}). Ask for one inside that prefix, or have an operator widen the key. The door's locked for this one — check the allowlist 🔒`,
758
- { model: input.model, required_prefix: input.authorization.modelPrefix },
759
- );
760
- if (!allowedForKey)
761
- throw new GatewayError("model_not_allowed", 403, "model is not allowed for this API key (request {requestId}). The door's locked for this one — check the allowlist 🔒", {
762
- model: input.model,
763
- });
764
772
  const plan = await this.deps.routingEngine.plan(
765
773
  input.model,
766
774
  snapshot,
@@ -768,6 +776,8 @@ export class ProxyRequestPreparer {
768
776
  undefined,
769
777
  allowCliMappings,
770
778
  input.authorization.cliMappingOwnerId ?? input.authorization.id,
779
+ undefined,
780
+ input.conversationId,
771
781
  );
772
782
  if (input.signal?.aborted)
773
783
  throw new GatewayError("transport_closed", 499, "The request was cancelled (request {requestId}). Send it again whenever ready 🙃");
@@ -1,5 +1,19 @@
1
1
  // Bounded LRU map: the one shared implementation for routing-layer caches.
2
2
 
3
+ /**
4
+ * Evicts the oldest-inserted entry while `map.size > cap`. The shared
5
+ * insertion-order bound for caches whose read path must not refresh recency
6
+ * (unlike `BoundedLruMap`, whose `get` does). Eviction is one `keys().next()`
7
+ * and no bookkeeping arrays.
8
+ */
9
+ export function evictOldestEntry<K>(map: Map<K, unknown>, cap: number): void {
10
+ while (map.size > cap) {
11
+ const oldest = map.keys().next().value;
12
+ if (oldest === undefined) break;
13
+ map.delete(oldest);
14
+ }
15
+ }
16
+
3
17
  /**
4
18
  * A capacity-bounded LRU `Map`.
5
19
  *
@@ -0,0 +1,129 @@
1
+ // Drain urgency and reserve scoring for candidate ordering.
2
+ //
3
+ // Ported from oh-my-pi's `windowRequiredDrain` and reserve semantics
4
+ // (`packages/ai/src/auth/usage-report.ts`, `policy.ts`): within the same rank
5
+ // group, an account whose remaining quota is about to expire unused should be
6
+ // burned first ("use it or lose it"), and an account parked in reserve (at or
7
+ // below the reserve percent) should yield to non-reserve siblings.
8
+ import {
9
+ totalRemainingPercent,
10
+ type ProviderQuotaWindow,
11
+ } from "../../providers/quota/quota-contracts";
12
+
13
+ /** Minimum time before reset that still counts as "some headroom" (1 minute). */
14
+ const MIN_REMAINING_MS = 60_000;
15
+
16
+ /** Usable fraction of a window: 0..1, with 0.5 neutral when unmeasured. */
17
+ function usedFraction(window: ProviderQuotaWindow): number {
18
+ const used = window.usedPercent;
19
+ if (typeof used === "number" && Number.isFinite(used)) {
20
+ return Math.min(1, Math.max(0, used / 100));
21
+ }
22
+ const remaining = window.remainingPercent;
23
+ if (typeof remaining === "number" && Number.isFinite(remaining)) {
24
+ return Math.min(1, Math.max(0, 1 - remaining / 100));
25
+ }
26
+ if (typeof window.used === "number" && Number.isFinite(window.used)) {
27
+ if (typeof window.limit === "number" && Number.isFinite(window.limit) && window.limit > 0) {
28
+ return Math.min(1, Math.max(0, window.used / window.limit));
29
+ }
30
+ }
31
+ if (typeof window.remaining === "number" && Number.isFinite(window.remaining)) {
32
+ if (typeof window.limit === "number" && Number.isFinite(window.limit) && window.limit > 0) {
33
+ return Math.min(1, Math.max(0, 1 - window.remaining / window.limit));
34
+ }
35
+ }
36
+ return 0.5;
37
+ }
38
+
39
+ /** Reset instant (epoch ms) for a window, or undefined when it has no clock. */
40
+ function resetAtMs(window: ProviderQuotaWindow): number | undefined {
41
+ if (window.resetsAt === null || window.resetsAt === undefined) return undefined;
42
+ const at = Date.parse(window.resetsAt);
43
+ return Number.isFinite(at) ? at : undefined;
44
+ }
45
+
46
+ /**
47
+ * Drain score for the most-urgent relevant window: headroom fraction divided by
48
+ * remaining hours until reset. Higher means quota that is about to expire
49
+ * unused and should be burned first. Returns 0 when no window carries usable
50
+ * evidence (the candidate is neutral, never penalized).
51
+ */
52
+ export function drainScore(
53
+ windows: readonly ProviderQuotaWindow[] | undefined,
54
+ nowMs = Date.now(),
55
+ ): number {
56
+ if (windows === undefined || windows.length === 0) return 0;
57
+ let best = 0;
58
+ for (const window of windows) {
59
+ const headroom = 1 - usedFraction(window);
60
+ if (headroom <= 0) continue;
61
+ const resetAt = resetAtMs(window);
62
+ const durationMs = windowAccountingDurationMs(window);
63
+ let remainingMs = resetAt === undefined ? (durationMs ?? MIN_REMAINING_MS) : resetAt - nowMs;
64
+ if (durationMs !== undefined && durationMs > 0) {
65
+ remainingMs = Math.min(remainingMs, durationMs);
66
+ }
67
+ const remainingHours = Math.max(remainingMs, MIN_REMAINING_MS) / (60 * 60 * 1000);
68
+ best = Math.max(best, headroom / remainingHours);
69
+ }
70
+ return best;
71
+ }
72
+
73
+ function windowAccountingDurationMs(window: ProviderQuotaWindow): number | undefined {
74
+ // Providers state window durations in the kind label (e.g. "5h", "7d") when
75
+ // they do not carry an explicit duration field. Parse the common spellings;
76
+ // an unparseable label contributes no clock.
77
+ const text = `${window.kind} ${window.label}`.toLowerCase();
78
+ const m = /(\d+(?:\.\d+)?)\s*(h|hour|hr|d|day|m|min|minute|s|sec|second)\b/.exec(text);
79
+ if (!m) return undefined;
80
+ const value = Number(m[1]);
81
+ if (!Number.isFinite(value)) return undefined;
82
+ const unit = m[2];
83
+ if (unit === "h" || unit === "hour" || unit === "hr") return value * 60 * 60 * 1000;
84
+ if (unit === "d" || unit === "day") return value * 24 * 60 * 60 * 1000;
85
+ if (unit === "m" || unit === "min" || unit === "minute") return value * 60 * 1000;
86
+ if (unit === "s" || unit === "sec" || unit === "second") return value * 1000;
87
+ return undefined;
88
+ }
89
+
90
+ /**
91
+ * The account's remaining fraction (0..1) across its pooled credit and
92
+ * independent windows, or undefined when nothing is measured. Mirrors
93
+ * `totalRemainingPercent` (the router's existing aggregate) but normalized to a
94
+ * 0..1 fraction.
95
+ */
96
+ export function remainingFraction(
97
+ windows: readonly ProviderQuotaWindow[] | undefined,
98
+ ): number | undefined {
99
+ if (windows === undefined || windows.length === 0) return undefined;
100
+ const percent = totalRemainingPercent(windows);
101
+ if (percent === null) return undefined;
102
+ return Math.min(1, Math.max(0, percent / 100));
103
+ }
104
+
105
+ /** True when the account's remaining fraction is at or below the reserve percent. */
106
+ export function isInReserve(
107
+ windows: readonly ProviderQuotaWindow[] | undefined,
108
+ reservePct: number,
109
+ ): boolean {
110
+ if (reservePct <= 0) return false;
111
+ const fraction = remainingFraction(windows);
112
+ if (fraction === undefined) return false;
113
+ return fraction * 100 <= reservePct;
114
+ }
115
+
116
+ /**
117
+ * True when a reserved account has healed above the reclaim threshold and may be
118
+ * reclaimed to full priority.
119
+ */
120
+ export function isReclaimReady(
121
+ windows: readonly ProviderQuotaWindow[] | undefined,
122
+ reservePct: number,
123
+ reclaimAbovePct: number,
124
+ ): boolean {
125
+ if (reservePct <= 0 || reclaimAbovePct <= reservePct) return false;
126
+ const fraction = remainingFraction(windows);
127
+ if (fraction === undefined) return false;
128
+ return fraction * 100 >= reclaimAbovePct;
129
+ }
@@ -17,7 +17,13 @@
17
17
  * costs one refused request that the cooldown machinery already handles; a
18
18
  * wrong paid match silently blackholes a model.
19
19
  */
20
- const FREE_PLAN_MARKERS: readonly string[] = ["free"];
20
+ /**
21
+ * The marker set that proves a plan string is free. Exported so every
22
+ * free-plan consumer — the routing plan gate here and the cooldown-tier
23
+ * derivation in `account-tier.ts` — reads the same list, so the two cannot
24
+ * drift apart about an account.
25
+ */
26
+ export const FREE_PLAN_MARKERS: readonly string[] = ["free"];
21
27
 
22
28
  export function isFreePlanAccount(plan: string | null | undefined): boolean {
23
29
  if (plan === null || plan === undefined) return false;
@@ -36,3 +36,63 @@ const MAX_ROUND_ROBIN_ENTRIES = 1000;
36
36
  export function createRoundRobinMap(): BoundedLruMap<string, RoundRobinState> {
37
37
  return new BoundedLruMap<string, RoundRobinState>(MAX_ROUND_ROBIN_ENTRIES);
38
38
  }
39
+
40
+ /** One session's sticky account: which provider account last served it, and when. */
41
+ export interface StickySession {
42
+ readonly accountId: string;
43
+ readonly lastUsedAtMs: number;
44
+ /** Set only by an explicit operator pin; automatic warm stickiness leaves it absent. */
45
+ readonly explicit?: true;
46
+ }
47
+
48
+ /**
49
+ * Session → account stickiness per provider, bounded by an LRU. Used by the
50
+ * router to prefer the account that last served a session within the warm
51
+ * window — prompt-cache warmth is worth more than perfect load balance. A
52
+ * session id that is absent, evicted, or past the warm window simply carries
53
+ * no preference.
54
+ */
55
+ export class StickySessionStore {
56
+ private readonly byProvider = new Map<string, BoundedLruMap<string, StickySession>>();
57
+
58
+ constructor(private readonly maxSessionsPerProvider: number) {}
59
+
60
+ private mapFor(providerId: string): BoundedLruMap<string, StickySession> {
61
+ let map = this.byProvider.get(providerId);
62
+ if (map === undefined) {
63
+ map = new BoundedLruMap<string, StickySession>(this.maxSessionsPerProvider);
64
+ this.byProvider.set(providerId, map);
65
+ }
66
+ return map;
67
+ }
68
+
69
+ /** The account last pinned to this session, or undefined. */
70
+ get(providerId: string, sessionId: string | undefined): StickySession | undefined {
71
+ if (sessionId === undefined) return undefined;
72
+ return this.mapFor(providerId).get(sessionId);
73
+ }
74
+
75
+ /** Record that `accountId` served `sessionId` now (or pin it explicitly). */
76
+ set(providerId: string, sessionId: string | undefined, accountId: string, explicit = false): void {
77
+ if (sessionId === undefined) return;
78
+ const now = Date.now();
79
+ this.mapFor(providerId).set(sessionId, {
80
+ accountId,
81
+ lastUsedAtMs: now,
82
+ ...(explicit ? { explicit: true as const } : {}),
83
+ });
84
+ }
85
+
86
+ /** Drop one session's stickiness (e.g. after the account is invalidated). */
87
+ clear(providerId: string, sessionId: string | undefined): void {
88
+ if (sessionId === undefined) return;
89
+ this.mapFor(providerId).delete(sessionId);
90
+ }
91
+
92
+ /** Total retained sticky entries across providers (for metrics/tests). */
93
+ size(): number {
94
+ let total = 0;
95
+ for (const map of this.byProvider.values()) total += map.size;
96
+ return total;
97
+ }
98
+ }