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
@@ -1,22 +1,43 @@
1
1
  /**
2
2
  * Per-provider proactive OAuth refresh lead.
3
3
  *
4
- * A single global "refresh 5 minutes before expiry" window is wrong for two
5
- * reasons. Some providers issue short-lived tokens that a 5-minute lead can
6
- * miss entirely under a loaded sweep, while others issue long-lived tokens
7
- * where re-minting every few minutes is pointless traffic. And some providers
8
- * (Google-backed) rate-limit the token endpoint aggressively, so refreshing
9
- * only when genuinely close to expiry matters more than reacting fast.
4
+ * The lead is how long *before* expiry a token becomes eligible for the
5
+ * scheduled refresh sweep. It exists so the sweep re-mints a token before it
6
+ * dies — never to push refreshes any more often than that.
10
7
  *
11
- * The lead is how long *before* expiry the token becomes eligible for
12
- * proactive refresh. It is a floor: a caller may pass a larger `skewMs`
13
- * explicitly, and `DEFAULT_REFRESH_LEAD_MS` applies to any provider not listed.
8
+ * Buckets are deliberately coarse. A single global "5 minutes before expiry"
9
+ * window is wrong: some providers issue short-lived tokens that a 5-minute
10
+ * lead can miss under a loaded sweep, while others issue very long-lived
11
+ * tokens where re-minting days early is pointless traffic. Three buckets cover
12
+ * both ends without a hand-tuned value per provider:
14
13
  *
15
- * Values mirror the leads published by the upstream clients these providers
16
- * model (Claude Code ~4h, Codex ~5 days, Antigravity/Kimi ~5 min), so a token
17
- * is re-minted on the same schedule the first-party tooling uses.
14
+ * - daily (default): tokens that live hours to days — refresh once a day at
15
+ * most, or when the sweep sees the token enter its final 24h.
16
+ * - 3 days: multi-day tokens — refresh at most ~3 days before expiry.
17
+ * - 7 weeks: very long-lived tokens — refresh ~7 weeks before expiry, so a
18
+ * ~6-month token is touched roughly once a quarter.
19
+ *
20
+ * Short-lived tokens (below the daily bucket) keep tighter leads because the
21
+ * invariant is "never let the token expire": a lead larger than the token
22
+ * lifetime would make the token permanently due and the sweep would re-mint it
23
+ * on every tick, which is the wasteful behavior these buckets exist to remove.
24
+ *
25
+ * Real expiries (from the first-party clients these providers model):
26
+ * - Google-backed (Antigravity): access tokens live ~1h; 5 min lead.
27
+ * - Claude Code: CLI session tokens live ~8h; 4h lead keeps one work
28
+ * session on a single token.
29
+ * - xAI/Grok CLI and Kimi: short-lived tokens; 5 min lead.
30
+ * - ChatGPT/Codex: access tokens live ~5 days; 3-day lead re-mints ~2 days
31
+ * before expiry.
32
+ * - iFlow: very long-lived tokens; 7-week lead.
18
33
  */
19
- export const DEFAULT_REFRESH_LEAD_MS = 5 * 60 * 1000;
34
+ export const DEFAULT_REFRESH_LEAD_MS = 24 * 60 * 60 * 1000;
35
+
36
+ /** 3-day lead for multi-day tokens. */
37
+ export const REFRESH_LEAD_3DAY_MS = 3 * 24 * 60 * 60 * 1000;
38
+
39
+ /** 7-week lead for very long-lived tokens. */
40
+ export const REFRESH_LEAD_7WEEK_MS = 7 * 7 * 24 * 60 * 60 * 1000;
20
41
 
21
42
  /**
22
43
  * Per-provider refresh lead in milliseconds. Providers absent here use
@@ -24,19 +45,22 @@ export const DEFAULT_REFRESH_LEAD_MS = 5 * 60 * 1000;
24
45
  * `provider_accounts.provider_id`.
25
46
  */
