cartethyia 2.7.0 → 2.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (320) hide show
  1. package/.env.example +107 -106
  2. package/README.md +25 -2
  3. package/dist/dashboard/assets/{BannedPage-P76QLGJA.js → BannedPage-D-J75hBM.js} +1 -1
  4. package/dist/dashboard/assets/BannedPage-D-J75hBM.js.gz +0 -0
  5. package/dist/dashboard/assets/{CliToolDetailPage-DdBggJS9.js → CliToolDetailPage-CVe6RaBH.js} +1 -1
  6. package/dist/dashboard/assets/CliToolDetailPage-CVe6RaBH.js.gz +0 -0
  7. package/dist/dashboard/assets/{CliToolsPage-aN1iXhhP.js → CliToolsPage-Dd8ZSamp.js} +1 -1
  8. package/dist/dashboard/assets/CliToolsPage-Dd8ZSamp.js.gz +0 -0
  9. package/dist/dashboard/assets/{CombosPage-DE-F1nxZ.js → CombosPage-QoJ6foxo.js} +1 -1
  10. package/dist/dashboard/assets/CombosPage-QoJ6foxo.js.gz +0 -0
  11. package/dist/dashboard/assets/ConsoleLogPage-DpNRay4C.js +1 -0
  12. package/dist/dashboard/assets/ConsoleLogPage-DpNRay4C.js.gz +0 -0
  13. package/dist/dashboard/assets/{CustomizationPage-3CUxMy2X.js → CustomizationPage-CKUFTcW4.js} +1 -1
  14. package/dist/dashboard/assets/CustomizationPage-CKUFTcW4.js.gz +0 -0
  15. package/dist/dashboard/assets/HealthEventsModal-CzSLiDzr.js +1 -0
  16. package/dist/dashboard/assets/HealthEventsModal-CzSLiDzr.js.gz +0 -0
  17. package/dist/dashboard/assets/LoginPage-DubdQcvX.js +1 -0
  18. package/dist/dashboard/assets/LoginPage-DubdQcvX.js.gz +0 -0
  19. package/dist/dashboard/assets/{ModelPicker-DKVslqG3.js → ModelPicker-BH8Tehdi.js} +1 -1
  20. package/dist/dashboard/assets/ModelPicker-BH8Tehdi.js.gz +0 -0
  21. package/dist/dashboard/assets/OverviewPage-DuWzAQFL.js +1 -0
  22. package/dist/dashboard/assets/OverviewPage-DuWzAQFL.js.gz +0 -0
  23. package/dist/dashboard/assets/ProviderDetailPage-BKEKe2Fm.js +4 -0
  24. package/dist/dashboard/assets/ProviderDetailPage-BKEKe2Fm.js.gz +0 -0
  25. package/dist/dashboard/assets/ProviderIcon-FB6yS4v1.js +1 -0
  26. package/dist/dashboard/assets/ProviderIcon-FB6yS4v1.js.gz +0 -0
  27. package/dist/dashboard/assets/ProvidersPage-Bwotu8UJ.js +1 -0
  28. package/dist/dashboard/assets/ProvidersPage-Bwotu8UJ.js.gz +0 -0
  29. package/dist/dashboard/assets/ProxyPage-D7hpWbg9.js +9 -0
  30. package/dist/dashboard/assets/ProxyPage-D7hpWbg9.js.gz +0 -0
  31. package/dist/dashboard/assets/{QuotaPage-BjldQwIU.js → QuotaPage-ATpaxrbl.js} +1 -1
  32. package/dist/dashboard/assets/QuotaPage-ATpaxrbl.js.gz +0 -0
  33. package/dist/dashboard/assets/{SettingsPage-DUIoLRxk.js → SettingsPage-CETophpx.js} +1 -1
  34. package/dist/dashboard/assets/SettingsPage-CETophpx.js.gz +0 -0
  35. package/dist/dashboard/assets/SetupPage-BdgECjqq.js +1 -0
  36. package/dist/dashboard/assets/SetupPage-BdgECjqq.js.gz +0 -0
  37. package/dist/dashboard/assets/{SortableList-fI7JkdIB.js → SortableList-CAK-EN9z.js} +1 -1
  38. package/dist/dashboard/assets/SortableList-CAK-EN9z.js.gz +0 -0
  39. package/dist/dashboard/assets/{StudioPage-CHVZqskZ.js → StudioPage-B7W2_RhT.js} +3 -3
  40. package/dist/dashboard/assets/StudioPage-B7W2_RhT.js.gz +0 -0
  41. package/dist/dashboard/assets/UsagePage-6T4EFDjW.js +56 -0
  42. package/dist/dashboard/assets/UsagePage-6T4EFDjW.js.gz +0 -0
  43. package/dist/dashboard/assets/{api-keys-DFY_fZU4.js → api-keys-CUi6UXsR.js} +1 -1
  44. package/dist/dashboard/assets/api-keys-CUi6UXsR.js.gz +0 -0
  45. package/dist/dashboard/assets/app-KvsUJUMF.css +2 -0
  46. package/dist/dashboard/assets/app-KvsUJUMF.css.gz +0 -0
  47. package/dist/dashboard/assets/{assert-CwdnZARt.js → assert-67xXH5rf.js} +1 -1
  48. package/dist/dashboard/assets/assert-67xXH5rf.js.gz +0 -0
  49. package/dist/dashboard/assets/{badge-Dr-uf0Au.js → badge-DC2L43Mj.js} +1 -1
  50. package/dist/dashboard/assets/badge-DC2L43Mj.js.gz +0 -0
  51. package/dist/dashboard/assets/{banner-BddmMdoS.js → banner-BBVcb0J3.js} +1 -1
  52. package/dist/dashboard/assets/banner-BBVcb0J3.js.gz +0 -0
  53. package/dist/dashboard/assets/{card-CK0PXWlX.js → card-Cca1XC6G.js} +1 -1
  54. package/dist/dashboard/assets/card-Cca1XC6G.js.gz +0 -0
  55. package/dist/dashboard/assets/{cli-tools-CcB6mGh2.js → cli-tools-KvsWa1Jl.js} +1 -1
  56. package/dist/dashboard/assets/cli-tools-KvsWa1Jl.js.gz +0 -0
  57. package/dist/dashboard/assets/{clipboard-button-DGb2P9Yx.js → clipboard-button-DUzEbynq.js} +1 -1
  58. package/dist/dashboard/assets/clipboard-button-DUzEbynq.js.gz +0 -0
  59. package/dist/dashboard/assets/{dialog-LkfuL50u.js → dialog-DFvWhwTa.js} +1 -1
  60. package/dist/dashboard/assets/dialog-DFvWhwTa.js.gz +0 -0
  61. package/dist/dashboard/assets/{drawer-ZbEZ_84c.js → drawer-CFM2sMX1.js} +1 -1
  62. package/dist/dashboard/assets/drawer-CFM2sMX1.js.gz +0 -0
  63. package/dist/dashboard/assets/{entry-CWBY5A0f.js → entry-BLBLxX0h.js} +3 -3
  64. package/dist/dashboard/assets/entry-BLBLxX0h.js.gz +0 -0
  65. package/dist/dashboard/assets/{entry-BJfsVMvA.js → entry-Bqy8RFvM.js} +1 -1
  66. package/dist/dashboard/assets/entry-Bqy8RFvM.js.gz +0 -0
  67. package/dist/dashboard/assets/{entry-CvC1-Zqx.js → entry-_xHo5x6r.js} +1 -1
  68. package/dist/dashboard/assets/entry-_xHo5x6r.js.gz +0 -0
  69. package/dist/dashboard/assets/formatters-mpBPqAQ8.js +1 -0
  70. package/dist/dashboard/assets/formatters-mpBPqAQ8.js.gz +0 -0
  71. package/dist/dashboard/assets/{github-badge-BxpznFLt.js → github-badge-C9e8Ts7e.js} +1 -1
  72. package/dist/dashboard/assets/github-badge-C9e8Ts7e.js.gz +0 -0
  73. package/dist/dashboard/assets/{index-CDzP3ovJ.js → index-D6QzEC_n.js} +2 -2
  74. package/dist/dashboard/assets/index-D6QzEC_n.js.gz +0 -0
  75. package/dist/dashboard/assets/{inline-Ae13MQ5w.js → inline-BFUcvllj.js} +1 -1
  76. package/dist/dashboard/assets/inline-BFUcvllj.js.gz +0 -0
  77. package/dist/dashboard/assets/{input-D23fo7cp.js → input-C_ILAvBb.js} +1 -1
  78. package/dist/dashboard/assets/input-C_ILAvBb.js.gz +0 -0
  79. package/dist/dashboard/assets/{layout-CSsxjj5P.js → layout-CBIyT6Rt.js} +1 -1
  80. package/dist/dashboard/assets/layout-CBIyT6Rt.js.gz +0 -0
  81. package/dist/dashboard/assets/{list-row-dmbwZMDY.js → list-row-ebJ_-Nsb.js} +1 -1
  82. package/dist/dashboard/assets/list-row-ebJ_-Nsb.js.gz +0 -0
  83. package/dist/dashboard/assets/{live-BG1oUsZZ.js → live-D0mm--ME.js} +1 -1
  84. package/dist/dashboard/assets/live-D0mm--ME.js.gz +0 -0
  85. package/dist/dashboard/assets/{mutation-DSF-e4ho.js → mutation-sMD965oO.js} +1 -1
  86. package/dist/dashboard/assets/mutation-sMD965oO.js.gz +0 -0
  87. package/dist/dashboard/assets/{numbers-Bm_aECY7.js → numbers-BO9dKaFr.js} +1 -1
  88. package/dist/dashboard/assets/numbers-BO9dKaFr.js.gz +0 -0
  89. package/dist/dashboard/assets/providers/inception.svg +5 -0
  90. package/dist/dashboard/assets/providers/inception.svg.gz +0 -0
  91. package/dist/dashboard/assets/{query-keys-CF56nYIV.js → query-keys-CfL_Zdoh.js} +1 -1
  92. package/dist/dashboard/assets/query-keys-CfL_Zdoh.js.gz +0 -0
  93. package/dist/dashboard/assets/{routing-CeA71n7G.js → routing-BnIpi1IJ.js} +1 -1
  94. package/dist/dashboard/assets/routing-BnIpi1IJ.js.gz +0 -0
  95. package/dist/dashboard/assets/{select-KIT0rC6G.js → select-Cj5Nw43g.js} +1 -1
  96. package/dist/dashboard/assets/select-Cj5Nw43g.js.gz +0 -0
  97. package/dist/dashboard/assets/{settings-B_2gkePf.js → settings-BAFhDgX8.js} +1 -1
  98. package/dist/dashboard/assets/settings-BAFhDgX8.js.gz +0 -0
  99. package/dist/dashboard/assets/{stack-D7WOSzcX.js → stack-EMrOMt4I.js} +1 -1
  100. package/dist/dashboard/assets/stack-EMrOMt4I.js.gz +0 -0
  101. package/dist/dashboard/assets/{state-BBxtKZOQ.js → state-CWoo7ty5.js} +1 -1
  102. package/dist/dashboard/assets/state-CWoo7ty5.js.gz +0 -0
  103. package/dist/dashboard/assets/{switch-DNS_D5Ea.js → switch-CCjT6IkF.js} +1 -1
  104. package/dist/dashboard/assets/switch-CCjT6IkF.js.gz +0 -0
  105. package/dist/dashboard/assets/{useMutation-BgoxdoMk.js → useMutation-DjkLHUcE.js} +1 -1
  106. package/dist/dashboard/assets/useMutation-DjkLHUcE.js.gz +0 -0
  107. package/dist/dashboard/index.html +1 -1
  108. package/dist/dashboard/index.html.gz +0 -0
  109. package/dist/dashboard/precompressed-assets.json +54 -53
  110. package/migrations/full-postgres/0024_add_pool_provider_cooldowns.sql +12 -0
  111. package/migrations/full-postgres/0025_add_pool_random_strategy.sql +3 -0
  112. package/migrations/lite-sqlite3/0024_add_pool_provider_cooldowns.sql +12 -0
  113. package/migrations/lite-sqlite3/0025_add_pool_random_strategy.sql +20 -0
  114. package/package.json +96 -96
  115. package/scripts/cli/console-menu.ts +5 -2
  116. package/scripts/cli/tray.ts +21 -6
  117. package/src/boot/app.ts +684 -714
  118. package/src/boot/dependencies.ts +65 -62
  119. package/src/config/cooldowns.ts +16 -0
  120. package/src/config/providers.ts +13 -1
  121. package/src/config/resources.ts +70 -70
  122. package/src/config/security.ts +37 -37
  123. package/src/config/spec.ts +371 -358
  124. package/src/console/console-router.ts +233 -225
  125. package/src/console/domain-registration.ts +398 -381
  126. package/src/console/providers/catalog/catalog-projections.ts +2 -6
  127. package/src/console/providers/catalog/contracts.ts +19 -0
  128. package/src/console/providers/catalog/provider-operations.ts +38 -0
  129. package/src/console/providers/catalog/routes.ts +14 -0
  130. package/src/console/providers/catalog/store-helpers.ts +326 -0
  131. package/src/console/providers/catalog/store.ts +1292 -1563
  132. package/src/console/quota/account-quota-global.ts +2 -2
  133. package/src/console/quota/account-quota-shared.ts +1 -1
  134. package/src/console/quota/account-quota-tenant.ts +35 -12
  135. package/src/console/quota/quota-view.ts +2 -2
  136. package/src/console/routing/pools/contracts.ts +41 -1
  137. package/src/console/routing/pools/operations.ts +183 -4
  138. package/src/console/routing/pools/relay-deploy.ts +17 -4
  139. package/src/console/routing/pools/routes.ts +30 -0
  140. package/src/console/routing/pools/store.ts +98 -8
  141. package/src/main.ts +183 -167
  142. package/src/network/outbound-fetch.ts +226 -365
  143. package/src/network/pool/agent.ts +8 -0
  144. package/src/network/pool/pool-state.ts +7 -0
  145. package/src/network/pool/selector.ts +110 -2
  146. package/src/network/pool-cooldown-store.ts +252 -0
  147. package/src/network/pool-health-machine.ts +5 -3
  148. package/src/network/pool-health.ts +36 -9
  149. package/src/observability/event-query.ts +57 -0
  150. package/src/observability/metrics.ts +467 -467
  151. package/src/observability/runtime-metrics.ts +269 -269
  152. package/src/persistence/health-event-flush.ts +2 -0
  153. package/src/persistence/schema.ts +19 -0
  154. package/src/persistence/usage-flush.ts +8 -3
  155. package/src/protocol/canonical-model.ts +1 -1
  156. package/src/protocol/dsml.ts +377 -0
  157. package/src/protocol/gateway-error.ts +232 -232
  158. package/src/{providers/provider-reasoning.ts → protocol/reasoning.ts} +10 -9
  159. package/src/protocol/request/chat.ts +7 -7
  160. package/src/protocol/request/codex.ts +2 -2
  161. package/src/protocol/request/gemini.ts +1 -1
  162. package/src/protocol/request/messages.ts +6 -6
  163. package/src/protocol/request/responses.ts +7 -7
  164. package/src/protocol/response/dsml.ts +12 -133
  165. package/src/{transport → protocol}/translation/attachment-integrity.ts +2 -2
  166. package/src/{transport → protocol}/translation/cache-controls.ts +1 -1
  167. package/src/{transport → protocol}/translation/capabilities.ts +2 -2
  168. package/src/{transport → protocol}/translation/normalize.ts +1 -1
  169. package/src/{transport → protocol}/translation/quirks.ts +1 -1
  170. package/src/{transport → protocol}/translation/search.ts +1 -1
  171. package/src/{transport → protocol}/translation/thinking.ts +23 -1
  172. package/src/{transport → protocol}/translation/tool-id.ts +1 -1
  173. package/src/{transport → protocol}/translation/tool-repair.ts +2 -2
  174. package/src/protocol/upstream-parsing.ts +176 -0
  175. package/src/providers/adapters/compatible-adapter.ts +11 -2
  176. package/src/providers/authentication/oauth-refresh-service.ts +9 -5
  177. package/src/providers/default-registry.ts +21 -5
  178. package/src/providers/discovery/probe-phases.ts +16 -3
  179. package/src/providers/discovery/probing-service.ts +4 -1
  180. package/src/providers/integrations/anthropic.ts +1 -1
  181. package/src/providers/integrations/buddy/buddy-catalog-shared.ts +1 -4
  182. package/src/providers/integrations/buddy/buddy-chat-shared.ts +141 -8
  183. package/src/providers/integrations/buddy/buddy-checkin.ts +5 -1
  184. package/src/providers/integrations/buddy/buddy-growth-shared.ts +0 -82
  185. package/src/providers/integrations/buddy/buddy-identity.ts +25 -0
  186. package/src/providers/integrations/buddy/buddy-server-tools.ts +240 -0
  187. package/src/providers/integrations/buddy/codebuddy/cn-catalog.ts +36 -0
  188. package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +6 -31
  189. package/src/providers/integrations/buddy/codebuddy/provider.ts +2 -0
  190. package/src/providers/integrations/buddy/codebuddy/shared.ts +19 -4
  191. package/src/providers/integrations/buddy/turing-device-token.ts +262 -0
  192. package/src/providers/integrations/buddy/workbuddy/provider.ts +2 -0
  193. package/src/providers/integrations/buddy/workbuddy/shared.ts +56 -3
  194. package/src/providers/integrations/claude/claude.ts +1 -1
  195. package/src/providers/integrations/cloudflareai/cloudflareai.ts +1 -1
  196. package/src/providers/integrations/codex/codex-errors.ts +1 -1
  197. package/src/providers/integrations/codex/codex.ts +1 -1
  198. package/src/providers/integrations/factory/factory.ts +1 -1
  199. package/src/providers/integrations/github/github.ts +1 -1
  200. package/src/providers/integrations/grok/grok.ts +1 -1
  201. package/src/providers/integrations/inception/provider.ts +91 -0
  202. package/src/providers/integrations/inferhub.ts +1 -1
  203. package/src/providers/integrations/kiro/kiro-request.ts +1 -1
  204. package/src/providers/integrations/mistral.ts +193 -19
  205. package/src/providers/integrations/opencode/fingerprint.ts +19 -2
  206. package/src/providers/integrations/opencode/prompt.ts +111 -0
  207. package/src/providers/integrations/opencode/provider.ts +101 -23
  208. package/src/providers/integrations/opencode/tool-definitions.ts +4 -4
  209. package/src/providers/integrations/qoder/qoder-checkin.ts +303 -0
  210. package/src/providers/integrations/qoder/qoder.ts +1641 -1458
  211. package/src/providers/integrations/shared/install-id.ts +2 -1
  212. package/src/providers/integrations/trae/trae-cache.ts +110 -0
  213. package/src/providers/integrations/trae/trae-oauth.ts +29 -4
  214. package/src/providers/integrations/trae/trae-protocol.ts +2 -1
  215. package/src/providers/integrations/trae/trae.ts +106 -23
  216. package/src/providers/operations/account-error-classification.ts +1660 -0
  217. package/src/providers/operations/account-health-service.ts +621 -2097
  218. package/src/providers/operations/account-reset-service.ts +2 -0
  219. package/src/providers/provider-contracts.ts +0 -2
  220. package/src/providers/provider-metadata.ts +25 -4
  221. package/src/providers/provider-probe.ts +2 -2
  222. package/src/{console → providers}/quota/cache.ts +1 -1
  223. package/src/{console → providers}/quota/refresh.ts +5 -5
  224. package/src/transport/dispatch/attempt-finalize.ts +114 -12
  225. package/src/transport/dispatch/attempt-loop.ts +3 -0
  226. package/src/transport/dispatch/empty-stream.ts +224 -217
  227. package/src/transport/dispatch/generation.ts +37 -55
  228. package/src/transport/dispatch/handler-deps.ts +32 -0
  229. package/src/transport/dispatch/leases.ts +9 -4
  230. package/src/transport/dispatch/proxy-request.ts +6 -30
  231. package/src/transport/dispatch/responses-compact.ts +22 -43
  232. package/src/transport/dispatch/streaming/finalize.ts +3 -0
  233. package/src/transport/dispatch/streaming/stream.ts +0 -8
  234. package/src/transport/dispatch/systemone.ts +22 -42
  235. package/src/transport/dispatch/token-count.ts +23 -39
  236. package/src/transport/dispatch/upstream.ts +1 -3
  237. package/src/transport/dispatch/websearch.ts +22 -41
  238. package/src/transport/failure-policy.ts +59 -178
  239. package/src/transport/guards/abuse-guard.ts +10 -0
  240. package/src/transport/guards/auth-guard.ts +109 -109
  241. package/src/transport/guards/error-lifecycle.ts +1 -1
  242. package/src/transport/guards/pipeline.ts +220 -220
  243. package/src/transport/guards/request-context.ts +245 -238
  244. package/src/transport/request/degradation.ts +20 -1
  245. package/src/transport/request/inflight.ts +4 -114
  246. package/src/transport/request/preparer.ts +6 -6
  247. package/src/transport/request/state.ts +66 -38
  248. package/src/transport/request/tenant-preferences.ts +1 -1
  249. package/src/transport/routing/route-catalog.ts +41 -31
  250. package/src/transport/routing/router.ts +2 -2
  251. package/src/transport/surface/messages/parse.ts +1 -1
  252. package/src/workers/buddy-growth-tasks.ts +346 -425
  253. package/src/workers/daily-checkin.ts +17 -6
  254. package/src/workers/quota-refresh-sweep.ts +2 -2
  255. package/src/workers/scheduler.ts +17 -1
  256. package/dist/dashboard/assets/BannedPage-P76QLGJA.js.gz +0 -0
  257. package/dist/dashboard/assets/CliToolDetailPage-DdBggJS9.js.gz +0 -0
  258. package/dist/dashboard/assets/CliToolsPage-aN1iXhhP.js.gz +0 -0
  259. package/dist/dashboard/assets/CombosPage-DE-F1nxZ.js.gz +0 -0
  260. package/dist/dashboard/assets/ConsoleLogPage-5SpmfLyw.js +0 -1
  261. package/dist/dashboard/assets/ConsoleLogPage-5SpmfLyw.js.gz +0 -0
  262. package/dist/dashboard/assets/CustomizationPage-3CUxMy2X.js.gz +0 -0
  263. package/dist/dashboard/assets/HealthEventsModal-BkmJQKC0.js +0 -1
  264. package/dist/dashboard/assets/HealthEventsModal-BkmJQKC0.js.gz +0 -0
  265. package/dist/dashboard/assets/LoginPage-B_0FUKWc.js +0 -1
  266. package/dist/dashboard/assets/LoginPage-B_0FUKWc.js.gz +0 -0
  267. package/dist/dashboard/assets/ModelPicker-DKVslqG3.js.gz +0 -0
  268. package/dist/dashboard/assets/OverviewPage-D-H4MEiT.js +0 -1
  269. package/dist/dashboard/assets/OverviewPage-D-H4MEiT.js.gz +0 -0
  270. package/dist/dashboard/assets/ProviderDetailPage-CEJw6Guo.js +0 -3
  271. package/dist/dashboard/assets/ProviderDetailPage-CEJw6Guo.js.gz +0 -0
  272. package/dist/dashboard/assets/ProviderIcon-CmfzxpVM.js +0 -1
  273. package/dist/dashboard/assets/ProviderIcon-CmfzxpVM.js.gz +0 -0
  274. package/dist/dashboard/assets/ProvidersPage-CEplAR-i.js +0 -1
  275. package/dist/dashboard/assets/ProvidersPage-CEplAR-i.js.gz +0 -0
  276. package/dist/dashboard/assets/ProxyPage-CAN7hy0f.js +0 -7
  277. package/dist/dashboard/assets/ProxyPage-CAN7hy0f.js.gz +0 -0
  278. package/dist/dashboard/assets/QuotaPage-BjldQwIU.js.gz +0 -0
  279. package/dist/dashboard/assets/SettingsPage-DUIoLRxk.js.gz +0 -0
  280. package/dist/dashboard/assets/SetupPage-_bCaTM0v.js +0 -1
  281. package/dist/dashboard/assets/SetupPage-_bCaTM0v.js.gz +0 -0
  282. package/dist/dashboard/assets/SortableList-fI7JkdIB.js.gz +0 -0
  283. package/dist/dashboard/assets/StudioPage-CHVZqskZ.js.gz +0 -0
  284. package/dist/dashboard/assets/UsagePage-DiFBymHj.js +0 -56
  285. package/dist/dashboard/assets/UsagePage-DiFBymHj.js.gz +0 -0
  286. package/dist/dashboard/assets/api-keys-DFY_fZU4.js.gz +0 -0
  287. package/dist/dashboard/assets/app-BOJ1_xUB.css +0 -2
  288. package/dist/dashboard/assets/app-BOJ1_xUB.css.gz +0 -0
  289. package/dist/dashboard/assets/assert-CwdnZARt.js.gz +0 -0
  290. package/dist/dashboard/assets/badge-Dr-uf0Au.js.gz +0 -0
  291. package/dist/dashboard/assets/banner-BddmMdoS.js.gz +0 -0
  292. package/dist/dashboard/assets/card-CK0PXWlX.js.gz +0 -0
  293. package/dist/dashboard/assets/cli-tools-CcB6mGh2.js.gz +0 -0
  294. package/dist/dashboard/assets/clipboard-button-DGb2P9Yx.js.gz +0 -0
  295. package/dist/dashboard/assets/cog-CdWhsajs.js +0 -1
  296. package/dist/dashboard/assets/cog-CdWhsajs.js.gz +0 -0
  297. package/dist/dashboard/assets/dialog-LkfuL50u.js.gz +0 -0
  298. package/dist/dashboard/assets/drawer-ZbEZ_84c.js.gz +0 -0
  299. package/dist/dashboard/assets/entry-BJfsVMvA.js.gz +0 -0
  300. package/dist/dashboard/assets/entry-CWBY5A0f.js.gz +0 -0
  301. package/dist/dashboard/assets/entry-CvC1-Zqx.js.gz +0 -0
  302. package/dist/dashboard/assets/github-badge-BxpznFLt.js.gz +0 -0
  303. package/dist/dashboard/assets/index-CDzP3ovJ.js.gz +0 -0
  304. package/dist/dashboard/assets/inline-Ae13MQ5w.js.gz +0 -0
  305. package/dist/dashboard/assets/input-D23fo7cp.js.gz +0 -0
  306. package/dist/dashboard/assets/layout-CSsxjj5P.js.gz +0 -0
  307. package/dist/dashboard/assets/list-row-dmbwZMDY.js.gz +0 -0
  308. package/dist/dashboard/assets/live-BG1oUsZZ.js.gz +0 -0
  309. package/dist/dashboard/assets/mutation-DSF-e4ho.js.gz +0 -0
  310. package/dist/dashboard/assets/numbers-Bm_aECY7.js.gz +0 -0
  311. package/dist/dashboard/assets/query-keys-CF56nYIV.js.gz +0 -0
  312. package/dist/dashboard/assets/routing-CeA71n7G.js.gz +0 -0
  313. package/dist/dashboard/assets/select-KIT0rC6G.js.gz +0 -0
  314. package/dist/dashboard/assets/settings-B_2gkePf.js.gz +0 -0
  315. package/dist/dashboard/assets/stack-D7WOSzcX.js.gz +0 -0
  316. package/dist/dashboard/assets/state-BBxtKZOQ.js.gz +0 -0
  317. package/dist/dashboard/assets/switch-DNS_D5Ea.js.gz +0 -0
  318. package/dist/dashboard/assets/useMutation-BgoxdoMk.js.gz +0 -0
  319. /package/dist/dashboard/assets/{app-CaXZ9SCn.js → app-BaWd0puM.js} +0 -0
  320. /package/dist/dashboard/assets/{app-CaXZ9SCn.js.gz → app-BaWd0puM.js.gz} +0 -0
