cartethyia 2.5.10 → 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 (367) hide show
  1. package/.env.example +8 -0
  2. package/dist/dashboard/assets/{BannedPage-Cd30Z0b9.js → BannedPage-l6XMnfr9.js} +1 -1
  3. package/dist/dashboard/assets/BannedPage-l6XMnfr9.js.gz +0 -0
  4. package/dist/dashboard/assets/{CliToolDetailPage-wf5u-ww6.js → CliToolDetailPage-DwOzAW45.js} +1 -1
  5. package/dist/dashboard/assets/CliToolDetailPage-DwOzAW45.js.gz +0 -0
  6. package/dist/dashboard/assets/{CliToolsPage-DykPvuck.js → CliToolsPage-BT3Up3QA.js} +1 -1
  7. package/dist/dashboard/assets/CliToolsPage-BT3Up3QA.js.gz +0 -0
  8. package/dist/dashboard/assets/{CombosPage-DwvSD3vs.js → CombosPage-6kYIUpzR.js} +1 -1
  9. package/dist/dashboard/assets/CombosPage-6kYIUpzR.js.gz +0 -0
  10. package/dist/dashboard/assets/{ConsoleLogPage-CIr7KDex.js → ConsoleLogPage-zRnj1mlR.js} +1 -1
  11. package/dist/dashboard/assets/ConsoleLogPage-zRnj1mlR.js.gz +0 -0
  12. package/dist/dashboard/assets/{CustomizationPage-82X0DDdH.js → CustomizationPage-C7m-H1Rz.js} +1 -1
  13. package/dist/dashboard/assets/CustomizationPage-C7m-H1Rz.js.gz +0 -0
  14. package/dist/dashboard/assets/HealthEventsModal-C8MR_BdB.js +1 -0
  15. package/dist/dashboard/assets/HealthEventsModal-C8MR_BdB.js.gz +0 -0
  16. package/dist/dashboard/assets/{LoginPage-CS5mPuVF.js → LoginPage-CbphLvjg.js} +1 -1
  17. package/dist/dashboard/assets/LoginPage-CbphLvjg.js.gz +0 -0
  18. package/dist/dashboard/assets/{ModelPicker-C0UCUsVJ.js → ModelPicker-DVny4S5_.js} +1 -1
  19. package/dist/dashboard/assets/ModelPicker-DVny4S5_.js.gz +0 -0
  20. package/dist/dashboard/assets/OverviewPage-DABu2Wgt.js +1 -0
  21. package/dist/dashboard/assets/OverviewPage-DABu2Wgt.js.gz +0 -0
  22. package/dist/dashboard/assets/ProviderDetailPage-C15x9q13.js +3 -0
  23. package/dist/dashboard/assets/ProviderDetailPage-C15x9q13.js.gz +0 -0
  24. package/dist/dashboard/assets/{ProvidersPage-vdja5ZPK.js → ProvidersPage-Dfb2896L.js} +1 -1
  25. package/dist/dashboard/assets/ProvidersPage-Dfb2896L.js.gz +0 -0
  26. package/dist/dashboard/assets/{ProxyPage-BB4CNm4-.js → ProxyPage-BaCIdAUt.js} +1 -1
  27. package/dist/dashboard/assets/ProxyPage-BaCIdAUt.js.gz +0 -0
  28. package/dist/dashboard/assets/QuotaPage-BsdoCIw1.js +1 -0
  29. package/dist/dashboard/assets/QuotaPage-BsdoCIw1.js.gz +0 -0
  30. package/dist/dashboard/assets/SettingsPage-KVJFh3Ip.js +1 -0
  31. package/dist/dashboard/assets/SettingsPage-KVJFh3Ip.js.gz +0 -0
  32. package/dist/dashboard/assets/{SetupPage-FadUOl9S.js → SetupPage-D4g6vOGc.js} +1 -1
  33. package/dist/dashboard/assets/SetupPage-D4g6vOGc.js.gz +0 -0
  34. package/dist/dashboard/assets/{SortableList-Cfy6_Khv.js → SortableList-C93i14BD.js} +1 -1
  35. package/dist/dashboard/assets/SortableList-C93i14BD.js.gz +0 -0
  36. package/dist/dashboard/assets/{StudioPage-CM92fgJf.js → StudioPage-SgQpymjm.js} +1 -1
  37. package/dist/dashboard/assets/{StudioPage-CM92fgJf.js.gz → StudioPage-SgQpymjm.js.gz} +0 -0
  38. package/dist/dashboard/assets/{UsagePage-BT4MyQeT.js → UsagePage-BFME-eba.js} +2 -2
  39. package/dist/dashboard/assets/UsagePage-BFME-eba.js.gz +0 -0
  40. package/dist/dashboard/assets/{api-keys-DQYLQxpW.js → api-keys-C23S9sgp.js} +1 -1
  41. package/dist/dashboard/assets/api-keys-C23S9sgp.js.gz +0 -0
  42. package/dist/dashboard/assets/{app-B7pVRhJ1.js → app-2KhtHCv9.js} +1 -1
  43. package/dist/dashboard/assets/{app-B7pVRhJ1.js.gz → app-2KhtHCv9.js.gz} +0 -0
  44. package/dist/dashboard/assets/app-CEeaMHOy.css +2 -0
  45. package/dist/dashboard/assets/app-CEeaMHOy.css.gz +0 -0
  46. package/dist/dashboard/assets/{assert-C9FD_JwJ.js → assert-DU6qrMA_.js} +1 -1
  47. package/dist/dashboard/assets/assert-DU6qrMA_.js.gz +0 -0
  48. package/dist/dashboard/assets/{badge-Bvyr7D0u.js → badge-CKsF123d.js} +1 -1
  49. package/dist/dashboard/assets/badge-CKsF123d.js.gz +0 -0
  50. package/dist/dashboard/assets/{banner-DIvKAZih.js → banner-8j7uChA9.js} +1 -1
  51. package/dist/dashboard/assets/banner-8j7uChA9.js.gz +0 -0
  52. package/dist/dashboard/assets/{card-D5_gYIS_.js → card-CdsSlWA3.js} +1 -1
  53. package/dist/dashboard/assets/card-CdsSlWA3.js.gz +0 -0
  54. package/dist/dashboard/assets/{cli-tools-CNqzrfdb.js → cli-tools-BaFUuFFF.js} +1 -1
  55. package/dist/dashboard/assets/cli-tools-BaFUuFFF.js.gz +0 -0
  56. package/dist/dashboard/assets/{clipboard-button-B6vDBtbm.js → clipboard-button-uL5aYLfW.js} +1 -1
  57. package/dist/dashboard/assets/clipboard-button-uL5aYLfW.js.gz +0 -0
  58. package/dist/dashboard/assets/{dialog-CpjrgdwJ.js → dialog-C07hr5_G.js} +1 -1
  59. package/dist/dashboard/assets/dialog-C07hr5_G.js.gz +0 -0
  60. package/dist/dashboard/assets/{drawer-BWmt3b5y.js → drawer-BPO1TcKH.js} +1 -1
  61. package/dist/dashboard/assets/drawer-BPO1TcKH.js.gz +0 -0
  62. package/dist/dashboard/assets/{entry-C_oakV3z.js → entry-Bz_CtDfC.js} +1 -1
  63. package/dist/dashboard/assets/entry-Bz_CtDfC.js.gz +0 -0
  64. package/dist/dashboard/assets/entry-CwPRKk0d.js +4 -0
  65. package/dist/dashboard/assets/entry-CwPRKk0d.js.gz +0 -0
  66. package/dist/dashboard/assets/entry-GJRbmrpA.js +1 -0
  67. package/dist/dashboard/assets/entry-GJRbmrpA.js.gz +0 -0
  68. package/dist/dashboard/assets/{github-badge-RRHHWGxh.js → github-badge-DmQreSDd.js} +1 -1
  69. package/dist/dashboard/assets/github-badge-DmQreSDd.js.gz +0 -0
  70. package/dist/dashboard/assets/{index-Bhey_rg_.js → index-Bblu6erR.js} +2 -2
  71. package/dist/dashboard/assets/index-Bblu6erR.js.gz +0 -0
  72. package/dist/dashboard/assets/{inline-C2_EQ5Uf.js → inline-C9dCYaBZ.js} +1 -1
  73. package/dist/dashboard/assets/inline-C9dCYaBZ.js.gz +0 -0
  74. package/dist/dashboard/assets/{input-BMRlQ450.js → input-CjCnXweJ.js} +1 -1
  75. package/dist/dashboard/assets/input-CjCnXweJ.js.gz +0 -0
  76. package/dist/dashboard/assets/{layout-XR5xP8ew.js → layout-V6y8DAkl.js} +1 -1
  77. package/dist/dashboard/assets/layout-V6y8DAkl.js.gz +0 -0
  78. package/dist/dashboard/assets/{list-row-Cw07dioc.js → list-row-CM1AaG_y.js} +1 -1
  79. package/dist/dashboard/assets/list-row-CM1AaG_y.js.gz +0 -0
  80. package/dist/dashboard/assets/{live-BivGfuUs.js → live-ta6GMb-8.js} +1 -1
  81. package/dist/dashboard/assets/live-ta6GMb-8.js.gz +0 -0
  82. package/dist/dashboard/assets/{model-bans-CmffjLuG.js → model-bans-BmYTmbAk.js} +1 -1
  83. package/dist/dashboard/assets/model-bans-BmYTmbAk.js.gz +0 -0
  84. package/dist/dashboard/assets/{mutation-46P0oWMe.js → mutation-g_fduiNw.js} +1 -1
  85. package/dist/dashboard/assets/mutation-g_fduiNw.js.gz +0 -0
  86. package/dist/dashboard/assets/{numbers-DrdA9oSM.js → numbers-BOevJzdz.js} +1 -1
  87. package/dist/dashboard/assets/numbers-BOevJzdz.js.gz +0 -0
  88. package/dist/dashboard/assets/{query-keys-BFhGpwR1.js → query-keys-UETqfytp.js} +1 -1
  89. package/dist/dashboard/assets/query-keys-UETqfytp.js.gz +0 -0
  90. package/dist/dashboard/assets/{routing-CAb4F37R.js → routing-BPUo-W8d.js} +1 -1
  91. package/dist/dashboard/assets/routing-BPUo-W8d.js.gz +0 -0
  92. package/dist/dashboard/assets/{select-CxVVB__N.js → select-C980c4cz.js} +1 -1
  93. package/dist/dashboard/assets/select-C980c4cz.js.gz +0 -0
  94. package/dist/dashboard/assets/{settings-DNX9sydi.js → settings--3hxSyry.js} +1 -1
  95. package/dist/dashboard/assets/settings--3hxSyry.js.gz +0 -0
  96. package/dist/dashboard/assets/{stack-uPOwM9Hs.js → stack-_gfTdLVB.js} +1 -1
  97. package/dist/dashboard/assets/stack-_gfTdLVB.js.gz +0 -0
  98. package/dist/dashboard/assets/{state-D1Yz79iL.js → state-2c5V4zo7.js} +1 -1
  99. package/dist/dashboard/assets/state-2c5V4zo7.js.gz +0 -0
  100. package/dist/dashboard/assets/{switch-sevsRrRo.js → switch-Dkke7Naf.js} +1 -1
  101. package/dist/dashboard/assets/switch-Dkke7Naf.js.gz +0 -0
  102. package/dist/dashboard/assets/{useMutation-Dem0lXQj.js → useMutation-EDFRkn1i.js} +1 -1
  103. package/dist/dashboard/assets/useMutation-EDFRkn1i.js.gz +0 -0
  104. package/dist/dashboard/index.html +1 -1
  105. package/dist/dashboard/index.html.gz +0 -0
  106. package/dist/dashboard/precompressed-assets.json +51 -51
  107. package/migrations/README.md +52 -52
  108. package/migrations/full-postgres/0020_add_telemetry_event_analytics.sql +77 -0
  109. package/migrations/full-postgres/0021_add_api_key_concurrency_tolerance.sql +13 -0
  110. package/migrations/full-postgres/0022_add_inferhub_config_key.sql +17 -0
  111. package/migrations/full-postgres/0023_drop_api_key_concurrency_tolerance.sql +13 -0
  112. package/migrations/lite-sqlite3/0020_add_telemetry_event_analytics.sql +69 -0
  113. package/migrations/lite-sqlite3/0021_add_api_key_concurrency_tolerance.sql +13 -0
  114. package/migrations/lite-sqlite3/0022_add_inferhub_config_key.sql +17 -0
  115. package/migrations/lite-sqlite3/0023_drop_api_key_concurrency_tolerance.sql +13 -0
  116. package/package.json +96 -95
  117. package/scripts/cli/admin.ts +126 -128
  118. package/scripts/cli/autostart.ts +237 -237
  119. package/scripts/cli/background.ts +120 -110
  120. package/scripts/cli/bin/carte.cjs +8 -8
  121. package/scripts/cli/bin/cartethyia.cjs +167 -167
  122. package/scripts/cli/console-menu.ts +39 -30
  123. package/scripts/cli/controller.ts +145 -0
  124. package/scripts/cli/env.ts +99 -99
  125. package/scripts/cli/gateway.ts +264 -260
  126. package/scripts/cli/instance.ts +234 -224
  127. package/scripts/cli/main.ts +188 -167
  128. package/scripts/cli/platform.ts +64 -64
  129. package/scripts/cli/prompt.ts +217 -79
  130. package/scripts/cli/services.ts +88 -88
  131. package/scripts/cli/setup.ts +445 -445
  132. package/scripts/cli/start.ts +85 -85
  133. package/scripts/cli/startup-signal.ts +23 -23
  134. package/scripts/cli/telemetry.ts +70 -0
  135. package/scripts/cli/tray/index.ts +263 -263
  136. package/scripts/cli/tray/windows.ps1 +107 -107
  137. package/scripts/cli/tray.ts +29 -4
  138. package/scripts/cli/update.ts +39 -39
  139. package/scripts/cli/version.ts +67 -67
  140. package/scripts/cli/wordmark.ts +58 -58
  141. package/src/boot/app.ts +16 -1
  142. package/src/boot/dependencies.ts +128 -25
  143. package/src/config/resources.ts +137 -0
  144. package/src/config/runtime.ts +133 -76
  145. package/src/config/spec.ts +30 -29
  146. package/src/config/telemetry.ts +0 -25
  147. package/src/console/api-keys/bucket-spend.ts +14 -17
  148. package/src/console/api-keys/contracts.ts +410 -410
  149. package/src/console/api-keys/routes.ts +12 -0
  150. package/src/console/audit/store.ts +2 -3
  151. package/src/console/auth/setup-service.ts +8 -1
  152. package/src/console/backup/routes.ts +7 -0
  153. package/src/console/backup/service.ts +463 -463
  154. package/src/console/backup/store.ts +16 -11
  155. package/src/console/backup/validate.ts +1 -4
  156. package/src/console/cli-tools/fs-ops.ts +18 -2
  157. package/src/console/data-management/contracts.ts +1 -8
  158. package/src/console/data-management/operations.ts +61 -48
  159. package/src/console/data-management/routes.ts +4 -2
  160. package/src/console/domain-registration.ts +25 -3
  161. package/src/console/inferhub/client.ts +139 -0
  162. package/src/console/inferhub/key-store.ts +129 -0
  163. package/src/console/inferhub/normalize.ts +347 -0
  164. package/src/console/inferhub/routes.ts +411 -0
  165. package/src/console/inferhub/store.ts +109 -0
  166. package/src/console/inferhub/types.ts +183 -0
  167. package/src/console/observability/contracts.ts +2 -41
  168. package/src/console/observability/store.ts +176 -371
  169. package/src/console/providers/catalog/public-model-store.ts +0 -0
  170. package/src/console/providers/catalog/store.ts +23 -36
  171. package/src/console/quota/account-quota-shared.ts +52 -52
  172. package/src/console/quota/account-quota-tenant.ts +16 -24
  173. package/src/console/quota/refresh.ts +441 -441
  174. package/src/console/routing/pools/import-parse.ts +206 -206
  175. package/src/console/routing/pools/operations.ts +543 -543
  176. package/src/console/routing/pools/probe-verdict.ts +132 -132
  177. package/src/console/routing/pools/routes.ts +181 -181
  178. package/src/console/settings/contracts.ts +42 -40
  179. package/src/console/settings/store.ts +25 -25
  180. package/src/console/share/share-stats.ts +22 -4
  181. package/src/console/share/share-usage.ts +30 -4
  182. package/src/console/shared/cursor.ts +15 -0
  183. package/src/console/updates/routes.ts +12 -0
  184. package/src/main.ts +13 -5
  185. package/src/network/pool/resolver.ts +1104 -1104
  186. package/src/observability/client-name.ts +22 -0
  187. package/src/observability/event-log.ts +5 -29
  188. package/src/observability/event-query.ts +690 -0
  189. package/src/observability/log-ring.ts +26 -2
  190. package/src/observability/log-sanitize.ts +118 -0
  191. package/src/observability/logger.ts +17 -13
  192. package/src/observability/metrics.ts +35 -0
  193. package/src/observability/payload-capture.ts +114 -41
  194. package/src/observability/payload-store.ts +249 -24
  195. package/src/observability/raw-event-writer.ts +118 -0
  196. package/src/observability/runtime-metrics.ts +71 -1
  197. package/src/observability/telemetry-buffer.ts +100 -17
  198. package/src/observability/telemetry-reconciliation.ts +417 -0
  199. package/src/persistence/boot.ts +20 -9
  200. package/src/persistence/db-pg.ts +13 -0
  201. package/src/persistence/db-sqlite.ts +569 -497
  202. package/src/persistence/dialect.ts +3 -2
  203. package/src/persistence/migrate.ts +181 -181
  204. package/src/persistence/schema.ts +1496 -1406
  205. package/src/persistence/shared-cache.ts +5 -1
  206. package/src/persistence/stores/api-key-store.ts +581 -581
  207. package/src/persistence/stores/telemetry-store.ts +124 -63
  208. package/src/protocol/reasoning-text.ts +30 -0
  209. package/src/protocol/response/chat.ts +33 -7
  210. package/src/protocol/response/codex.ts +14 -2
  211. package/src/protocol/response/messages.ts +5 -2
  212. package/src/providers/authentication/oauth-flow-store.ts +3 -10
  213. package/src/providers/bundled-discovery-providers.ts +122 -122
  214. package/src/providers/default-registry.ts +10 -0
  215. package/src/providers/discovery/catalog/catalog-blob.ts +18 -18
  216. package/src/providers/discovery/models-dev-catalog.ts +354 -354
  217. package/src/providers/discovery/probe-phases.ts +43 -7
  218. package/src/providers/discovery/probing-service.ts +70 -32
  219. package/src/providers/integrations/antigravity/antigravity.ts +9 -2
  220. package/src/providers/integrations/buddy/buddy-catalog-shared.ts +85 -17
  221. package/src/providers/integrations/buddy/buddy-chat-shared.ts +934 -934
  222. package/src/providers/integrations/buddy/buddy-checkin.ts +5 -9
  223. package/src/providers/integrations/buddy/buddy-discovery-shared.ts +3 -7
  224. package/src/providers/integrations/buddy/buddy-quota-shared.ts +3 -1
  225. package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +57 -49
  226. package/src/providers/integrations/buddy/codebuddy/provider.ts +180 -180
  227. package/src/providers/integrations/buddy/workbuddy/provider.ts +206 -206
  228. package/src/providers/integrations/buddy/workbuddy/quota.ts +161 -161
  229. package/src/providers/integrations/claude/claude-quota.ts +2 -2
  230. package/src/providers/integrations/claude/claude-user-profiles.ts +3 -2
  231. package/src/providers/integrations/claude/claude.ts +3 -4
  232. package/src/providers/integrations/cline/cline.ts +94 -8
  233. package/src/providers/integrations/codex/codex.ts +61 -37
  234. package/src/providers/integrations/cursor/cursor-catalog.ts +368 -368
  235. package/src/providers/integrations/cursor/cursor-http2.ts +300 -300
  236. package/src/providers/integrations/cursor/cursor-identity.ts +2 -4
  237. package/src/providers/integrations/cursor/cursor-request.ts +11 -0
  238. package/src/providers/integrations/cursor/cursor.ts +29 -17
  239. package/src/providers/integrations/cursor/protocol/framing.ts +129 -129
  240. package/src/providers/integrations/cursor/protocol/messages.ts +2380 -2380
  241. package/src/providers/integrations/cursor/protocol/struct.ts +305 -305
  242. package/src/providers/integrations/cursor/protocol/tool-canonical.ts +275 -0
  243. package/src/providers/integrations/grok/grok-quota.ts +279 -279
  244. package/src/providers/integrations/grok/grok-ratelimit-store.ts +73 -73
  245. package/src/providers/integrations/grok/grok-ratelimit.ts +218 -218
  246. package/src/providers/integrations/hermes.ts +7 -1
  247. package/src/providers/integrations/inferhub.ts +201 -73
  248. package/src/providers/integrations/kilo/kilo.ts +152 -152
  249. package/src/providers/integrations/kiro/kiro-machine-id.ts +2 -4
  250. package/src/providers/integrations/openrouter/provider.ts +129 -129
  251. package/src/providers/integrations/qoder/qoder-quota.ts +193 -192
  252. package/src/providers/integrations/qoder/qoder-realm.ts +106 -106
  253. package/src/providers/integrations/trae/trae-oauth.ts +4 -26
  254. package/src/providers/integrations/trae/trae-quota.ts +389 -384
  255. package/src/providers/integrations/trae/trae-realm.ts +15 -2
  256. package/src/providers/integrations/xiaomi-mimo/think-stream.ts +100 -24
  257. package/src/providers/integrations/zcode/oauth.ts +1 -1
  258. package/src/providers/operations/account-health-service.ts +605 -279
  259. package/src/providers/provider-metadata.ts +12 -1
  260. package/src/providers/provider-model-definition.ts +373 -373
  261. package/src/providers/quota/quota-contracts.ts +3 -7
  262. package/src/providers/search/search-provider.ts +2 -6
  263. package/src/security/admission/contracts.ts +10 -0
  264. package/src/security/admission/in-memory-store.ts +89 -14
  265. package/src/security/admission/lua.ts +191 -182
  266. package/src/security/admission/redis-store.ts +30 -0
  267. package/src/security/admission/service.ts +18 -0
  268. package/src/security/api-key-auth-cache.ts +96 -0
  269. package/src/security/api-key-auth.ts +11 -100
  270. package/src/security/client-router-fingerprint.ts +3 -7
  271. package/src/security/ip-boundary.ts +7 -3
  272. package/src/shared/coerce.ts +67 -0
  273. package/src/shared/hash.ts +6 -0
  274. package/src/shared/time.ts +7 -0
  275. package/src/transport/dispatch/attempt-finalize.ts +26 -138
  276. package/src/transport/dispatch/attempt-loop.ts +103 -81
  277. package/src/transport/dispatch/capture-policy.ts +78 -0
  278. package/src/transport/dispatch/proxy-request.ts +4 -2
  279. package/src/transport/dispatch/streaming/encoder.ts +111 -5
  280. package/src/transport/dispatch/streaming/stream.ts +19 -2
  281. package/src/transport/dispatch/streaming/thinking-loop.ts +300 -0
  282. package/src/transport/failure-policy.ts +27 -0
  283. package/src/transport/guards/abuse-guard.ts +2 -2
  284. package/src/transport/guards/auth-guard.ts +17 -0
  285. package/src/transport/guards/body-policy.ts +3 -2
  286. package/src/transport/guards/concurrency-guard.ts +89 -13
  287. package/src/transport/guards/early-rejection-capture.ts +43 -3
  288. package/src/transport/guards/error-lifecycle.ts +10 -2
  289. package/src/transport/guards/guard-errors.ts +23 -0
  290. package/src/transport/guards/headroom-guard.ts +41 -7
  291. package/src/transport/guards/pipeline.ts +24 -0
  292. package/src/transport/guards/request-context.ts +2 -1
  293. package/src/transport/routing/admission.ts +51 -9
  294. package/src/transport/routing/bounded-lru.ts +49 -0
  295. package/src/transport/routing/candidate-index.ts +88 -0
  296. package/src/transport/routing/reservations.ts +121 -7
  297. package/src/transport/routing/round-robin.ts +10 -22
  298. package/src/transport/routing/route-catalog.ts +26 -13
  299. package/src/transport/routing/route-model.ts +22 -2
  300. package/src/transport/routing/router.ts +53 -31
  301. package/src/transport/surface/chat/encode.ts +2 -3
  302. package/src/transport/surface/chat/parse.ts +2 -1
  303. package/src/transport/surface/dialects.ts +11 -3
  304. package/src/transport/surface/messages/encode.ts +2 -1
  305. package/src/transport/surface/messages/parse.ts +3 -1
  306. package/src/transport/surface/messages/stream.ts +2 -6
  307. package/src/workers/maintenance-lease.ts +200 -0
  308. package/src/workers/scheduler.ts +101 -1
  309. package/dist/dashboard/assets/BannedPage-Cd30Z0b9.js.gz +0 -0
  310. package/dist/dashboard/assets/CliToolDetailPage-wf5u-ww6.js.gz +0 -0
  311. package/dist/dashboard/assets/CliToolsPage-DykPvuck.js.gz +0 -0
  312. package/dist/dashboard/assets/CombosPage-DwvSD3vs.js.gz +0 -0
  313. package/dist/dashboard/assets/ConsoleLogPage-CIr7KDex.js.gz +0 -0
  314. package/dist/dashboard/assets/CustomizationPage-82X0DDdH.js.gz +0 -0
  315. package/dist/dashboard/assets/HealthEventsModal-BYPucTTn.js +0 -1
  316. package/dist/dashboard/assets/HealthEventsModal-BYPucTTn.js.gz +0 -0
  317. package/dist/dashboard/assets/LoginPage-CS5mPuVF.js.gz +0 -0
  318. package/dist/dashboard/assets/ModelPicker-C0UCUsVJ.js.gz +0 -0
  319. package/dist/dashboard/assets/OverviewPage-Dk_ew3YU.js +0 -1
  320. package/dist/dashboard/assets/OverviewPage-Dk_ew3YU.js.gz +0 -0
  321. package/dist/dashboard/assets/ProviderDetailPage-CAiRn2Q0.js +0 -3
  322. package/dist/dashboard/assets/ProviderDetailPage-CAiRn2Q0.js.gz +0 -0
  323. package/dist/dashboard/assets/ProvidersPage-vdja5ZPK.js.gz +0 -0
  324. package/dist/dashboard/assets/ProxyPage-BB4CNm4-.js.gz +0 -0
  325. package/dist/dashboard/assets/QuotaPage-FEc4iNyq.js +0 -1
  326. package/dist/dashboard/assets/QuotaPage-FEc4iNyq.js.gz +0 -0
  327. package/dist/dashboard/assets/SettingsPage-Dlfi5L4N.js +0 -1
  328. package/dist/dashboard/assets/SettingsPage-Dlfi5L4N.js.gz +0 -0
  329. package/dist/dashboard/assets/SetupPage-FadUOl9S.js.gz +0 -0
  330. package/dist/dashboard/assets/SortableList-Cfy6_Khv.js.gz +0 -0
  331. package/dist/dashboard/assets/UsagePage-BT4MyQeT.js.gz +0 -0
  332. package/dist/dashboard/assets/api-keys-DQYLQxpW.js.gz +0 -0
  333. package/dist/dashboard/assets/app-CX_Z8cMs.css +0 -2
  334. package/dist/dashboard/assets/app-CX_Z8cMs.css.gz +0 -0
  335. package/dist/dashboard/assets/assert-C9FD_JwJ.js.gz +0 -0
  336. package/dist/dashboard/assets/badge-Bvyr7D0u.js.gz +0 -0
  337. package/dist/dashboard/assets/banner-DIvKAZih.js.gz +0 -0
  338. package/dist/dashboard/assets/card-D5_gYIS_.js.gz +0 -0
  339. package/dist/dashboard/assets/cli-tools-CNqzrfdb.js.gz +0 -0
  340. package/dist/dashboard/assets/clipboard-button-B6vDBtbm.js.gz +0 -0
  341. package/dist/dashboard/assets/dialog-CpjrgdwJ.js.gz +0 -0
  342. package/dist/dashboard/assets/drawer-BWmt3b5y.js.gz +0 -0
  343. package/dist/dashboard/assets/entry-B-gvimrg.js +0 -1
  344. package/dist/dashboard/assets/entry-B-gvimrg.js.gz +0 -0
  345. package/dist/dashboard/assets/entry-C_oakV3z.js.gz +0 -0
  346. package/dist/dashboard/assets/entry-Dj0cHfEc.js +0 -4
  347. package/dist/dashboard/assets/entry-Dj0cHfEc.js.gz +0 -0
  348. package/dist/dashboard/assets/github-badge-RRHHWGxh.js.gz +0 -0
  349. package/dist/dashboard/assets/index-Bhey_rg_.js.gz +0 -0
  350. package/dist/dashboard/assets/inline-C2_EQ5Uf.js.gz +0 -0
  351. package/dist/dashboard/assets/input-BMRlQ450.js.gz +0 -0
  352. package/dist/dashboard/assets/layout-XR5xP8ew.js.gz +0 -0
  353. package/dist/dashboard/assets/list-row-Cw07dioc.js.gz +0 -0
  354. package/dist/dashboard/assets/live-BivGfuUs.js.gz +0 -0
  355. package/dist/dashboard/assets/model-bans-CmffjLuG.js.gz +0 -0
  356. package/dist/dashboard/assets/mutation-46P0oWMe.js.gz +0 -0
  357. package/dist/dashboard/assets/numbers-DrdA9oSM.js.gz +0 -0
  358. package/dist/dashboard/assets/query-keys-BFhGpwR1.js.gz +0 -0
  359. package/dist/dashboard/assets/routing-CAb4F37R.js.gz +0 -0
  360. package/dist/dashboard/assets/select-CxVVB__N.js.gz +0 -0
  361. package/dist/dashboard/assets/settings-DNX9sydi.js.gz +0 -0
  362. package/dist/dashboard/assets/stack-uPOwM9Hs.js.gz +0 -0
  363. package/dist/dashboard/assets/state-D1Yz79iL.js.gz +0 -0
  364. package/dist/dashboard/assets/switch-sevsRrRo.js.gz +0 -0
  365. package/dist/dashboard/assets/useMutation-Dem0lXQj.js.gz +0 -0
  366. package/src/observability/event-compaction.ts +0 -317
  367. package/src/observability/event-day-index.ts +0 -439