26
47
  export const REFRESH_LEAD_MS: Readonly<Record<string, number>> = Object.freeze({
27
- // Google-backed token endpoints throttle; a wide lead with infrequent
28
- // refreshes is safer than a tight one that retries often.
48
+ // Google-backed token endpoints throttle, and the access token itself lives
49
+ // only ~1h — a tight lead (below the token lifetime) is what keeps the
50
+ // sweep from re-minting on every tick.
29
51
  antigravity: 5 * 60 * 1000,
30
- // Claude Code access tokens live ~8h and the CLI re-mints well ahead of
31
- // expiry; 4h keeps a full work session on one token.
52
+ // Claude Code access tokens live ~8h; 4h keeps a full work session on one
53
+ // token without permanent due-state.
32
54
  claude: 4 * 60 * 60 * 1000,
33
- // Codex/ChatGPT access tokens live for days; refresh on a 5-day lead.
34
- codex: 5 * 24 * 60 * 60 * 1000,
35
- // xAI/Grok CLI and Kimi both re-mint on a 5-minute lead.
55
+ // xAI/Grok CLI and Kimi re-mint on a 5-minute lead (short-lived tokens).
36
56
  grok: 5 * 60 * 1000,
37
57
  kimi: 5 * 60 * 1000,
38
- // iFlow tokens are long-lived; a 1-day lead avoids pointless churn.
39
- iflow: 24 * 60 * 60 * 1000,
58
+ // ChatGPT/Codex access tokens live ~5 days; a 3-day lead re-mints ~2 days
59
+ // before expiry — one refresh per weekend, not per request.
60
+ codex: REFRESH_LEAD_3DAY_MS,
61
+ // iFlow tokens are very long-lived; a 7-week lead avoids pointless churn
62
+ // while still re-minting well before expiry.
63
+ iflow: REFRESH_LEAD_7WEEK_MS,
40
64
  });
41
65
 
42
66
  /** The refresh lead for `providerId`, falling back to the shared default. */
@@ -1,16 +1,17 @@
1
1
  /**
2
- * Process-lifetime model-catalog cache.
3
- *
4
- * `bundledModelCatalog` is called during startup seeding and again whenever a
5
- * caller rebuilds the routing/console catalog. Caching the loader *promise*
6
- * (not just the resolved value) makes concurrent and repeated calls for one
7
- * provider share a single load, and lets the dynamic catalog imports run once
8
- * per process. A rejected load is evicted so a later call can retry.
2
+ * In-process caches for model catalogs, model discovery, and client/provider versions.
9
3
  */
4
+
10
5
  import type { ModelDefinition } from "../provider-model-definition";
11
6
  import { trackModelCatalogLoad } from "../../observability/performance-metrics";
12
7
  import { resolveRedisClient } from "../../persistence/redis";
13
8
  import { cachedRead } from "../../console/shared/read-through-cache";
9
+ import { TtlCacheFamily } from "../../persistence/ttl-cache";
10
+ import { retryWithBackoff } from "../../network/util/retry";
11
+
12
+ /* -------------------------------------------------------------------------- */
13
+ /* 1. Model Catalog Cache */
14
+ /* -------------------------------------------------------------------------- */
14
15
 
15
16
  /**
16
17
  * Provider entries retained before the least-recently-used catalog is evicted.
@@ -37,8 +38,7 @@ const catalogCache = new Map<string, Promise<readonly ModelDefinition[]>>();
37
38
  /**
38
39
  * Evicts the least-recently-used provider before inserting a new one once the
39
40
  * cache is at its bound. `Map` preserves insertion order and every cache hit
40
- * re-inserts its entry at the tail, so the head is the LRU key. Without this
41
- * bound the process-lifetime cache pinned one catalog per provider forever.
41
+ * re-inserts its entry at the tail, so the head is the LRU key.
42
42
  */
