cartethyia 2.5.9 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (386) hide show
  1. package/.env.example +15 -42
  2. package/README.md +11 -10
  3. package/dist/dashboard/assets/{BannedPage-Brvuj_Eb.js → BannedPage-l6XMnfr9.js} +1 -1
  4. package/dist/dashboard/assets/BannedPage-l6XMnfr9.js.gz +0 -0
  5. package/dist/dashboard/assets/{CliToolDetailPage-CqfXtI3y.js → CliToolDetailPage-DwOzAW45.js} +1 -1
  6. package/dist/dashboard/assets/CliToolDetailPage-DwOzAW45.js.gz +0 -0
  7. package/dist/dashboard/assets/{CliToolsPage-GQeS91bg.js → CliToolsPage-BT3Up3QA.js} +1 -1
  8. package/dist/dashboard/assets/CliToolsPage-BT3Up3QA.js.gz +0 -0
  9. package/dist/dashboard/assets/{CombosPage-BDRsTXBB.js → CombosPage-6kYIUpzR.js} +1 -1
  10. package/dist/dashboard/assets/CombosPage-6kYIUpzR.js.gz +0 -0
  11. package/dist/dashboard/assets/{ConsoleLogPage-zP8AFHX0.js → ConsoleLogPage-zRnj1mlR.js} +1 -1
  12. package/dist/dashboard/assets/ConsoleLogPage-zRnj1mlR.js.gz +0 -0
  13. package/dist/dashboard/assets/{CustomizationPage-BmfrNZ14.js → CustomizationPage-C7m-H1Rz.js} +1 -1
  14. package/dist/dashboard/assets/CustomizationPage-C7m-H1Rz.js.gz +0 -0
  15. package/dist/dashboard/assets/HealthEventsModal-C8MR_BdB.js +1 -0
  16. package/dist/dashboard/assets/HealthEventsModal-C8MR_BdB.js.gz +0 -0
  17. package/dist/dashboard/assets/{LoginPage-DoreIgm8.js → LoginPage-CbphLvjg.js} +1 -1
  18. package/dist/dashboard/assets/LoginPage-CbphLvjg.js.gz +0 -0
  19. package/dist/dashboard/assets/{ModelPicker-C7pDb41R.js → ModelPicker-DVny4S5_.js} +1 -1
  20. package/dist/dashboard/assets/ModelPicker-DVny4S5_.js.gz +0 -0
  21. package/dist/dashboard/assets/OverviewPage-DABu2Wgt.js +1 -0
  22. package/dist/dashboard/assets/OverviewPage-DABu2Wgt.js.gz +0 -0
  23. package/dist/dashboard/assets/ProviderDetailPage-C15x9q13.js +3 -0
  24. package/dist/dashboard/assets/ProviderDetailPage-C15x9q13.js.gz +0 -0
  25. package/dist/dashboard/assets/{ProvidersPage-BnoPgA7w.js → ProvidersPage-Dfb2896L.js} +1 -1
  26. package/dist/dashboard/assets/ProvidersPage-Dfb2896L.js.gz +0 -0
  27. package/dist/dashboard/assets/{ProxyPage-Bq_TGgdb.js → ProxyPage-BaCIdAUt.js} +1 -1
  28. package/dist/dashboard/assets/ProxyPage-BaCIdAUt.js.gz +0 -0
  29. package/dist/dashboard/assets/QuotaPage-BsdoCIw1.js +1 -0
  30. package/dist/dashboard/assets/QuotaPage-BsdoCIw1.js.gz +0 -0
  31. package/dist/dashboard/assets/SettingsPage-KVJFh3Ip.js +1 -0
  32. package/dist/dashboard/assets/SettingsPage-KVJFh3Ip.js.gz +0 -0
  33. package/dist/dashboard/assets/{SetupPage-Day3mCOn.js → SetupPage-D4g6vOGc.js} +1 -1
  34. package/dist/dashboard/assets/SetupPage-D4g6vOGc.js.gz +0 -0
  35. package/dist/dashboard/assets/{SortableList-DQxllnB5.js → SortableList-C93i14BD.js} +1 -1
  36. package/dist/dashboard/assets/SortableList-C93i14BD.js.gz +0 -0
  37. package/dist/dashboard/assets/{StudioPage-uMExoWu1.js → StudioPage-SgQpymjm.js} +1 -1
  38. package/dist/dashboard/assets/{StudioPage-uMExoWu1.js.gz → StudioPage-SgQpymjm.js.gz} +0 -0
  39. package/dist/dashboard/assets/{UsagePage-DXa1oR-4.js → UsagePage-BFME-eba.js} +2 -2
  40. package/dist/dashboard/assets/UsagePage-BFME-eba.js.gz +0 -0
  41. package/dist/dashboard/assets/{api-keys-DQYLQxpW.js → api-keys-C23S9sgp.js} +1 -1
  42. package/dist/dashboard/assets/api-keys-C23S9sgp.js.gz +0 -0
  43. package/dist/dashboard/assets/{app-BIt446yE.js → app-2KhtHCv9.js} +1 -1
  44. package/dist/dashboard/assets/{app-BIt446yE.js.gz → app-2KhtHCv9.js.gz} +0 -0
  45. package/dist/dashboard/assets/app-CEeaMHOy.css +2 -0
  46. package/dist/dashboard/assets/app-CEeaMHOy.css.gz +0 -0
  47. package/dist/dashboard/assets/{assert-C9FD_JwJ.js → assert-DU6qrMA_.js} +1 -1
  48. package/dist/dashboard/assets/assert-DU6qrMA_.js.gz +0 -0
  49. package/dist/dashboard/assets/{badge-CR72dasT.js → badge-CKsF123d.js} +1 -1
  50. package/dist/dashboard/assets/badge-CKsF123d.js.gz +0 -0
  51. package/dist/dashboard/assets/{banner-CI9_eSfO.js → banner-8j7uChA9.js} +1 -1
  52. package/dist/dashboard/assets/banner-8j7uChA9.js.gz +0 -0
  53. package/dist/dashboard/assets/{card-DS7CQEsO.js → card-CdsSlWA3.js} +1 -1
  54. package/dist/dashboard/assets/card-CdsSlWA3.js.gz +0 -0
  55. package/dist/dashboard/assets/{cli-tools-CNqzrfdb.js → cli-tools-BaFUuFFF.js} +1 -1
  56. package/dist/dashboard/assets/cli-tools-BaFUuFFF.js.gz +0 -0
  57. package/dist/dashboard/assets/{clipboard-button-611pSsaX.js → clipboard-button-uL5aYLfW.js} +1 -1
  58. package/dist/dashboard/assets/clipboard-button-uL5aYLfW.js.gz +0 -0
  59. package/dist/dashboard/assets/{dialog-k37exjNt.js → dialog-C07hr5_G.js} +1 -1
  60. package/dist/dashboard/assets/dialog-C07hr5_G.js.gz +0 -0
  61. package/dist/dashboard/assets/{drawer-CX7N8ucR.js → drawer-BPO1TcKH.js} +1 -1
  62. package/dist/dashboard/assets/drawer-BPO1TcKH.js.gz +0 -0
  63. package/dist/dashboard/assets/{entry-CTP8AyiS.js → entry-Bz_CtDfC.js} +1 -1
  64. package/dist/dashboard/assets/entry-Bz_CtDfC.js.gz +0 -0
  65. package/dist/dashboard/assets/entry-CwPRKk0d.js +4 -0
  66. package/dist/dashboard/assets/entry-CwPRKk0d.js.gz +0 -0
  67. package/dist/dashboard/assets/entry-GJRbmrpA.js +1 -0
  68. package/dist/dashboard/assets/entry-GJRbmrpA.js.gz +0 -0
  69. package/dist/dashboard/assets/{github-badge-sltk1qUi.js → github-badge-DmQreSDd.js} +1 -1
  70. package/dist/dashboard/assets/github-badge-DmQreSDd.js.gz +0 -0
  71. package/dist/dashboard/assets/{index-z3zDOSMM.js → index-Bblu6erR.js} +2 -2
  72. package/dist/dashboard/assets/index-Bblu6erR.js.gz +0 -0
  73. package/dist/dashboard/assets/{inline-BhY2sSnO.js → inline-C9dCYaBZ.js} +1 -1
  74. package/dist/dashboard/assets/inline-C9dCYaBZ.js.gz +0 -0
  75. package/dist/dashboard/assets/{input-V1umFiBk.js → input-CjCnXweJ.js} +1 -1
  76. package/dist/dashboard/assets/input-CjCnXweJ.js.gz +0 -0
  77. package/dist/dashboard/assets/{layout-BBOYycyS.js → layout-V6y8DAkl.js} +1 -1
  78. package/dist/dashboard/assets/layout-V6y8DAkl.js.gz +0 -0
  79. package/dist/dashboard/assets/{list-row-1zzZYM0B.js → list-row-CM1AaG_y.js} +1 -1
  80. package/dist/dashboard/assets/list-row-CM1AaG_y.js.gz +0 -0
  81. package/dist/dashboard/assets/{live-BivGfuUs.js → live-ta6GMb-8.js} +1 -1
  82. package/dist/dashboard/assets/live-ta6GMb-8.js.gz +0 -0
  83. package/dist/dashboard/assets/{model-bans-CmffjLuG.js → model-bans-BmYTmbAk.js} +1 -1
  84. package/dist/dashboard/assets/model-bans-BmYTmbAk.js.gz +0 -0
  85. package/dist/dashboard/assets/{mutation-46P0oWMe.js → mutation-g_fduiNw.js} +1 -1
  86. package/dist/dashboard/assets/mutation-g_fduiNw.js.gz +0 -0
  87. package/dist/dashboard/assets/{numbers-DkfMR5Qs.js → numbers-BOevJzdz.js} +1 -1
  88. package/dist/dashboard/assets/numbers-BOevJzdz.js.gz +0 -0
  89. package/dist/dashboard/assets/{query-keys-BFhGpwR1.js → query-keys-UETqfytp.js} +1 -1
  90. package/dist/dashboard/assets/query-keys-UETqfytp.js.gz +0 -0
  91. package/dist/dashboard/assets/{routing-CAb4F37R.js → routing-BPUo-W8d.js} +1 -1
  92. package/dist/dashboard/assets/routing-BPUo-W8d.js.gz +0 -0
  93. package/dist/dashboard/assets/{select-C6MMiaio.js → select-C980c4cz.js} +1 -1
  94. package/dist/dashboard/assets/select-C980c4cz.js.gz +0 -0
  95. package/dist/dashboard/assets/{settings-DNX9sydi.js → settings--3hxSyry.js} +1 -1
  96. package/dist/dashboard/assets/settings--3hxSyry.js.gz +0 -0
  97. package/dist/dashboard/assets/{stack-BHTfXdz_.js → stack-_gfTdLVB.js} +1 -1
  98. package/dist/dashboard/assets/stack-_gfTdLVB.js.gz +0 -0
  99. package/dist/dashboard/assets/{state-mPDRlQkl.js → state-2c5V4zo7.js} +1 -1
  100. package/dist/dashboard/assets/state-2c5V4zo7.js.gz +0 -0
  101. package/dist/dashboard/assets/{switch-DOmJxg7H.js → switch-Dkke7Naf.js} +1 -1
  102. package/dist/dashboard/assets/switch-Dkke7Naf.js.gz +0 -0
  103. package/dist/dashboard/assets/{useMutation-Dem0lXQj.js → useMutation-EDFRkn1i.js} +1 -1
  104. package/dist/dashboard/assets/useMutation-EDFRkn1i.js.gz +0 -0
  105. package/dist/dashboard/index.html +1 -1
  106. package/dist/dashboard/index.html.gz +0 -0
  107. package/dist/dashboard/precompressed-assets.json +51 -51
  108. package/migrations/README.md +52 -52
  109. package/migrations/full-postgres/0020_add_telemetry_event_analytics.sql +77 -0
  110. package/migrations/full-postgres/0021_add_api_key_concurrency_tolerance.sql +13 -0
  111. package/migrations/full-postgres/0022_add_inferhub_config_key.sql +17 -0
  112. package/migrations/full-postgres/0023_drop_api_key_concurrency_tolerance.sql +13 -0
  113. package/migrations/lite-sqlite3/0020_add_telemetry_event_analytics.sql +69 -0
  114. package/migrations/lite-sqlite3/0021_add_api_key_concurrency_tolerance.sql +13 -0
  115. package/migrations/lite-sqlite3/0022_add_inferhub_config_key.sql +17 -0
  116. package/migrations/lite-sqlite3/0023_drop_api_key_concurrency_tolerance.sql +13 -0
  117. package/package.json +96 -97
  118. package/scripts/cli/admin.ts +126 -125
  119. package/scripts/cli/autostart.ts +237 -237
  120. package/scripts/cli/background.ts +120 -110
  121. package/scripts/cli/bin/carte.cjs +8 -8
  122. package/scripts/cli/bin/cartethyia.cjs +167 -167
  123. package/scripts/cli/console-menu.ts +308 -275
  124. package/scripts/cli/controller.ts +145 -0
  125. package/scripts/cli/doctor.ts +1 -1
  126. package/scripts/cli/env.ts +99 -99
  127. package/scripts/cli/gateway.ts +264 -261
  128. package/scripts/{health.ts → cli/health.ts} +4 -4
  129. package/scripts/cli/instance.ts +234 -224
  130. package/scripts/cli/main.ts +188 -164
  131. package/scripts/cli/platform.ts +64 -64
  132. package/scripts/cli/prompt.ts +275 -107
  133. package/scripts/cli/services.ts +88 -88
  134. package/scripts/cli/setup.ts +445 -445
  135. package/scripts/cli/start.ts +85 -82
  136. package/scripts/cli/startup-signal.ts +23 -23
  137. package/scripts/cli/telemetry.ts +70 -0
  138. package/scripts/cli/tray/index.ts +263 -263
  139. package/scripts/cli/tray/windows.ps1 +107 -107
  140. package/scripts/cli/tray.ts +192 -208
  141. package/scripts/cli/update.ts +39 -39
  142. package/scripts/cli/version.ts +67 -67
  143. package/scripts/cli/wordmark.ts +58 -58
  144. package/src/boot/app.ts +16 -1
  145. package/src/boot/dependencies.ts +128 -25
  146. package/src/config/package-paths.ts +24 -0
  147. package/src/config/resources.ts +137 -0
  148. package/src/config/runtime.ts +133 -76
  149. package/src/config/spec.ts +30 -29
  150. package/src/config/telemetry.ts +0 -25
  151. package/src/console/api-keys/bucket-spend.ts +14 -17
  152. package/src/console/api-keys/contracts.ts +410 -410
  153. package/src/console/api-keys/routes.ts +12 -0
  154. package/src/console/audit/store.ts +2 -3
  155. package/src/console/auth/setup-service.ts +8 -1
  156. package/src/console/backup/routes.ts +7 -0
  157. package/src/console/backup/service.ts +463 -463
  158. package/src/console/backup/store.ts +16 -11
  159. package/src/console/backup/validate.ts +1 -4
  160. package/src/console/cli-tools/fs-ops.ts +18 -2
  161. package/src/console/data-management/contracts.ts +1 -8
  162. package/src/console/data-management/operations.ts +61 -48
  163. package/src/console/data-management/routes.ts +4 -2
  164. package/src/console/domain-registration.ts +25 -3
  165. package/src/console/inferhub/client.ts +139 -0
  166. package/src/console/inferhub/key-store.ts +129 -0
  167. package/src/console/inferhub/normalize.ts +347 -0
  168. package/src/console/inferhub/routes.ts +411 -0
  169. package/src/console/inferhub/store.ts +109 -0
  170. package/src/console/inferhub/types.ts +183 -0
  171. package/src/console/observability/contracts.ts +2 -41
  172. package/src/console/observability/store.ts +176 -371
  173. package/src/console/providers/catalog/public-model-store.ts +0 -0
  174. package/src/console/providers/catalog/store.ts +23 -36
  175. package/src/console/quota/account-quota-shared.ts +52 -52
  176. package/src/console/quota/account-quota-tenant.ts +16 -24
  177. package/src/console/quota/refresh.ts +441 -441
  178. package/src/console/remote-support/service.ts +270 -114
  179. package/src/console/routing/pools/import-parse.ts +206 -206
  180. package/src/console/routing/pools/operations.ts +543 -543
  181. package/src/console/routing/pools/probe-verdict.ts +132 -132
  182. package/src/console/routing/pools/routes.ts +181 -181
  183. package/src/console/routing/pools/store.ts +1 -1
  184. package/src/console/settings/contracts.ts +42 -40
  185. package/src/console/settings/store.ts +25 -25
  186. package/src/console/share/share-stats.ts +22 -4
  187. package/src/console/share/share-usage.ts +30 -4
  188. package/src/console/shared/cursor.ts +15 -0
  189. package/src/console/updates/repository-updates.ts +40 -39
  190. package/src/console/updates/routes.ts +12 -0
  191. package/src/main.ts +13 -5
  192. package/src/network/pool/resolver.ts +1104 -1104
  193. package/src/observability/client-name.ts +22 -0
  194. package/src/observability/event-log.ts +5 -29
  195. package/src/observability/event-query.ts +690 -0
  196. package/src/observability/log-ring.ts +26 -2
  197. package/src/observability/log-sanitize.ts +118 -0
  198. package/src/observability/logger.ts +17 -13
  199. package/src/observability/metrics.ts +35 -0
  200. package/src/observability/payload-capture.ts +114 -41
  201. package/src/observability/payload-store.ts +249 -24
  202. package/src/observability/raw-event-writer.ts +118 -0
  203. package/src/observability/runtime-metrics.ts +71 -1
  204. package/src/observability/telemetry-buffer.ts +100 -17
  205. package/src/observability/telemetry-reconciliation.ts +417 -0
  206. package/src/persistence/boot.ts +20 -9
  207. package/src/persistence/db-pg.ts +13 -0
  208. package/src/persistence/db-sqlite.ts +569 -497
  209. package/src/persistence/dialect.ts +3 -2
  210. package/src/persistence/migrate.ts +181 -181
  211. package/src/persistence/schema.ts +1496 -1406
  212. package/src/persistence/shared-cache.ts +5 -1
  213. package/src/persistence/stores/api-key-store.ts +581 -581
  214. package/src/persistence/stores/telemetry-store.ts +124 -63
  215. package/src/protocol/reasoning-text.ts +30 -0
  216. package/src/protocol/registry.ts +2 -0
  217. package/src/protocol/response/chat.ts +33 -7
  218. package/src/protocol/response/codex.ts +14 -2
  219. package/src/protocol/response/messages.ts +5 -2
  220. package/src/providers/authentication/oauth-flow-store.ts +3 -10
  221. package/src/providers/bundled-discovery-providers.ts +122 -122
  222. package/src/providers/default-registry.ts +14 -0
  223. package/src/providers/discovery/catalog/catalog-blob.ts +18 -18
  224. package/src/providers/discovery/models-dev-catalog.ts +354 -354
  225. package/src/providers/discovery/probe-phases.ts +43 -7
  226. package/src/providers/discovery/probing-service.ts +70 -32
  227. package/src/providers/integrations/antigravity/antigravity.ts +9 -2
  228. package/src/providers/integrations/buddy/buddy-catalog-shared.ts +85 -17
  229. package/src/providers/integrations/buddy/buddy-chat-shared.ts +934 -934
  230. package/src/providers/integrations/buddy/buddy-checkin.ts +5 -9
  231. package/src/providers/integrations/buddy/buddy-discovery-shared.ts +3 -7
  232. package/src/providers/integrations/buddy/buddy-quota-shared.ts +3 -1
  233. package/src/providers/integrations/buddy/capabilities.ts +4 -0
  234. package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +57 -49
  235. package/src/providers/integrations/buddy/codebuddy/provider.ts +180 -180
  236. package/src/providers/integrations/buddy/workbuddy/provider.ts +206 -206
  237. package/src/providers/integrations/buddy/workbuddy/quota.ts +161 -161
  238. package/src/providers/integrations/claude/claude-quota.ts +2 -2
  239. package/src/providers/integrations/claude/claude-user-profiles.ts +3 -2
  240. package/src/providers/integrations/claude/claude.ts +3 -4
  241. package/src/providers/integrations/cline/cline.ts +94 -8
  242. package/src/providers/integrations/codex/codex.ts +61 -37
  243. package/src/providers/integrations/cursor/cursor-catalog.ts +368 -368
  244. package/src/providers/integrations/cursor/cursor-http2.ts +300 -300
  245. package/src/providers/integrations/cursor/cursor-identity.ts +2 -4
  246. package/src/providers/integrations/cursor/cursor-request.ts +11 -0
  247. package/src/providers/integrations/cursor/cursor.ts +29 -17
  248. package/src/providers/integrations/cursor/protocol/framing.ts +129 -129
  249. package/src/providers/integrations/cursor/protocol/messages.ts +2380 -2380
  250. package/src/providers/integrations/cursor/protocol/struct.ts +305 -305
  251. package/src/providers/integrations/cursor/protocol/tool-canonical.ts +275 -0
  252. package/src/providers/integrations/github/github.ts +3 -1
  253. package/src/providers/integrations/grok/grok-quota.ts +279 -279
  254. package/src/providers/integrations/grok/grok-ratelimit-store.ts +73 -73
  255. package/src/providers/integrations/grok/grok-ratelimit.ts +218 -218
  256. package/src/providers/integrations/hermes.ts +7 -1
  257. package/src/providers/integrations/inferhub.ts +201 -73
  258. package/src/providers/integrations/kilo/kilo.ts +152 -152
  259. package/src/providers/integrations/kiro/kiro-machine-id.ts +2 -4
  260. package/src/providers/integrations/opencode/provider.ts +2 -0
  261. package/src/providers/integrations/openrouter/provider.ts +129 -129
  262. package/src/providers/integrations/qoder/qoder-quota.ts +193 -192
  263. package/src/providers/integrations/qoder/qoder-realm.ts +106 -106
  264. package/src/providers/integrations/trae/trae-headers.ts +17 -48
  265. package/src/providers/integrations/trae/trae-oauth.ts +4 -26
  266. package/src/providers/integrations/trae/trae-quota.ts +389 -384
  267. package/src/providers/integrations/trae/trae-realm.ts +15 -2
  268. package/src/providers/integrations/trae/trae.ts +22 -24
  269. package/src/providers/integrations/xiaomi-mimo/think-stream.ts +100 -24
  270. package/src/providers/integrations/zcode/oauth.ts +1 -1
  271. package/src/providers/operations/account-health-service.ts +605 -279
  272. package/src/providers/provider-metadata.ts +12 -1
  273. package/src/providers/provider-model-definition.ts +373 -373
  274. package/src/providers/provider-probe.ts +3 -0
  275. package/src/providers/quota/quota-contracts.ts +3 -7
  276. package/src/providers/search/search-provider.ts +2 -6
  277. package/src/security/admission/contracts.ts +10 -0
  278. package/src/security/admission/in-memory-store.ts +89 -14
  279. package/src/security/admission/lua.ts +191 -182
  280. package/src/security/admission/redis-store.ts +30 -0
  281. package/src/security/admission/service.ts +18 -0
  282. package/src/security/api-key-auth-cache.ts +96 -0
  283. package/src/security/api-key-auth.ts +11 -100
  284. package/src/security/client-router-fingerprint.ts +3 -7
  285. package/src/security/ip-boundary.ts +7 -3
  286. package/src/shared/coerce.ts +67 -0
  287. package/src/shared/hash.ts +6 -0
  288. package/src/shared/time.ts +7 -0
  289. package/src/transport/dispatch/attempt-finalize.ts +26 -138
  290. package/src/transport/dispatch/attempt-loop.ts +103 -81
  291. package/src/transport/dispatch/capture-policy.ts +78 -0
  292. package/src/transport/dispatch/proxy-request.ts +4 -2
  293. package/src/transport/dispatch/streaming/encoder.ts +111 -5
  294. package/src/transport/dispatch/streaming/stream.ts +19 -2
  295. package/src/transport/dispatch/streaming/thinking-loop.ts +300 -0
  296. package/src/transport/failure-policy.ts +27 -0
  297. package/src/transport/guards/abuse-guard.ts +2 -2
  298. package/src/transport/guards/auth-guard.ts +17 -0
  299. package/src/transport/guards/body-policy.ts +3 -2
  300. package/src/transport/guards/concurrency-guard.ts +89 -13
  301. package/src/transport/guards/early-rejection-capture.ts +43 -3
  302. package/src/transport/guards/error-lifecycle.ts +10 -2
  303. package/src/transport/guards/guard-errors.ts +23 -0
  304. package/src/transport/guards/headroom-guard.ts +41 -7
  305. package/src/transport/guards/pipeline.ts +24 -0
  306. package/src/transport/guards/request-context.ts +2 -1
  307. package/src/transport/routing/admission.ts +51 -9
  308. package/src/transport/routing/bounded-lru.ts +49 -0
  309. package/src/transport/routing/candidate-index.ts +88 -0
  310. package/src/transport/routing/reservations.ts +121 -7
  311. package/src/transport/routing/round-robin.ts +10 -22
  312. package/src/transport/routing/route-catalog.ts +26 -13
  313. package/src/transport/routing/route-model.ts +22 -2
  314. package/src/transport/routing/router.ts +53 -31
  315. package/src/transport/surface/chat/encode.ts +2 -3
  316. package/src/transport/surface/chat/parse.ts +2 -1
  317. package/src/transport/surface/dialects.ts +11 -3
  318. package/src/transport/surface/messages/encode.ts +2 -1
  319. package/src/transport/surface/messages/parse.ts +3 -1
  320. package/src/transport/surface/messages/stream.ts +2 -6
  321. package/src/workers/maintenance-lease.ts +200 -0
  322. package/src/workers/scheduler.ts +101 -1
  323. package/dist/dashboard/assets/BannedPage-Brvuj_Eb.js.gz +0 -0
  324. package/dist/dashboard/assets/CliToolDetailPage-CqfXtI3y.js.gz +0 -0
  325. package/dist/dashboard/assets/CliToolsPage-GQeS91bg.js.gz +0 -0
  326. package/dist/dashboard/assets/CombosPage-BDRsTXBB.js.gz +0 -0
  327. package/dist/dashboard/assets/ConsoleLogPage-zP8AFHX0.js.gz +0 -0
  328. package/dist/dashboard/assets/CustomizationPage-BmfrNZ14.js.gz +0 -0
  329. package/dist/dashboard/assets/HealthEventsModal-Dn3M6WZc.js +0 -1
  330. package/dist/dashboard/assets/HealthEventsModal-Dn3M6WZc.js.gz +0 -0
  331. package/dist/dashboard/assets/LoginPage-DoreIgm8.js.gz +0 -0
  332. package/dist/dashboard/assets/ModelPicker-C7pDb41R.js.gz +0 -0
  333. package/dist/dashboard/assets/OverviewPage-CvvD1Ugk.js +0 -1
  334. package/dist/dashboard/assets/OverviewPage-CvvD1Ugk.js.gz +0 -0
  335. package/dist/dashboard/assets/ProviderDetailPage-Clbn1gsW.js +0 -3
  336. package/dist/dashboard/assets/ProviderDetailPage-Clbn1gsW.js.gz +0 -0
  337. package/dist/dashboard/assets/ProvidersPage-BnoPgA7w.js.gz +0 -0
  338. package/dist/dashboard/assets/ProxyPage-Bq_TGgdb.js.gz +0 -0
  339. package/dist/dashboard/assets/QuotaPage-D9DHXvom.js +0 -1
  340. package/dist/dashboard/assets/QuotaPage-D9DHXvom.js.gz +0 -0
  341. package/dist/dashboard/assets/SettingsPage-7xV8WkWL.js +0 -1
  342. package/dist/dashboard/assets/SettingsPage-7xV8WkWL.js.gz +0 -0
  343. package/dist/dashboard/assets/SetupPage-Day3mCOn.js.gz +0 -0
  344. package/dist/dashboard/assets/SortableList-DQxllnB5.js.gz +0 -0
  345. package/dist/dashboard/assets/UsagePage-DXa1oR-4.js.gz +0 -0
  346. package/dist/dashboard/assets/api-keys-DQYLQxpW.js.gz +0 -0
  347. package/dist/dashboard/assets/app-CX_Z8cMs.css +0 -2
  348. package/dist/dashboard/assets/app-CX_Z8cMs.css.gz +0 -0
  349. package/dist/dashboard/assets/assert-C9FD_JwJ.js.gz +0 -0
  350. package/dist/dashboard/assets/badge-CR72dasT.js.gz +0 -0
  351. package/dist/dashboard/assets/banner-CI9_eSfO.js.gz +0 -0
  352. package/dist/dashboard/assets/card-DS7CQEsO.js.gz +0 -0
  353. package/dist/dashboard/assets/cli-tools-CNqzrfdb.js.gz +0 -0
  354. package/dist/dashboard/assets/clipboard-button-611pSsaX.js.gz +0 -0
  355. package/dist/dashboard/assets/dialog-k37exjNt.js.gz +0 -0
  356. package/dist/dashboard/assets/drawer-CX7N8ucR.js.gz +0 -0
  357. package/dist/dashboard/assets/entry-B9Xvtnfj.js +0 -1
  358. package/dist/dashboard/assets/entry-B9Xvtnfj.js.gz +0 -0
  359. package/dist/dashboard/assets/entry-ByMGkR3C.js +0 -4
  360. package/dist/dashboard/assets/entry-ByMGkR3C.js.gz +0 -0
  361. package/dist/dashboard/assets/entry-CTP8AyiS.js.gz +0 -0
  362. package/dist/dashboard/assets/github-badge-sltk1qUi.js.gz +0 -0
  363. package/dist/dashboard/assets/index-z3zDOSMM.js.gz +0 -0
  364. package/dist/dashboard/assets/inline-BhY2sSnO.js.gz +0 -0
  365. package/dist/dashboard/assets/input-V1umFiBk.js.gz +0 -0
  366. package/dist/dashboard/assets/layout-BBOYycyS.js.gz +0 -0
  367. package/dist/dashboard/assets/list-row-1zzZYM0B.js.gz +0 -0
  368. package/dist/dashboard/assets/live-BivGfuUs.js.gz +0 -0
  369. package/dist/dashboard/assets/model-bans-CmffjLuG.js.gz +0 -0
  370. package/dist/dashboard/assets/mutation-46P0oWMe.js.gz +0 -0
  371. package/dist/dashboard/assets/numbers-DkfMR5Qs.js.gz +0 -0
  372. package/dist/dashboard/assets/query-keys-BFhGpwR1.js.gz +0 -0
  373. package/dist/dashboard/assets/routing-CAb4F37R.js.gz +0 -0
  374. package/dist/dashboard/assets/select-C6MMiaio.js.gz +0 -0
  375. package/dist/dashboard/assets/settings-DNX9sydi.js.gz +0 -0
  376. package/dist/dashboard/assets/stack-BHTfXdz_.js.gz +0 -0
  377. package/dist/dashboard/assets/state-mPDRlQkl.js.gz +0 -0
  378. package/dist/dashboard/assets/switch-DOmJxg7H.js.gz +0 -0
  379. package/dist/dashboard/assets/useMutation-Dem0lXQj.js.gz +0 -0
  380. package/scripts/build/aot.ts +0 -143
  381. package/scripts/build/build.ts +0 -24
  382. package/scripts/build/compile.ts +0 -88
  383. package/scripts/build/restart.ts +0 -120
  384. package/scripts/build/start.ts +0 -25
  385. package/src/observability/event-compaction.ts +0 -317
  386. package/src/observability/event-day-index.ts +0 -440