@@ -9,6 +9,7 @@
9
9
  * segment. `mapProviderRow` is the single `providers` row projection.
10
10
  */
11
11
  import { isBundledProviderId } from "../../../providers/provider-contracts";
12
+ import { BUILTIN_DEFAULT_ENDPOINTS } from "../../../protocol/codecs";
12
13
 
13
14
  import { providerDisplayName, providerServiceKinds } from "../../../providers/provider-metadata";
14
15
  import type { CompatibilityProfile } from "../../../providers/provider-metadata";
@@ -26,12 +27,7 @@ export interface ProviderDiscoveryLookup {
26
27
  hasModelDiscovery(providerId: string): boolean;
27
28
  }
28
29
 
29
- export const DEFAULT_ENDPOINT_BY_WIRE_FAMILY: Record<WireFamily, string> = {
30
- chat: "/v1/chat/completions",
31
- responses: "/v1/responses",
32
- messages: "/v1/messages",
33
- gemini: "/api/llm/g/v1/generate",
34
- };
30
+ export const DEFAULT_ENDPOINT_BY_WIRE_FAMILY: Record<WireFamily, string> = BUILTIN_DEFAULT_ENDPOINTS;
35
31
  export function endpointPathForProviderModel(
36
32
  wireFamily: WireFamily,
37
33
  compatibilityProfile: unknown,
@@ -557,6 +557,14 @@ export interface ProviderAccountExport {
557
557
  accessToken: string;
558
558
  /** Decrypted OAuth refresh token, omitted when the account has none. */
559
559
  refreshToken?: string;
560
+ /**
561
+ * Non-secret upstream auth configuration carried on the account (the
562
+ * upstream user id, provisioned region, auth method, profile). Re-import
563
+ * must restore it: Trae refresh is addressed per upstream user id, and the
564
+ * data-plane host is region-scoped, so dropping it breaks re-imported
565
+ * accounts on their first refresh or dispatch.
566
+ */
567
+ authState?: Readonly<Record<string, unknown>>;
560
568
  createdAt: string;
561
569
  /** Live routing admission count at export time; absent when unavailable. */
562
570
  inflight?: number;
@@ -771,6 +779,17 @@ export interface ProviderCatalogStore {
771
779
  syncModels(tenantId: string, providerId: string): Promise<{ synced: number }>;
772
780
  listAccounts(tenantId: string, providerId: string): Promise<readonly ProviderAccountResponse[]>;
773
781
  listAllAccounts(tenantId: string): Promise<readonly ProviderAccountResponse[]>;
782
+ /**
783
+ * Non-secret upstream auth configuration for the given owned accounts,
784
+ * keyed by account id. Used by the account-export route so a re-import can
785
+ * restore the upstream user id / region that refresh and dispatch are
786
+ * addressed by; never surfaced on the read models.
787
+ */
788
+ listAccountAuthStates(
789
+ tenantId: string,
790
+ providerId: string,
791
+ accountIds: readonly string[],
792
+ ): Promise<Readonly<Record<string, Readonly<Record<string, unknown>>>>>;
774
793
  createAccount(
775
794
  tenantId: string,
776
795
  providerId: string,
@@ -17,6 +17,7 @@ import {
17
17
  requireTenantAnyScope,
18
18
  } from "../../shared/errors";
19
19
  import type { AccessDecision } from "../../../security/access-control";
20
+ import type { ProviderUsageDayRow } from "../../../observability/event-query";
20
21
  import { isBundledProviderId } from "../../../providers/provider-contracts";
21
22
  import type { ProviderRegistry } from "../../../providers/provider-registry";
22
23
  import { getProviderClientVersion, getRouteIdentityVersions } from "../../../providers/operations/client-versions";
@@ -146,6 +147,16 @@ export interface ProviderCatalogConfig {
146
147
  readonly members: readonly string[];
147
148
  }[];
148
149
  }>;
150
+ /**
151
+ * Day × model usage aggregate feeding the provider's Activity Matrix card.
152
+ * Optional so hosts without telemetry reads (and existing test doubles) keep
153
+ * the route returning an empty matrix instead of failing.
154
+ */
155
+ readonly providerUsageMatrix?: (
156
+ tenantId: string,
157
+ providerId: string,
158
+ days: number,
159
+ ) => Promise<readonly ProviderUsageDayRow[]>;
149
160
  /**
150
161
  * Resolves the stored credential and optional OAuth refresh token for export,
151
162
  * decrypting them without applying provider-specific dispatch codecs.
@@ -249,6 +260,19 @@ export function createProviderCatalogOperations(config: ProviderCatalogConfig) {
249
260
  throw new ConsoleDomainError("provider_not_found", 404, `Provider ${providerId} not found`);
250
261
  return attachProviderCapabilities(sanitizeProviderResponse(rec, true), config.providerRegistry);
251
262
  },
263
+ async getUsageMatrix(
264
+ access: AccessDecision | undefined,
265
+ providerId: string,
266
+ days: number,
267
+ ): Promise<{ readonly days: number; readonly rows: readonly ProviderUsageDayRow[] }> {
268
+ const a = requireTenantAnyScope(access, PROVIDER_READ_SCOPES);
269
+ // Bounded window even on a hostile query string: the card reads a
270
+ // calendar, not history, and the aggregate scan cost grows with `days`.
271
+ const bounded = Math.min(Math.max(Math.trunc(days) || 1, 1), 366);
272
+ if (!config.providerUsageMatrix) return { days: bounded, rows: [] };
273
+ const rows = await config.providerUsageMatrix(a.tenantId, providerId, bounded);
274
+ return { days: bounded, rows };
275
+ },
252
276
  async createProvider(
253
277
  access: AccessDecision | undefined,
254
278
  request: CreateProviderRequest,
@@ -639,6 +663,11 @@ export function createProviderCatalogOperations(config: ProviderCatalogConfig) {
639
663
  const selected = owned.filter(
640
664
  (account) => wanted.has(account.id) && account.tenantId === a.tenantId,
641
665
  );
666
+ const authStates = await config.store.listAccountAuthStates(
667
+ a.tenantId,
668
+ providerId,
669
+ selected.map((account) => account.id),
670
+ );
642
671
  const accounts = await Promise.all(
643
672
  selected.map(async (account): Promise<ProviderAccountExport> => {
644
673
  let accessToken = "";
@@ -661,6 +690,15 @@ export function createProviderCatalogOperations(config: ProviderCatalogConfig) {
661
690
  status: account.status,
662
691
  accessToken,
663
692
  ...(refreshToken === undefined ? {} : { refreshToken }),
693
+ // Non-secret auth state is part of the credential export: without
694
+ // it a re-import loses the upstream user id and region that Trae
695
+ // refresh and dispatch are addressed by.
696
+ ...(() => {
697
+ const authState = authStates[account.id];
698
+ return authState !== undefined && Object.keys(authState).length > 0
699
+ ? { authState }
700
+ : {};
701
+ })(),
664
702
  ...(account.inflight === undefined ? {} : { inflight: account.inflight }),
665
703
  createdAt: account.createdAt,
666
704
  ...(account.cooldownUntil ? { cooldownUntil: account.cooldownUntil } : {}),
@@ -147,6 +147,9 @@ const deleteModelBody = t.Object({
147
147
  const bulkDeleteModelsBody = t.Object({
148
148
  items: t.Array(deleteModelBody, { minItems: 1, maxItems: 1000 }),
149
149
  });
150
+ const usageMatrixQuery = t.Object({
151
+ days: t.Optional(t.Integer({ minimum: 1, maximum: 366 })),
152
+ });
150
153
  export function createProviderCatalogRoutes(config: ProviderCatalogConfig): Elysia {
151
154
  const factory = createProviderCatalogOperations(config);
152
155
  const modelFactory = createModelCatalogOperations(config);
@@ -191,6 +194,17 @@ export function createProviderCatalogRoutes(config: ProviderCatalogConfig): Elys
191
194
  .get("/:providerId", async ({ request, params }) => {
192
195
  return await factory.getProviderDetail(config.accessResolver(request), params.providerId);
193
196
  })
197
+ .get(
198
+ "/:providerId/usage/matrix",
199
+ { query: usageMatrixQuery },
200
+ async ({ request, params, query }) => {
201
+ return await factory.getUsageMatrix(
202
+ config.accessResolver(request),
203
+ params.providerId,
204
+ query.days ?? 30,
205
+ );
206
+ },
207
+ )
194
208
  .post("/", { body: createProviderBody }, async ({ request, body, set }) => {
195
209
  set.status = 201;
196
210
  return await factory.createProvider(
@@ -0,0 +1,326 @@
1
+ /**
2
+ * Pure helpers for the provider catalog store: credential parsing, generated
3
+ * labels, and row projections. These are the parts of the store that do not
4
+ * touch the database, kept here so the Drizzle store stays about persistence.
5
+ */
6
+ import { decodeJwtPayload } from "../../../providers/authentication/oauth-flow-store";
7
+ import {
8
+ asRecord,
9
+ firstNonEmptyString as firstString,
10
+ tryParseJson as safeParseJson,
11
+ } from "../../../shared/coerce";
12
+ import type { CredentialMode, ModelCatalogEntry } from "./contracts";
13
+ import {
14
+ FALLBACK_CONTEXT_LIMIT,
15
+ FALLBACK_OUTPUT_LIMIT,
16
+ } from "../../../providers/provider-model-definition";
17
+ import { modelsDevCatalog } from "../../../providers/discovery/models-dev-catalog";
18
+ import { models } from "../../../persistence/schema";
19
+ /** Field names a pasted OAuth credential may carry its access token under. */
20
+ const OAUTH_ACCESS_FIELDS = ["accessToken", "access_token", "access", "token"] as const;
21
+ /** Field names a pasted OAuth credential may carry its refresh token under. */
22
+ const OAUTH_REFRESH_FIELDS = ["refreshToken", "refresh_token", "refresh"] as const;
23
+
24
+ /** Parses an expiry field that may be an ISO string, an epoch ms, or epoch seconds. */
25
+ function parseExpiry(value: unknown): Date | undefined {
26
+ if (typeof value === "number" && Number.isFinite(value)) {
27
+ // Heuristic: a 10-digit value is epoch seconds, a 13-digit value is ms.
28
+ const ms = value < 1e12 ? value * 1000 : value;
29
+ const date = new Date(ms);
30
+ return Number.isFinite(date.getTime()) ? date : undefined;
31
+ }
32
+ if (typeof value === "string" && value.trim().length > 0) {
33
+ const asNumber = Number(value);
34
+ if (Number.isFinite(asNumber)) return parseExpiry(asNumber);
35
+ const date = new Date(value);
36
+ return Number.isFinite(date.getTime()) ? date : undefined;
37
+ }
38
+ return undefined;
39
+ }
40
+ /**
41
+ * Reads the two claims a static token's own payload can answer: when it stops
42
+ * working, and who issued it.
43
+ *
44
+ * The issuer is kept because a static JWT carries no other hint of its origin:
45
+ * an operator who pasted one from the wrong surface has nothing in the console
46
+ * to check it against, and several issuers accept the same token shape.
47
+ */
48
+ export function parseJwtCredential(
49
+ raw: string,
50
+ ): { readonly expiresAt?: Date; readonly issuer?: string } | undefined {
51
+ const payload = decodeJwtPayload(raw.trim());
52
+ if (payload === undefined) return undefined;
53
+ const expiresAt = parseExpiry(payload["exp"]);
54
+ const iss = payload["iss"];
55
+ const issuer = typeof iss === "string" && iss.trim().length > 0 ? iss.trim() : undefined;
56
+ return {
57
+ ...(expiresAt === undefined ? {} : { expiresAt }),
58
+ ...(issuer === undefined ? {} : { issuer }),
59
+ };
60
+ }
61
+
62
+ /**
63
+ * Splits a pasted OAuth credential into its access token, refresh token, and
64
+ * expiry.
65
+ *
66
+ * The console accepts either a bare access token or the JSON export a client
67
+ * produces (`{accessToken, refreshToken, expiresAt, …}`, possibly nested under
68
+ * `data`). A bare token has no refresh token — the account can be dispatched
69
+ * with it but cannot be refreshed, so the caller stores it as a static
70
+ * API credential instead of sending the access token to a refresh endpoint.
71
+ */
72
+ /**
73
+ * Middle segment of an auto-generated account label, from how the credential
74
+ * was actually detected.
75
+ *
76
+ * Only the two kinds that arrive with no name of their own are named here. An
77
+ * OAuth paste carries its own identity in `authState` (the account it was
78
+ * exported from), so it keeps whatever the export or the operator supplied —
79
+ * generating a name for it would overwrite an identity that was already there.
80
+ * An unrecognized mode falls back to `key`, matching the mode it is stored as.
81
+ */
82
+ export function accountLabelKindSegment(credentialMode: CredentialMode): string {
83
+ return credentialMode === "jwt" ? "jwt" : "key";
84
+ }
85
+
86
+ /**
87
+ * First unused `dcint-key-N` / `dcint-jwt-N` label for one provider.
88
+ *
89
+ * Polished tokens and plain JWTs are the credentials that arrive nameless: an
90
+ * operator pastes several into one provider and every row came out as
91
+ * "<providerId> account", so five accounts were five identical labels and the
92
+ * list could not tell them apart — which account failed, or is cooling, was
93
+ * unreadable.
94
+ *
95
+ * Numbering runs per kind, so the first JWT is `jwt-1` rather than inheriting
96
+ * whatever number the api_key rows reached (`jwt-3` with no `jwt-1` reads as
97
+ * two missing accounts). Counting is case-insensitive on the whole label, so a
98
+ * hand-typed `DCINT-KEY-2` still reserves its number.
99
+ *
100
+ * Pure, so the numbering can be pinned without a database.
101
+ */
102
+ export function nextGeneratedAccountLabel(
103
+ providerId: string,
104
+ credentialMode: CredentialMode,
105
+ existingLabels: readonly string[],
106
+ ): string {
107
+ const kind = accountLabelKindSegment(credentialMode);
108
+ const prefix = `${providerId}-${kind}-`;
109
+ const taken = new Set(
110
+ existingLabels.map((label) => label.trim().toLowerCase()),
111
+ );
112
+ for (let n = 1; ; n += 1) {
113
+ const candidate = `${prefix}${n}`;
114
+ if (!taken.has(candidate.toLowerCase())) return candidate;
115
+ }
116
+ }
117
+
118
+ export function parsePastedOAuthCredential(raw: string): {
119
+ readonly accessToken: string;
120
+ readonly refreshToken: string;
121
+ readonly expiresAt: Date | undefined;
122
+ /**
123
+ * Non-secret account configuration the export carried — the profile the
124
+ * account is bound to, how it authenticates, the region it was minted in.
125
+ * Empty for a bare token, which carries none.
126
+ */
127
+ readonly authState: Readonly<Record<string, unknown>>;
128
+ } {
129
+ let record: Record<string, unknown> | undefined;
130
+ try {
131
+ const parsed = JSON.parse(raw) as unknown;
132
+ if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
133
+ record = parsed as Record<string, unknown>;
134
+ }
135
+ } catch {
136
+ record = undefined;
137
+ }
138
+ if (record === undefined) {
139
+ // Bare token: it is the access token, and there is no refresh token.
140
+ const jwt = parseJwtCredential(raw);
141
+ return {
142
+ accessToken: raw,
143
+ refreshToken: raw,
144
+ expiresAt: jwt?.expiresAt,
145
+ authState: {},
146
+ };
147
+ }
148
+ // A nested `data` object is unwrapped the same way the dashboard's parser does.
149
+ const nested =
150
+ record["data"] !== null && typeof record["data"] === "object" && !Array.isArray(record["data"])
151
+ ? (record["data"] as Record<string, unknown>)
152
+ : undefined;
153
+ const accessToken =
154
+ firstString(record, OAUTH_ACCESS_FIELDS) ??
155
+ (nested ? firstString(nested, OAUTH_ACCESS_FIELDS) : undefined) ??
156
+ raw;
157
+ const refreshToken =
158
+ firstString(record, OAUTH_REFRESH_FIELDS) ??
159
+ (nested ? firstString(nested, OAUTH_REFRESH_FIELDS) : undefined) ??
160
+ accessToken;
161
+ const expiresAt =
162
+ parseExpiry(
163
+ record["expiresAt"] ??
164
+ record["expires_at"] ??
165
+ (nested ? (nested["expiresAt"] ?? nested["expires_at"]) : undefined) ??
166
+ record["expires"] ??
167
+ (nested ? nested["expires"] : undefined),
168
+ ) ?? parseJwtCredential(accessToken)?.expiresAt;
169
+ return { accessToken, refreshToken, expiresAt, authState: pastedAuthState(record, nested) };
170
+ }
171
+
172
+ /**
173
+ * The account configuration a pasted export carries beside its tokens.
174
+ *
175
+ * Kiro refuses a generation request that omits the profile the account is bound
176
+ * to, and exports from older installs carry that profile under a different key.
177
+ * Reading neither is how an imported account probes with no
178
+ * profile and is answered `400 profileArn is required`.
179
+ */
180
+ function pastedAuthState(
181
+ record: Record<string, unknown>,
182
+ nested: Record<string, unknown> | undefined,
183
+ ): Readonly<Record<string, unknown>> {
184
+ const sources = [record, nested, asRecord(record["providerSpecificData"]), asRecord(record["authState"]), asRecord(nested?.["authState"])];
185
+ const profileArn = firstStringFrom(sources, ["profileArn", "profile_arn"]);
186
+ const authMethod = firstStringFrom(sources, ["authMethod", "auth_method"]);
187
+ const region = firstStringFrom(sources, ["region"]);
188
+ // The upstream user id, carried on a re-import of an exported account. Trae
189
+ // refresh is addressed per upstream user id, so restoring it is what keeps a
190
+ // re-imported account refreshable.
191
+ const userId = firstStringFrom(sources, ["userId", "user_id", "uid", "UserID"]);
192
+ return {
193
+ ...(profileArn === undefined ? {} : { profileArn }),
194
+ ...(authMethod === undefined ? {} : { authMethod }),
195
+ ...(region === undefined ? {} : { region }),
196
+ ...(userId === undefined ? {} : { userId }),
197
+ };
198
+ }
199
+
200
+ /**
201
+ * Adds or removes the `image` input modality on a stored modalities blob.
202
+ *
203
+ * The blob is a JSON string whose shape varies by source (bundled seed,
204
+ * discovery, manual add), so it is normalized rather than pattern-matched: an
205
+ * unparseable blob is replaced with the minimal text shape rather than left to
206
+ * corrupt capability preflight.
207
+ */
208
+ export function withImageModality(stored: string | null | undefined, image: boolean): string {
209
+ const parsed = asRecord(safeParseJson(stored));
210
+ const input = Array.isArray(parsed?.input)
211
+ ? (parsed.input as unknown[]).filter((m): m is string => typeof m === "string")
212
+ : ["text"];
213
+ const output = Array.isArray(parsed?.output)
214
+ ? (parsed.output as unknown[]).filter((m): m is string => typeof m === "string")
215
+ : ["text"];
216
+ const nextInput = image
217
+ ? Array.from(new Set([...input, "image"]))
218
+ : input.filter((m) => m !== "image");
219
+ return JSON.stringify({
220
+ ...(parsed ?? {}),
221
+ input: nextInput.length > 0 ? nextInput : ["text"],
222
+ output: output.length > 0 ? output : ["text"],
223
+ });
224
+ }
225
+
226
+ /** Normalizes a text column read as `unknown` back to a string or null. */
227
+ export function asStoredText(value: unknown): string | null {
228
+ return typeof value === "string" ? value : null;
229
+ }
230
+
231
+ function firstStringFrom(
232
+ sources: readonly (Record<string, unknown> | undefined)[],
233
+ fields: readonly string[],
234
+ ): string | undefined {
235
+ for (const source of sources) {
236
+ if (source === undefined) continue;
237
+ const value = firstString(source, fields);
238
+ if (value !== undefined) return value;
239
+ }
240
+ return undefined;
241
+ }
242
+
243
+ /**
244
+ * The account's still-active per-model backoffs, or `undefined` when none are.
245
+ *
246
+ * Expired entries are dropped rather than returned: the sweep that clears them
247
+ * runs on a timer, so between the deadline passing and the sweep a raw read
248
+ * would report a cooldown that is not in force.
249
+ */
250
+ export function liveModelCooldowns(
251
+ raw: unknown,
252
+ ): { modelCooldowns: Record<string, string> } | undefined {
253
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return undefined;
254
+ const now = Date.now();
255
+ const live: Record<string, string> = {};
256
+ for (const [modelId, value] of Object.entries(raw as Record<string, unknown>)) {
257
+ if (typeof value !== "string") continue;
258
+ const at = new Date(value).getTime();
259
+ if (Number.isFinite(at) && at > now) live[modelId] = value;
260
+ }
261
+ return Object.keys(live).length > 0 ? { modelCooldowns: live } : undefined;
262
+ }
263
+
264
+
265
+ /**
266
+ * Projects one persisted `models` row into the console's catalog entry.
267
+ *
268
+ * `disabled` is resolved by the caller (from `tenant_disabled_models`), because
269
+ * the bulk read has to look it up across every provider at once while the
270
+ * single-provider read looks it up for one; the projection itself is identical
271
+ * either way, so it lives here rather than being written twice.
272
+ *
273
+ * Exported so a test can pin the unsized-model substitution without a database:
274
+ * which rows get the fallback, and that they are the same rows marked estimated.
275
+ */
276
+ export function mapModelRow(
277
+ row: typeof models.$inferSelect,
278
+ disabled: boolean,
279
+ ): ModelCatalogEntry {
280
+ const modalities =
281
+ row.modalities && typeof row.modalities === "object"
282
+ ? (row.modalities as { input?: unknown; output?: unknown })
283
+ : undefined;
284
+ const inputModalities = Array.isArray(modalities?.input) ? modalities.input : [];
285
+ const outputModalities = Array.isArray(modalities?.output) ? modalities.output : [];
286
+ // Nothing filed this model — not its own row, not the base catalog under this
287
+ // provider's id, not a majority across the catalog's other resellers. The
288
+ // fallback is substituted here rather than persisted: the stored `null` is the
289
+ // honest record of what upstream said, and substituting at the write would
290
+ // make a guess indistinguishable from a measurement on the next read. The
291
+ // marker rides along so the card can say which one it is showing.
292
+ const unfiled =
293
+ row.contextLimit === null &&
294
+ modelsDevCatalog.resolve(row.providerId, row.modelId) === undefined &&
295
+ modelsDevCatalog.majorityFor(row.modelId) === undefined;
296
+ return {
297
+ modelId: row.modelId,
298
+ route: row.endpointPath,
299
+ provider: row.providerId,
300
+ wireFamily: row.wireFamily,
301
+ serviceKind: row.serviceKind,
302
+ enabled: disabled ? false : row.enabled,
303
+ // A fetched model that published no context size read `n/a ctx · n/a out`
304
+ // while still being routable — a working model that looked unusable. These
305
+ // rows get the same ceilings a manual add is offered, flagged as estimated.
306
+ contextLimit: unfiled ? FALLBACK_CONTEXT_LIMIT : row.contextLimit,
307
+ outputLimit: unfiled ? FALLBACK_OUTPUT_LIMIT : row.outputLimit,
308
+ reasoning: row.reasoning,
309
+ toolCall: row.toolCall,
310
+ vision: inputModalities.includes("image"),
311
+ document: inputModalities.includes("document"),
312
+ audio: inputModalities.includes("audio"),
313
+ mediaGeneration: outputModalities.includes("image"),
314
+ cost: row.cost as ModelCatalogEntry["cost"],
315
+ source: row.source,
316
+ ...(unfiled ? { limitsEstimated: true } : {}),
317
+ // Three-valued on the wire as it is in the column: only a row that claims
318
+ // `true` is pro-only. Absence means nobody established the tier, and the
319
+ // surface must render that as unknown rather than as free.
320
+ ...(row.proOnly === true ? { proOnly: true } : {}),
321
+ // Same three-valued rule, from the provider's own directory. Qoder states
322
+ // `is_free` per row, so a plan change moves the pill without a code change.
323
+ ...(row.freeTier === true ? { freeTier: true } : {}),
324
+ sourceUpdatedAt: row.sourceUpdatedAt?.toISOString() ?? null,
325
+ } satisfies ModelCatalogEntry;
326
+ }