43
43
  function evictLeastRecentlyUsedCatalog(maxEntries: number): void {
44
44
  if (catalogCache.size < maxEntries) return;
@@ -87,13 +87,65 @@ export function resetModelCatalogCacheForTesting(): void {
87
87
  catalogCache.clear();
88
88
  }
89
89
 
90
- /**
91
- * Live entry count, for the `/metrics` gauge.
92
- *
93
- * Exported so a rebuild that saturates the cap is visible from outside the
94
- * process: before this, every in-process cache except the quota tier was
95
- * invisible, and growth could only be found with a heap dump.
96
- */
90
+ /** Live entry count, for the `/metrics` gauge. */
97
91
  export function modelCatalogCacheSize(): number {
98
92
  return catalogCache.size;
99
93
  }
94
+
95
+ /* -------------------------------------------------------------------------- */
96
+ /* 2. Model Discovery Cache */
97
+ /* -------------------------------------------------------------------------- */
98
+
99
+ const MODEL_DISCOVERY_CACHE_TTL_MS = 10 * 60 * 1000;
100
+ const discoveryCaches = new TtlCacheFamily<readonly ModelDefinition[]>({
101
+ defaultTtlMs: MODEL_DISCOVERY_CACHE_TTL_MS,
102
+ });
103
+
104
+ export interface CachedModelDiscoveryOptions {
105
+ readonly ttlMs?: number;
106
+ }
107
+
108
+ /** Returns cached models, retrying null/empty loads on the next call. */
109
+ export async function getCachedModelDiscovery(
110
+ key: string,
111
+ loader: () => Promise<readonly ModelDefinition[] | null>,
112
+ options: CachedModelDiscoveryOptions = {},
113
+ ): Promise<readonly ModelDefinition[] | null> {
114
+ return discoveryCaches.for(options.ttlMs).load(key, loader, {
115
+ namespace: "model-discovery",
116
+ shouldCache: (models) => models.length > 0,
117
+ });
118
+ }
119
+
120
+ /** Drops all cached discoveries. Test-only. */
121
+ export function resetModelDiscoveryCacheForTesting(): void {
122
+ discoveryCaches.clear();
123
+ }
124
+
125
+ /* -------------------------------------------------------------------------- */
126
+ /* 3. Provider Version Cache */
127
+ /* -------------------------------------------------------------------------- */
128
+
129
+ const VERSION_CACHE_TTL_MS = 5 * 60 * 1000;
130
+ const versionCaches = new TtlCacheFamily<string>({ defaultTtlMs: VERSION_CACHE_TTL_MS });
131
+
132
+ export interface CachedVersionOptions {
133
+ readonly ttlMs?: number;
134
+ readonly maxRetries?: number;
135
+ }
136
+
137
+ export async function getCachedVersion(
138
+ key: string,
139
+ loader: () => Promise<string | null>,
140
+ options: CachedVersionOptions = {},
141
+ ): Promise<string | null> {
142
+ const loadWithRetry = () => retryWithBackoff(loader, { maxRetries: options.maxRetries ?? 2 });
143
+ return versionCaches.for(options.ttlMs).load(key, loadWithRetry, {
144
+ namespace: "provider-version",
145
+ });
146
+ }
147
+
148
+ /** Drops all cached versions. Test-only. */
149
+ export function resetVersionCacheForTesting(): void {
150
+ versionCaches.clear();
151
+ }
@@ -10,7 +10,7 @@ import { ProviderRegistry } from "../provider-registry";
10
10
  import { OpenAICompatibleAdapter } from "../adapters/compatible-adapter";
11
11
  import { resolveByokWireProfile } from "./byok-wire-profile";
12
12
  import { BUNDLED_PROVIDER_MODULES } from "../default-registry";
13
- import { getCachedModels } from "./model-catalog-cache";
13
+ import { getCachedModels } from "./provider-caches";
14
14
  import { seedBundledModels } from "./provider-catalog-seeder";
15
15
  import type { CartethyiaDatabase } from "../../persistence/db-handle";
16
16
  import { providers } from "../../persistence/schema";
@@ -6,7 +6,7 @@ import { providerAccounts, providerOauthStates } from "../../persistence/schema"
6
6
  import { decryptCredentialToString } from "../../security/crypto";
7
7
  import { GatewayError } from "../../protocol/gateway-error";
8
8
  import type { OAuthRefreshService, OAuthTokenRefresher } from "../authentication/oauth-refresh-service";
9
- import { CredentialResolver, type CredentialAlternative } from "../provider-credential-resolver";
9
+ import { CredentialResolver, type CredentialAlternative } from "../provider-credentials";
10
10
  import { parseProviderId, type CredentialKind } from "../provider-contracts";
11
11
  import type { ResolvedCredential } from "../provider-contracts";
12
12
  import { refreshLeadMs } from "./oauth-refresh-lead";
@@ -145,19 +145,62 @@ export interface AccountWithFreshness {
145
145
  readonly dueAt: number | undefined;
146
146
  }
147
147
 
148
+ /** Columns every account+OAuth-state reader selects; narrower readers pick from it. */
149
+ export interface AccountWithOauthStateRow {
150
+ readonly id: string;
151
+ readonly providerId: string;
152
+ readonly credentialKind: CredentialKind;
153
+ readonly credentialCiphertext: Buffer | null;
154
+ readonly refreshCiphertext: Buffer | null;
155
+ readonly authState: unknown;
156
+ readonly clientSecretCiphertext: Buffer | null;
157
+ readonly staticToken: boolean;
158
+ }
159
+
160
+ /** Which optional columns a reader wants on top of the shared base. */
161
+ export interface LoadAccountWithOauthStateOptions {
162
+ /** Select `status` from providerAccounts. */
163
+ readonly includeStatus?: boolean;
164
+ /** Select the last health category from providerAccounts. */
165
+ readonly includeLastErrorCategory?: boolean;
166
+ /** Select `expiresAt` from providerOauthStates. */
167
+ readonly includeExpiry?: boolean;
168
+ /** Also compute the skew-adjusted OAuth due time (see `loadAccountWithFreshness`). */
169
+ readonly includeDueAt?: boolean;
170
+ /** Refresh lead override for the `dueAt` skew; defaults to `refreshLeadMs(providerId)`. */
171
+ readonly skewMs?: number;
172
+ }
173
+
174
+ /** One loaded account row plus whatever optional columns were requested. */
175
+ export interface LoadedAccountWithOauthState {
176
+ readonly id: string;
177
+ readonly providerId: string;
178
+ readonly credentialKind: CredentialKind;
179
+ readonly lastErrorCategory?: string | null;
180
+ readonly credentialCiphertext: Buffer | null;
181
+ readonly refreshCiphertext: Buffer | null;
182
+ readonly authState: unknown;
183
+ readonly clientSecretCiphertext: Buffer | null;
184
+ readonly staticToken: boolean;
185
+ readonly status?: string;
186
+ readonly expiresAt?: Date | null;
187
+ readonly dueAt?: number;
188
+ }
189
+
148
190
  /**
149
- * Loads an account and its optional OAuth state in one query.
191
+ * Shared account+OAuth-state loader behind every status/health reader.
150
192
  *
151
- * `dueAt` is the OAuth expiry minus the provider's refresh lead
152
- * (`refreshLeadMs(providerId)`), so a token becomes due at the point that
153
- * provider re-mints its own (Claude ~4h, Codex ~5 days, Antigravity ~5 min).
154
- * Non-OAuth accounts and accounts without OAuth state have no due time.
193
+ * Builds the same `select(...).from(providerAccounts)` with the conditional
194
+ * `leftJoin` on `providerOauthStates` (the memory/Postgres dialect guard), so
195
+ * a schema or dialect change is fixed once instead of in four copies. The
196
+ * `dueAt` skew logic lives here too; `loadAccountWithFreshness` is a thin
197
+ * wrapper that returns it beside the row.
155
198
  */
156
- export async function loadAccountWithFreshness(
199
+ export async function loadAccountWithOauthState(
157
200
  db: CartethyiaDatabase,
158
201
  accountId: string,
159
- skewMs?: number,
160
- ): Promise<AccountWithFreshness | undefined> {
202
+ options: LoadAccountWithOauthStateOptions = {},
203
+ ): Promise<LoadedAccountWithOauthState | undefined> {
161
204
  const selectBuilder = db
162
205
  .select({
163
206
  id: providerAccounts.id,
@@ -167,8 +210,14 @@ export async function loadAccountWithFreshness(
167
210
  refreshCiphertext: providerOauthStates.refreshCiphertext,
168
211
  authState: providerAccounts.authState,
169
212
  clientSecretCiphertext: providerOauthStates.clientSecretCiphertext,
170
- expiresAt: providerOauthStates.expiresAt,
171
213
  staticToken: providerAccounts.staticToken,
214
+ ...(options.includeStatus === true ? { status: providerAccounts.status } : {}),
215
+ ...(options.includeLastErrorCategory === true
216
+ ? { lastErrorCategory: providerAccounts.lastErrorCategory }
217
+ : {}),
218
+ ...(options.includeExpiry === true || options.includeDueAt === true
219
+ ? { expiresAt: providerOauthStates.expiresAt }
220
+ : {}),
172
221
  })
173
222
  .from(providerAccounts);
174
223
  const query = "leftJoin" in selectBuilder
@@ -180,37 +229,60 @@ export async function loadAccountWithFreshness(
180
229
  const rows = await query.where(eq(providerAccounts.id, accountId)).limit(1);
181
230
  const row = rows[0];
182
231
  if (!row) return undefined;
183
- const lead = skewMs ?? refreshLeadMs(row.providerId);
184
- // A refresh-capable OAuth account with no stored expiry is unknown-liveness:
185
- // we have no evidence the access token still works. The old rule left
186
- // `dueAt` undefined for these, so `resolveCredentialForAccount` skipped the
187
- // refresh branch and dispatch sent the pasted token as-is. For a token that
188
- // had already expired on another installation (a 9router/Omniroute import
189
- // carrying `accessToken` + `refreshToken` + a past `expiresAt` that was
190
- // dropped by validation, a hand-pasted OAuth bundle with no expiry field,
191
- // or an upstream that stopped minting expiries) that produced the exact
192
- // "re-authenticate" refusal the refresh grant exists to prevent. Marking the
193
- // token due immediately refreshes it on first use; the sweep handles the
194
- // normal case where `expires_at` is populated.
195
- const hasRefreshGrant = row.refreshCiphertext != null;
196
- const kindOauth = row.credentialKind === "oauth";
197
- const unknownExpiryButRefreshable =
198
- kindOauth && !row.staticToken && hasRefreshGrant && row.expiresAt == null;
232
+ if (options.includeDueAt !== true) return row;
233
+ const lead = options.skewMs ?? refreshLeadMs(row.providerId);
234
+ // A refresh-capable OAuth account with no stored expiry has an unknown
235
+ // liveness, but the scheduled sweep is the only refresher: marking it due
236
+ // here would refresh on every load, and the dispatch path deliberately no
237
+ // longer refreshes inline. Unknown expiry therefore means "not due yet" —
238
+ // if the token actually died, the upstream's 401 triggers the dispatch
239
+ // retry refresh (see `attempt-loop.ts`).
240
+ // A static token is never "due": it is used as issued and there is no
241
+ // refresh to run, so the dispatch path must not treat it as stale.
242
+ const dueAt =
243
+ row.staticToken
244
+ ? undefined
245
+ : row.expiresAt != null
246
+ ? row.expiresAt.getTime() - lead
247
+ : undefined;
199
248
  return {
200
- row,
201
- // A static token is never "due": it is used as issued and there is no
202
- // refresh to run, so the dispatch path must not treat it as stale.
203
- dueAt:
204
- row.staticToken
205
- ? undefined
206
- : row.expiresAt != null
207
- ? row.expiresAt.getTime() - lead
208
- : unknownExpiryButRefreshable
209
- ? 0
210
- : undefined,
249
+ ...row,
250
+ ...(dueAt === undefined ? {} : { dueAt }),
211
251
  };
212
252
  }
213
253
 
254
+ /**
255
+ * Loads an account and its optional OAuth state in one query.
256
+ *
257
+ * `dueAt` is the OAuth expiry minus the provider's refresh lead
258
+ * (`refreshLeadMs(providerId)`), so a token becomes due at the point that
259
+ * provider re-mints its own (Claude ~4h, Codex ~5 days, Antigravity ~5 min).
260
+ * Non-OAuth accounts and accounts without OAuth state have no due time.
261
+ */
262
+ export async function loadAccountWithFreshness(
263
+ db: CartethyiaDatabase,
264
+ accountId: string,
265
+ skewMs?: number,
266
+ ): Promise<AccountWithFreshness | undefined> {
267
+ const loaded = await loadAccountWithOauthState(db, accountId, {
268
+ includeDueAt: true,
269
+ ...(skewMs === undefined ? {} : { skewMs }),
270
+ });
271
+ if (!loaded) return undefined;
272
+ const row: AccountWithFreshnessRow = {
273
+ id: loaded.id,
274
+ providerId: loaded.providerId,
275
+ credentialKind: loaded.credentialKind,
276
+ credentialCiphertext: loaded.credentialCiphertext,
277
+ refreshCiphertext: loaded.refreshCiphertext,
278
+ authState: loaded.authState,
279
+ clientSecretCiphertext: loaded.clientSecretCiphertext,
280
+ expiresAt: loaded.expiresAt ?? null,
281
+ staticToken: loaded.staticToken,
282
+ };
283
+ return { row, dueAt: loaded.dueAt };
284
+ }
285
+
214
286
  /** OAuth refresh collaborators, injected by the composition root. */
215
287
  export interface ResolveCredentialOAuth {
216
288
  /** Registry-backed refresher lookup; providers without a refresher are skipped. */
@@ -223,7 +295,7 @@ export async function resolveCredentialForAccount(
223
295
  db: CartethyiaDatabase,
224
296
  providerId: string,
225
297
  accountId: string,
226
- oauth?: ResolveCredentialOAuth,
298
+ _oauth?: ResolveCredentialOAuth,
227
299
  ): Promise<ResolvedCredential> {
228
300
  // Cache hit: skip DB round-trip + AES-GCM decrypt.
229
301
  // Don't cache OAuth credentials whose token is due for refresh soon —
@@ -268,19 +340,14 @@ export async function resolveCredentialForAccount(
268
340
  );
269
341
  }
270
342
  }
271
- if (row.credentialKind === "oauth" && oauth && dueAt !== undefined && dueAt <= Date.now()) {
272
- const refresher = await oauth.resolveRefresher(row.providerId);
273
- if (refresher) {
274
- // Try to refresh. If it fails (refresh grant dead, upstream unreachable,
275
- // etc.) keep the already-decrypted stale secret rather than wiping it:
276
- // dispatch then sends the token we have, and the upstream's own 401 is
277
- // the authoritative verdict. Wiping it here turned "token expired" into
278
- // a credential-less GatewayError before the request ever left, which is
279
- // worse than letting the server say so.
280
- const refreshed = await oauth.refreshService.ensureFreshAccessToken(accountId, refresher);
281
- if (refreshed) secret = refreshed;
282
- }
283
- }
343
+ // No inline refresh on the request path. OAuth tokens are kept fresh by the
344
+ // scheduled refresh sweep (`oauth-refresh-sweep`, due by `refreshLeadMs`),
345
+ // and a token that still expired before its sweep turn is answered by the
346
+ // upstream's own 401, which the dispatch attempt-loop turns into a forced
347
+ // refresh (see `attempt-loop.ts`). Refreshing here blocked a client request
348
+ // on the token endpoint — a hung endpoint held the request for the full 30 s
349
+ // refresh timeout — and re-minted tokens on every expired request instead of
350
+ // once per schedule.
284
351
 
285
352
  const authState = jsonbRecord(row.authState);
286
353
  const alternative: CredentialAlternative = {
@@ -0,0 +1,48 @@
1
+ import type {
2
+ AccountErrorRule,
3
+ } from "../account-error-classification";
4
+
5
+ /**
6
+ * Checks whether an upstream body indicates Copilot is not licensed.
7
+ * Copilot answers 401 for a bad credential and 403 for a
8
+ * license/entitlement refusal, so forbidden/unauthorized on this surface
9
+ * is the same verdict.
10
+ */
11
+ export function copilotNotLicensed(lower: string): boolean {
12
+ return (
13
+ lower.includes("not licensed") ||
14
+ lower.includes("forbidden") ||
15
+ lower.includes("unauthorized") ||
16
+ lower.includes("license")
17
+ );
18
+ }
19
+
20
+ export const githubRules: readonly AccountErrorRule[] = [
21
+ {
22
+ // GitHub Copilot answers a license/entitlement refusal with 403 and
23
+ // "not licensed to use Copilot" / "forbidden" / "unauthorized". That is a
24
+ // verdict on the account: the subscription cannot serve this credential,
25
+ // and no amount of waiting or refreshing changes it. Cooldown cycling
26
+ // (auth_invalidated + 5m backoff) kept a dead account in rotation where a
27
+ // client's retry budget burned sibling accounts — the same failure the
28
+ // buddy ban exists to prevent. Disable immediately. Must run before
29
+ // `credential-rejected`, which would claim the bare 403 as a refreshable
30
+ // auth rejection.
31
+ name: "github-copilot-not-licensed",
32
+ match: (s) =>
33
+ s.origin === "upstream" &&
34
+ s.providerId === "github" &&
35
+ copilotNotLicensed(s.lower),
36
+ decide: (s) =>
37
+ s.result(
38
+ "policy_blocked_banned",
39
+ "disabled",
40
+ 0,
41
+ null,
42
+ s.reason("GitHub Copilot account not licensed"),
43
+ // A license refusal is evidence about the account even at provider
44
+ // scope, for the same reason a named revocation or a buddy ban is.
45
+ true,
46
+ ),
47
+ },
48
+ ];
@@ -0,0 +1,33 @@
1
+ import type { AccountErrorSignals } from "../account-error-classification";
2
+
3
+ /**
4
+ * xAI Grok Build's free tier resets on a rolling 24-hour window, so its
5
+ * exhaustion is a full-day quota cooldown — never the generic 1h fallback,
6
+ * which let the account re-enter rotation inside the window.
7
+ */
8
+ export const GROK_QUOTA_COOLDOWN_MS = 24 * 60 * 60 * 1000;
9
+
10
+ /**
11
+ * Detects whether a quota failure belongs to Grok's free tier or spending limit.
12
+ */
13
+ export function isGrokFreeTierExhaustion(s: AccountErrorSignals): boolean {
14
+ if (s.providerId !== undefined && s.providerId !== "grok") {
15
+ // If providerId is known and not grok, only match if the providerCode is explicitly grok subscription
16
+ if (!s.providerCode.startsWith("subscription:")) return false;
17
+ }
18
+ return (
19
+ s.providerCode === "subscription:free-usage-exhausted" ||
20
+ s.providerCode.startsWith("subscription:") ||
21
+ s.providerCode.includes("spending") ||
22
+ s.providerCode.includes("credits") ||
23
+ s.lower.includes("free-usage") ||
24
+ s.lower.includes("free usage") ||
25
+ s.lower.includes("included free usage") ||
26
+ s.lower.includes("rolling 24-hour") ||
27
+ s.lower.includes("rolling 24 hour") ||
28
+ s.lower.includes("spending limit") ||
29
+ s.lower.includes("out of credits") ||
30
+ s.lower.includes("run out of credits") ||
31
+ s.lower.includes("need a grok subscription")
32
+ );
33
+ }
@@ -0,0 +1,49 @@
1
+ import type {
2
+ AccountErrorRule,
3
+ } from "../account-error-classification";
4
+
5
+ /**
6
+ * Inception's per-minute limit window (1 minute).
7
+ *
8
+ * Inception's Free tier throttles per minute (1 000 requests / 1M input
9
+ * tokens / 100K output tokens) and the window resets every minute. The generic
10
+ * 15-minute rate-limit fallback would park a healthy key long after the window
11
+ * refilled; 1 minute is the provider's own cadence, hardcoded because it is a
12
+ * fact about the upstream, not a deployment knob.
13
+ */
14
+ export const INCEPTION_RATE_LIMIT_COOLDOWN_MS = 60_000;
15
+
16
+ export const inceptionRules: readonly AccountErrorRule[] = [
17
+ {
18
+ // Inception's Free tier throttles per minute (1 000 requests / 1M input
19
+ // tokens / 100K output tokens) and the window resets every minute. A
20
+ // generic 429 would cool the account for the 15-minute default, leaving a
21
+ // healthy key parked long after the window refilled — the source of
22
+ // "limitasiya lumayan keras". This arm must run before `rate-limit`: it
23
+ // matches the same signals and shortens the window to 1 minute so routing
24
+ // moves to the next account and the flagged one returns almost at once.
25
+ name: "inception-per-minute-limit",
26
+ match: (s) =>
27
+ s.origin === "upstream" &&
28
+ s.providerId === "inception" &&
29
+ (s.statusCode === 429 ||
30
+ s.providerCode === "rate_limit_reached" ||
31
+ s.providerCode === "rate_limit_exceeded" ||
32
+ s.lower.includes("rate_limit") ||
33
+ s.lower.includes("rate limit") ||
34
+ s.lower.includes("too many requests") ||
35
+ s.lower.includes("tokens per minute") ||
36
+ s.lower.includes("input token")),
37
+ decide: (s) => {
38
+ const cooldownMs = s.statedCooldownMs ?? INCEPTION_RATE_LIMIT_COOLDOWN_MS;
39
+ return s.result(
40
+ "rate_limit_transient",
41
+ "cooldown",
42
+ cooldownMs,
43
+ new Date(Date.now() + cooldownMs),
44
+ s.reason("Inception per-minute input-token limit"),
45
+ s.origin === "upstream",
46
+ );
47
+ },
48
+ },
49
+ ];
@@ -0,0 +1,32 @@
1
+ export {
2
+ TRAE_REGION_COOLDOWN_MS,
3
+ TRAE_QUOTA_COOLDOWN_MS,
4
+ traeRules,
5
+ } from "./trae";
6
+
7
+ export {
8
+ QODER_QUOTA_COOLDOWN_MS,
9
+ qoderPlanGatedRule,
10
+ qoderModelQuotaRule,
11
+ qoderRules,
12
+ } from "./qoder";
13
+
14
+ export {
15
+ githubRules,
16
+ copilotNotLicensed,
17
+ } from "./github";
18
+
19
+ export {
20
+ INCEPTION_RATE_LIMIT_COOLDOWN_MS,
21
+ inceptionRules,
22
+ } from "./inception";
23
+
24
+ export {
25
+ PERCH_TURN_RATE_LIMIT_COOLDOWN_MS,
26
+ perchRules,
27
+ } from "./perch";
28
+
29
+ export {
30
+ GROK_QUOTA_COOLDOWN_MS,
31
+ isGrokFreeTierExhaustion,
32
+ } from "./grok";
@@ -0,0 +1,37 @@
1
+ import type {
2
+ AccountErrorRule,
3
+ } from "../account-error-classification";
4
+
5
+ /**
6
+ * Perch's `turn_rate_limited` window. The upstream does not state a reset and
7
+ * answers the same on every ticket mint while the window is exhausted, so a
8
+ * day matches the practical cadence and stops the redial loop.
9
+ */
10
+ export const PERCH_TURN_RATE_LIMIT_COOLDOWN_MS = 24 * 60 * 60 * 1000;
11
+
12
+ export const perchRules: readonly AccountErrorRule[] = [
13
+ {
14
+ // Perch's `turn_rate_limited` is a per-turn API window, not a per-request
15
+ // 429: the account keeps answering but every call mints another ticket
16
+ // into the same exhausted window. The stated remedy ("contact us for
17
+ // direct API access") implies the window is long — park the account for a
18
+ // day so routing picks a sibling instead of hammering the same key.
19
+ name: "perch-turn-rate-limit",
20
+ match: (s) =>
21
+ s.origin === "upstream" &&
22
+ s.providerId === "perch" &&
23
+ (s.providerCode === "turn_rate_limited" ||
24
+ (s.statusCode === 429 && s.lower.includes("turn rate"))),
25
+ decide: (s) => {
26
+ const cooldownMs = s.statedCooldownMs ?? PERCH_TURN_RATE_LIMIT_COOLDOWN_MS;
27
+ return s.result(
28
+ "rate_limit_transient",
29
+ "cooldown",
30
+ cooldownMs,
31
+ new Date(Date.now() + cooldownMs),
32
+ s.reason("Perch turn rate limit"),
33
+ s.origin === "upstream",
34
+ );
35
+ },
36
+ },
37
+ ];