cartethyia 2.7.3 → 2.7.5

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 (625) hide show
  1. package/.env.example +2 -0
  2. package/README.md +17 -5
  3. package/dist/dashboard/assets/{BannedPage-2TJD37Hb.js → BannedPage-Cj15GsMp.js} +1 -1
  4. package/dist/dashboard/assets/BannedPage-Cj15GsMp.js.gz +0 -0
  5. package/dist/dashboard/assets/ClaimGiftsButton-D83pBJ6J.js +1 -0
  6. package/dist/dashboard/assets/ClaimGiftsButton-D83pBJ6J.js.gz +0 -0
  7. package/dist/dashboard/assets/{CliToolDetailPage-xefJNRmJ.js → CliToolDetailPage-DZKmVuJN.js} +1 -1
  8. package/dist/dashboard/assets/CliToolDetailPage-DZKmVuJN.js.gz +0 -0
  9. package/dist/dashboard/assets/{CliToolsPage-Rj9iHQ3F.js → CliToolsPage-BIccZmeL.js} +1 -1
  10. package/dist/dashboard/assets/CliToolsPage-BIccZmeL.js.gz +0 -0
  11. package/dist/dashboard/assets/CombosPage-DEEKkYD-.js +1 -0
  12. package/dist/dashboard/assets/CombosPage-DEEKkYD-.js.gz +0 -0
  13. package/dist/dashboard/assets/ConsoleLogPage-D5rol2j2.js +1 -0
  14. package/dist/dashboard/assets/ConsoleLogPage-D5rol2j2.js.gz +0 -0
  15. package/dist/dashboard/assets/CustomizationPage-BKj_r0rp.js +1 -0
  16. package/dist/dashboard/assets/CustomizationPage-BKj_r0rp.js.gz +0 -0
  17. package/dist/dashboard/assets/InjectSystemPromptPanel-B5nZvmf0.js +656 -0
  18. package/dist/dashboard/assets/InjectSystemPromptPanel-B5nZvmf0.js.gz +0 -0
  19. package/dist/dashboard/assets/{LoginPage-C4w7vea-.js → LoginPage-DkXb6dx-.js} +1 -1
  20. package/dist/dashboard/assets/LoginPage-DkXb6dx-.js.gz +0 -0
  21. package/dist/dashboard/assets/{ModelPicker-BtPcAfot.js → ModelPicker-DnQPSIA8.js} +1 -1
  22. package/dist/dashboard/assets/ModelPicker-DnQPSIA8.js.gz +0 -0
  23. package/dist/dashboard/assets/OverviewPage-Ci2wQKUz.js +1 -0
  24. package/dist/dashboard/assets/OverviewPage-Ci2wQKUz.js.gz +0 -0
  25. package/dist/dashboard/assets/ProviderDetailPage-odlgYrOg.js +5 -0
  26. package/dist/dashboard/assets/ProviderDetailPage-odlgYrOg.js.gz +0 -0
  27. package/dist/dashboard/assets/ProviderIcon-EfeloBn4.js +1 -0
  28. package/dist/dashboard/assets/ProviderIcon-EfeloBn4.js.gz +0 -0
  29. package/dist/dashboard/assets/ProvidersPage-CqFBQEB8.js +1 -0
  30. package/dist/dashboard/assets/ProvidersPage-CqFBQEB8.js.gz +0 -0
  31. package/dist/dashboard/assets/ProxyPage-5SnUSy3j.js +9 -0
  32. package/dist/dashboard/assets/ProxyPage-5SnUSy3j.js.gz +0 -0
  33. package/dist/dashboard/assets/QuotaPage-CmnMDzhP.js +1 -0
  34. package/dist/dashboard/assets/QuotaPage-CmnMDzhP.js.gz +0 -0
  35. package/dist/dashboard/assets/SettingsPage-BJtRZ4qD.js +1 -0
  36. package/dist/dashboard/assets/SettingsPage-BJtRZ4qD.js.gz +0 -0
  37. package/dist/dashboard/assets/{SetupPage-BQS0Rx_u.js → SetupPage-WTa0sktC.js} +1 -1
  38. package/dist/dashboard/assets/SetupPage-WTa0sktC.js.gz +0 -0
  39. package/dist/dashboard/assets/{StudioPage-TP7voAQ0.js → StudioPage-BfAb5-o_.js} +3 -3
  40. package/dist/dashboard/assets/StudioPage-BfAb5-o_.js.gz +0 -0
  41. package/dist/dashboard/assets/UsagePage-BNzeZ2Vk.js +56 -0
  42. package/dist/dashboard/assets/UsagePage-BNzeZ2Vk.js.gz +0 -0
  43. package/dist/dashboard/assets/api-keys-l29JROPv.js +1 -0
  44. package/dist/dashboard/assets/api-keys-l29JROPv.js.gz +0 -0
  45. package/dist/dashboard/assets/app--PJWq8_C.css +2 -0
  46. package/dist/dashboard/assets/app--PJWq8_C.css.gz +0 -0
  47. package/dist/dashboard/assets/{assert-C0YYuZJ4.js → assert-C0K-c4eo.js} +1 -1
  48. package/dist/dashboard/assets/assert-C0K-c4eo.js.gz +0 -0
  49. package/dist/dashboard/assets/{badge-B_DMpMJN.js → badge-DfbhQU4k.js} +1 -1
  50. package/dist/dashboard/assets/badge-DfbhQU4k.js.gz +0 -0
  51. package/dist/dashboard/assets/{banner-DMrMPmth.js → banner-BllT4LcF.js} +1 -1
  52. package/dist/dashboard/assets/banner-BllT4LcF.js.gz +0 -0
  53. package/dist/dashboard/assets/brand/login_register.jpg +0 -0
  54. package/dist/dashboard/assets/brand/login_register_initial.jpg +0 -0
  55. package/dist/dashboard/assets/brand/og_bansos.jpg +0 -0
  56. package/dist/dashboard/assets/brand/og_image.jpg +0 -0
  57. package/dist/dashboard/assets/{card-Be5Uzamx.js → card-CtkX66Wd.js} +1 -1
  58. package/dist/dashboard/assets/card-CtkX66Wd.js.gz +0 -0
  59. package/dist/dashboard/assets/chevron-left-Bf6CaTSJ.js +1 -0
  60. package/dist/dashboard/assets/chevron-left-Bf6CaTSJ.js.gz +0 -0
  61. package/dist/dashboard/assets/{cli-tools-muFRphDu.js → cli-tools-CQXVMn4P.js} +1 -1
  62. package/dist/dashboard/assets/cli-tools-CQXVMn4P.js.gz +0 -0
  63. package/dist/dashboard/assets/{clipboard-button-DOfrVmsi.js → clipboard-button-BQqBQd2n.js} +1 -1
  64. package/dist/dashboard/assets/clipboard-button-BQqBQd2n.js.gz +0 -0
  65. package/dist/dashboard/assets/{dialog-C9omdjQt.js → dialog-BEK7KP0H.js} +1 -1
  66. package/dist/dashboard/assets/dialog-BEK7KP0H.js.gz +0 -0
  67. package/dist/dashboard/assets/{drawer-Bm8WKaGY.js → drawer-BrTEZ8zu.js} +1 -1
  68. package/dist/dashboard/assets/drawer-BrTEZ8zu.js.gz +0 -0
  69. package/dist/dashboard/assets/entry-CHWxbS56.js +1 -0
  70. package/dist/dashboard/assets/entry-CHWxbS56.js.gz +0 -0
  71. package/dist/dashboard/assets/{entry-q5HwZ8nN.js → entry-EBHJ3DrE.js} +1 -1
  72. package/dist/dashboard/assets/entry-EBHJ3DrE.js.gz +0 -0
  73. package/dist/dashboard/assets/entry-LCS85hiq.js +4 -0
  74. package/dist/dashboard/assets/entry-LCS85hiq.js.gz +0 -0
  75. package/dist/dashboard/assets/{formatters-BTScrQpe.js → formatters-BAACA727.js} +1 -1
  76. package/dist/dashboard/assets/formatters-BAACA727.js.gz +0 -0
  77. package/dist/dashboard/assets/{github-badge-Orjuzemr.js → github-badge-YSCE4UC2.js} +1 -1
  78. package/dist/dashboard/assets/github-badge-YSCE4UC2.js.gz +0 -0
  79. package/dist/dashboard/assets/{index-cPrX5vn6.js → index-FQlj0HUT.js} +2 -2
  80. package/dist/dashboard/assets/index-FQlj0HUT.js.gz +0 -0
  81. package/dist/dashboard/assets/{inline-Dvux8w-0.js → inline-C4gU5iOL.js} +1 -1
  82. package/dist/dashboard/assets/inline-C4gU5iOL.js.gz +0 -0
  83. package/dist/dashboard/assets/{input-CU1Rgf3p.js → input-BHHX_Tmm.js} +1 -1
  84. package/dist/dashboard/assets/input-BHHX_Tmm.js.gz +0 -0
  85. package/dist/dashboard/assets/{layout-C_7A6H55.js → layout-CajrYUnD.js} +1 -1
  86. package/dist/dashboard/assets/{layout-C_7A6H55.js.gz → layout-CajrYUnD.js.gz} +0 -0
  87. package/dist/dashboard/assets/{list-row-CzFvogbj.js → list-row-Bx2I7XUe.js} +1 -1
  88. package/dist/dashboard/assets/list-row-Bx2I7XUe.js.gz +0 -0
  89. package/dist/dashboard/assets/live-DNNqRbj2.js +1 -0
  90. package/dist/dashboard/assets/live-DNNqRbj2.js.gz +0 -0
  91. package/dist/dashboard/assets/minimize-2-BTmMTK4o.js +1 -0
  92. package/dist/dashboard/assets/minimize-2-BTmMTK4o.js.gz +0 -0
  93. package/dist/dashboard/assets/{mutation-JJGQm8TD.js → mutation-Rr0TIvcj.js} +1 -1
  94. package/dist/dashboard/assets/mutation-Rr0TIvcj.js.gz +0 -0
  95. package/dist/dashboard/assets/network-C5UBACrJ.js +1 -0
  96. package/dist/dashboard/assets/network-C5UBACrJ.js.gz +0 -0
  97. package/dist/dashboard/assets/numbers-CUWAZK4J.js +2 -0
  98. package/dist/dashboard/assets/numbers-CUWAZK4J.js.gz +0 -0
  99. package/dist/dashboard/assets/pagination-Xsi9Qx04.js +1 -0
  100. package/dist/dashboard/assets/pagination-Xsi9Qx04.js.gz +0 -0
  101. package/dist/dashboard/assets/{query-keys-CLaNz8Xz.js → query-keys-BloKtaXJ.js} +1 -1
  102. package/dist/dashboard/assets/query-keys-BloKtaXJ.js.gz +0 -0
  103. package/dist/dashboard/assets/quota-DvQbD_sr.js +1 -0
  104. package/dist/dashboard/assets/quota-DvQbD_sr.js.gz +0 -0
  105. package/dist/dashboard/assets/{routing-BEH32QOC.js → routing-in0f_IfK.js} +1 -1
  106. package/dist/dashboard/assets/routing-in0f_IfK.js.gz +0 -0
  107. package/dist/dashboard/assets/{select-B3iTetHN.js → select-BLULNqTp.js} +2 -2
  108. package/dist/dashboard/assets/select-BLULNqTp.js.gz +0 -0
  109. package/dist/dashboard/assets/settings-CcGczj6I.js +1 -0
  110. package/dist/dashboard/assets/settings-CcGczj6I.js.gz +0 -0
  111. package/dist/dashboard/assets/shield-BMQNxORh.js +1 -0
  112. package/dist/dashboard/assets/shield-BMQNxORh.js.gz +0 -0
  113. package/dist/dashboard/assets/{stack-KrOrm7JZ.js → stack-D2lIR0tg.js} +1 -1
  114. package/dist/dashboard/assets/stack-D2lIR0tg.js.gz +0 -0
  115. package/dist/dashboard/assets/{state-DLS7IDMy.js → state-DTARjfiW.js} +1 -1
  116. package/dist/dashboard/assets/state-DTARjfiW.js.gz +0 -0
  117. package/dist/dashboard/assets/{switch-42ItvbAs.js → switch-c0oPSnqW.js} +1 -1
  118. package/dist/dashboard/assets/switch-c0oPSnqW.js.gz +0 -0
  119. package/dist/dashboard/assets/{useMutation-Dmnfh1Ue.js → useMutation-m8WGNPrz.js} +1 -1
  120. package/dist/dashboard/assets/useMutation-m8WGNPrz.js.gz +0 -0
  121. package/dist/dashboard/console-backgrounds.jpg +0 -0
  122. package/dist/dashboard/console-backgrounds_initial.jpg +0 -0
  123. package/dist/dashboard/index.html +2 -2
  124. package/dist/dashboard/index.html.gz +0 -0
  125. package/dist/dashboard/precompressed-assets.json +59 -57
  126. package/migrations/full-postgres/0027_qoder_identity_genuine_seed.sql +9 -10
  127. package/migrations/full-postgres/0032_add_provider_pool_accounts.sql +53 -0
  128. package/migrations/full-postgres/0033_add_api_key_ban.sql +9 -0
  129. package/migrations/full-postgres/0034_add_api_key_ban_until.sql +7 -0
  130. package/migrations/full-postgres/0035_align_pool_account_status_enum.sql +21 -0
  131. package/migrations/full-postgres/0036_system_prompt_presets_refs.sql +10 -0
  132. package/migrations/full-postgres/0037_share_concurrency_scope.sql +3 -0
  133. package/migrations/full-postgres/0038_add_accounts_quota_snapshot.sql +7 -0
  134. package/migrations/full-postgres/0039_add_telemetry_provider_model_indexes.sql +13 -0
  135. package/migrations/full-postgres/0040_add_models_dialect.sql +3 -0
  136. package/migrations/full-postgres/0041_add_daily_checkin_ledger_account_fk.sql +7 -0
  137. package/migrations/lite-sqlite3/0027_qoder_identity_genuine_seed.sql +9 -10
  138. package/migrations/lite-sqlite3/0032_add_provider_pool_accounts.sql +44 -0
  139. package/migrations/lite-sqlite3/0033_add_api_key_ban.sql +8 -0
  140. package/migrations/lite-sqlite3/0034_add_api_key_ban_until.sql +5 -0
  141. package/migrations/lite-sqlite3/0035_align_pool_account_status_enum.sql +6 -0
  142. package/migrations/lite-sqlite3/0036_system_prompt_presets_refs.sql +14 -0
  143. package/migrations/lite-sqlite3/0037_share_concurrency_scope.sql +3 -0
  144. package/migrations/lite-sqlite3/0038_add_accounts_quota_snapshot.sql +4 -0
  145. package/migrations/lite-sqlite3/0039_add_telemetry_provider_model_indexes.sql +6 -0
  146. package/migrations/lite-sqlite3/0040_add_models_dialect.sql +3 -0
  147. package/migrations/lite-sqlite3/0041_add_daily_checkin_ledger_account_fk.sql +26 -0
  148. package/package.json +4 -5
  149. package/scripts/cli/update.ts +15 -41
  150. package/src/boot/app.ts +67 -10
  151. package/src/boot/dependencies.ts +786 -732
  152. package/src/boot/lifecycle.ts +11 -12
  153. package/src/config/cooldowns.ts +34 -55
  154. package/src/config/network.ts +8 -4
  155. package/src/config/package-paths.ts +1 -1
  156. package/src/config/providers.ts +13 -14
  157. package/src/config/resources.ts +0 -5
  158. package/src/config/runtime.ts +2 -5
  159. package/src/config/spec.ts +26 -6
  160. package/src/console/api-keys/ban-constants.ts +38 -0
  161. package/src/console/api-keys/contracts.ts +92 -31
  162. package/src/console/api-keys/routes.ts +131 -6
  163. package/src/console/audit/routes.ts +3 -3
  164. package/src/console/auth/access.ts +4 -4
  165. package/src/console/auth/account-reset.ts +5 -5
  166. package/src/console/auth/audit-recorder.ts +1 -1
  167. package/src/console/auth/lockout.ts +7 -3
  168. package/src/console/auth/row-coercion.ts +57 -12
  169. package/src/console/auth/session-resolver.ts +1 -1
  170. package/src/console/auth/session-service.ts +8 -2
  171. package/src/console/auth/session.ts +40 -34
  172. package/src/console/auth/setup-service.ts +4 -3
  173. package/src/console/auth/user-store.ts +1 -1
  174. package/src/console/backup/9router-map.ts +0 -3
  175. package/src/console/backup/contracts.ts +11 -9
  176. package/src/console/backup/export-endpoint.ts +11 -12
  177. package/src/console/backup/nine-router.ts +1 -7
  178. package/src/console/backup/routes.ts +32 -36
  179. package/src/console/backup/service.ts +468 -468
  180. package/src/console/backup/store.ts +47 -29
  181. package/src/console/backup/validate.ts +11 -1
  182. package/src/console/cli-tools/contracts.ts +7 -7
  183. package/src/console/cli-tools/injectors/cline.ts +110 -110
  184. package/src/console/cli-tools/injectors/codex.ts +181 -181
  185. package/src/console/cli-tools/injectors/copilot.ts +108 -108
  186. package/src/console/cli-tools/injectors/driver.ts +247 -247
  187. package/src/console/cli-tools/injectors/jcode.ts +115 -115
  188. package/src/console/cli-tools/injectors/openclaw.ts +161 -161
  189. package/src/console/cli-tools/injectors/opencode.ts +153 -153
  190. package/src/console/cli-tools/routes.ts +4 -4
  191. package/src/console/cli-tools/service.ts +1 -1
  192. package/src/console/cli-tools/store.ts +2 -2
  193. package/src/console/console-router-guards.ts +2 -2
  194. package/src/console/console-router.ts +36 -4
  195. package/src/console/dashboard-assets.ts +9 -9
  196. package/src/console/data-management/routes.ts +3 -3
  197. package/src/console/domain-registration.ts +18 -4
  198. package/src/console/inferhub/client.ts +0 -7
  199. package/src/console/inferhub/key-store.ts +1 -1
  200. package/src/console/inferhub/normalize.ts +2 -2
  201. package/src/console/inferhub/routes.ts +1 -1
  202. package/src/console/inferhub/types.ts +1 -1
  203. package/src/console/observability/contracts.ts +5 -1
  204. package/src/console/observability/live.ts +34 -21
  205. package/src/console/observability/logs.ts +4 -4
  206. package/src/console/observability/performance.ts +3 -3
  207. package/src/console/observability/routes.ts +3 -3
  208. package/src/console/observability/sse.ts +62 -8
  209. package/src/console/observability/store.ts +36 -20
  210. package/src/console/providers/catalog/catalog-projections.ts +1 -1
  211. package/src/console/providers/catalog/contracts.ts +82 -22
  212. package/src/console/providers/catalog/model-operations.ts +1 -1
  213. package/src/console/providers/catalog/provider-operations.ts +110 -17
  214. package/src/console/providers/catalog/public-model-store.ts +0 -0
  215. package/src/console/providers/catalog/routes.ts +30 -6
  216. package/src/console/providers/catalog/store-helpers.ts +349 -326
  217. package/src/console/providers/catalog/store.ts +1662 -1292
  218. package/src/console/providers/detail/routes.ts +4 -4
  219. package/src/console/providers/oauth/callback-listener.ts +100 -18
  220. package/src/console/providers/oauth/routes.ts +95 -16
  221. package/src/console/quota/account-quota-global.ts +41 -3
  222. package/src/console/quota/account-quota-shared.ts +10 -3
  223. package/src/console/quota/account-quota-tenant.ts +285 -67
  224. package/src/console/quota/account-test-egress.ts +4 -4
  225. package/src/console/quota/daily-growth-job.ts +308 -0
  226. package/src/console/quota/native-identity.ts +2 -2
  227. package/src/console/quota/quota-view.ts +1 -1
  228. package/src/console/remote-support/routes.ts +1 -1
  229. package/src/console/remote-support/service.ts +2 -2
  230. package/src/console/routing/model/contracts.ts +1 -1
  231. package/src/console/routing/model/routes.ts +3 -3
  232. package/src/console/routing/pools/contracts.ts +13 -10
  233. package/src/console/routing/pools/import-parse.ts +3 -3
  234. package/src/console/routing/pools/operations.ts +14 -7
  235. package/src/console/routing/pools/probe-verdict.ts +8 -3
  236. package/src/console/routing/pools/relay-deploy.ts +12 -12
  237. package/src/console/routing/pools/routes.ts +3 -3
  238. package/src/console/routing/pools/store.ts +21 -1
  239. package/src/console/settings/contracts.ts +29 -2
  240. package/src/console/settings/routes.ts +3 -3
  241. package/src/console/settings/store.ts +20 -0
  242. package/src/console/share/share-router.ts +112 -44
  243. package/src/console/share/share-stats.ts +109 -39
  244. package/src/console/share/share-usage.ts +1 -1
  245. package/src/console/shared/errors.ts +2 -2
  246. package/src/console/shared/object.ts +18 -0
  247. package/src/console/shared/read-through-cache.ts +42 -6
  248. package/src/console/shared/transaction.ts +14 -0
  249. package/src/console/system-prompt/contracts.ts +89 -12
  250. package/src/console/system-prompt/routes.ts +98 -45
  251. package/src/console/updates/apply-update.ts +244 -0
  252. package/src/console/updates/routes.ts +256 -98
  253. package/src/main.ts +11 -5
  254. package/src/network/http2-bidi.ts +1 -1
  255. package/src/network/ip-utils.ts +298 -0
  256. package/src/network/outbound-fetch.ts +76 -5
  257. package/src/network/pool/agent.ts +7 -6
  258. package/src/network/pool/byte-accounting.ts +1 -1
  259. package/src/network/pool/resolver.ts +55 -51
  260. package/src/network/pool/selector.ts +150 -136
  261. package/src/network/pool-cooldown-store.ts +71 -16
  262. package/src/network/pool-health-machine.ts +22 -2
  263. package/src/network/rate-limit-verdict.ts +68 -0
  264. package/src/network/retry.ts +1 -1
  265. package/src/network/ssrf.ts +24 -75
  266. package/src/observability/event-log.ts +2 -2
  267. package/src/observability/event-query.ts +286 -214
  268. package/src/observability/log-sanitize.ts +1 -1
  269. package/src/observability/metrics.ts +4 -4
  270. package/src/observability/payload-capture.ts +6 -5
  271. package/src/observability/payload-store.ts +1 -1
  272. package/src/observability/runtime-metrics.ts +1 -1
  273. package/src/observability/telemetry-buffer.ts +12 -3
  274. package/src/observability/telemetry-reconciliation.ts +3 -3
  275. package/src/observability/telemetry-status.ts +1 -1
  276. package/src/observability/token-speed.ts +1 -1
  277. package/src/persistence/connection-url.ts +1 -1
  278. package/src/persistence/db-handle.ts +1 -1
  279. package/src/persistence/db-pg.ts +2 -2
  280. package/src/persistence/db-sqlite.ts +10 -5
  281. package/src/persistence/dialect.ts +5 -1
  282. package/src/persistence/readiness.ts +19 -7
  283. package/src/persistence/redis.ts +1 -1
  284. package/src/persistence/schema.ts +177 -45
  285. package/src/persistence/shared-cache.ts +2 -2
  286. package/src/persistence/sort-order.ts +1 -1
  287. package/src/persistence/stores/api-key-store.ts +109 -4
  288. package/src/persistence/stores/share-store.ts +20 -8
  289. package/src/persistence/stores/telemetry-store.ts +1 -1
  290. package/src/persistence/system-prompt-store.ts +63 -18
  291. package/src/persistence/tenant-preferences.ts +1 -1
  292. package/src/persistence/ttl-cache.ts +3 -3
  293. package/src/persistence/usage-flush.ts +7 -2
  294. package/src/protocol/canonical-model.ts +1 -1
  295. package/src/protocol/codecs.ts +12 -16
  296. package/src/protocol/dsml.ts +3 -49
  297. package/src/protocol/gateway-error.ts +6 -3
  298. package/src/protocol/image-clamp.ts +2 -2
  299. package/src/protocol/image-generation.ts +2 -2
  300. package/src/protocol/reasoning-text.ts +1 -1
  301. package/src/protocol/reasoning.ts +7 -52
  302. package/src/protocol/registry.ts +50 -15
  303. package/src/protocol/request/chat.ts +46 -19
  304. package/src/protocol/request/codex.ts +1 -1
  305. package/src/protocol/request/messages.ts +46 -14
  306. package/src/protocol/request/responses.ts +19 -10
  307. package/src/protocol/response/chat.ts +27 -12
  308. package/src/protocol/response/codex.ts +2 -2
  309. package/src/protocol/response/gemini.ts +1 -1
  310. package/src/protocol/response/messages.ts +6 -6
  311. package/src/protocol/response/omp-tool-call-markup.ts +1 -1
  312. package/src/protocol/response/responses.ts +3 -3
  313. package/src/protocol/sse-decoder.ts +64 -4
  314. package/src/protocol/stop-reason.ts +2 -2
  315. package/src/protocol/think-split.ts +347 -0
  316. package/src/protocol/translation/attachment-integrity.ts +3 -3
  317. package/src/protocol/translation/cache-controls.ts +8 -20
  318. package/src/protocol/translation/capabilities.ts +8 -8
  319. package/src/protocol/translation/dialect/deepseek.ts +10 -0
  320. package/src/protocol/translation/dialect/inband.ts +39 -0
  321. package/src/protocol/translation/dialect/model-thinking.ts +89 -0
  322. package/src/protocol/translation/dialect/qwen.ts +114 -0
  323. package/src/protocol/translation/dialect/resolver.ts +83 -0
  324. package/src/protocol/translation/dialect/rules.ts +93 -0
  325. package/src/protocol/translation/dialect/thinking-ladder.ts +245 -0
  326. package/src/protocol/translation/dialect/types.ts +58 -0
  327. package/src/protocol/translation/quirks.ts +18 -4
  328. package/src/protocol/translation/thinking.ts +12 -252
  329. package/src/protocol/translation/tool-id.ts +3 -3
  330. package/src/protocol/translation/tool-repair.ts +1 -1
  331. package/src/protocol/upstream/post-json.ts +6 -1
  332. package/src/protocol/upstream/send-claude-messages.ts +3 -3
  333. package/src/providers/adapters/base-adapter.ts +1 -1
  334. package/src/providers/adapters/compatible-adapter.ts +44 -5
  335. package/src/providers/authentication/jwt-validator.ts +0 -5
  336. package/src/providers/authentication/oauth-client.ts +254 -253
  337. package/src/providers/authentication/oauth-flow-store.ts +42 -7
  338. package/src/providers/authentication/oauth-refresh-service.ts +2 -2
  339. package/src/providers/bundled-discovery-providers.ts +6 -30
  340. package/src/providers/configured-openai-providers.ts +82 -97
  341. package/src/providers/configured-provider.ts +1 -1
  342. package/src/providers/default-registry.ts +23 -35
  343. package/src/providers/discovery/catalog/catalog-format.ts +13 -2
  344. package/src/providers/discovery/discovery-types.ts +0 -1
  345. package/src/providers/discovery/openai-model-discovery.ts +2 -2
  346. package/src/providers/discovery/probe-phases.ts +50 -15
  347. package/src/providers/discovery/probing-service.ts +18 -20
  348. package/src/providers/integrations/agentrouter/agentrouter.ts +14 -5
  349. package/src/providers/integrations/alibabacp/alibabacp.ts +2 -2
  350. package/src/providers/integrations/anthropic.ts +4 -11
  351. package/src/providers/integrations/antigravity/antigravity-oauth.ts +0 -1
  352. package/src/providers/integrations/antigravity/antigravity-protocol.ts +0 -14
  353. package/src/providers/integrations/antigravity/antigravity.ts +15 -26
  354. package/src/providers/integrations/buddy/buddy-catalog-shared.ts +2 -2
  355. package/src/providers/integrations/buddy/buddy-chat-shared.ts +20 -4
  356. package/src/providers/integrations/buddy/buddy-cli-headers.ts +19 -52
  357. package/src/providers/integrations/buddy/buddy-discovery-shared.ts +37 -6
  358. package/src/providers/integrations/buddy/buddy-identity.ts +108 -1
  359. package/src/providers/integrations/buddy/buddy-media.ts +16 -16
  360. package/src/providers/integrations/buddy/buddy-oauth-shared.ts +1 -1
  361. package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +1 -2
  362. package/src/providers/integrations/buddy/codebuddy/provider.ts +5 -16
  363. package/src/providers/integrations/buddy/codebuddy/shared.ts +30 -9
  364. package/src/providers/integrations/buddy/workbuddy/provider.ts +6 -10
  365. package/src/providers/integrations/buddy/workbuddy/shared.ts +21 -3
  366. package/src/providers/integrations/claude/claude-betas.ts +1 -1
  367. package/src/providers/integrations/claude/claude-cch.ts +148 -148
  368. package/src/providers/integrations/claude/claude-credentials.ts +1 -1
  369. package/src/providers/integrations/claude/claude-slow-lane-retry.ts +1 -1
  370. package/src/providers/integrations/claude/claude.ts +3 -10
  371. package/src/providers/integrations/claude/fallback-credit.ts +3 -7
  372. package/src/providers/integrations/cline/cline.ts +2 -2
  373. package/src/providers/integrations/cloudflareai/cloudflareai.ts +4 -7
  374. package/src/providers/integrations/codex/codex-device-code.ts +101 -101
  375. package/src/providers/integrations/codex/codex-errors.ts +1 -1
  376. package/src/providers/integrations/codex/codex-oauth.ts +0 -1
  377. package/src/providers/integrations/codex/codex.ts +14 -15
  378. package/src/providers/integrations/commandcode/commandcode.ts +76 -116
  379. package/src/providers/integrations/commandcode/quota.ts +132 -48
  380. package/src/providers/integrations/cursor/cursor-http2.ts +11 -14
  381. package/src/providers/integrations/cursor/cursor-quota.ts +2 -6
  382. package/src/providers/integrations/cursor/cursor-request.ts +0 -87
  383. package/src/providers/integrations/cursor/cursor-shared.ts +0 -4
  384. package/src/providers/integrations/cursor/cursor.ts +53 -20
  385. package/src/providers/integrations/cursor/protocol/framing.ts +0 -56
  386. package/src/providers/integrations/cursor/protocol/messages.ts +0 -96
  387. package/src/providers/integrations/factory/factory.ts +13 -13
  388. package/src/providers/integrations/gemini.ts +11 -11
  389. package/src/providers/integrations/github/github-discovery.ts +55 -10
  390. package/src/providers/integrations/github/github-oauth.ts +6 -5
  391. package/src/providers/integrations/github/github.ts +47 -20
  392. package/src/providers/integrations/grok/grok-oauth.ts +261 -261
  393. package/src/providers/integrations/grok/grok-quota-frame.ts +1 -1
  394. package/src/providers/integrations/grok/grok-quota.ts +16 -7
  395. package/src/providers/integrations/grok/grok-ratelimit-store.ts +0 -5
  396. package/src/providers/integrations/grok/grok-ratelimit.ts +3 -0
  397. package/src/providers/integrations/grok/grok.ts +88 -42
  398. package/src/providers/integrations/inferhub.ts +4 -11
  399. package/src/providers/integrations/kilo/kilo-oauth.ts +1 -1
  400. package/src/providers/integrations/kimi/kimi.ts +4 -11
  401. package/src/providers/integrations/kiro/kiro-cache.ts +31 -0
  402. package/src/providers/integrations/kiro/kiro-catalog.ts +31 -5
  403. package/src/providers/integrations/kiro/kiro-discovery.ts +47 -43
  404. package/src/providers/integrations/kiro/kiro-machine-id.ts +6 -3
  405. package/src/providers/integrations/kiro/kiro-oauth.ts +523 -255
  406. package/src/providers/integrations/kiro/kiro-quota.ts +35 -47
  407. package/src/providers/integrations/kiro/kiro-request.ts +8 -11
  408. package/src/providers/integrations/kiro/kiro-stream.ts +13 -0
  409. package/src/providers/integrations/kiro/kiro.ts +91 -78
  410. package/src/providers/integrations/muse/muse-oauth.ts +0 -1
  411. package/src/providers/integrations/muse/muse-quota.ts +0 -4
  412. package/src/providers/integrations/muse/muse.ts +6 -9
  413. package/src/providers/integrations/opencode/fingerprint.ts +7 -5
  414. package/src/providers/integrations/opencode/provider.ts +17 -24
  415. package/src/providers/integrations/perplexity.ts +2 -2
  416. package/src/providers/integrations/qoder/qoder-catalog.ts +132 -17
  417. package/src/providers/integrations/qoder/qoder-checkin.ts +26 -8
  418. package/src/providers/integrations/qoder/qoder-native-identity-store.ts +9 -8
  419. package/src/providers/integrations/qoder/qoder-oauth.ts +165 -25
  420. package/src/providers/integrations/qoder/qoder-quota.ts +48 -6
  421. package/src/providers/integrations/qoder/qoder-realm.ts +22 -28
  422. package/src/providers/integrations/qoder/qoder-sse.ts +159 -0
  423. package/src/providers/integrations/qoder/qoder-umid.ts +33 -38
  424. package/src/providers/integrations/qoder/qoder-virtual.ts +14 -2
  425. package/src/providers/integrations/qoder/qoder.ts +521 -63
  426. package/src/providers/integrations/shared/connect.ts +0 -54
  427. package/src/providers/integrations/shared/hash.ts +15 -0
  428. package/src/providers/integrations/shared/session-cache.ts +136 -0
  429. package/src/providers/integrations/tokenharbor.ts +2 -2
  430. package/src/providers/integrations/trae/trae-cache.ts +7 -87
  431. package/src/providers/integrations/trae/trae-catalog.ts +1 -6
  432. package/src/providers/integrations/trae/trae-protocol.ts +17 -8
  433. package/src/providers/integrations/trae/trae-realm.ts +0 -7
  434. package/src/providers/integrations/trae/trae.ts +59 -19
  435. package/src/providers/integrations/xai/xai-oauth.ts +5 -5
  436. package/src/providers/integrations/zcode/oauth.ts +1 -1
  437. package/src/providers/integrations/zcode-start-plan/gifts.ts +265 -0
  438. package/src/providers/integrations/zcode-start-plan/oauth.ts +154 -0
  439. package/src/providers/integrations/zcode-start-plan/provider.ts +131 -0
  440. package/src/providers/integrations/zcode-start-plan/quota.ts +152 -0
  441. package/src/providers/integrations/zcode-start-plan/zcode_prompt.json +10 -0
  442. package/src/providers/operations/account-error-classification.ts +131 -116
  443. package/src/providers/operations/account-health-service.ts +31 -39
  444. package/src/providers/operations/account-reset-service.ts +58 -1
  445. package/src/providers/operations/cli-platform.ts +31 -5
  446. package/src/providers/operations/client-version-resolver.ts +4 -6
  447. package/src/providers/operations/client-versions.ts +10 -7
  448. package/src/providers/operations/provider-catalog-seeder.ts +2 -0
  449. package/src/providers/operations/provider-catalog-service.ts +22 -6
  450. package/src/providers/operations/provider-credential-service.ts +35 -6
  451. package/src/providers/operations/upstream-deadline.ts +3 -3
  452. package/src/providers/provider-contracts.ts +65 -10
  453. package/src/providers/provider-credential-resolver.ts +1 -1
  454. package/src/providers/provider-metadata.ts +17 -2
  455. package/src/providers/provider-model-definition.ts +22 -1
  456. package/src/providers/provider-probe.ts +4 -2
  457. package/src/providers/provider-registry.ts +4 -4
  458. package/src/providers/provider-usage.ts +81 -16
  459. package/src/providers/quota/cache.ts +29 -0
  460. package/src/providers/quota/quota-contracts.ts +28 -10
  461. package/src/providers/quota/refresh.ts +29 -6
  462. package/src/providers/quota/snapshot.ts +94 -0
  463. package/src/providers/search/search-provider.ts +6 -8
  464. package/src/providers/search/search-providers.ts +2 -1
  465. package/src/security/abuse.ts +8 -8
  466. package/src/security/admission/reasons.ts +17 -17
  467. package/src/security/admission/service.ts +2 -2
  468. package/src/security/admission/ttl.ts +61 -61
  469. package/src/security/api-key-auth.ts +20 -8
  470. package/src/security/client-router-fingerprint.ts +4 -4
  471. package/src/security/credential-codec.ts +3 -3
  472. package/src/security/csrf.ts +2 -2
  473. package/src/security/ip-boundary.ts +16 -189
  474. package/src/security/model-access-rule.ts +1 -1
  475. package/src/security/outbound-headers.ts +18 -0
  476. package/src/shared/coerce.ts +19 -0
  477. package/src/shared/invoke-guard.ts +47 -0
  478. package/src/transport/contracts.ts +13 -4
  479. package/src/transport/dispatch/attempt-finalize.ts +5 -4
  480. package/src/transport/dispatch/attempt-loop.ts +10 -7
  481. package/src/transport/dispatch/capture-policy.ts +34 -37
  482. package/src/transport/dispatch/empty-stream.ts +28 -25
  483. package/src/transport/dispatch/generation.ts +33 -53
  484. package/src/transport/dispatch/leases.ts +8 -8
  485. package/src/transport/dispatch/native-shared.ts +127 -0
  486. package/src/transport/dispatch/proxy-request.ts +10 -8
  487. package/src/transport/dispatch/responses-compact.ts +35 -56
  488. package/src/transport/dispatch/retry-policy.ts +4 -67
  489. package/src/transport/dispatch/stream-bridge.ts +1 -1
  490. package/src/transport/dispatch/streaming/encoder.ts +18 -72
  491. package/src/transport/dispatch/streaming/finalize.ts +11 -10
  492. package/src/transport/dispatch/streaming/stream.ts +7 -3
  493. package/src/transport/dispatch/streaming/thinking-loop.ts +1 -1
  494. package/src/transport/dispatch/systemone.ts +27 -55
  495. package/src/transport/dispatch/token-count.ts +49 -46
  496. package/src/transport/dispatch/websearch.ts +24 -50
  497. package/src/transport/drain-endpoint.ts +3 -3
  498. package/src/transport/failure-policy.ts +19 -6
  499. package/src/transport/guards/auth-guard.ts +6 -11
  500. package/src/transport/guards/body-policy.ts +7 -7
  501. package/src/transport/guards/error-lifecycle.ts +18 -3
  502. package/src/transport/guards/guard-errors.ts +7 -3
  503. package/src/transport/guards/headroom-guard.ts +2 -2
  504. package/src/transport/guards/pipeline.ts +7 -7
  505. package/src/transport/guards/readiness-guard.ts +16 -2
  506. package/src/transport/guards/request-context.ts +15 -20
  507. package/src/transport/request/preparer.ts +306 -27
  508. package/src/transport/request/rtk/compress-request.ts +3 -3
  509. package/src/transport/request/rtk/rtk-constants.ts +2 -2
  510. package/src/transport/request/rtk/rtk-filters.ts +8 -8
  511. package/src/transport/request/state.ts +5 -20
  512. package/src/transport/request/system-prompt-injection.ts +74 -11
  513. package/src/transport/request/tenant-preferences.ts +22 -0
  514. package/src/transport/routing/candidate-index.ts +2 -2
  515. package/src/transport/routing/round-robin.ts +1 -1
  516. package/src/transport/routing/route-catalog.ts +113 -19
  517. package/src/transport/routing/route-model.ts +131 -38
  518. package/src/transport/routing/router.ts +154 -39
  519. package/src/transport/shutdown-notice.ts +21 -9
  520. package/src/transport/surface/adapters.ts +3 -3
  521. package/src/transport/surface/chat/encode.ts +7 -7
  522. package/src/transport/surface/chat/parse.ts +7 -7
  523. package/src/transport/surface/completion.ts +3 -3
  524. package/src/transport/surface/content-parts.ts +1 -1
  525. package/src/transport/surface/dialects.ts +2 -2
  526. package/src/transport/surface/messages/encode.ts +1 -1
  527. package/src/transport/surface/messages/parse.ts +3 -3
  528. package/src/transport/surface/responses/adapter.ts +1 -1
  529. package/src/transport/surface/responses/errors.ts +0 -9
  530. package/src/transport/vision-handoff-cache.ts +58 -0
  531. package/src/transport/vision-handoff.ts +376 -0
  532. package/src/workers/account-identity-sweep.ts +2 -2
  533. package/src/workers/buddy-growth-tasks.ts +392 -346
  534. package/src/workers/daily-checkin.ts +102 -49
  535. package/src/workers/maintenance-lease.ts +2 -35
  536. package/src/workers/oauth-refresh-sweep.ts +8 -1
  537. package/src/workers/quota-refresh-sweep.ts +120 -82
  538. package/src/workers/scheduler.ts +67 -4
  539. package/src/workers/sweep-runner.ts +24 -5
  540. package/src/workers/zcode-gift-claim.ts +108 -0
  541. package/dist/dashboard/assets/AccountCooldown-Bjo92Qat.js +0 -1
  542. package/dist/dashboard/assets/AccountCooldown-Bjo92Qat.js.gz +0 -0
  543. package/dist/dashboard/assets/BannedPage-2TJD37Hb.js.gz +0 -0
  544. package/dist/dashboard/assets/CliToolDetailPage-xefJNRmJ.js.gz +0 -0
  545. package/dist/dashboard/assets/CliToolsPage-Rj9iHQ3F.js.gz +0 -0
  546. package/dist/dashboard/assets/CombosPage-JEbYJFxw.js +0 -1
  547. package/dist/dashboard/assets/CombosPage-JEbYJFxw.js.gz +0 -0
  548. package/dist/dashboard/assets/ConsoleLogPage-C3xx_inM.js +0 -1
  549. package/dist/dashboard/assets/ConsoleLogPage-C3xx_inM.js.gz +0 -0
  550. package/dist/dashboard/assets/CustomizationPage-Dw306gJU.js +0 -1
  551. package/dist/dashboard/assets/CustomizationPage-Dw306gJU.js.gz +0 -0
  552. package/dist/dashboard/assets/HealthEventsModal-jaBo1TWV.js +0 -1
  553. package/dist/dashboard/assets/HealthEventsModal-jaBo1TWV.js.gz +0 -0
  554. package/dist/dashboard/assets/LoginPage-C4w7vea-.js.gz +0 -0
  555. package/dist/dashboard/assets/ModelPicker-BtPcAfot.js.gz +0 -0
  556. package/dist/dashboard/assets/OverviewPage-CGC5gNs1.js +0 -1
  557. package/dist/dashboard/assets/OverviewPage-CGC5gNs1.js.gz +0 -0
  558. package/dist/dashboard/assets/ProviderDetailPage-DQcthNGt.js +0 -4
  559. package/dist/dashboard/assets/ProviderDetailPage-DQcthNGt.js.gz +0 -0
  560. package/dist/dashboard/assets/ProviderIcon-FB6yS4v1.js +0 -1
  561. package/dist/dashboard/assets/ProviderIcon-FB6yS4v1.js.gz +0 -0
  562. package/dist/dashboard/assets/ProvidersPage-0OsXRGXe.js +0 -1
  563. package/dist/dashboard/assets/ProvidersPage-0OsXRGXe.js.gz +0 -0
  564. package/dist/dashboard/assets/ProxyPage-DdPc8slb.js +0 -9
  565. package/dist/dashboard/assets/ProxyPage-DdPc8slb.js.gz +0 -0
  566. package/dist/dashboard/assets/QuotaPage-Ci5O4BnH.js +0 -1
  567. package/dist/dashboard/assets/QuotaPage-Ci5O4BnH.js.gz +0 -0
  568. package/dist/dashboard/assets/SettingsPage-CDZjTt87.js +0 -1
  569. package/dist/dashboard/assets/SettingsPage-CDZjTt87.js.gz +0 -0
  570. package/dist/dashboard/assets/SetupPage-BQS0Rx_u.js.gz +0 -0
  571. package/dist/dashboard/assets/SortableList-BAL5npBC.js +0 -1
  572. package/dist/dashboard/assets/SortableList-BAL5npBC.js.gz +0 -0
  573. package/dist/dashboard/assets/StudioPage-TP7voAQ0.js.gz +0 -0
  574. package/dist/dashboard/assets/UsagePage-Cj_fEzhg.js +0 -56
  575. package/dist/dashboard/assets/UsagePage-Cj_fEzhg.js.gz +0 -0
  576. package/dist/dashboard/assets/api-keys-MbI7e7rt.js +0 -1
  577. package/dist/dashboard/assets/api-keys-MbI7e7rt.js.gz +0 -0
  578. package/dist/dashboard/assets/app-B2vyPoII.css +0 -2
  579. package/dist/dashboard/assets/app-B2vyPoII.css.gz +0 -0
  580. package/dist/dashboard/assets/assert-C0YYuZJ4.js.gz +0 -0
  581. package/dist/dashboard/assets/badge-B_DMpMJN.js.gz +0 -0
  582. package/dist/dashboard/assets/banner-DMrMPmth.js.gz +0 -0
  583. package/dist/dashboard/assets/card-Be5Uzamx.js.gz +0 -0
  584. package/dist/dashboard/assets/cli-tools-muFRphDu.js.gz +0 -0
  585. package/dist/dashboard/assets/clipboard-button-DOfrVmsi.js.gz +0 -0
  586. package/dist/dashboard/assets/dialog-C9omdjQt.js.gz +0 -0
  587. package/dist/dashboard/assets/drawer-Bm8WKaGY.js.gz +0 -0
  588. package/dist/dashboard/assets/entry-DS90gUks.js +0 -1
  589. package/dist/dashboard/assets/entry-DS90gUks.js.gz +0 -0
  590. package/dist/dashboard/assets/entry-j40LJotO.js +0 -4
  591. package/dist/dashboard/assets/entry-j40LJotO.js.gz +0 -0
  592. package/dist/dashboard/assets/entry-q5HwZ8nN.js.gz +0 -0
  593. package/dist/dashboard/assets/formatters-BTScrQpe.js.gz +0 -0
  594. package/dist/dashboard/assets/github-badge-Orjuzemr.js.gz +0 -0
  595. package/dist/dashboard/assets/index-cPrX5vn6.js.gz +0 -0
  596. package/dist/dashboard/assets/inline-Dvux8w-0.js.gz +0 -0
  597. package/dist/dashboard/assets/input-CU1Rgf3p.js.gz +0 -0
  598. package/dist/dashboard/assets/list-row-CzFvogbj.js.gz +0 -0
  599. package/dist/dashboard/assets/live-KaG15HLQ.js +0 -1
  600. package/dist/dashboard/assets/live-KaG15HLQ.js.gz +0 -0
  601. package/dist/dashboard/assets/minimize-2-D-MN8W8a.js +0 -1
  602. package/dist/dashboard/assets/minimize-2-D-MN8W8a.js.gz +0 -0
  603. package/dist/dashboard/assets/mutation-JJGQm8TD.js.gz +0 -0
  604. package/dist/dashboard/assets/numbers-BNDZnsUE.js +0 -2
  605. package/dist/dashboard/assets/numbers-BNDZnsUE.js.gz +0 -0
  606. package/dist/dashboard/assets/query-keys-CLaNz8Xz.js.gz +0 -0
  607. package/dist/dashboard/assets/quota-DZGyp0N5.js +0 -1
  608. package/dist/dashboard/assets/quota-DZGyp0N5.js.gz +0 -0
  609. package/dist/dashboard/assets/routing-BEH32QOC.js.gz +0 -0
  610. package/dist/dashboard/assets/select-B3iTetHN.js.gz +0 -0
  611. package/dist/dashboard/assets/settings-Dt3Eqbnq.js +0 -1
  612. package/dist/dashboard/assets/settings-Dt3Eqbnq.js.gz +0 -0
  613. package/dist/dashboard/assets/stack-KrOrm7JZ.js.gz +0 -0
  614. package/dist/dashboard/assets/state-DLS7IDMy.js.gz +0 -0
  615. package/dist/dashboard/assets/stream-retry-5QnVuSSo.js +0 -1
  616. package/dist/dashboard/assets/stream-retry-5QnVuSSo.js.gz +0 -0
  617. package/dist/dashboard/assets/switch-42ItvbAs.js.gz +0 -0
  618. package/dist/dashboard/assets/useMutation-Dmnfh1Ue.js.gz +0 -0
  619. package/src/network/response-headers.ts +0 -39
  620. package/src/protocol/response/dsml.ts +0 -38
  621. package/src/providers/integrations/xiaomi-mimo/think-stream.ts +0 -229
  622. /package/dist/dashboard/assets/{app-wvwCi4Eh.js → app-Bgd3zANQ.js} +0 -0
  623. /package/dist/dashboard/assets/{app-wvwCi4Eh.js.gz → app-Bgd3zANQ.js.gz} +0 -0
  624. /package/dist/dashboard/assets/brand/{og_bansos.webp → og_bansos_initial.webp} +0 -0
  625. /package/dist/dashboard/assets/brand/{og_image.webp → og_image_initial.webp} +0 -0