@@ -1,373 +1,373 @@
1
- /**
2
- * Canonical model-definition helpers.
3
- *
4
- * Every static provider catalog describes the same `ModelDefinition` shape with
5
- * only a handful of per-provider differences (wire family, endpoint path,
6
- * vision/reasoning/tool flags, pricing). `defineModel` is the single place that
7
- * shape is constructed. Catalogs pass only the fields that differ, so a catalog
8
- * edit cannot silently drift from the shared defaults. Limits and pricing fall
9
- * back to the prebuilt base catalog; capability does not — see `defineModel`.
10
- */
11
- import type { ServiceKind, WireFamily } from "../protocol/canonical-model";
12
- import { modelsDevCatalog } from "./discovery/models-dev-catalog";
13
-
14
- /** Billing metadata for one provider model. */
15
- export interface ModelCostDefinition {
16
- readonly input: number | null;
17
- readonly output: number | null;
18
- readonly cache_read?: number;
19
- readonly cache_write?: number;
20
- readonly pricing_model?: "pay-per-use" | "subscription" | "unknown";
21
- }
22
-
23
- /**
24
- * Per-token cost for a plan whose tier is free. Zero is a tier fact owned by
25
- * the provider (Cline's `free` bucket, mirroring the upstream client's
26
- * deliberate cost zeroing), not a price copied from the base catalog: the base
27
- * catalog prices the *underlying* model, which the free tier does not bill.
28
- */
29
- export const FREE_TIER_COST: ModelCostDefinition = {
30
- input: 0,
31
- output: 0,
32
- pricing_model: "pay-per-use",
33
- };
34
-
35
- /** Canonical static metadata for one provider model. */
36
- export interface ModelDefinition {
37
- readonly modelId: string;
38
- readonly wireFamily: WireFamily;
39
- /**
40
- * Which protocol shape this model speaks. Absent means `"llm"` (the canonical
41
- * chat pipeline). A non-`llm` row is served by its native passthrough route,
42
- * not by the surface codecs, and `wireFamily`/`endpointPath` describe that
43
- * native route's transport rather than a chat wire.
44
- *
45
- * Optional so the hand-built catalog literals that predate this dimension
46
- * stay valid unchanged; read it as `definition.serviceKind ?? "llm"`.
47
- */
48
- readonly serviceKind?: ServiceKind;
49
- readonly endpointPath: string;
50
- readonly contextLimit: number | null;
51
- readonly outputLimit: number | null;
52
- readonly modalities: { readonly input: readonly string[]; readonly output: readonly string[] };
53
- readonly reasoning: boolean;
54
- /** Explicitly supported reasoning effort scale (e.g. `["minimal", "low", "medium", "high", "xhigh"]`). */
55
- readonly reasoningEfforts?: readonly string[];
56
- readonly toolCall: boolean;
57
- readonly cost: ModelCostDefinition;
58
- /**
59
- * Whether the provider serves this row on a free plan tier.
60
- *
61
- * Deliberately not the same thing as `free` on the input row. `free` is a
62
- * *billing* statement — bill zero per token — and a subscription roster also
63
- * bills zero per token. This is a *catalog* statement: the row belongs to the
64
- * tier the provider publishes as free. Discovery has to tell them apart
65
- * because the free tier is a distinct group in the model list, and `cost`
66
- * cannot answer it — a reseller may legitimately price a model at `$0.00`
67
- * without it being the provider's free tier.
68
- *
69
- * Optional, and absence is the answer "not a free-tier row": the field is a
70
- * marker only a discovery module that reads a provider's free tier can set,
71
- * so a hand-built definition correctly says nothing. Read it as `=== true`.
72
- */
73
- readonly freeTier?: boolean;
74
- /**
75
- * Whether the provider refuses this row on a free plan.
76
- *
77
- * The opposite statement to `freeTier`, and not its absence: `freeTier` says
78
- * "served on the free tier", while this says "refused there". A row neither
79
- * marked is simply one whose tier nobody established — which is most rows,
80
- * and must not be rendered as "free".
81
- *
82
- * CodeBuddy is why both exist. Its console directory publishes no field that
83
- * predicts access: `credits` and `vendor` were both measured against real
84
- * dispatches and neither correlates — `kimi-k3` bills `x1.62` and is served
85
- * on a free account while `gpt-5.6-luna` bills `x0.14` and is refused. The
86
- * only signal is the refusal itself: `11158 "model [X] is not allowed for
87
- * user type [personal_free_user]"`. See
88
- * `.skills/cartethyia-reverse-engineering/references/codebuddy-dispatch-headers.md`.
89
- *
90
- * Optional, and absence is "tier unknown" rather than "served on free". Read
91
- * it as `=== true`; a UI that shows a tier must treat undefined as unknown,
92
- * not as the permissive answer.
93
- */
94
- readonly proOnly?: boolean;
95
- /**
96
- * True when neither the row nor the catalog could state this model's limits,
97
- * so `contextLimit`/`outputLimit` are the shared fallback rather than
98
- * anything measured.
99
- *
100
- * A model nobody has filed — a brand-new id the catalog has not caught up
101
- * with — must still be routable, and a hard number is what routing needs to
102
- * size a request. But publishing that number as if it were the model's own
103
- * turns a gap in our catalog into a claim about someone else's model.
104
- */
105
- readonly limitsEstimated?: boolean;
106
- }
107
-
108
- /**
109
- * Limits assumed when neither the catalog nor the operator supplies any.
110
- *
111
- * Deliberately generous: a context ceiling that is too *low* truncates a
112
- * working request, while one that is too high only means the upstream rejects
113
- * the call on its own terms. Chosen as a round number above every current
114
- * frontier model so the fallback is never the binding constraint.
115
- *
116
- * A row carrying these because nothing filed the model is flagged as estimated,
117
- * so the UI can say the number is a guess rather than presenting it as a fact
118
- * about the model.
119
- */
120
- export const FALLBACK_CONTEXT_LIMIT = 512_000;
121
- export const FALLBACK_OUTPUT_LIMIT = 131_072;
122
-
123
- /**
124
- * Limits assumed for a model the catalog has never filed.
125
- *
126
- * Fit comfortably rather than tightly: a fallback that is too small silently
127
- * truncates a request that would have been fine, while one that is too large
128
- * only costs an admission estimate no upstream enforces. The two spellings above
129
- * are one number, so a row rendered in the console and a row routed through
130
- * discovery cannot disagree about what "unfiled" means.
131
- */
132
- const UNFILED_CONTEXT_LIMIT = FALLBACK_CONTEXT_LIMIT;
133
- const UNFILED_OUTPUT_LIMIT = FALLBACK_OUTPUT_LIMIT;
134
-
135
- /** Fields accepted by `defineModel`; every field except `id` falls back to the shared default. */
136
- export interface ModelHelperRow {
137
- readonly id: string;
138
- /**
139
- * Canonical provider id this row is served by. Optional only so shared
140
- * helper rows can omit it; supplying it is what makes base-catalog limits
141
- * and pricing resolve to the right provider's entry.
142
- */
143
- readonly providerId?: string;
144
- /** Defaults to `"chat"`. */
145
- readonly wireFamily?: ModelDefinition["wireFamily"];
146
- /** Defaults to `"llm"`. A non-`llm` row is served by its native route. */
147
- readonly serviceKind?: ServiceKind;
148
- /** Defaults to the family's conventional path (`/chat/completions`, `/responses`, `/messages`). */
149
- readonly endpoint?: string;
150
- /** Defaults to 128k. */
151
- readonly ctx?: number | null;
152
- /** Defaults to 8k. */
153
- readonly out?: number | null;
154
- readonly vision?: boolean;
155
- /** Accepts PDF/plain-file input parts (`document`/`file` canonical kinds). */
156
- readonly document?: boolean;
157
- /** Accepts audio input parts. */
158
- readonly audio?: boolean;
159
- readonly reasoning?: boolean;
160
- /**
161
- * Output modalities this row produces. Defaults to `["text"]`.
162
- *
163
- * A generation row sets `["image"]` or `["video"]`: the capability profile's
164
- * `mediaGeneration` flag reads `output.includes("image")`, and before this
165
- * field existed every catalog row was hardcoded to text, so no static row
166
- * could ever declare that it returns an image.
167
- */
168
- readonly output?: readonly ModelDefinition["modalities"]["output"][number][];
169
- /** Explicitly supported reasoning effort scale. */
170
- readonly reasoningEfforts?: readonly string[];
171
- readonly toolCall?: boolean;
172
- /**
173
- * Marks a row served by a free plan tier. Such rows bill zero per token
174
- * regardless of what the base catalog charges for the underlying model.
175
- */
176
- readonly free?: boolean;
177
- /**
178
- * Marks a row that belongs to the tier the provider publishes as free.
179
- *
180
- * Separate from `free` because the two answer different questions: a
181
- * subscription roster bills zero per token (`free`) without being the free
182
- * tier. This one is what the model list groups on, so a provider's free-tier
183
- * discovery can be labelled as such instead of mixed into ordinary fetched
184
- * rows. Defaults to false — a row nobody marked is not a free-tier row.
185
- */
186
- readonly freeTier?: boolean;
187
- /** Whether the provider refuses this row on a free plan. See `ModelDefinition.proOnly`. */
188
- readonly proOnly?: boolean;
189
- }
190
-
191
- function defaultEndpoint(wireFamily: ModelDefinition["wireFamily"]): string {
192
- if (wireFamily === "chat") return "/chat/completions";
193
- if (wireFamily === "responses") return "/responses";
194
- return "/messages";
195
- }
196
-
197
- /**
198
- * Model ids whose families speak native OpenAI Responses on every gateway that
199
- * lists them (GPT-5/6, o3/o4). Verified live: `gpt-5.6-terra` answers on
200
- * `/v1/responses` in ~2s with its gateway routing prefix intact, and the
201
- * codex/openai static catalogs already carry these families as responses-wire.
202
- *
203
- * Both the static inferhub catalog and the tolerant `/models` discovery path
204
- * classify wire family from the raw id, so the rule is stated once here: a
205
- * gateway that lists one of these families must not pin it to the chat wire.
206
- */
207
- export function isResponsesNativeModelId(modelId: string): boolean {
208
- const id = modelId.toLowerCase();
209
- return id.includes("gpt-5") || id.includes("gpt-6") || id.includes("/o3") || id.includes("/o4");
210
- }
211
-
212
- /**
213
- * Builds one static catalog row. No empty-id guard: the Anthropic and Claude
214
- * catalogs intentionally ship empty-id rows, so validation here would break
215
- * module load.
216
- *
217
- * Capability is declared per row and never read from the base catalog. The base
218
- * catalog is keyed by bare model id, and a bare id maps to many provider rows
219
- * where only the first survives (`models-dev-catalog.ts`), so a capability read
220
- * from it can describe a different provider's serving of the same model. What a
221
- * row does not declare is left to the routing ladder's filter → degrade path
222
- * rather than guessed from a stranger's metadata.
223
- */
224
- export function defineModel(row: ModelHelperRow): ModelDefinition {
225
- const wireFamily = row.wireFamily ?? "chat";
226
- // `row.providerId` names the provider whose catalog this row belongs to, so
227
- // limits and cost come from that provider's own base-catalog entry. Without
228
- // it the lookup falls back to the bare id, which returns `undefined` when the
229
- // catalog disagrees about that id (428 bare keys disagree on context alone).
230
- const fallback = row.id ? modelsDevCatalog.resolve(row.providerId ?? "", row.id) : undefined;
231
- // A provider the catalog does not file under our id (or files nowhere at
232
- // all) leaves `resolve` undefined, which would fall through to the fixed
233
- // default below. The majority vote is the honest middle tier: the model is
234
- // known, the catalog merely disagrees about it, and the most-agreed row
235
- // describes the model itself rather than one reseller's serving. Only the
236
- // field the vote actually answered is taken; a model nobody records stays
237
- // at the default.
238
- const majority = fallback === undefined && row.id ? modelsDevCatalog.majorityFor(row.id) : undefined;
239
-
240
- // Nothing filed this model: not the row, not the provider's catalog entry,
241
- // not a majority across the catalog's other resellers. The fallback keeps it
242
- // routable, and the marker keeps the number honest.
243
- const limitsEstimated = row.ctx === undefined && fallback?.contextLimit === undefined && majority?.contextLimit === undefined;
244
-
245
- const contextLimit =
246
- row.ctx !== undefined
247
- ? row.ctx
248
- : (fallback?.contextLimit ?? majority?.contextLimit ?? UNFILED_CONTEXT_LIMIT);
249
-
250
- const declaredOutput =
251
- row.out !== undefined
252
- ? row.out
253
- : (fallback?.outputLimit ?? majority?.outputLimit ?? UNFILED_OUTPUT_LIMIT);
254
-
255
- // A maximum output above the context window is unsatisfiable: no request can
256
- // reserve more output tokens than the model accepts as input. The base
257
- // catalog carries 1084 rows that state output === context and 69 that state
258
- // output > context (a Kimi K2.6 row is filed as 262144/262144), so an
259
- // unclamped cap would publish a limit the provider rejects and make the
260
- // admission estimate reserve the whole window as output headroom.
261
- const outputLimit =
262
- contextLimit === null || declaredOutput === null
263
- ? declaredOutput
264
- : Math.min(declaredOutput, contextLimit);
265
-
266
- const cost = row.free ? FREE_TIER_COST : modelsDevCatalog.costFor(row.providerId ?? "", row.id);
267
-
268
- return {
269
- modelId: row.id,
270
- wireFamily,
271
- serviceKind: row.serviceKind ?? "llm",
272
- endpointPath: row.endpoint ?? defaultEndpoint(wireFamily),
273
- contextLimit,
274
- outputLimit,
275
- modalities: {
276
- input: [
277
- "text",
278
- ...(row.vision ? ["image"] : []),
279
- ...(row.document ? ["document"] : []),
280
- ...(row.audio ? ["audio"] : []),
281
- ],
282
- output: row.output ?? ["text"],
283
- },
284
- reasoning: row.reasoning ?? false,
285
- ...(row.reasoningEfforts !== undefined ? { reasoningEfforts: row.reasoningEfforts } : {}),
286
- toolCall: row.toolCall ?? true,
287
- cost,
288
- ...(row.freeTier === undefined ? {} : { freeTier: row.freeTier }),
289
- ...(row.proOnly === undefined ? {} : { proOnly: row.proOnly }),
290
- ...(limitsEstimated ? { limitsEstimated: true } : {}),
291
- };
292
- }
293
-
294
- /** Capability metadata persisted for one manually registered model id. */
295
- export interface ManualModelMetadata {
296
- readonly contextLimit: number | null;
297
- readonly outputLimit: number | null;
298
- readonly modalities: { readonly input: readonly string[]; readonly output: readonly string[] };
299
- readonly reasoning: boolean;
300
- readonly toolCall: boolean;
301
- }
302
-
303
- /**
304
- * Resolves capability metadata for a manually registered model id.
305
- *
306
- * A manual add performs no upstream probe and has no static catalog row. Its
307
- * capabilities are therefore declared up front rather than inferred from a
308
- * `defineModel(...)` entry: tools, vision, and reasoning are always enabled. A
309
- * false negative here is not cosmetic — capability preflight reads `toolCall`
310
- * and silently strips `tools` from the request, so a manually added model
311
- * would appear unable to call tools at all. The prebuilt base catalog still
312
- * supplies the non-capability facts (context/output limits, cost) when it
313
- * knows the id.
314
- */
315
- export function resolveManualModelMetadata(
316
- providerId: string,
317
- modelId: string,
318
- ): ManualModelMetadata {
319
- const meta = modelsDevCatalog.resolve(providerId, modelId);
320
- const catalogInput = meta?.modalities.input ?? [];
321
- return {
322
- contextLimit: meta?.contextLimit ?? null,
323
- outputLimit: meta?.outputLimit ?? null,
324
- modalities: {
325
- input: ["text", "image", ...catalogInput.filter((m) => m !== "text" && m !== "image")],
326
- output: meta?.modalities.output ?? ["text"],
327
- },
328
- reasoning: true,
329
- toolCall: true,
330
- };
331
- }
332
-
333
- /**
334
- * Folds operator-supplied metadata over a resolved model's metadata.
335
- *
336
- * `passthrough` wins over the individual flags: it grants everything, so a
337
- * caller that cannot know which capabilities a model has does not have to
338
- * guess. When it is set the per-flag fields are ignored rather than merged —
339
- * merging would let a stale `image: false` silently revoke what passthrough
340
- * just granted.
341
- */
342
- export function applyModelMetadataOverride(
343
- base: ManualModelMetadata,
344
- override: {
345
- readonly contextLimit?: number | null;
346
- readonly outputLimit?: number | null;
347
- readonly passthrough?: boolean;
348
- readonly image?: boolean;
349
- readonly reasoning?: boolean;
350
- readonly toolCall?: boolean;
351
- } | undefined,
352
- ): ManualModelMetadata {
353
- if (override === undefined) return base;
354
- const passthrough = override.passthrough === true;
355
- const image = passthrough ? true : override.image;
356
- const reasoning = passthrough ? true : override.reasoning;
357
- const toolCall = passthrough ? true : override.toolCall;
358
- return {
359
- contextLimit: override.contextLimit ?? base.contextLimit,
360
- outputLimit: override.outputLimit ?? base.outputLimit,
361
- modalities: {
362
- ...base.modalities,
363
- input:
364
- image === undefined
365
- ? base.modalities.input
366
- : image
367
- ? Array.from(new Set([...base.modalities.input, "image"]))
368
- : base.modalities.input.filter((m) => m !== "image"),
369
- },
370
- reasoning: reasoning ?? base.reasoning,
371
- toolCall: toolCall ?? base.toolCall,
372
- };
373
- }
1
+ /**
2
+ * Canonical model-definition helpers.
3
+ *
4
+ * Every static provider catalog describes the same `ModelDefinition` shape with
5
+ * only a handful of per-provider differences (wire family, endpoint path,
6
+ * vision/reasoning/tool flags, pricing). `defineModel` is the single place that
7
+ * shape is constructed. Catalogs pass only the fields that differ, so a catalog
8
+ * edit cannot silently drift from the shared defaults. Limits and pricing fall
9
+ * back to the prebuilt base catalog; capability does not — see `defineModel`.
10
+ */
11
+ import type { ServiceKind, WireFamily } from "../protocol/canonical-model";
12
+ import { modelsDevCatalog } from "./discovery/models-dev-catalog";
13
+
14
+ /** Billing metadata for one provider model. */
15
+ export interface ModelCostDefinition {
16
+ readonly input: number | null;
17
+ readonly output: number | null;
18
+ readonly cache_read?: number;
19
+ readonly cache_write?: number;
20
+ readonly pricing_model?: "pay-per-use" | "subscription" | "unknown";
21
+ }
22
+
23
+ /**
24
+ * Per-token cost for a plan whose tier is free. Zero is a tier fact owned by
25
+ * the provider (Cline's `free` bucket, mirroring the upstream client's
26
+ * deliberate cost zeroing), not a price copied from the base catalog: the base
27
+ * catalog prices the *underlying* model, which the free tier does not bill.
28
+ */
29
+ export const FREE_TIER_COST: ModelCostDefinition = {
30
+ input: 0,
31
+ output: 0,
32
+ pricing_model: "pay-per-use",
33
+ };
34
+
35
+ /** Canonical static metadata for one provider model. */
36
+ export interface ModelDefinition {
37
+ readonly modelId: string;
38
+ readonly wireFamily: WireFamily;
39
+ /**
40
+ * Which protocol shape this model speaks. Absent means `"llm"` (the canonical
41
+ * chat pipeline). A non-`llm` row is served by its native passthrough route,
42
+ * not by the surface codecs, and `wireFamily`/`endpointPath` describe that
43
+ * native route's transport rather than a chat wire.
44
+ *
45
+ * Optional so the hand-built catalog literals that predate this dimension
46
+ * stay valid unchanged; read it as `definition.serviceKind ?? "llm"`.
47
+ */
48
+ readonly serviceKind?: ServiceKind;
49
+ readonly endpointPath: string;
50
+ readonly contextLimit: number | null;
51
+ readonly outputLimit: number | null;
52
+ readonly modalities: { readonly input: readonly string[]; readonly output: readonly string[] };
53
+ readonly reasoning: boolean;
54
+ /** Explicitly supported reasoning effort scale (e.g. `["minimal", "low", "medium", "high", "xhigh"]`). */
55
+ readonly reasoningEfforts?: readonly string[];
56
+ readonly toolCall: boolean;
57
+ readonly cost: ModelCostDefinition;
58
+ /**
59
+ * Whether the provider serves this row on a free plan tier.
60
+ *
61
+ * Deliberately not the same thing as `free` on the input row. `free` is a
62
+ * *billing* statement — bill zero per token — and a subscription roster also
63
+ * bills zero per token. This is a *catalog* statement: the row belongs to the
64
+ * tier the provider publishes as free. Discovery has to tell them apart
65
+ * because the free tier is a distinct group in the model list, and `cost`
66
+ * cannot answer it — a reseller may legitimately price a model at `$0.00`
67
+ * without it being the provider's free tier.
68
+ *
69
+ * Optional, and absence is the answer "not a free-tier row": the field is a
70
+ * marker only a discovery module that reads a provider's free tier can set,
71
+ * so a hand-built definition correctly says nothing. Read it as `=== true`.
72
+ */
73
+ readonly freeTier?: boolean;
74
+ /**
75
+ * Whether the provider refuses this row on a free plan.
76
+ *
77
+ * The opposite statement to `freeTier`, and not its absence: `freeTier` says
78
+ * "served on the free tier", while this says "refused there". A row neither
79
+ * marked is simply one whose tier nobody established — which is most rows,
80
+ * and must not be rendered as "free".
81
+ *
82
+ * CodeBuddy is why both exist. Its console directory publishes no field that
83
+ * predicts access: `credits` and `vendor` were both measured against real
84
+ * dispatches and neither correlates — `kimi-k3` bills `x1.62` and is served
85
+ * on a free account while `gpt-5.6-luna` bills `x0.14` and is refused. The
86
+ * only signal is the refusal itself: `11158 "model [X] is not allowed for
87
+ * user type [personal_free_user]"`. See
88
+ * `.skills/cartethyia-reverse-engineering/references/codebuddy-dispatch-headers.md`.
89
+ *
90
+ * Optional, and absence is "tier unknown" rather than "served on free". Read
91
+ * it as `=== true`; a UI that shows a tier must treat undefined as unknown,
92
+ * not as the permissive answer.
93
+ */
94
+ readonly proOnly?: boolean;
95
+ /**
96
+ * True when neither the row nor the catalog could state this model's limits,
97
+ * so `contextLimit`/`outputLimit` are the shared fallback rather than
98
+ * anything measured.
99
+ *
100
+ * A model nobody has filed — a brand-new id the catalog has not caught up
101
+ * with — must still be routable, and a hard number is what routing needs to
102
+ * size a request. But publishing that number as if it were the model's own
103
+ * turns a gap in our catalog into a claim about someone else's model.
104
+ */
105
+ readonly limitsEstimated?: boolean;
106
+ }
107
+
108
+ /**
109
+ * Limits assumed when neither the catalog nor the operator supplies any.
110
+ *
111
+ * Deliberately generous: a context ceiling that is too *low* truncates a
112
+ * working request, while one that is too high only means the upstream rejects
113
+ * the call on its own terms. Chosen as a round number above every current
114
+ * frontier model so the fallback is never the binding constraint.
115
+ *
116
+ * A row carrying these because nothing filed the model is flagged as estimated,
117
+ * so the UI can say the number is a guess rather than presenting it as a fact
118
+ * about the model.
119
+ */
120
+ export const FALLBACK_CONTEXT_LIMIT = 512_000;
121
+ export const FALLBACK_OUTPUT_LIMIT = 131_072;
122
+
123
+ /**
124
+ * Limits assumed for a model the catalog has never filed.
125
+ *
126
+ * Fit comfortably rather than tightly: a fallback that is too small silently
127
+ * truncates a request that would have been fine, while one that is too large
128
+ * only costs an admission estimate no upstream enforces. The two spellings above
129
+ * are one number, so a row rendered in the console and a row routed through
130
+ * discovery cannot disagree about what "unfiled" means.
131
+ */
132
+ const UNFILED_CONTEXT_LIMIT = FALLBACK_CONTEXT_LIMIT;
133
+ const UNFILED_OUTPUT_LIMIT = FALLBACK_OUTPUT_LIMIT;
134
+
135
+ /** Fields accepted by `defineModel`; every field except `id` falls back to the shared default. */
136
+ export interface ModelHelperRow {
137
+ readonly id: string;
138
+ /**
139
+ * Canonical provider id this row is served by. Optional only so shared
140
+ * helper rows can omit it; supplying it is what makes base-catalog limits
141
+ * and pricing resolve to the right provider's entry.
142
+ */
143
+ readonly providerId?: string;
144
+ /** Defaults to `"chat"`. */
145
+ readonly wireFamily?: ModelDefinition["wireFamily"];
146
+ /** Defaults to `"llm"`. A non-`llm` row is served by its native route. */
147
+ readonly serviceKind?: ServiceKind;
148
+ /** Defaults to the family's conventional path (`/chat/completions`, `/responses`, `/messages`). */
149
+ readonly endpoint?: string;
150
+ /** Defaults to 128k. */
151
+ readonly ctx?: number | null;
152
+ /** Defaults to 8k. */
153
+ readonly out?: number | null;
154
+ readonly vision?: boolean;
155
+ /** Accepts PDF/plain-file input parts (`document`/`file` canonical kinds). */
156
+ readonly document?: boolean;
157
+ /** Accepts audio input parts. */
158
+ readonly audio?: boolean;
159
+ readonly reasoning?: boolean;
160
+ /**
161
+ * Output modalities this row produces. Defaults to `["text"]`.
162
+ *
163
+ * A generation row sets `["image"]` or `["video"]`: the capability profile's
164
+ * `mediaGeneration` flag reads `output.includes("image")`, and before this
165
+ * field existed every catalog row was hardcoded to text, so no static row
166
+ * could ever declare that it returns an image.
167
+ */
168
+ readonly output?: readonly ModelDefinition["modalities"]["output"][number][];
169
+ /** Explicitly supported reasoning effort scale. */
170
+ readonly reasoningEfforts?: readonly string[];
171
+ readonly toolCall?: boolean;
172
+ /**
173
+ * Marks a row served by a free plan tier. Such rows bill zero per token
174
+ * regardless of what the base catalog charges for the underlying model.
175
+ */
176
+ readonly free?: boolean;
177
+ /**
178
+ * Marks a row that belongs to the tier the provider publishes as free.
179
+ *
180
+ * Separate from `free` because the two answer different questions: a
181
+ * subscription roster bills zero per token (`free`) without being the free
182
+ * tier. This one is what the model list groups on, so a provider's free-tier
183
+ * discovery can be labelled as such instead of mixed into ordinary fetched
184
+ * rows. Defaults to false — a row nobody marked is not a free-tier row.
185
+ */
186
+ readonly freeTier?: boolean;
187
+ /** Whether the provider refuses this row on a free plan. See `ModelDefinition.proOnly`. */
188
+ readonly proOnly?: boolean;
189
+ }
190
+
191
+ function defaultEndpoint(wireFamily: ModelDefinition["wireFamily"]): string {
192
+ if (wireFamily === "chat") return "/chat/completions";
193
+ if (wireFamily === "responses") return "/responses";
194
+ return "/messages";
195
+ }
196
+
197
+ /**
198
+ * Model ids whose families speak native OpenAI Responses on every gateway that
199
+ * lists them (GPT-5/6, o3/o4). Verified live: `gpt-5.6-terra` answers on
200
+ * `/v1/responses` in ~2s with its gateway routing prefix intact, and the
201
+ * codex/openai static catalogs already carry these families as responses-wire.
202
+ *
203
+ * Both the static inferhub catalog and the tolerant `/models` discovery path
204
+ * classify wire family from the raw id, so the rule is stated once here: a
205
+ * gateway that lists one of these families must not pin it to the chat wire.
206
+ */
207
+ export function isResponsesNativeModelId(modelId: string): boolean {
208
+ const id = modelId.toLowerCase();
209
+ return id.includes("gpt-5") || id.includes("gpt-6") || id.includes("/o3") || id.includes("/o4");
210
+ }
211
+
212
+ /**
213
+ * Builds one static catalog row. No empty-id guard: the Anthropic and Claude
214
+ * catalogs intentionally ship empty-id rows, so validation here would break
215
+ * module load.
216
+ *
217
+ * Capability is declared per row and never read from the base catalog. The base
218
+ * catalog is keyed by bare model id, and a bare id maps to many provider rows
219
+ * where only the first survives (`models-dev-catalog.ts`), so a capability read
220
+ * from it can describe a different provider's serving of the same model. What a
221
+ * row does not declare is left to the routing ladder's filter → degrade path
222
+ * rather than guessed from a stranger's metadata.
223
+ */
224
+ export function defineModel(row: ModelHelperRow): ModelDefinition {
225
+ const wireFamily = row.wireFamily ?? "chat";
226
+ // `row.providerId` names the provider whose catalog this row belongs to, so
227
+ // limits and cost come from that provider's own base-catalog entry. Without
228
+ // it the lookup falls back to the bare id, which returns `undefined` when the
229
+ // catalog disagrees about that id (428 bare keys disagree on context alone).
230
+ const fallback = row.id ? modelsDevCatalog.resolve(row.providerId ?? "", row.id) : undefined;
231
+ // A provider the catalog does not file under our id (or files nowhere at
232
+ // all) leaves `resolve` undefined, which would fall through to the fixed
233
+ // default below. The majority vote is the honest middle tier: the model is
234
+ // known, the catalog merely disagrees about it, and the most-agreed row
235
+ // describes the model itself rather than one reseller's serving. Only the
236
+ // field the vote actually answered is taken; a model nobody records stays
237
+ // at the default.
238
+ const majority = fallback === undefined && row.id ? modelsDevCatalog.majorityFor(row.id) : undefined;
239
+
240
+ // Nothing filed this model: not the row, not the provider's catalog entry,
241
+ // not a majority across the catalog's other resellers. The fallback keeps it
242
+ // routable, and the marker keeps the number honest.
243
+ const limitsEstimated = row.ctx === undefined && fallback?.contextLimit === undefined && majority?.contextLimit === undefined;
244
+
245
+ const contextLimit =
246
+ row.ctx !== undefined
247
+ ? row.ctx
248
+ : (fallback?.contextLimit ?? majority?.contextLimit ?? UNFILED_CONTEXT_LIMIT);
249
+
250
+ const declaredOutput =
251
+ row.out !== undefined
252
+ ? row.out
253
+ : (fallback?.outputLimit ?? majority?.outputLimit ?? UNFILED_OUTPUT_LIMIT);
254
+
255
+ // A maximum output above the context window is unsatisfiable: no request can
256
+ // reserve more output tokens than the model accepts as input. The base
257
+ // catalog carries 1084 rows that state output === context and 69 that state
258
+ // output > context (a Kimi K2.6 row is filed as 262144/262144), so an
259
+ // unclamped cap would publish a limit the provider rejects and make the
260
+ // admission estimate reserve the whole window as output headroom.
261
+ const outputLimit =
262
+ contextLimit === null || declaredOutput === null
263
+ ? declaredOutput
264
+ : Math.min(declaredOutput, contextLimit);
265
+
266
+ const cost = row.free ? FREE_TIER_COST : modelsDevCatalog.costFor(row.providerId ?? "", row.id);
267
+
268
+ return {
269
+ modelId: row.id,
270
+ wireFamily,
271
+ serviceKind: row.serviceKind ?? "llm",
272
+ endpointPath: row.endpoint ?? defaultEndpoint(wireFamily),
273
+ contextLimit,
274
+ outputLimit,
275
+ modalities: {
276
+ input: [
277
+ "text",
278
+ ...(row.vision ? ["image"] : []),
279
+ ...(row.document ? ["document"] : []),
280
+ ...(row.audio ? ["audio"] : []),
281
+ ],
282
+ output: row.output ?? ["text"],
283
+ },
284
+ reasoning: row.reasoning ?? false,
285
+ ...(row.reasoningEfforts !== undefined ? { reasoningEfforts: row.reasoningEfforts } : {}),
286
+ toolCall: row.toolCall ?? true,
287
+ cost,
288
+ ...(row.freeTier === undefined ? {} : { freeTier: row.freeTier }),
289
+ ...(row.proOnly === undefined ? {} : { proOnly: row.proOnly }),
290
+ ...(limitsEstimated ? { limitsEstimated: true } : {}),
291
+ };
292
+ }
293
+
294
+ /** Capability metadata persisted for one manually registered model id. */
295
+ export interface ManualModelMetadata {
296
+ readonly contextLimit: number | null;
297
+ readonly outputLimit: number | null;
298
+ readonly modalities: { readonly input: readonly string[]; readonly output: readonly string[] };
299
+ readonly reasoning: boolean;
300
+ readonly toolCall: boolean;
301
+ }
302
+
303
+ /**
304
+ * Resolves capability metadata for a manually registered model id.
305
+ *
306
+ * A manual add performs no upstream probe and has no static catalog row. Its
307
+ * capabilities are therefore declared up front rather than inferred from a
308
+ * `defineModel(...)` entry: tools, vision, and reasoning are always enabled. A
309
+ * false negative here is not cosmetic — capability preflight reads `toolCall`
310
+ * and silently strips `tools` from the request, so a manually added model
311
+ * would appear unable to call tools at all. The prebuilt base catalog still
312
+ * supplies the non-capability facts (context/output limits, cost) when it
313
+ * knows the id.
314
+ */
315
+ export function resolveManualModelMetadata(
316
+ providerId: string,
317
+ modelId: string,
318
+ ): ManualModelMetadata {
319
+ const meta = modelsDevCatalog.resolve(providerId, modelId);
320
+ const catalogInput = meta?.modalities.input ?? [];
321
+ return {
322
+ contextLimit: meta?.contextLimit ?? null,
323
+ outputLimit: meta?.outputLimit ?? null,
324
+ modalities: {
325
+ input: ["text", "image", ...catalogInput.filter((m) => m !== "text" && m !== "image")],
326
+ output: meta?.modalities.output ?? ["text"],
327
+ },
328
+ reasoning: true,
329
+ toolCall: true,
330
+ };
331
+ }
332
+
333
+ /**
334
+ * Folds operator-supplied metadata over a resolved model's metadata.
335
+ *
336
+ * `passthrough` wins over the individual flags: it grants everything, so a
337
+ * caller that cannot know which capabilities a model has does not have to
338
+ * guess. When it is set the per-flag fields are ignored rather than merged —
339
+ * merging would let a stale `image: false` silently revoke what passthrough
340
+ * just granted.
341
+ */
342
+ export function applyModelMetadataOverride(
343
+ base: ManualModelMetadata,
344
+ override: {
345
+ readonly contextLimit?: number | null;
346
+ readonly outputLimit?: number | null;
347
+ readonly passthrough?: boolean;
348
+ readonly image?: boolean;
349
+ readonly reasoning?: boolean;
350
+ readonly toolCall?: boolean;
351
+ } | undefined,
352
+ ): ManualModelMetadata {
353
+ if (override === undefined) return base;
354
+ const passthrough = override.passthrough === true;
355
+ const image = passthrough ? true : override.image;
356
+ const reasoning = passthrough ? true : override.reasoning;
357
+ const toolCall = passthrough ? true : override.toolCall;
358
+ return {
359
+ contextLimit: override.contextLimit ?? base.contextLimit,
360
+ outputLimit: override.outputLimit ?? base.outputLimit,
361
+ modalities: {
362
+ ...base.modalities,
363
+ input:
364
+ image === undefined
365
+ ? base.modalities.input
366
+ : image
367
+ ? Array.from(new Set([...base.modalities.input, "image"]))
368
+ : base.modalities.input.filter((m) => m !== "image"),
369
+ },
370
+ reasoning: reasoning ?? base.reasoning,
371
+ toolCall: toolCall ?? base.toolCall,
372
+ };
373
+ }