@@ -1,368 +1,368 @@
1
- // Cursor live model catalog and Run-origin discovery.
2
- //
3
- // Two unary Connect-RPC calls on the API host back this module:
4
- // `/aiserver.v1.AiService/AvailableModels` for the roster and
5
- // `/aiserver.v1.ServerConfigService/GetServerConfig` for the region-specific Run
6
- // origin. Both go through the validated outbound fetch the dispatch path hands
7
- // the adapter, so a catalog refresh obeys the same SSRF policy and the same
8
- // network pool as a dispatch.
9
- //
10
- // Nothing here invents a capability. A model is marked image-capable, tool-
11
- // capable, or reasoning-capable only because Cursor declared it; limits come
12
- // from Cursor's own variant parameter or its own context fields.
13
- import { defineModel } from "../../provider-model-definition";
14
- import type { ModelDefinition } from "../../provider-model-definition";
15
- import {
16
- AVAILABLE_MODELS_PATH,
17
- CURSOR_API_BASE_URL,
18
- CURSOR_PROVIDER_ID,
19
- FALLBACK_CLIENT_VERSION,
20
- SERVER_CONFIG_PATH,
21
- normalizeAgentRunOrigin,
22
- } from "./cursor-shared";
23
- import { createCursorChecksumHeader, resolveCursorDeviceIds } from "./cursor-identity";
24
-
25
- /** One parameter value of a Cursor-declared model variant. */
26
- export interface CursorParameterValue {
27
- readonly id: string;
28
- readonly value: string;
29
- }
30
-
31
- /** One variant (parameter tuple) Cursor advertises for a model. */
32
- export interface CursorModelVariant {
33
- readonly displayName: string;
34
- readonly parameterValues: readonly CursorParameterValue[];
35
- readonly isDefaultNonMax: boolean;
36
- readonly isDefaultMax: boolean;
37
- readonly isMaxMode: boolean;
38
- }
39
-
40
- /** One model entry as Cursor declares it. */
41
- export interface CursorModelInfo {
42
- readonly id: string;
43
- readonly displayName: string;
44
- readonly maxContext?: number;
45
- readonly maxContextForMaxMode?: number;
46
- readonly supportsImages?: boolean;
47
- readonly supportsThinking?: boolean;
48
- readonly supportsAgent?: boolean;
49
- readonly supportsMaxMode?: boolean;
50
- readonly variants: readonly CursorModelVariant[];
51
- }
52
-
53
- /** Headers every Cursor unary call carries, matching the CLI's identity. */
54
- async function unaryHeaders(
55
- credential: string,
56
- clientVersion: string,
57
- ): Promise<Record<string, string>> {
58
- const { machineId, macMachineId } = await resolveCursorDeviceIds();
59
- return {
60
- authorization: `Bearer ${credential}`,
61
- "connect-protocol-version": "1",
62
- "content-type": "application/json",
63
- accept: "application/json",
64
- "x-cursor-client-type": "cli",
65
- "x-cursor-client-version": clientVersion,
66
- "x-cursor-checksum": createCursorChecksumHeader(machineId, macMachineId),
67
- "x-ghost-mode": "true",
68
- "x-request-id": crypto.randomUUID(),
69
- };
70
- }
71
-
72
- async function postUnary(
73
- fetcher: typeof fetch,
74
- path: string,
75
- credential: string,
76
- body: unknown,
77
- clientVersion: string,
78
- signal: AbortSignal | undefined,
79
- ): Promise<Record<string, unknown>> {
80
- const response = await fetcher(`${CURSOR_API_BASE_URL}${path}`, {
81
- method: "POST",
82
- headers: await unaryHeaders(credential, clientVersion),
83
- body: JSON.stringify(body),
84
- ...(signal === undefined ? {} : { signal }),
85
- });
86
- const text = await response.text();
87
- if (!response.ok) {
88
- throw new Error(
89
- `Cursor ${path} failed (${response.status}): ${text.slice(0, 200)}`,
90
- );
91
- }
92
- try {
93
- return JSON.parse(text) as Record<string, unknown>;
94
- } catch {
95
- throw new Error(`Cursor ${path} returned malformed JSON`);
96
- }
97
- }
98
-
99
- function isPlainRecord(value: unknown): value is Record<string, unknown> {
100
- if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
101
- const prototype = Object.getPrototypeOf(value);
102
- return prototype === Object.prototype || prototype === null;
103
- }
104
-
105
- function optionalBoolean(
106
- record: Record<string, unknown>,
107
- names: readonly string[],
108
- ): boolean | undefined {
109
- for (const name of names) {
110
- if (!Object.hasOwn(record, name)) continue;
111
- const value = record[name];
112
- return typeof value === "boolean" ? value : undefined;
113
- }
114
- return undefined;
115
- }
116
-
117
- function positiveNumber(value: unknown): number | undefined {
118
- return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : undefined;
119
- }
120
-
121
- /** Parses Cursor's `200k` / `1m` context spelling into tokens. */
122
- export function parseCursorContextLimit(value: unknown): number | undefined {
123
- if (typeof value !== "string") return undefined;
124
- const text = value.trim();
125
- const match = /^(\d+(?:\.\d+)?)\s*([km])$/i.exec(text);
126
- if (match !== null) {
127
- const multiplier = match[2]!.toLowerCase() === "k" ? 1_000 : 1_000_000;
128
- const limit = Number(match[1]) * multiplier;
129
- return Number.isSafeInteger(limit) && limit > 0 ? limit : undefined;
130
- }
131
- const numeric = Number(text);
132
- return Number.isSafeInteger(numeric) && numeric > 0 ? numeric : undefined;
133
- }
134
-
135
- function normalizeParameterValues(value: unknown): CursorParameterValue[] | undefined {
136
- if (value === undefined) return [];
137
- if (!Array.isArray(value)) return undefined;
138
- const parameters: CursorParameterValue[] = [];
139
- for (const entry of value) {
140
- if (!isPlainRecord(entry)) return undefined;
141
- const id = typeof entry["id"] === "string" ? entry["id"].trim() : "";
142
- const parameterValue = entry["value"] ?? entry["value"];
143
- if (id.length === 0 || typeof parameterValue !== "string") return undefined;
144
- parameters.push({ id, value: parameterValue });
145
- }
146
- return parameters;
147
- }
148
-
149
- /**
150
- * Maps a live `AvailableModels` response onto Cursor model entries.
151
- *
152
- * Every entry's capability is Cursor's own declaration. A malformed variant or
153
- * a non-boolean capability throws rather than being coerced: a catalog that
154
- * guesses "supports images" where Cursor said nothing routes image traffic to a
155
- * model that will refuse it.
156
- */
157
- export function mapAvailableModelsResponse(raw: Record<string, unknown>): CursorModelInfo[] {
158
- const entries = Array.isArray(raw["models"]) ? raw["models"] : [];
159
- const models: CursorModelInfo[] = [];
160
- for (const entry of entries) {
161
- if (!isPlainRecord(entry)) continue;
162
- const id = typeof entry["name"] === "string" ? entry["name"].trim() : "";
163
- if (id.length === 0) continue;
164
- const rawVariants = entry["variants"] ?? [];
165
- if (!Array.isArray(rawVariants)) {
166
- throw new Error(`Cursor AvailableModels returned invalid variants for ${id}`);
167
- }
168
- const variants: CursorModelVariant[] = [];
169
- for (const variant of rawVariants) {
170
- if (!isPlainRecord(variant)) {
171
- throw new Error(`Cursor AvailableModels returned an invalid variant for ${id}`);
172
- }
173
- const parameterValues = normalizeParameterValues(
174
- variant["parameterValues"] ?? variant["parameter_values"],
175
- );
176
- if (parameterValues === undefined) {
177
- throw new Error(`Cursor AvailableModels returned invalid parameter values for ${id}`);
178
- }
179
- const isMaxMode = optionalBoolean(variant, ["isMaxMode", "is_max_mode"]) ?? false;
180
- variants.push({
181
- displayName: (typeof variant["displayName"] === "string"
182
- ? variant["displayName"]
183
- : typeof variant["display_name"] === "string"
184
- ? variant["display_name"]
185
- : id) as string,
186
- parameterValues,
187
- isDefaultNonMax:
188
- optionalBoolean(variant, ["isDefaultNonMaxConfig", "is_default_non_max_config"]) ?? false,
189
- isDefaultMax:
190
- optionalBoolean(variant, ["isDefaultMaxConfig", "is_default_max_config"]) ?? false,
191
- isMaxMode,
192
- });
193
- }
194
- const contextOf = (variant: CursorModelVariant | undefined): number | undefined =>
195
- parseCursorContextLimit(variant?.parameterValues.find((p) => p.id === "context")?.value);
196
- const baseContext =
197
- contextOf(variants.find((variant) => variant.isDefaultNonMax)) ??
198
- contextOf(variants.find((variant) => variant.isDefaultMax) ?? variants[0]) ??
199
- positiveNumber(entry["contextTokenLimit"] ?? entry["context_token_limit"]);
200
- const maxContext =
201
- contextOf(variants.find((variant) => variant.isDefaultMax)) ??
202
- positiveNumber(entry["contextTokenLimitForMaxMode"] ?? entry["context_token_limit_for_max_mode"]);
203
- models.push({
204
- id,
205
- displayName: (typeof entry["clientDisplayName"] === "string"
206
- ? entry["clientDisplayName"]
207
- : typeof entry["client_display_name"] === "string"
208
- ? entry["client_display_name"]
209
- : id) as string,
210
- ...(baseContext === undefined ? {} : { maxContext: baseContext }),
211
- ...(maxContext === undefined ? {} : { maxContextForMaxMode: maxContext }),
212
- ...(() => {
213
- const images = optionalBoolean(entry, ["supportsImages", "supports_images"]);
214
- return images === undefined ? {} : { supportsImages: images };
215
- })(),
216
- supportsThinking: optionalBoolean(entry, ["supportsThinking", "supports_thinking"]) ?? false,
217
- supportsAgent: optionalBoolean(entry, ["supportsAgent", "supports_agent"]) ?? false,
218
- supportsMaxMode: optionalBoolean(entry, ["supportsMaxMode", "supports_max_mode"]) ?? false,
219
- variants,
220
- });
221
- }
222
- return models;
223
- }
224
-
225
- /**
226
- * Cursor's declared default parameter tuple for one model.
227
- *
228
- * The public catalog lists one row per model and sends Cursor's own default:
229
- * no variant picker is part of this integration, so the tuple is the one Cursor
230
- * marked default (its max default when the model runs in max mode, otherwise its
231
- * non-max default). An empty tuple is correct for a model that declares none.
232
- */
233
- export function defaultParameterValues(
234
- model: CursorModelInfo | undefined,
235
- maxMode = false,
236
- ): CursorParameterValue[] {
237
- if (model === undefined || model.variants.length === 0) return [];
238
- const tier = model.variants.filter((variant) => variant.isMaxMode === maxMode);
239
- const candidates = tier.length > 0 ? tier : model.variants;
240
- const declared = candidates.find((variant) =>
241
- maxMode ? variant.isDefaultMax : variant.isDefaultNonMax,
242
- );
243
- const chosen = declared ?? candidates.find((variant) => variant.isMaxMode === maxMode) ?? candidates[0];
244
- return chosen === undefined ? [] : chosen.parameterValues.map((parameter) => ({ ...parameter }));
245
- }
246
-
247
- /** Projects one Cursor model entry onto a Cartethyia catalog row. */
248
- export function cursorModelDefinition(model: CursorModelInfo): ModelDefinition {
249
- return cursorModel(model.id, model.maxContext, {
250
- vision: model.supportsImages === true,
251
- reasoning: model.supportsThinking === true,
252
- });
253
- }
254
-
255
- function cursorModel(
256
- modelId: string,
257
- ctx?: number,
258
- options: { vision?: boolean; reasoning?: boolean; out?: number } = {},
259
- ): ModelDefinition {
260
- return defineModel({
261
- id: modelId,
262
- providerId: CURSOR_PROVIDER_ID,
263
- wireFamily: "chat",
264
- endpoint: "/agent.v1.AgentService/Run",
265
- ...(ctx === undefined ? {} : { ctx }),
266
- ...(options.out === undefined ? {} : { out: options.out }),
267
- // Tools are served through Cursor's MCP exec channel, which this adapter
268
- // answers for every request that declares tools — so the row advertises
269
- // them and the adapter refuses anything it did not advertise.
270
- toolCall: true,
271
- vision: options.vision ?? true,
272
- reasoning: options.reasoning ?? false,
273
- });
274
- }
275
-
276
- /**
277
- * Static fallback catalog for Cursor.
278
- *
279
- * The authoritative roster is resolved per account from `AvailableModels`,
280
- * while these static rows give the provider offline floor visibility and
281
- * establish canonical IDs for Auto and Grok 4.6/4.7.
282
- *
283
- * `auto` is the only id surfaced for Cursor's Auto model: the upstream wire id
284
- * is `default` and the two are the same model, not two entries. Dispatch maps
285
- * `auto` is an alias: Cursor's own roster names its Auto model `default`, and
286
- * dispatch maps `auto` onto it, so seeding the upstream id as well would list
287
- * the same model twice. Grok ids are already upstream ids and are not rewritten.
288
- *
289
- * Output is stated explicitly: `defineModel` would otherwise inherit a
290
- * base-catalog output equal to the whole 256k window, which is not a limit
291
- * Cursor serves and makes admission reserve the entire context as headroom.
292
- */
293
- export const CURSOR_MODELS: readonly ModelDefinition[] = [
294
- cursorModel("auto", 256_000, { vision: true, reasoning: false, out: 64_000 }),
295
- cursorModel("grok-4.6", 256_000, { vision: true, reasoning: true, out: 64_000 }),
296
- cursorModel("grok-4.7", 256_000, { vision: true, reasoning: true, out: 64_000 }),
297
- ];
298
-
299
- /** Fetches Cursor's live model roster through validated egress. */
300
- export async function fetchCursorModels(input: {
301
- readonly credential: string;
302
- readonly fetcher?: typeof fetch;
303
- readonly signal?: AbortSignal;
304
- readonly clientVersion?: string;
305
- }): Promise<CursorModelInfo[]> {
306
- const fetcher = input.fetcher ?? globalThis.fetch;
307
- const body = await postUnary(
308
- fetcher,
309
- AVAILABLE_MODELS_PATH,
310
- input.credential,
311
- // Parameterized effort/context/fast variants, as Cursor IDE requests them.
312
- {
313
- includeLongContextModels: true,
314
- useModelParameters: true,
315
- useCloudAgentEffortModes: true,
316
- },
317
- input.clientVersion ?? FALLBACK_CLIENT_VERSION,
318
- input.signal,
319
- );
320
- return mapAvailableModelsResponse(body);
321
- }
322
-
323
- /**
324
- * Resolves the Run-stream origin Cursor routes this account to.
325
- *
326
- * Fails closed: a region-routed account is silently rejected by the wrong
327
- * regional host, so a missing, malformed, non-HTTPS, or non-`*.cursor.sh` value
328
- * is an error rather than a fallback to the API host. The API origin does serve
329
- * auth and catalog calls, but it is not the origin `GetServerConfig` names for
330
- * Run, and assuming it does is what makes a Run land on a host that refuses it.
331
- */
332
- export async function fetchCursorRunOrigin(input: {
333
- readonly credential: string;
334
- readonly fetcher?: typeof fetch;
335
- readonly signal?: AbortSignal;
336
- readonly clientVersion?: string;
337
- }): Promise<string> {
338
- const fetcher = input.fetcher ?? globalThis.fetch;
339
- const body = await postUnary(
340
- fetcher,
341
- SERVER_CONFIG_PATH,
342
- input.credential,
343
- { telem_enabled: false },
344
- input.clientVersion ?? FALLBACK_CLIENT_VERSION,
345
- input.signal,
346
- );
347
- const config = isPlainRecord(body["agentUrlConfig"]) ? body["agentUrlConfig"] : undefined;
348
- const raw = config?.["agentnUrl"] ?? config?.["agentUrl"];
349
- const origin = normalizeAgentRunOrigin(typeof raw === "string" ? raw : undefined);
350
- if (origin === null) {
351
- throw new Error(
352
- raw === undefined
353
- ? "Cursor GetServerConfig response missing agentUrlConfig.agentnUrl"
354
- : "Cursor GetServerConfig returned an invalid Cursor agent URL",
355
- );
356
- }
357
- return origin;
358
- }
359
-
360
- /** Discovery entry point: the live roster, as catalog rows. */
361
- export async function discoverCursorModels(input: {
362
- readonly credential: string;
363
- readonly fetcher?: typeof fetch;
364
- readonly signal?: AbortSignal;
365
- }): Promise<readonly ModelDefinition[]> {
366
- const models = await fetchCursorModels(input);
367
- return models.map(cursorModelDefinition);
368
- }
1
+ // Cursor live model catalog and Run-origin discovery.
2
+ //
3
+ // Two unary Connect-RPC calls on the API host back this module:
4
+ // `/aiserver.v1.AiService/AvailableModels` for the roster and
5
+ // `/aiserver.v1.ServerConfigService/GetServerConfig` for the region-specific Run
6
+ // origin. Both go through the validated outbound fetch the dispatch path hands
7
+ // the adapter, so a catalog refresh obeys the same SSRF policy and the same
8
+ // network pool as a dispatch.
9
+ //
10
+ // Nothing here invents a capability. A model is marked image-capable, tool-
11
+ // capable, or reasoning-capable only because Cursor declared it; limits come
12
+ // from Cursor's own variant parameter or its own context fields.
13
+ import { defineModel } from "../../provider-model-definition";
14
+ import type { ModelDefinition } from "../../provider-model-definition";
15
+ import {
16
+ AVAILABLE_MODELS_PATH,
17
+ CURSOR_API_BASE_URL,
18
+ CURSOR_PROVIDER_ID,
19
+ FALLBACK_CLIENT_VERSION,
20
+ SERVER_CONFIG_PATH,
21
+ normalizeAgentRunOrigin,
22
+ } from "./cursor-shared";
23
+ import { createCursorChecksumHeader, resolveCursorDeviceIds } from "./cursor-identity";
24
+
25
+ /** One parameter value of a Cursor-declared model variant. */
26
+ export interface CursorParameterValue {
27
+ readonly id: string;
28
+ readonly value: string;
29
+ }
30
+
31
+ /** One variant (parameter tuple) Cursor advertises for a model. */
32
+ export interface CursorModelVariant {
33
+ readonly displayName: string;
34
+ readonly parameterValues: readonly CursorParameterValue[];
35
+ readonly isDefaultNonMax: boolean;
36
+ readonly isDefaultMax: boolean;
37
+ readonly isMaxMode: boolean;
38
+ }
39
+
40
+ /** One model entry as Cursor declares it. */
41
+ export interface CursorModelInfo {
42
+ readonly id: string;
43
+ readonly displayName: string;
44
+ readonly maxContext?: number;
45
+ readonly maxContextForMaxMode?: number;
46
+ readonly supportsImages?: boolean;
47
+ readonly supportsThinking?: boolean;
48
+ readonly supportsAgent?: boolean;
49
+ readonly supportsMaxMode?: boolean;
50
+ readonly variants: readonly CursorModelVariant[];
51
+ }
52
+
53
+ /** Headers every Cursor unary call carries, matching the CLI's identity. */
54
+ async function unaryHeaders(
55
+ credential: string,
56
+ clientVersion: string,
57
+ ): Promise<Record<string, string>> {
58
+ const { machineId, macMachineId } = await resolveCursorDeviceIds();
59
+ return {
60
+ authorization: `Bearer ${credential}`,
61
+ "connect-protocol-version": "1",
62
+ "content-type": "application/json",
63
+ accept: "application/json",
64
+ "x-cursor-client-type": "cli",
65
+ "x-cursor-client-version": clientVersion,
66
+ "x-cursor-checksum": createCursorChecksumHeader(machineId, macMachineId),
67
+ "x-ghost-mode": "true",
68
+ "x-request-id": crypto.randomUUID(),
69
+ };
70
+ }
71
+
72
+ async function postUnary(
73
+ fetcher: typeof fetch,
74
+ path: string,
75
+ credential: string,
76
+ body: unknown,
77
+ clientVersion: string,
78
+ signal: AbortSignal | undefined,
79
+ ): Promise<Record<string, unknown>> {
80
+ const response = await fetcher(`${CURSOR_API_BASE_URL}${path}`, {
81
+ method: "POST",
82
+ headers: await unaryHeaders(credential, clientVersion),
83
+ body: JSON.stringify(body),
84
+ ...(signal === undefined ? {} : { signal }),
85
+ });
86
+ const text = await response.text();
87
+ if (!response.ok) {
88
+ throw new Error(
89
+ `Cursor ${path} failed (${response.status}): ${text.slice(0, 200)}`,
90
+ );
91
+ }
92
+ try {
93
+ return JSON.parse(text) as Record<string, unknown>;
94
+ } catch {
95
+ throw new Error(`Cursor ${path} returned malformed JSON`);
96
+ }
97
+ }
98
+
99
+ function isPlainRecord(value: unknown): value is Record<string, unknown> {
100
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
101
+ const prototype = Object.getPrototypeOf(value);
102
+ return prototype === Object.prototype || prototype === null;
103
+ }
104
+
105
+ function optionalBoolean(
106
+ record: Record<string, unknown>,
107
+ names: readonly string[],
108
+ ): boolean | undefined {
109
+ for (const name of names) {
110
+ if (!Object.hasOwn(record, name)) continue;
111
+ const value = record[name];
112
+ return typeof value === "boolean" ? value : undefined;
113
+ }
114
+ return undefined;
115
+ }
116
+
117
+ function positiveNumber(value: unknown): number | undefined {
118
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : undefined;
119
+ }
120
+
121
+ /** Parses Cursor's `200k` / `1m` context spelling into tokens. */
122
+ export function parseCursorContextLimit(value: unknown): number | undefined {
123
+ if (typeof value !== "string") return undefined;
124
+ const text = value.trim();
125
+ const match = /^(\d+(?:\.\d+)?)\s*([km])$/i.exec(text);
126
+ if (match !== null) {
127
+ const multiplier = match[2]!.toLowerCase() === "k" ? 1_000 : 1_000_000;
128
+ const limit = Number(match[1]) * multiplier;
129
+ return Number.isSafeInteger(limit) && limit > 0 ? limit : undefined;
130
+ }
131
+ const numeric = Number(text);
132
+ return Number.isSafeInteger(numeric) && numeric > 0 ? numeric : undefined;
133
+ }
134
+
135
+ function normalizeParameterValues(value: unknown): CursorParameterValue[] | undefined {
136
+ if (value === undefined) return [];
137
+ if (!Array.isArray(value)) return undefined;
138
+ const parameters: CursorParameterValue[] = [];
139
+ for (const entry of value) {
140
+ if (!isPlainRecord(entry)) return undefined;
141
+ const id = typeof entry["id"] === "string" ? entry["id"].trim() : "";
142
+ const parameterValue = entry["value"] ?? entry["value"];
143
+ if (id.length === 0 || typeof parameterValue !== "string") return undefined;
144
+ parameters.push({ id, value: parameterValue });
145
+ }
146
+ return parameters;
147
+ }
148
+
149
+ /**
150
+ * Maps a live `AvailableModels` response onto Cursor model entries.
151
+ *
152
+ * Every entry's capability is Cursor's own declaration. A malformed variant or
153
+ * a non-boolean capability throws rather than being coerced: a catalog that
154
+ * guesses "supports images" where Cursor said nothing routes image traffic to a
155
+ * model that will refuse it.
156
+ */
157
+ export function mapAvailableModelsResponse(raw: Record<string, unknown>): CursorModelInfo[] {
158
+ const entries = Array.isArray(raw["models"]) ? raw["models"] : [];
159
+ const models: CursorModelInfo[] = [];
160
+ for (const entry of entries) {
161
+ if (!isPlainRecord(entry)) continue;
162
+ const id = typeof entry["name"] === "string" ? entry["name"].trim() : "";
163
+ if (id.length === 0) continue;
164
+ const rawVariants = entry["variants"] ?? [];
165
+ if (!Array.isArray(rawVariants)) {
166
+ throw new Error(`Cursor AvailableModels returned invalid variants for ${id}`);
167
+ }
168
+ const variants: CursorModelVariant[] = [];
169
+ for (const variant of rawVariants) {
170
+ if (!isPlainRecord(variant)) {
171
+ throw new Error(`Cursor AvailableModels returned an invalid variant for ${id}`);
172
+ }
173
+ const parameterValues = normalizeParameterValues(
174
+ variant["parameterValues"] ?? variant["parameter_values"],
175
+ );
176
+ if (parameterValues === undefined) {
177
+ throw new Error(`Cursor AvailableModels returned invalid parameter values for ${id}`);
178
+ }
179
+ const isMaxMode = optionalBoolean(variant, ["isMaxMode", "is_max_mode"]) ?? false;
180
+ variants.push({
181
+ displayName: (typeof variant["displayName"] === "string"
182
+ ? variant["displayName"]
183
+ : typeof variant["display_name"] === "string"
184
+ ? variant["display_name"]
185
+ : id) as string,
186
+ parameterValues,
187
+ isDefaultNonMax:
188
+ optionalBoolean(variant, ["isDefaultNonMaxConfig", "is_default_non_max_config"]) ?? false,
189
+ isDefaultMax:
190
+ optionalBoolean(variant, ["isDefaultMaxConfig", "is_default_max_config"]) ?? false,
191
+ isMaxMode,
192
+ });
193
+ }
194
+ const contextOf = (variant: CursorModelVariant | undefined): number | undefined =>
195
+ parseCursorContextLimit(variant?.parameterValues.find((p) => p.id === "context")?.value);
196
+ const baseContext =
197
+ contextOf(variants.find((variant) => variant.isDefaultNonMax)) ??
198
+ contextOf(variants.find((variant) => variant.isDefaultMax) ?? variants[0]) ??
199
+ positiveNumber(entry["contextTokenLimit"] ?? entry["context_token_limit"]);
200
+ const maxContext =
201
+ contextOf(variants.find((variant) => variant.isDefaultMax)) ??
202
+ positiveNumber(entry["contextTokenLimitForMaxMode"] ?? entry["context_token_limit_for_max_mode"]);
203
+ models.push({
204
+ id,
205
+ displayName: (typeof entry["clientDisplayName"] === "string"
206
+ ? entry["clientDisplayName"]
207
+ : typeof entry["client_display_name"] === "string"
208
+ ? entry["client_display_name"]
209
+ : id) as string,
210
+ ...(baseContext === undefined ? {} : { maxContext: baseContext }),
211
+ ...(maxContext === undefined ? {} : { maxContextForMaxMode: maxContext }),
212
+ ...(() => {
213
+ const images = optionalBoolean(entry, ["supportsImages", "supports_images"]);
214
+ return images === undefined ? {} : { supportsImages: images };
215
+ })(),
216
+ supportsThinking: optionalBoolean(entry, ["supportsThinking", "supports_thinking"]) ?? false,
217
+ supportsAgent: optionalBoolean(entry, ["supportsAgent", "supports_agent"]) ?? false,
218
+ supportsMaxMode: optionalBoolean(entry, ["supportsMaxMode", "supports_max_mode"]) ?? false,
219
+ variants,
220
+ });
221
+ }
222
+ return models;
223
+ }
224
+
225
+ /**
226
+ * Cursor's declared default parameter tuple for one model.
227
+ *
228
+ * The public catalog lists one row per model and sends Cursor's own default:
229
+ * no variant picker is part of this integration, so the tuple is the one Cursor
230
+ * marked default (its max default when the model runs in max mode, otherwise its
231
+ * non-max default). An empty tuple is correct for a model that declares none.
232
+ */
233
+ export function defaultParameterValues(
234
+ model: CursorModelInfo | undefined,
235
+ maxMode = false,
236
+ ): CursorParameterValue[] {
237
+ if (model === undefined || model.variants.length === 0) return [];
238
+ const tier = model.variants.filter((variant) => variant.isMaxMode === maxMode);
239
+ const candidates = tier.length > 0 ? tier : model.variants;
240
+ const declared = candidates.find((variant) =>
241
+ maxMode ? variant.isDefaultMax : variant.isDefaultNonMax,
242
+ );
243
+ const chosen = declared ?? candidates.find((variant) => variant.isMaxMode === maxMode) ?? candidates[0];
244
+ return chosen === undefined ? [] : chosen.parameterValues.map((parameter) => ({ ...parameter }));
245
+ }
246
+
247
+ /** Projects one Cursor model entry onto a Cartethyia catalog row. */
248
+ export function cursorModelDefinition(model: CursorModelInfo): ModelDefinition {
249
+ return cursorModel(model.id, model.maxContext, {
250
+ vision: model.supportsImages === true,
251
+ reasoning: model.supportsThinking === true,
252
+ });
253
+ }
254
+
255
+ function cursorModel(
256
+ modelId: string,
257
+ ctx?: number,
258
+ options: { vision?: boolean; reasoning?: boolean; out?: number } = {},
259
+ ): ModelDefinition {
260
+ return defineModel({
261
+ id: modelId,
262
+ providerId: CURSOR_PROVIDER_ID,
263
+ wireFamily: "chat",
264
+ endpoint: "/agent.v1.AgentService/Run",
265
+ ...(ctx === undefined ? {} : { ctx }),
266
+ ...(options.out === undefined ? {} : { out: options.out }),
267
+ // Tools are served through Cursor's MCP exec channel, which this adapter
268
+ // answers for every request that declares tools — so the row advertises
269
+ // them and the adapter refuses anything it did not advertise.
270
+ toolCall: true,
271
+ vision: options.vision ?? true,
272
+ reasoning: options.reasoning ?? false,
273
+ });
274
+ }
275
+
276
+ /**
277
+ * Static fallback catalog for Cursor.
278
+ *
279
+ * The authoritative roster is resolved per account from `AvailableModels`,
280
+ * while these static rows give the provider offline floor visibility and
281
+ * establish canonical IDs for Auto and Grok 4.6/4.7.
282
+ *
283
+ * `auto` is the only id surfaced for Cursor's Auto model: the upstream wire id
284
+ * is `default` and the two are the same model, not two entries. Dispatch maps
285
+ * `auto` is an alias: Cursor's own roster names its Auto model `default`, and
286
+ * dispatch maps `auto` onto it, so seeding the upstream id as well would list
287
+ * the same model twice. Grok ids are already upstream ids and are not rewritten.
288
+ *
289
+ * Output is stated explicitly: `defineModel` would otherwise inherit a
290
+ * base-catalog output equal to the whole 256k window, which is not a limit
291
+ * Cursor serves and makes admission reserve the entire context as headroom.
292
+ */
293
+ export const CURSOR_MODELS: readonly ModelDefinition[] = [
294
+ cursorModel("auto", 256_000, { vision: true, reasoning: false, out: 64_000 }),
295
+ cursorModel("grok-4.6", 256_000, { vision: true, reasoning: true, out: 64_000 }),
296
+ cursorModel("grok-4.7", 256_000, { vision: true, reasoning: true, out: 64_000 }),
297
+ ];
298
+
299
+ /** Fetches Cursor's live model roster through validated egress. */
300
+ export async function fetchCursorModels(input: {
301
+ readonly credential: string;
302
+ readonly fetcher?: typeof fetch;
303
+ readonly signal?: AbortSignal;
304
+ readonly clientVersion?: string;
305
+ }): Promise<CursorModelInfo[]> {
306
+ const fetcher = input.fetcher ?? globalThis.fetch;
307
+ const body = await postUnary(
308
+ fetcher,
309
+ AVAILABLE_MODELS_PATH,
310
+ input.credential,
311
+ // Parameterized effort/context/fast variants, as Cursor IDE requests them.
312
+ {
313
+ includeLongContextModels: true,
314
+ useModelParameters: true,
315
+ useCloudAgentEffortModes: true,
316
+ },
317
+ input.clientVersion ?? FALLBACK_CLIENT_VERSION,
318
+ input.signal,
319
+ );
320
+ return mapAvailableModelsResponse(body);
321
+ }
322
+
323
+ /**
324
+ * Resolves the Run-stream origin Cursor routes this account to.
325
+ *
326
+ * Fails closed: a region-routed account is silently rejected by the wrong
327
+ * regional host, so a missing, malformed, non-HTTPS, or non-`*.cursor.sh` value
328
+ * is an error rather than a fallback to the API host. The API origin does serve
329
+ * auth and catalog calls, but it is not the origin `GetServerConfig` names for
330
+ * Run, and assuming it does is what makes a Run land on a host that refuses it.
331
+ */
332
+ export async function fetchCursorRunOrigin(input: {
333
+ readonly credential: string;
334
+ readonly fetcher?: typeof fetch;
335
+ readonly signal?: AbortSignal;
336
+ readonly clientVersion?: string;
337
+ }): Promise<string> {
338
+ const fetcher = input.fetcher ?? globalThis.fetch;
339
+ const body = await postUnary(
340
+ fetcher,
341
+ SERVER_CONFIG_PATH,
342
+ input.credential,
343
+ { telem_enabled: false },
344
+ input.clientVersion ?? FALLBACK_CLIENT_VERSION,
345
+ input.signal,
346
+ );
347
+ const config = isPlainRecord(body["agentUrlConfig"]) ? body["agentUrlConfig"] : undefined;
348
+ const raw = config?.["agentnUrl"] ?? config?.["agentUrl"];
349
+ const origin = normalizeAgentRunOrigin(typeof raw === "string" ? raw : undefined);
350
+ if (origin === null) {
351
+ throw new Error(
352
+ raw === undefined
353
+ ? "Cursor GetServerConfig response missing agentUrlConfig.agentnUrl"
354
+ : "Cursor GetServerConfig returned an invalid Cursor agent URL",
355
+ );
356
+ }
357
+ return origin;
358
+ }
359
+
360
+ /** Discovery entry point: the live roster, as catalog rows. */
361
+ export async function discoverCursorModels(input: {
362
+ readonly credential: string;
363
+ readonly fetcher?: typeof fetch;
364
+ readonly signal?: AbortSignal;
365
+ }): Promise<readonly ModelDefinition[]> {
366
+ const models = await fetchCursorModels(input);
367
+ return models.map(cursorModelDefinition);
368
+ }