@@ -1,326 +1,349 @@
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
- }
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
+ asJsonbRecord,
9
+ asRecord,
10
+ firstNonEmptyString as firstString,
11
+ tryParseJson as safeParseJson,
12
+ } from "../../../shared/coerce";
13
+ import type { CredentialMode, ModelCatalogEntry } from "./contracts";
14
+ import {
15
+ FALLBACK_CONTEXT_LIMIT,
16
+ FALLBACK_OUTPUT_LIMIT,
17
+ } from "../../../providers/provider-model-definition";
18
+ import { modelsDevCatalog } from "../../../providers/discovery/models-dev-catalog";
19
+ import { models } from "../../../persistence/schema";
20
+ /** Field names a pasted OAuth credential may carry its access token under. */
21
+ const OAUTH_ACCESS_FIELDS = ["accessToken", "access_token", "access", "token"] as const;
22
+ /** Field names a pasted OAuth credential may carry its refresh token under. */
23
+ const OAUTH_REFRESH_FIELDS = ["refreshToken", "refresh_token", "refresh"] as const;
24
+
25
+ /** Parses an expiry field that may be an ISO string, an epoch ms, or epoch seconds. */
26
+ function parseExpiry(value: unknown): Date | undefined {
27
+ if (typeof value === "number" && Number.isFinite(value)) {
28
+ // Heuristic: a 10-digit value is epoch seconds, a 13-digit value is ms.
29
+ const ms = value < 1e12 ? value * 1000 : value;
30
+ const date = new Date(ms);
31
+ return Number.isFinite(date.getTime()) ? date : undefined;
32
+ }
33
+ if (typeof value === "string" && value.trim().length > 0) {
34
+ const asNumber = Number(value);
35
+ if (Number.isFinite(asNumber)) return parseExpiry(asNumber);
36
+ const date = new Date(value);
37
+ return Number.isFinite(date.getTime()) ? date : undefined;
38
+ }
39
+ return undefined;
40
+ }
41
+ /**
42
+ * Reads the two claims a static token's own payload can answer: when it stops
43
+ * working, and who issued it.
44
+ *
45
+ * The issuer is kept because a static JWT carries no other hint of its origin:
46
+ * an operator who pasted one from the wrong surface has nothing in the console
47
+ * to check it against, and several issuers accept the same token shape.
48
+ */
49
+ export function parseJwtCredential(
50
+ raw: string,
51
+ ): { readonly expiresAt?: Date; readonly issuer?: string } | undefined {
52
+ const payload = decodeJwtPayload(raw.trim());
53
+ if (payload === undefined) return undefined;
54
+ const expiresAt = parseExpiry(payload["exp"]);
55
+ const iss = payload["iss"];
56
+ const issuer = typeof iss === "string" && iss.trim().length > 0 ? iss.trim() : undefined;
57
+ return {
58
+ ...(expiresAt === undefined ? {} : { expiresAt }),
59
+ ...(issuer === undefined ? {} : { issuer }),
60
+ };
61
+ }
62
+
63
+ /**
64
+ * Splits a pasted OAuth credential into its access token, refresh token, and
65
+ * expiry.
66
+ *
67
+ * The console accepts either a bare access token or the JSON export a client
68
+ * produces (`{accessToken, refreshToken, expiresAt, …}`, possibly nested under
69
+ * `data`). A bare token has no refresh token — the account can be dispatched
70
+ * with it but cannot be refreshed, so the caller stores it as a static
71
+ * API credential instead of sending the access token to a refresh endpoint.
72
+ */
73
+ /**
74
+ * Middle segment of an auto-generated account label, from how the credential
75
+ * was actually detected.
76
+ *
77
+ * Only the two kinds that arrive with no name of their own are named here. An
78
+ * OAuth paste carries its own identity in `authState` (the account it was
79
+ * exported from), so it keeps whatever the export or the operator supplied —
80
+ * generating a name for it would overwrite an identity that was already there.
81
+ * An unrecognized mode falls back to `key`, matching the mode it is stored as.
82
+ */
83
+ export function accountLabelKindSegment(credentialMode: CredentialMode): string {
84
+ return credentialMode === "jwt" ? "jwt" : "key";
85
+ }
86
+
87
+ /**
88
+ * First unused `dcint-key-N` / `dcint-jwt-N` label for one provider.
89
+ *
90
+ * Polished tokens and plain JWTs are the credentials that arrive nameless: an
91
+ * operator pastes several into one provider and every row came out as
92
+ * "<providerId> account", so five accounts were five identical labels and the
93
+ * list could not tell them apart — which account failed, or is cooling, was
94
+ * unreadable.
95
+ *
96
+ * Numbering runs per kind, so the first JWT is `jwt-1` rather than inheriting
97
+ * whatever number the api_key rows reached (`jwt-3` with no `jwt-1` reads as
98
+ * two missing accounts). Counting is case-insensitive on the whole label, so a
99
+ * hand-typed `DCINT-KEY-2` still reserves its number.
100
+ *
101
+ * Pure, so the numbering can be pinned without a database.
102
+ */
103
+ export function nextGeneratedAccountLabel(
104
+ providerId: string,
105
+ credentialMode: CredentialMode,
106
+ existingLabels: readonly string[],
107
+ ): string {
108
+ const kind = accountLabelKindSegment(credentialMode);
109
+ const prefix = `${providerId}-${kind}-`;
110
+ const taken = new Set(
111
+ existingLabels.map((label) => label.trim().toLowerCase()),
112
+ );
113
+ for (let n = 1; ; n += 1) {
114
+ const candidate = `${prefix}${n}`;
115
+ if (!taken.has(candidate.toLowerCase())) return candidate;
116
+ }
117
+ }
118
+
119
+ export function parsePastedOAuthCredential(raw: string): {
120
+ readonly accessToken: string;
121
+ readonly refreshToken: string;
122
+ readonly expiresAt: Date | undefined;
123
+ /**
124
+ * Non-secret account configuration the export carried — the profile the
125
+ * account is bound to, how it authenticates, the region it was minted in.
126
+ * Empty for a bare token, which carries none.
127
+ */
128
+ readonly authState: Readonly<Record<string, unknown>>;
129
+ } {
130
+ let record: Record<string, unknown> | undefined;
131
+ try {
132
+ const parsed = JSON.parse(raw) as unknown;
133
+ if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
134
+ record = parsed as Record<string, unknown>;
135
+ }
136
+ } catch {
137
+ record = undefined;
138
+ }
139
+ if (record === undefined) {
140
+ // Bare token: it is the access token, and there is no refresh token.
141
+ const jwt = parseJwtCredential(raw);
142
+ return {
143
+ accessToken: raw,
144
+ refreshToken: raw,
145
+ expiresAt: jwt?.expiresAt,
146
+ authState: {},
147
+ };
148
+ }
149
+ // A nested `data` object is unwrapped the same way the dashboard's parser does.
150
+ const nested =
151
+ record["data"] !== null && typeof record["data"] === "object" && !Array.isArray(record["data"])
152
+ ? (record["data"] as Record<string, unknown>)
153
+ : undefined;
154
+ const accessToken =
155
+ firstString(record, OAUTH_ACCESS_FIELDS) ??
156
+ (nested ? firstString(nested, OAUTH_ACCESS_FIELDS) : undefined) ??
157
+ raw;
158
+ const refreshToken =
159
+ firstString(record, OAUTH_REFRESH_FIELDS) ??
160
+ (nested ? firstString(nested, OAUTH_REFRESH_FIELDS) : undefined) ??
161
+ accessToken;
162
+ const expiresAt =
163
+ parseExpiry(
164
+ record["expiresAt"] ??
165
+ record["expires_at"] ??
166
+ (nested ? (nested["expiresAt"] ?? nested["expires_at"]) : undefined) ??
167
+ record["expires"] ??
168
+ (nested ? nested["expires"] : undefined),
169
+ ) ?? parseJwtCredential(accessToken)?.expiresAt;
170
+ return { accessToken, refreshToken, expiresAt, authState: pastedAuthState(record, nested) };
171
+ }
172
+
173
+ /**
174
+ * The account configuration a pasted export carries beside its tokens.
175
+ *
176
+ * Kiro refuses a generation request that omits the profile the account is bound
177
+ * to, and exports from older installs carry that profile under a different key.
178
+ * Reading neither is how an imported account probes with no
179
+ * profile and is answered `400 profileArn is required`.
180
+ *
181
+ * A wrapper's own `authState` bag travels wholesale rather than cherry-picked:
182
+ * adapters read provider-specific keys this picker cannot enumerate (Qoder's
183
+ * `machineId`, buddy's `qimei36` device binding, cursor's `method`, coreweave's
184
+ * project, kiro's `startUrl`), and dropping the bag silently degraded every
185
+ * imported account to a half-identity credential — failing closed at dispatch
186
+ * (Qoder answered 401 "missing its user identity"). The flat aliases below only
187
+ * fill what an older export shape carries beside the bag, and can never invent
188
+ * a key the bag did not have.
189
+ */
190
+ function pastedAuthState(
191
+ record: Record<string, unknown>,
192
+ nested: Record<string, unknown> | undefined,
193
+ ): Readonly<Record<string, unknown>> {
194
+ const carried = {
195
+ ...(asJsonbRecord(record["providerSpecificData"]) ?? {}),
196
+ ...(asJsonbRecord(record["authState"]) ?? {}),
197
+ ...(asJsonbRecord(nested?.["authState"]) ?? {}),
198
+ };
199
+ // These three are recomputed for the pasted tokens at insert time; a carried
200
+ // copy would describe the previous token, not this one.
201
+ delete carried.credentialMode;
202
+ delete carried.tokenExpiresAt;
203
+ delete carried.jwtIssuer;
204
+ const sources = [record, nested, asJsonbRecord(record["providerSpecificData"])];
205
+ const profileArn = firstStringFrom(sources, ["profileArn", "profile_arn"]);
206
+ const authMethod = firstStringFrom(sources, ["authMethod", "auth_method"]);
207
+ const region = firstStringFrom(sources, ["region"]);
208
+ // The upstream user id, carried on a re-import of an exported account. Trae
209
+ // refresh is addressed per upstream user id, so restoring it is what keeps a
210
+ // re-imported account refreshable.
211
+ const userId = firstStringFrom(sources, ["userId", "user_id", "uid", "UserID"]);
212
+ const machineId = firstStringFrom(sources, ["machineId", "machine_id"]);
213
+ return {
214
+ ...carried,
215
+ ...(profileArn === undefined ? {} : { profileArn }),
216
+ ...(authMethod === undefined ? {} : { authMethod }),
217
+ ...(region === undefined ? {} : { region }),
218
+ ...(userId === undefined ? {} : { userId }),
219
+ ...(machineId === undefined ? {} : { machineId }),
220
+ };
221
+ }
222
+
223
+ /**
224
+ * Adds or removes the `image` input modality on a stored modalities blob.
225
+ *
226
+ * The blob is a JSON string whose shape varies by source (bundled seed,
227
+ * discovery, manual add), so it is normalized rather than pattern-matched: an
228
+ * unparseable blob is replaced with the minimal text shape rather than left to
229
+ * corrupt capability preflight.
230
+ */
231
+ export function withImageModality(stored: string | null | undefined, image: boolean): string {
232
+ const parsed = asRecord(safeParseJson(stored));
233
+ const input = Array.isArray(parsed?.input)
234
+ ? (parsed.input as unknown[]).filter((m): m is string => typeof m === "string")
235
+ : ["text"];
236
+ const output = Array.isArray(parsed?.output)
237
+ ? (parsed.output as unknown[]).filter((m): m is string => typeof m === "string")
238
+ : ["text"];
239
+ const nextInput = image
240
+ ? Array.from(new Set([...input, "image"]))
241
+ : input.filter((m) => m !== "image");
242
+ return JSON.stringify({
243
+ ...(parsed ?? {}),
244
+ input: nextInput.length > 0 ? nextInput : ["text"],
245
+ output: output.length > 0 ? output : ["text"],
246
+ });
247
+ }
248
+
249
+ /** Normalizes a text column read as `unknown` back to a string or null. */
250
+ export function asStoredText(value: unknown): string | null {
251
+ return typeof value === "string" ? value : null;
252
+ }
253
+
254
+ function firstStringFrom(
255
+ sources: readonly (Record<string, unknown> | undefined)[],
256
+ fields: readonly string[],
257
+ ): string | undefined {
258
+ for (const source of sources) {
259
+ if (source === undefined) continue;
260
+ const value = firstString(source, fields);
261
+ if (value !== undefined) return value;
262
+ }
263
+ return undefined;
264
+ }
265
+
266
+ /**
267
+ * The account's still-active per-model backoffs, or `undefined` when none are.
268
+ *
269
+ * Expired entries are dropped rather than returned: the sweep that clears them
270
+ * runs on a timer, so between the deadline passing and the sweep a raw read
271
+ * would report a cooldown that is not in force.
272
+ */
273
+ export function liveModelCooldowns(
274
+ raw: unknown,
275
+ ): { modelCooldowns: Record<string, string> } | undefined {
276
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return undefined;
277
+ const now = Date.now();
278
+ const live: Record<string, string> = {};
279
+ for (const [modelId, value] of Object.entries(raw as Record<string, unknown>)) {
280
+ if (typeof value !== "string") continue;
281
+ const at = new Date(value).getTime();
282
+ if (Number.isFinite(at) && at > now) live[modelId] = value;
283
+ }
284
+ return Object.keys(live).length > 0 ? { modelCooldowns: live } : undefined;
285
+ }
286
+
287
+
288
+ /**
289
+ * Projects one persisted `models` row into the console's catalog entry.
290
+ *
291
+ * `disabled` is resolved by the caller (from `tenant_disabled_models`), because
292
+ * the bulk read has to look it up across every provider at once while the
293
+ * single-provider read looks it up for one; the projection itself is identical
294
+ * either way, so it lives here rather than being written twice.
295
+ *
296
+ * Exported so a test can pin the unsized-model substitution without a database:
297
+ * which rows get the fallback, and that they are the same rows marked estimated.
298
+ */
299
+ export function mapModelRow(
300
+ row: typeof models.$inferSelect,
301
+ disabled: boolean,
302
+ ): ModelCatalogEntry {
303
+ const modalities =
304
+ row.modalities && typeof row.modalities === "object"
305
+ ? (row.modalities as { input?: unknown; output?: unknown })
306
+ : undefined;
307
+ const inputModalities = Array.isArray(modalities?.input) ? modalities.input : [];
308
+ const outputModalities = Array.isArray(modalities?.output) ? modalities.output : [];
309
+ // Nothing filed this model — not its own row, not the base catalog under this
310
+ // provider's id, not a majority across the catalog's other resellers. The
311
+ // fallback is substituted here rather than persisted: the stored `null` is the
312
+ // honest record of what upstream said, and substituting at the write would
313
+ // make a guess indistinguishable from a measurement on the next read. The
314
+ // marker rides along so the card can say which one it is showing.
315
+ const unfiled =
316
+ row.contextLimit === null &&
317
+ modelsDevCatalog.resolve(row.providerId, row.modelId) === undefined &&
318
+ modelsDevCatalog.majorityFor(row.modelId) === undefined;
319
+ return {
320
+ modelId: row.modelId,
321
+ route: row.endpointPath,
322
+ provider: row.providerId,
323
+ wireFamily: row.wireFamily,
324
+ serviceKind: row.serviceKind,
325
+ enabled: disabled ? false : row.enabled,
326
+ // A fetched model that published no context size read `n/a ctx · n/a out`
327
+ // while still being routable — a working model that looked unusable. These
328
+ // rows get the same ceilings a manual add is offered, flagged as estimated.
329
+ contextLimit: unfiled ? FALLBACK_CONTEXT_LIMIT : row.contextLimit,
330
+ outputLimit: unfiled ? FALLBACK_OUTPUT_LIMIT : row.outputLimit,
331
+ reasoning: row.reasoning,
332
+ toolCall: row.toolCall,
333
+ vision: inputModalities.includes("image"),
334
+ document: inputModalities.includes("document"),
335
+ audio: inputModalities.includes("audio"),
336
+ mediaGeneration: outputModalities.includes("image"),
337
+ cost: row.cost as ModelCatalogEntry["cost"],
338
+ source: row.source,
339
+ ...(unfiled ? { limitsEstimated: true } : {}),
340
+ // Three-valued on the wire as it is in the column: only a row that claims
341
+ // `true` is pro-only. Absence means nobody established the tier, and the
342
+ // surface must render that as unknown rather than as free.
343
+ ...(row.proOnly === true ? { proOnly: true } : {}),
344
+ // Same three-valued rule, from the provider's own directory. Qoder states
345
+ // `is_free` per row, so a plan change moves the pill without a code change.
346
+ ...(row.freeTier === true ? { freeTier: true } : {}),
347
+ sourceUpdatedAt: row.sourceUpdatedAt?.toISOString() ?? null,
348
+ } satisfies ModelCatalogEntry;
349
+ }