cartethyia 2.7.4 → 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 (553) hide show
  1. package/.env.example +2 -0
  2. package/README.md +17 -5
  3. package/dist/dashboard/assets/{BannedPage-ChG5svFb.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-BIcWQun7.js → CliToolDetailPage-DZKmVuJN.js} +1 -1
  8. package/dist/dashboard/assets/CliToolDetailPage-DZKmVuJN.js.gz +0 -0
  9. package/dist/dashboard/assets/{CliToolsPage-Bf2VelDe.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-ggeMSjte.js → CustomizationPage-BKj_r0rp.js} +1 -1
  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-U6AKaJcF.js → LoginPage-DkXb6dx-.js} +1 -1
  20. package/dist/dashboard/assets/LoginPage-DkXb6dx-.js.gz +0 -0
  21. package/dist/dashboard/assets/{ModelPicker-BMS2t8BA.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-lsrx7NYg.js → ProxyPage-5SnUSy3j.js} +1 -1
  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-f0DE1h0-.js → SetupPage-WTa0sktC.js} +1 -1
  38. package/dist/dashboard/assets/SetupPage-WTa0sktC.js.gz +0 -0
  39. package/dist/dashboard/assets/{StudioPage-Cq5tW7hW.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-C0K-c4eo.js +1 -0
  48. package/dist/dashboard/assets/assert-C0K-c4eo.js.gz +0 -0
  49. package/dist/dashboard/assets/{badge-D9CS6dZk.js → badge-DfbhQU4k.js} +1 -1
  50. package/dist/dashboard/assets/badge-DfbhQU4k.js.gz +0 -0
  51. package/dist/dashboard/assets/{banner-DympBNq4.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-ibRWU_uT.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-CQXVMn4P.js +1 -0
  62. package/dist/dashboard/assets/cli-tools-CQXVMn4P.js.gz +0 -0
  63. package/dist/dashboard/assets/{clipboard-button-DJm-9pz7.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-8fiE6tec.js → dialog-BEK7KP0H.js} +1 -1
  66. package/dist/dashboard/assets/dialog-BEK7KP0H.js.gz +0 -0
  67. package/dist/dashboard/assets/{drawer-CqGYeOrd.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-BSAsYsmV.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-D-MTJxMr.js → formatters-BAACA727.js} +1 -1
  76. package/dist/dashboard/assets/formatters-BAACA727.js.gz +0 -0
  77. package/dist/dashboard/assets/{github-badge-CPK47xQx.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-DBNPOfhV.js → index-FQlj0HUT.js} +2 -2
  80. package/dist/dashboard/assets/index-FQlj0HUT.js.gz +0 -0
  81. package/dist/dashboard/assets/{inline-BT0zntEr.js → inline-C4gU5iOL.js} +1 -1
  82. package/dist/dashboard/assets/inline-C4gU5iOL.js.gz +0 -0
  83. package/dist/dashboard/assets/{input-Da2ZViWg.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-BXNeLw6M.js → layout-CajrYUnD.js} +1 -1
  86. package/dist/dashboard/assets/layout-CajrYUnD.js.gz +0 -0
  87. package/dist/dashboard/assets/{list-row-BycOrrIV.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/network-C5UBACrJ.js +1 -0
  92. package/dist/dashboard/assets/network-C5UBACrJ.js.gz +0 -0
  93. package/dist/dashboard/assets/numbers-CUWAZK4J.js +2 -0
  94. package/dist/dashboard/assets/numbers-CUWAZK4J.js.gz +0 -0
  95. package/dist/dashboard/assets/pagination-Xsi9Qx04.js +1 -0
  96. package/dist/dashboard/assets/pagination-Xsi9Qx04.js.gz +0 -0
  97. package/dist/dashboard/assets/quota-DvQbD_sr.js +1 -0
  98. package/dist/dashboard/assets/quota-DvQbD_sr.js.gz +0 -0
  99. package/dist/dashboard/assets/routing-in0f_IfK.js +1 -0
  100. package/dist/dashboard/assets/routing-in0f_IfK.js.gz +0 -0
  101. package/dist/dashboard/assets/{select-BIYIBkMU.js → select-BLULNqTp.js} +1 -1
  102. package/dist/dashboard/assets/select-BLULNqTp.js.gz +0 -0
  103. package/dist/dashboard/assets/settings-CcGczj6I.js +1 -0
  104. package/dist/dashboard/assets/settings-CcGczj6I.js.gz +0 -0
  105. package/dist/dashboard/assets/shield-BMQNxORh.js +1 -0
  106. package/dist/dashboard/assets/shield-BMQNxORh.js.gz +0 -0
  107. package/dist/dashboard/assets/{stack-DfgnFDWY.js → stack-D2lIR0tg.js} +1 -1
  108. package/dist/dashboard/assets/stack-D2lIR0tg.js.gz +0 -0
  109. package/dist/dashboard/assets/{state-CkfdYWel.js → state-DTARjfiW.js} +1 -1
  110. package/dist/dashboard/assets/state-DTARjfiW.js.gz +0 -0
  111. package/dist/dashboard/assets/{switch-SYm237HG.js → switch-c0oPSnqW.js} +1 -1
  112. package/dist/dashboard/assets/switch-c0oPSnqW.js.gz +0 -0
  113. package/dist/dashboard/assets/{useMutation-BQqCRNIi.js → useMutation-m8WGNPrz.js} +1 -1
  114. package/dist/dashboard/assets/useMutation-m8WGNPrz.js.gz +0 -0
  115. package/dist/dashboard/console-backgrounds.jpg +0 -0
  116. package/dist/dashboard/console-backgrounds_initial.jpg +0 -0
  117. package/dist/dashboard/index.html +2 -2
  118. package/dist/dashboard/index.html.gz +0 -0
  119. package/dist/dashboard/precompressed-assets.json +56 -55
  120. package/migrations/full-postgres/0036_system_prompt_presets_refs.sql +10 -0
  121. package/migrations/full-postgres/0037_share_concurrency_scope.sql +3 -0
  122. package/migrations/full-postgres/0038_add_accounts_quota_snapshot.sql +7 -0
  123. package/migrations/full-postgres/0039_add_telemetry_provider_model_indexes.sql +13 -0
  124. package/migrations/full-postgres/0040_add_models_dialect.sql +3 -0
  125. package/migrations/full-postgres/0041_add_daily_checkin_ledger_account_fk.sql +7 -0
  126. package/migrations/lite-sqlite3/0036_system_prompt_presets_refs.sql +14 -0
  127. package/migrations/lite-sqlite3/0037_share_concurrency_scope.sql +3 -0
  128. package/migrations/lite-sqlite3/0038_add_accounts_quota_snapshot.sql +4 -0
  129. package/migrations/lite-sqlite3/0039_add_telemetry_provider_model_indexes.sql +6 -0
  130. package/migrations/lite-sqlite3/0040_add_models_dialect.sql +3 -0
  131. package/migrations/lite-sqlite3/0041_add_daily_checkin_ledger_account_fk.sql +26 -0
  132. package/package.json +4 -6
  133. package/scripts/cli/update.ts +15 -41
  134. package/src/boot/app.ts +58 -2
  135. package/src/boot/dependencies.ts +786 -732
  136. package/src/boot/lifecycle.ts +11 -12
  137. package/src/config/cooldowns.ts +34 -55
  138. package/src/config/network.ts +1 -1
  139. package/src/config/package-paths.ts +1 -1
  140. package/src/config/providers.ts +13 -14
  141. package/src/config/resources.ts +0 -5
  142. package/src/config/runtime.ts +2 -5
  143. package/src/config/spec.ts +26 -6
  144. package/src/console/api-keys/ban-constants.ts +38 -0
  145. package/src/console/api-keys/contracts.ts +71 -62
  146. package/src/console/api-keys/routes.ts +48 -12
  147. package/src/console/auth/access.ts +4 -4
  148. package/src/console/auth/account-reset.ts +1 -1
  149. package/src/console/auth/audit-recorder.ts +1 -1
  150. package/src/console/auth/lockout.ts +7 -3
  151. package/src/console/auth/row-coercion.ts +57 -12
  152. package/src/console/auth/session-resolver.ts +1 -1
  153. package/src/console/auth/session-service.ts +8 -2
  154. package/src/console/auth/session.ts +37 -31
  155. package/src/console/auth/setup-service.ts +4 -3
  156. package/src/console/auth/user-store.ts +1 -1
  157. package/src/console/backup/9router-map.ts +0 -3
  158. package/src/console/backup/contracts.ts +9 -9
  159. package/src/console/backup/export-endpoint.ts +5 -6
  160. package/src/console/backup/nine-router.ts +1 -7
  161. package/src/console/backup/routes.ts +29 -33
  162. package/src/console/backup/service.ts +468 -468
  163. package/src/console/backup/store.ts +47 -29
  164. package/src/console/backup/validate.ts +11 -1
  165. package/src/console/cli-tools/contracts.ts +7 -7
  166. package/src/console/cli-tools/injectors/cline.ts +110 -110
  167. package/src/console/cli-tools/injectors/codex.ts +181 -181
  168. package/src/console/cli-tools/injectors/copilot.ts +108 -108
  169. package/src/console/cli-tools/injectors/driver.ts +247 -247
  170. package/src/console/cli-tools/injectors/jcode.ts +115 -115
  171. package/src/console/cli-tools/injectors/openclaw.ts +161 -161
  172. package/src/console/cli-tools/injectors/opencode.ts +153 -153
  173. package/src/console/cli-tools/routes.ts +1 -1
  174. package/src/console/cli-tools/service.ts +1 -1
  175. package/src/console/cli-tools/store.ts +2 -2
  176. package/src/console/console-router.ts +32 -0
  177. package/src/console/dashboard-assets.ts +9 -9
  178. package/src/console/domain-registration.ts +16 -2
  179. package/src/console/inferhub/client.ts +0 -7
  180. package/src/console/inferhub/key-store.ts +1 -1
  181. package/src/console/inferhub/normalize.ts +2 -2
  182. package/src/console/inferhub/routes.ts +1 -1
  183. package/src/console/inferhub/types.ts +1 -1
  184. package/src/console/observability/contracts.ts +1 -1
  185. package/src/console/observability/live.ts +31 -18
  186. package/src/console/observability/logs.ts +1 -1
  187. package/src/console/observability/sse.ts +62 -8
  188. package/src/console/observability/store.ts +20 -4
  189. package/src/console/providers/catalog/catalog-projections.ts +1 -1
  190. package/src/console/providers/catalog/contracts.ts +39 -0
  191. package/src/console/providers/catalog/model-operations.ts +1 -1
  192. package/src/console/providers/catalog/provider-operations.ts +95 -17
  193. package/src/console/providers/catalog/public-model-store.ts +0 -0
  194. package/src/console/providers/catalog/routes.ts +27 -3
  195. package/src/console/providers/catalog/store.ts +147 -8
  196. package/src/console/providers/detail/routes.ts +1 -1
  197. package/src/console/providers/oauth/callback-listener.ts +100 -18
  198. package/src/console/providers/oauth/routes.ts +92 -13
  199. package/src/console/quota/account-quota-global.ts +41 -3
  200. package/src/console/quota/account-quota-shared.ts +10 -3
  201. package/src/console/quota/account-quota-tenant.ts +259 -66
  202. package/src/console/quota/daily-growth-job.ts +308 -0
  203. package/src/console/quota/native-identity.ts +2 -2
  204. package/src/console/quota/quota-view.ts +1 -1
  205. package/src/console/remote-support/routes.ts +1 -1
  206. package/src/console/remote-support/service.ts +2 -2
  207. package/src/console/routing/model/contracts.ts +1 -1
  208. package/src/console/routing/pools/contracts.ts +2 -2
  209. package/src/console/routing/pools/import-parse.ts +3 -3
  210. package/src/console/routing/pools/relay-deploy.ts +3 -3
  211. package/src/console/settings/contracts.ts +29 -2
  212. package/src/console/settings/store.ts +20 -0
  213. package/src/console/share/share-router.ts +100 -32
  214. package/src/console/share/share-stats.ts +4 -4
  215. package/src/console/share/share-usage.ts +1 -1
  216. package/src/console/shared/errors.ts +2 -2
  217. package/src/console/shared/object.ts +18 -0
  218. package/src/console/shared/read-through-cache.ts +4 -4
  219. package/src/console/shared/transaction.ts +14 -0
  220. package/src/console/system-prompt/contracts.ts +89 -12
  221. package/src/console/system-prompt/routes.ts +98 -45
  222. package/src/console/updates/apply-update.ts +244 -0
  223. package/src/console/updates/routes.ts +252 -97
  224. package/src/main.ts +11 -5
  225. package/src/network/http2-bidi.ts +1 -1
  226. package/src/network/ip-utils.ts +298 -0
  227. package/src/network/outbound-fetch.ts +1 -1
  228. package/src/network/pool/agent.ts +7 -6
  229. package/src/network/pool/byte-accounting.ts +1 -1
  230. package/src/network/pool/resolver.ts +16 -12
  231. package/src/network/pool/selector.ts +150 -136
  232. package/src/network/pool-health-machine.ts +1 -1
  233. package/src/network/rate-limit-verdict.ts +1 -1
  234. package/src/network/retry.ts +1 -1
  235. package/src/network/ssrf.ts +20 -71
  236. package/src/observability/event-log.ts +2 -2
  237. package/src/observability/event-query.ts +59 -291
  238. package/src/observability/log-sanitize.ts +1 -1
  239. package/src/observability/metrics.ts +4 -4
  240. package/src/observability/payload-capture.ts +6 -5
  241. package/src/observability/payload-store.ts +1 -1
  242. package/src/observability/runtime-metrics.ts +1 -1
  243. package/src/observability/telemetry-buffer.ts +12 -3
  244. package/src/observability/telemetry-reconciliation.ts +3 -3
  245. package/src/observability/token-speed.ts +1 -1
  246. package/src/persistence/connection-url.ts +1 -1
  247. package/src/persistence/db-handle.ts +1 -1
  248. package/src/persistence/db-pg.ts +2 -2
  249. package/src/persistence/db-sqlite.ts +3 -3
  250. package/src/persistence/dialect.ts +5 -1
  251. package/src/persistence/readiness.ts +1 -1
  252. package/src/persistence/redis.ts +1 -1
  253. package/src/persistence/schema.ts +113 -40
  254. package/src/persistence/shared-cache.ts +2 -2
  255. package/src/persistence/sort-order.ts +1 -1
  256. package/src/persistence/stores/api-key-store.ts +10 -1
  257. package/src/persistence/stores/share-store.ts +20 -8
  258. package/src/persistence/system-prompt-store.ts +63 -18
  259. package/src/persistence/tenant-preferences.ts +1 -1
  260. package/src/persistence/ttl-cache.ts +3 -3
  261. package/src/persistence/usage-flush.ts +7 -2
  262. package/src/protocol/canonical-model.ts +1 -1
  263. package/src/protocol/codecs.ts +6 -10
  264. package/src/protocol/dsml.ts +3 -49
  265. package/src/protocol/image-clamp.ts +2 -2
  266. package/src/protocol/reasoning-text.ts +1 -1
  267. package/src/protocol/reasoning.ts +7 -52
  268. package/src/protocol/registry.ts +50 -15
  269. package/src/protocol/request/chat.ts +46 -19
  270. package/src/protocol/request/codex.ts +1 -1
  271. package/src/protocol/request/messages.ts +46 -14
  272. package/src/protocol/request/responses.ts +19 -10
  273. package/src/protocol/response/chat.ts +25 -10
  274. package/src/protocol/response/codex.ts +1 -1
  275. package/src/protocol/response/messages.ts +1 -1
  276. package/src/protocol/response/omp-tool-call-markup.ts +1 -1
  277. package/src/protocol/response/responses.ts +1 -1
  278. package/src/protocol/sse-decoder.ts +50 -1
  279. package/src/protocol/stop-reason.ts +2 -2
  280. package/src/protocol/think-split.ts +347 -0
  281. package/src/protocol/translation/attachment-integrity.ts +3 -3
  282. package/src/protocol/translation/cache-controls.ts +8 -20
  283. package/src/protocol/translation/capabilities.ts +6 -6
  284. package/src/protocol/translation/dialect/deepseek.ts +10 -0
  285. package/src/protocol/translation/dialect/inband.ts +39 -0
  286. package/src/protocol/translation/dialect/model-thinking.ts +89 -0
  287. package/src/protocol/translation/dialect/qwen.ts +114 -0
  288. package/src/protocol/translation/dialect/resolver.ts +83 -0
  289. package/src/protocol/translation/dialect/rules.ts +93 -0
  290. package/src/protocol/translation/dialect/thinking-ladder.ts +245 -0
  291. package/src/protocol/translation/dialect/types.ts +58 -0
  292. package/src/protocol/translation/quirks.ts +18 -4
  293. package/src/protocol/translation/thinking.ts +12 -252
  294. package/src/protocol/translation/tool-id.ts +3 -3
  295. package/src/protocol/translation/tool-repair.ts +1 -1
  296. package/src/protocol/upstream/post-json.ts +6 -1
  297. package/src/providers/adapters/compatible-adapter.ts +42 -3
  298. package/src/providers/authentication/jwt-validator.ts +0 -5
  299. package/src/providers/authentication/oauth-client.ts +254 -253
  300. package/src/providers/authentication/oauth-flow-store.ts +42 -7
  301. package/src/providers/bundled-discovery-providers.ts +6 -30
  302. package/src/providers/configured-openai-providers.ts +82 -97
  303. package/src/providers/default-registry.ts +23 -35
  304. package/src/providers/discovery/catalog/catalog-format.ts +13 -2
  305. package/src/providers/discovery/discovery-types.ts +0 -1
  306. package/src/providers/discovery/openai-model-discovery.ts +2 -2
  307. package/src/providers/discovery/probe-phases.ts +48 -13
  308. package/src/providers/discovery/probing-service.ts +6 -8
  309. package/src/providers/integrations/agentrouter/agentrouter.ts +12 -2
  310. package/src/providers/integrations/antigravity/antigravity-oauth.ts +0 -1
  311. package/src/providers/integrations/antigravity/antigravity-protocol.ts +0 -14
  312. package/src/providers/integrations/antigravity/antigravity.ts +2 -2
  313. package/src/providers/integrations/buddy/buddy-catalog-shared.ts +1 -1
  314. package/src/providers/integrations/buddy/buddy-chat-shared.ts +20 -4
  315. package/src/providers/integrations/buddy/buddy-cli-headers.ts +19 -52
  316. package/src/providers/integrations/buddy/buddy-identity.ts +86 -0
  317. package/src/providers/integrations/buddy/buddy-oauth-shared.ts +1 -1
  318. package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +1 -2
  319. package/src/providers/integrations/buddy/codebuddy/provider.ts +4 -15
  320. package/src/providers/integrations/buddy/codebuddy/shared.ts +30 -9
  321. package/src/providers/integrations/buddy/workbuddy/provider.ts +5 -9
  322. package/src/providers/integrations/buddy/workbuddy/shared.ts +21 -3
  323. package/src/providers/integrations/claude/claude-cch.ts +148 -148
  324. package/src/providers/integrations/claude/fallback-credit.ts +3 -7
  325. package/src/providers/integrations/cline/cline.ts +1 -1
  326. package/src/providers/integrations/codex/codex-device-code.ts +101 -101
  327. package/src/providers/integrations/codex/codex-oauth.ts +0 -1
  328. package/src/providers/integrations/commandcode/commandcode.ts +75 -115
  329. package/src/providers/integrations/commandcode/quota.ts +131 -51
  330. package/src/providers/integrations/cursor/cursor-http2.ts +0 -3
  331. package/src/providers/integrations/cursor/cursor-quota.ts +2 -6
  332. package/src/providers/integrations/cursor/cursor-request.ts +0 -87
  333. package/src/providers/integrations/cursor/cursor-shared.ts +0 -4
  334. package/src/providers/integrations/cursor/cursor.ts +38 -5
  335. package/src/providers/integrations/cursor/protocol/framing.ts +0 -56
  336. package/src/providers/integrations/cursor/protocol/messages.ts +0 -96
  337. package/src/providers/integrations/factory/factory.ts +9 -9
  338. package/src/providers/integrations/github/github-discovery.ts +55 -10
  339. package/src/providers/integrations/github/github-oauth.ts +6 -5
  340. package/src/providers/integrations/github/github.ts +41 -14
  341. package/src/providers/integrations/grok/grok-oauth.ts +261 -261
  342. package/src/providers/integrations/grok/grok-quota-frame.ts +1 -1
  343. package/src/providers/integrations/grok/grok-quota.ts +16 -7
  344. package/src/providers/integrations/grok/grok-ratelimit-store.ts +0 -5
  345. package/src/providers/integrations/grok/grok-ratelimit.ts +3 -0
  346. package/src/providers/integrations/grok/grok.ts +86 -36
  347. package/src/providers/integrations/kilo/kilo-oauth.ts +1 -1
  348. package/src/providers/integrations/kiro/kiro-cache.ts +31 -0
  349. package/src/providers/integrations/kiro/kiro-catalog.ts +31 -5
  350. package/src/providers/integrations/kiro/kiro-discovery.ts +47 -43
  351. package/src/providers/integrations/kiro/kiro-machine-id.ts +6 -3
  352. package/src/providers/integrations/kiro/kiro-oauth.ts +523 -255
  353. package/src/providers/integrations/kiro/kiro-quota.ts +35 -47
  354. package/src/providers/integrations/kiro/kiro-request.ts +8 -11
  355. package/src/providers/integrations/kiro/kiro-stream.ts +13 -0
  356. package/src/providers/integrations/kiro/kiro.ts +81 -68
  357. package/src/providers/integrations/muse/muse-oauth.ts +0 -1
  358. package/src/providers/integrations/muse/muse-quota.ts +0 -4
  359. package/src/providers/integrations/opencode/fingerprint.ts +7 -5
  360. package/src/providers/integrations/opencode/provider.ts +17 -24
  361. package/src/providers/integrations/qoder/qoder-catalog.ts +85 -8
  362. package/src/providers/integrations/qoder/qoder-oauth.ts +165 -25
  363. package/src/providers/integrations/qoder/qoder-quota.ts +48 -6
  364. package/src/providers/integrations/qoder/qoder-realm.ts +10 -31
  365. package/src/providers/integrations/qoder/qoder-sse.ts +159 -0
  366. package/src/providers/integrations/qoder/qoder-umid.ts +21 -25
  367. package/src/providers/integrations/qoder/qoder-virtual.ts +12 -0
  368. package/src/providers/integrations/qoder/qoder.ts +354 -62
  369. package/src/providers/integrations/shared/connect.ts +0 -54
  370. package/src/providers/integrations/shared/hash.ts +15 -0
  371. package/src/providers/integrations/shared/session-cache.ts +136 -0
  372. package/src/providers/integrations/trae/trae-cache.ts +7 -87
  373. package/src/providers/integrations/trae/trae-catalog.ts +1 -6
  374. package/src/providers/integrations/trae/trae-protocol.ts +17 -8
  375. package/src/providers/integrations/trae/trae-realm.ts +0 -7
  376. package/src/providers/integrations/trae/trae.ts +53 -10
  377. package/src/providers/integrations/xai/xai-oauth.ts +5 -5
  378. package/src/providers/integrations/zcode-start-plan/gifts.ts +265 -0
  379. package/src/providers/integrations/zcode-start-plan/oauth.ts +154 -0
  380. package/src/providers/integrations/zcode-start-plan/provider.ts +131 -0
  381. package/src/providers/integrations/zcode-start-plan/quota.ts +152 -0
  382. package/src/providers/integrations/zcode-start-plan/zcode_prompt.json +10 -0
  383. package/src/providers/operations/account-error-classification.ts +131 -116
  384. package/src/providers/operations/account-health-service.ts +31 -39
  385. package/src/providers/operations/account-reset-service.ts +58 -1
  386. package/src/providers/operations/cli-platform.ts +31 -5
  387. package/src/providers/operations/client-version-resolver.ts +4 -6
  388. package/src/providers/operations/client-versions.ts +10 -7
  389. package/src/providers/operations/provider-catalog-seeder.ts +2 -0
  390. package/src/providers/operations/provider-catalog-service.ts +18 -2
  391. package/src/providers/operations/provider-credential-service.ts +19 -2
  392. package/src/providers/provider-contracts.ts +18 -7
  393. package/src/providers/provider-metadata.ts +17 -2
  394. package/src/providers/provider-model-definition.ts +22 -1
  395. package/src/providers/provider-probe.ts +4 -2
  396. package/src/providers/provider-usage.ts +81 -16
  397. package/src/providers/quota/cache.ts +29 -0
  398. package/src/providers/quota/quota-contracts.ts +4 -0
  399. package/src/providers/quota/refresh.ts +27 -4
  400. package/src/providers/quota/snapshot.ts +94 -0
  401. package/src/providers/search/search-provider.ts +1 -3
  402. package/src/providers/search/search-providers.ts +2 -1
  403. package/src/security/abuse.ts +4 -4
  404. package/src/security/admission/service.ts +2 -2
  405. package/src/security/admission/ttl.ts +61 -61
  406. package/src/security/api-key-auth.ts +16 -4
  407. package/src/security/client-router-fingerprint.ts +4 -4
  408. package/src/security/credential-codec.ts +3 -3
  409. package/src/security/csrf.ts +2 -2
  410. package/src/security/ip-boundary.ts +16 -189
  411. package/src/security/model-access-rule.ts +1 -1
  412. package/src/security/outbound-headers.ts +18 -0
  413. package/src/shared/invoke-guard.ts +47 -0
  414. package/src/transport/contracts.ts +13 -4
  415. package/src/transport/dispatch/attempt-finalize.ts +1 -1
  416. package/src/transport/dispatch/capture-policy.ts +34 -37
  417. package/src/transport/dispatch/empty-stream.ts +23 -20
  418. package/src/transport/dispatch/generation.ts +25 -45
  419. package/src/transport/dispatch/native-shared.ts +127 -0
  420. package/src/transport/dispatch/proxy-request.ts +3 -1
  421. package/src/transport/dispatch/responses-compact.ts +25 -46
  422. package/src/transport/dispatch/streaming/encoder.ts +18 -72
  423. package/src/transport/dispatch/streaming/finalize.ts +2 -2
  424. package/src/transport/dispatch/streaming/stream.ts +7 -3
  425. package/src/transport/dispatch/streaming/thinking-loop.ts +1 -1
  426. package/src/transport/dispatch/systemone.ts +20 -48
  427. package/src/transport/dispatch/token-count.ts +41 -38
  428. package/src/transport/dispatch/websearch.ts +19 -45
  429. package/src/transport/drain-endpoint.ts +1 -1
  430. package/src/transport/failure-policy.ts +6 -0
  431. package/src/transport/guards/auth-guard.ts +0 -6
  432. package/src/transport/guards/body-policy.ts +3 -3
  433. package/src/transport/guards/headroom-guard.ts +1 -1
  434. package/src/transport/guards/pipeline.ts +1 -1
  435. package/src/transport/guards/request-context.ts +12 -17
  436. package/src/transport/request/preparer.ts +286 -7
  437. package/src/transport/request/rtk/compress-request.ts +3 -3
  438. package/src/transport/request/rtk/rtk-constants.ts +2 -2
  439. package/src/transport/request/rtk/rtk-filters.ts +8 -8
  440. package/src/transport/request/state.ts +1 -13
  441. package/src/transport/request/system-prompt-injection.ts +74 -11
  442. package/src/transport/request/tenant-preferences.ts +22 -0
  443. package/src/transport/routing/candidate-index.ts +2 -2
  444. package/src/transport/routing/round-robin.ts +1 -1
  445. package/src/transport/routing/route-catalog.ts +17 -18
  446. package/src/transport/routing/route-model.ts +126 -33
  447. package/src/transport/routing/router.ts +152 -37
  448. package/src/transport/shutdown-notice.ts +2 -2
  449. package/src/transport/surface/adapters.ts +3 -3
  450. package/src/transport/surface/chat/encode.ts +7 -7
  451. package/src/transport/surface/chat/parse.ts +7 -7
  452. package/src/transport/surface/completion.ts +2 -2
  453. package/src/transport/surface/content-parts.ts +1 -1
  454. package/src/transport/surface/dialects.ts +2 -2
  455. package/src/transport/surface/messages/parse.ts +3 -3
  456. package/src/transport/surface/responses/adapter.ts +1 -1
  457. package/src/transport/surface/responses/errors.ts +0 -9
  458. package/src/transport/vision-handoff-cache.ts +58 -0
  459. package/src/transport/vision-handoff.ts +376 -0
  460. package/src/workers/account-identity-sweep.ts +2 -2
  461. package/src/workers/buddy-growth-tasks.ts +392 -346
  462. package/src/workers/daily-checkin.ts +102 -49
  463. package/src/workers/maintenance-lease.ts +2 -35
  464. package/src/workers/oauth-refresh-sweep.ts +8 -1
  465. package/src/workers/quota-refresh-sweep.ts +120 -82
  466. package/src/workers/scheduler.ts +67 -4
  467. package/src/workers/sweep-runner.ts +24 -5
  468. package/src/workers/zcode-gift-claim.ts +108 -0
  469. package/dist/dashboard/assets/AccountCooldown-Bjo92Qat.js +0 -1
  470. package/dist/dashboard/assets/AccountCooldown-Bjo92Qat.js.gz +0 -0
  471. package/dist/dashboard/assets/BannedPage-ChG5svFb.js.gz +0 -0
  472. package/dist/dashboard/assets/CliToolDetailPage-BIcWQun7.js.gz +0 -0
  473. package/dist/dashboard/assets/CliToolsPage-Bf2VelDe.js.gz +0 -0
  474. package/dist/dashboard/assets/CombosPage-DCgm5g6g.js +0 -1
  475. package/dist/dashboard/assets/CombosPage-DCgm5g6g.js.gz +0 -0
  476. package/dist/dashboard/assets/ConsoleLogPage-a8mD0QF1.js +0 -1
  477. package/dist/dashboard/assets/ConsoleLogPage-a8mD0QF1.js.gz +0 -0
  478. package/dist/dashboard/assets/CustomizationPage-ggeMSjte.js.gz +0 -0
  479. package/dist/dashboard/assets/HealthEventsModal-DzSWzE_j.js +0 -1
  480. package/dist/dashboard/assets/HealthEventsModal-DzSWzE_j.js.gz +0 -0
  481. package/dist/dashboard/assets/LoginPage-U6AKaJcF.js.gz +0 -0
  482. package/dist/dashboard/assets/ModelPicker-BMS2t8BA.js.gz +0 -0
  483. package/dist/dashboard/assets/OverviewPage-D2D4IiHl.js +0 -1
  484. package/dist/dashboard/assets/OverviewPage-D2D4IiHl.js.gz +0 -0
  485. package/dist/dashboard/assets/ProviderDetailPage-CrNf7j_X.js +0 -5
  486. package/dist/dashboard/assets/ProviderDetailPage-CrNf7j_X.js.gz +0 -0
  487. package/dist/dashboard/assets/ProviderIcon-FB6yS4v1.js +0 -1
  488. package/dist/dashboard/assets/ProviderIcon-FB6yS4v1.js.gz +0 -0
  489. package/dist/dashboard/assets/ProvidersPage-BSDbquc1.js +0 -1
  490. package/dist/dashboard/assets/ProvidersPage-BSDbquc1.js.gz +0 -0
  491. package/dist/dashboard/assets/ProxyPage-lsrx7NYg.js.gz +0 -0
  492. package/dist/dashboard/assets/QuotaPage-L0skzML8.js +0 -1
  493. package/dist/dashboard/assets/QuotaPage-L0skzML8.js.gz +0 -0
  494. package/dist/dashboard/assets/SettingsPage-CmAbjRHW.js +0 -1
  495. package/dist/dashboard/assets/SettingsPage-CmAbjRHW.js.gz +0 -0
  496. package/dist/dashboard/assets/SetupPage-f0DE1h0-.js.gz +0 -0
  497. package/dist/dashboard/assets/SortableList-ClzpwyET.js +0 -1
  498. package/dist/dashboard/assets/SortableList-ClzpwyET.js.gz +0 -0
  499. package/dist/dashboard/assets/StudioPage-Cq5tW7hW.js.gz +0 -0
  500. package/dist/dashboard/assets/UsagePage-Dfc9rWQn.js +0 -56
  501. package/dist/dashboard/assets/UsagePage-Dfc9rWQn.js.gz +0 -0
  502. package/dist/dashboard/assets/api-keys-C38iXoO4.js +0 -1
  503. package/dist/dashboard/assets/api-keys-C38iXoO4.js.gz +0 -0
  504. package/dist/dashboard/assets/app-a159k0ub.css +0 -2
  505. package/dist/dashboard/assets/app-a159k0ub.css.gz +0 -0
  506. package/dist/dashboard/assets/assert-DbRA8EG2.js +0 -15
  507. package/dist/dashboard/assets/assert-DbRA8EG2.js.gz +0 -0
  508. package/dist/dashboard/assets/badge-D9CS6dZk.js.gz +0 -0
  509. package/dist/dashboard/assets/banner-DympBNq4.js.gz +0 -0
  510. package/dist/dashboard/assets/card-ibRWU_uT.js.gz +0 -0
  511. package/dist/dashboard/assets/cli-tools-63qBGaRw.js +0 -1
  512. package/dist/dashboard/assets/cli-tools-63qBGaRw.js.gz +0 -0
  513. package/dist/dashboard/assets/clipboard-button-DJm-9pz7.js.gz +0 -0
  514. package/dist/dashboard/assets/dialog-8fiE6tec.js.gz +0 -0
  515. package/dist/dashboard/assets/drawer-CqGYeOrd.js.gz +0 -0
  516. package/dist/dashboard/assets/entry-BSAsYsmV.js.gz +0 -0
  517. package/dist/dashboard/assets/entry-CiIQULZD.js +0 -4
  518. package/dist/dashboard/assets/entry-CiIQULZD.js.gz +0 -0
  519. package/dist/dashboard/assets/entry-Daz0lJ-W.js +0 -1
  520. package/dist/dashboard/assets/entry-Daz0lJ-W.js.gz +0 -0
  521. package/dist/dashboard/assets/formatters-D-MTJxMr.js.gz +0 -0
  522. package/dist/dashboard/assets/github-badge-CPK47xQx.js.gz +0 -0
  523. package/dist/dashboard/assets/index-DBNPOfhV.js.gz +0 -0
  524. package/dist/dashboard/assets/inline-BT0zntEr.js.gz +0 -0
  525. package/dist/dashboard/assets/input-Da2ZViWg.js.gz +0 -0
  526. package/dist/dashboard/assets/layout-BXNeLw6M.js.gz +0 -0
  527. package/dist/dashboard/assets/list-row-BycOrrIV.js.gz +0 -0
  528. package/dist/dashboard/assets/live-DCQNZpF7.js +0 -1
  529. package/dist/dashboard/assets/live-DCQNZpF7.js.gz +0 -0
  530. package/dist/dashboard/assets/network-9Yj2ZyLo.js +0 -1
  531. package/dist/dashboard/assets/network-9Yj2ZyLo.js.gz +0 -0
  532. package/dist/dashboard/assets/numbers-CEagj6ZZ.js +0 -2
  533. package/dist/dashboard/assets/numbers-CEagj6ZZ.js.gz +0 -0
  534. package/dist/dashboard/assets/quota-Caw7wqci.js +0 -1
  535. package/dist/dashboard/assets/quota-Caw7wqci.js.gz +0 -0
  536. package/dist/dashboard/assets/routing-CO9zCUus.js +0 -1
  537. package/dist/dashboard/assets/routing-CO9zCUus.js.gz +0 -0
  538. package/dist/dashboard/assets/select-BIYIBkMU.js.gz +0 -0
  539. package/dist/dashboard/assets/settings-jS3-PNSA.js +0 -1
  540. package/dist/dashboard/assets/settings-jS3-PNSA.js.gz +0 -0
  541. package/dist/dashboard/assets/stack-DfgnFDWY.js.gz +0 -0
  542. package/dist/dashboard/assets/state-CkfdYWel.js.gz +0 -0
  543. package/dist/dashboard/assets/stream-retry-5QnVuSSo.js +0 -1
  544. package/dist/dashboard/assets/stream-retry-5QnVuSSo.js.gz +0 -0
  545. package/dist/dashboard/assets/switch-SYm237HG.js.gz +0 -0
  546. package/dist/dashboard/assets/useMutation-BQqCRNIi.js.gz +0 -0
  547. package/src/network/response-headers.ts +0 -39
  548. package/src/protocol/response/dsml.ts +0 -38
  549. package/src/providers/integrations/xiaomi-mimo/think-stream.ts +0 -229
  550. /package/dist/dashboard/assets/{app-BayTTy01.js → app-Bgd3zANQ.js} +0 -0
  551. /package/dist/dashboard/assets/{app-BayTTy01.js.gz → app-Bgd3zANQ.js.gz} +0 -0
  552. /package/dist/dashboard/assets/brand/{og_bansos.webp → og_bansos_initial.webp} +0 -0
  553. /package/dist/dashboard/assets/brand/{og_image.webp → og_image_initial.webp} +0 -0
@@ -10,7 +10,7 @@ import { globalOrOwnedBy, ownedByOnly } from "../../../persistence/tenant-scope"
10
10
  import { nextSortIndex, reorderSortIndex } from "../../../persistence/sort-order";
11
11
  import type { CartethyiaDatabase } from "../../../persistence/db-handle";
12
12
  import { buddyTaskLedger, models, networkPools, providerAccounts, providerOauthStates, providerPoolAccounts, providers, telemetryEvents, telemetryUsageTotals, tenantDisabledModels } from "../../../persistence/schema";
13
- import { breakdownTelemetry, IS_GATEWAY_ERROR_SQL } from "../../../observability/event-query";
13
+ import { IS_GATEWAY_ERROR_SQL } from "../../../observability/event-query";
14
14
  import type { WireFamily } from "../../../protocol/canonical-model";
15
15
  import { listAccountHealthEvents, recoverAccount, deleteAccountHealthEvents, type AccountHealthEventRecord } from "../../../providers/operations/account-health-service";
16
16
  import { invalidateCredentialCache } from "../../../providers/operations/provider-credential-service";
@@ -679,15 +679,13 @@ export class DrizzleProviderCatalogStore implements ProviderCatalogStore {
679
679
  rows: readonly (typeof providerAccounts.$inferSelect)[],
680
680
  ): Promise<readonly ProviderAccountResponse[]> {
681
681
  if (rows.length === 0) return [];
682
- const todayStart = new Date();
683
- todayStart.setUTCHours(0, 0, 0, 0);
684
682
  const accountIds = rows.map((row) => row.id);
685
683
  // Today's per-account usage is one GROUP BY, not a read of every event
686
- // since midnight. `breakdownTelemetry("account")` is capped at 100 groups;
687
- // a page of accounts never exceeds it, and the lookup below is by id, so a
688
- // group that fell outside the cap simply reads as "no usage today" — the
689
- // same value an account with no traffic gets.
690
- const todayGroups = await breakdownTelemetry(this.db, tenantId, "account", todayStart);
684
+ // since midnight. The read is scoped to the accounts on this page via the
685
+ // `(tenant, account, created_at)` index — a full-tenant breakdown scanned
686
+ // every of today's events and capped at 100 groups, so an account past
687
+ // the cap silently read as "no usage today".
688
+ const todayGroups = await this.accountTodayUsage(tenantId, accountIds);
691
689
  const todayByAccount = new Map(
692
690
  todayGroups.map((group) => [
693
691
  group.key,
@@ -740,6 +738,52 @@ export class DrizzleProviderCatalogStore implements ProviderCatalogStore {
740
738
  });
741
739
  }
742
740
 
741
+ /**
742
+ * Today's per-account usage for a concrete id set. Scoped to those ids so
743
+ * the `(tenant, account, created_at)` index serves the range instead of a
744
+ * full-tenant window scan; returns exactly one group per account with
745
+ * traffic, so an account is never lost to a top-N cap.
746
+ */
747
+ private async accountTodayUsage(
748
+ tenantId: string,
749
+ accountIds: readonly string[],
750
+ ): Promise<
751
+ Array<{ key: string; requests: number; errors: number; inputTokens: number; outputTokens: number }>
752
+ > {
753
+ if (accountIds.length === 0) return [];
754
+ const todayStart = new Date();
755
+ todayStart.setUTCHours(0, 0, 0, 0);
756
+ const rows = await this.db
757
+ .select({
758
+ accountId: telemetryEvents.accountId,
759
+ requests: sql<number>`count(*)`,
760
+ errors: sql<number>`sum(case when ${IS_GATEWAY_ERROR_SQL} then 1 else 0 end)`,
761
+ inputTokens: sql<number>`coalesce(sum(${telemetryEvents.inputTokens}), 0)`,
762
+ outputTokens: sql<number>`coalesce(sum(${telemetryEvents.outputTokens}), 0)`,
763
+ })
764
+ .from(telemetryEvents)
765
+ .where(
766
+ and(
767
+ eq(telemetryEvents.tenantId, tenantId),
768
+ gte(telemetryEvents.createdAt, todayStart),
769
+ inArray(telemetryEvents.accountId, accountIds),
770
+ ),
771
+ )
772
+ .groupBy(telemetryEvents.accountId);
773
+ return rows.flatMap((row) => {
774
+ if (row.accountId === null) return [];
775
+ return [
776
+ {
777
+ key: row.accountId,
778
+ requests: Number(row.requests),
779
+ errors: Number(row.errors),
780
+ inputTokens: Number(row.inputTokens),
781
+ outputTokens: Number(row.outputTokens),
782
+ },
783
+ ];
784
+ });
785
+ }
786
+
743
787
  private async accountInflightFor(
744
788
  tenantId: string,
745
789
  providerId: string | undefined,
@@ -873,6 +917,27 @@ export class DrizzleProviderCatalogStore implements ProviderCatalogStore {
873
917
  return [...accounts, ...poolAccounts];
874
918
  }
875
919
 
920
+ /**
921
+ * One cheap id-only read of an account set, for callers that need the set
922
+ * (e.g. the routing-inflight reader) without the usage aggregates
923
+ * `listAccounts` computes (P-02). No ORDER BY: the set is order-insensitive.
924
+ */
925
+ async listAccountIds(
926
+ tenantId: string,
927
+ providerId: string,
928
+ ): Promise<readonly string[]> {
929
+ const rows = await this.db
930
+ .select({ id: providerAccounts.id })
931
+ .from(providerAccounts)
932
+ .where(
933
+ and(
934
+ eq(providerAccounts.providerId, providerId),
935
+ globalOrOwnedBy(providerAccounts.tenantId, tenantId),
936
+ ),
937
+ );
938
+ return rows.map((row) => row.id);
939
+ }
940
+
876
941
  async listAllAccounts(tenantId: string): Promise<readonly ProviderAccountResponse[]> {
877
942
  const rows = await this.db
878
943
  .select()
@@ -885,6 +950,77 @@ export class DrizzleProviderCatalogStore implements ProviderCatalogStore {
885
950
  return [...accounts, ...poolAccounts];
886
951
  }
887
952
 
953
+ async listAllAccountSummaries(
954
+ tenantId: string,
955
+ ): Promise<readonly { providerId: string; status: string }[]> {
956
+ const rows = await this.db
957
+ .select({ providerId: providerAccounts.providerId, status: providerAccounts.status })
958
+ .from(providerAccounts)
959
+ .where(globalOrOwnedBy(providerAccounts.tenantId, tenantId));
960
+ return rows.map((row) => ({ providerId: row.providerId, status: row.status }));
961
+ }
962
+
963
+ /**
964
+ * Per-provider account totals for one tenant, from a single grouped read (one
965
+ * statement, served by the `(provider_id, tenant_id)` index). `active` counts
966
+ * every status except `disabled` — an account parked by the health machine
967
+ * (`cooldown`) is still a connection, matching how the account-overview badge
968
+ * reads status. Used by the provider list overview (`accountCount` /
969
+ * `activeAccountCount`); totals are never counted in a per-provider loop.
970
+ */
971
+ async providerAccountCounts(
972
+ tenantId: string,
973
+ ): Promise<readonly { providerId: string; total: number; active: number }[]> {
974
+ const rows = await this.db
975
+ .select({
976
+ providerId: providerAccounts.providerId,
977
+ total: sql<number>`count(*)`,
978
+ active: sql<number>`sum(case when ${providerAccounts.status} <> 'disabled' then 1 else 0 end)`,
979
+ })
980
+ .from(providerAccounts)
981
+ .where(globalOrOwnedBy(providerAccounts.tenantId, tenantId))
982
+ .groupBy(providerAccounts.providerId);
983
+ return rows.map((row) => ({
984
+ providerId: row.providerId,
985
+ total: Number(row.total),
986
+ active: Number(row.active),
987
+ }));
988
+ }
989
+
990
+ /**
991
+ * Per-provider enabled model-row counts for one tenant, from a single grouped
992
+ * read over `models ⋈ providers`. A model counts only when the row is enabled
993
+ * and the tenant has not suppressed it via `tenant_disabled_models` — the
994
+ * same visibility rule the per-provider model listing applies, so the
995
+ * overview badge never disagrees with what the detail page would list.
996
+ */
997
+ async providerModelCounts(
998
+ tenantId: string,
999
+ ): Promise<readonly { providerId: string; count: number }[]> {
1000
+ const rows = await this.db
1001
+ .select({ providerId: models.providerId, count: sql<number>`count(*)` })
1002
+ .from(models)
1003
+ .innerJoin(providers, eq(models.providerId, providers.id))
1004
+ .leftJoin(
1005
+ tenantDisabledModels,
1006
+ and(
1007
+ eq(tenantDisabledModels.tenantId, tenantId),
1008
+ eq(tenantDisabledModels.providerId, models.providerId),
1009
+ eq(tenantDisabledModels.modelId, models.modelId),
1010
+ eq(tenantDisabledModels.endpointPath, models.endpointPath),
1011
+ ),
1012
+ )
1013
+ .where(
1014
+ and(
1015
+ eq(models.enabled, true),
1016
+ globalOrOwnedBy(providers.tenantId, tenantId),
1017
+ isNull(tenantDisabledModels.id),
1018
+ ),
1019
+ )
1020
+ .groupBy(models.providerId);
1021
+ return rows.map((row) => ({ providerId: row.providerId, count: Number(row.count) }));
1022
+ }
1023
+
888
1024
  /**
889
1025
  * Pool-as-account rows for one provider. Only providers whose rate limit
890
1026
  * follows the egress address materialize these; every other provider's pool
@@ -1391,6 +1527,8 @@ export class DrizzleProviderCatalogStore implements ProviderCatalogStore {
1391
1527
  typeof authState?.["tokenExpiresAt"] === "string" ? authState["tokenExpiresAt"] : undefined;
1392
1528
  const jwtIssuer =
1393
1529
  typeof authState?.["jwtIssuer"] === "string" ? authState["jwtIssuer"] : undefined;
1530
+ const authMethod =
1531
+ typeof authState?.["authMethod"] === "string" ? authState["authMethod"] : undefined;
1394
1532
  return {
1395
1533
  id: row.id,
1396
1534
  providerId: row.providerId,
@@ -1400,6 +1538,7 @@ export class DrizzleProviderCatalogStore implements ProviderCatalogStore {
1400
1538
  ...(credentialMode === undefined ? {} : { credentialMode }),
1401
1539
  ...(tokenExpiresAt === undefined ? {} : { tokenExpiresAt }),
1402
1540
  ...(jwtIssuer === undefined ? {} : { jwtIssuer }),
1541
+ ...(authMethod === undefined ? {} : { authMethod }),
1403
1542
  status: row.status,
1404
1543
  ...(usage.inflight === undefined ? {} : { inflight: usage.inflight }),
1405
1544
  usageToday: usage.today,
@@ -30,7 +30,7 @@ const ROUTING_BOUNDS = {
30
30
  maxInflightPerProxy: PROVIDER_ROUTING_MAX_INFLIGHT_BOUNDS,
31
31
  } as const;
32
32
 
33
- export function createProviderDetailOperations(deps: ProviderDetailConfig) {
33
+ function createProviderDetailOperations(deps: ProviderDetailConfig) {
34
34
  const operations = {
35
35
  async getRouting(
36
36
  access: AccessDecision | undefined,
@@ -28,7 +28,7 @@ import { log } from "../../../observability/logger";
28
28
  import { ConsoleDomainError } from "../../shared/errors";
29
29
 
30
30
  /** A loopback endpoint a callback can actually be delivered to. */
31
- export interface LoopbackCallbackEndpoint {
31
+ interface LoopbackCallbackEndpoint {
32
32
  readonly port: number;
33
33
  readonly path: string;
34
34
  }
@@ -44,7 +44,7 @@ const LOOPBACK_HOSTS = new Set(["127.0.0.1", "localhost", "::1", "[::1]"]);
44
44
  * installed app and a remote host is delivered over the network; neither is
45
45
  * something this process can bind, and both keep the manual path.
46
46
  */
47
- export function loopbackCallbackEndpoint(redirectUri: string): LoopbackCallbackEndpoint | undefined {
47
+ function loopbackCallbackEndpoint(redirectUri: string): LoopbackCallbackEndpoint | undefined {
48
48
  let url: URL;
49
49
  try {
50
50
  url = new URL(redirectUri);
@@ -59,12 +59,19 @@ export function loopbackCallbackEndpoint(redirectUri: string): LoopbackCallbackE
59
59
  }
60
60
 
61
61
  /** Completes one callback once its code and state have been read. */
62
- export interface OAuthCallbackCompleter {
62
+ interface OAuthCallbackCompleter {
63
63
  complete(
64
64
  providerId: string,
65
65
  code: string,
66
66
  state: string,
67
- ): Promise<{ readonly ok: boolean; readonly message: string }>;
67
+ /** Extra query params the callback carried (e.g. Kiro's `login_option`). */
68
+ callbackParams?: Readonly<Record<string, string>>,
69
+ ): Promise<{
70
+ readonly ok: boolean;
71
+ readonly message: string;
72
+ readonly redirectUrl?: string;
73
+ readonly continueState?: string;
74
+ }>;
68
75
  }
69
76
 
70
77
  /** Minimal `Bun.serve` surface this module depends on. */
@@ -79,7 +86,7 @@ type ServeFn = (options: {
79
86
  readonly fetch: (request: Request) => Response | Promise<Response>;
80
87
  }) => LoopbackServer;
81
88
 
82
- export interface CallbackListenerOptions {
89
+ interface CallbackListenerOptions {
83
90
  readonly completer: OAuthCallbackCompleter;
84
91
  /** Injected in tests; defaults to `Bun.serve`. */
85
92
  readonly serve?: ServeFn;
@@ -92,7 +99,10 @@ const DEFAULT_TTL_MS = 900_000;
92
99
 
93
100
  interface RegisteredFlow {
94
101
  readonly providerId: string;
102
+ /** Paths this flow's callback may arrive on (e.g. Kiro serves both `/oauth/callback` and `/signin/callback`). */
103
+ readonly paths: readonly string[];
95
104
  readonly timer: ReturnType<typeof setTimeout>;
105
+ inFlight: boolean;
96
106
  }
97
107
 
98
108
  interface PortListener {
@@ -134,17 +144,30 @@ export class OAuthCallbackListener {
134
144
  if (endpoint === undefined) return false;
135
145
  const existing = this.#ports.get(endpoint.port);
136
146
  if (existing !== undefined) {
137
- this.#remember(existing, state, providerId);
147
+ this.#remember(existing, state, providerId, this.#callbackPaths(endpoint.path));
138
148
  return true;
139
149
  }
140
150
  const servers = this.#bind(endpoint);
141
151
  const listener: PortListener = { port: endpoint.port, servers, flows: new Map() };
142
152
  this.#ports.set(endpoint.port, listener);
143
- this.#remember(listener, state, providerId);
153
+ this.#remember(listener, state, providerId, this.#callbackPaths(endpoint.path));
144
154
  log.info(`[oauth] callback listener bound on 127.0.0.1:${endpoint.port}${endpoint.path}`);
145
155
  return true;
146
156
  }
147
157
 
158
+ /**
159
+ * The paths one login may deliver its callback on. Kiro's portal is observed
160
+ * (unpacked IDE 1.4.62) to redirect to both `/oauth/callback` and
161
+ * `/signin/callback`; binding only the advertised path left a redirect to the
162
+ * other one hitting a 404 and the login hanging. Other providers neither
163
+ * register nor expect `/signin/callback`, so adding it is harmless.
164
+ */
165
+ #callbackPaths(advertised: string): readonly string[] {
166
+ return advertised === "/signin/callback"
167
+ ? [advertised]
168
+ : [advertised, "/signin/callback"];
169
+ }
170
+
148
171
  /** Drops a flow and frees its port when nothing else is waiting on it. */
149
172
  release(redirectUri: string, state: string): void {
150
173
  const endpoint = loopbackCallbackEndpoint(redirectUri);
@@ -154,6 +177,25 @@ export class OAuthCallbackListener {
154
177
  this.#forget(endpoint.port, listener, state);
155
178
  }
156
179
 
180
+ /**
181
+ * Re-registers an existing flow state under a new callback state.
182
+ *
183
+ * Kiro's two-phase portal login starts a second authorization leg with a
184
+ * *fresh* state (the IDE mints a new UUID for the IdC authorize step), so
185
+ * the loopback listener must retain the pending flow under that second
186
+ * state too — otherwise the phase-2 callback answers "OAuth session not
187
+ * found or expired" even though the flow is still alive.
188
+ */
189
+ track(redirectUri: string, state: string, newState: string): void {
190
+ const endpoint = loopbackCallbackEndpoint(redirectUri);
191
+ if (endpoint === undefined) return;
192
+ const listener = this.#ports.get(endpoint.port);
193
+ if (listener === undefined) return;
194
+ const flow = listener.flows.get(state);
195
+ if (flow === undefined) return;
196
+ this.#remember(listener, newState, flow.providerId, flow.paths);
197
+ }
198
+
157
199
  /**
158
200
  * Drops every flow for `providerId` on `redirectUri`'s port.
159
201
  *
@@ -182,7 +224,7 @@ export class OAuthCallbackListener {
182
224
  }
183
225
  }
184
226
 
185
- #remember(listener: PortListener, state: string, providerId: string): void {
227
+ #remember(listener: PortListener, state: string, providerId: string, paths: readonly string[]): void {
186
228
  const previous = listener.flows.get(state);
187
229
  if (previous !== undefined) clearTimeout(previous.timer);
188
230
  const timer = setTimeout(() => {
@@ -190,7 +232,7 @@ export class OAuthCallbackListener {
190
232
  }, this.#ttlMs);
191
233
  // A pending login must not hold the process open on its own.
192
234
  (timer as unknown as { unref?: () => void }).unref?.();
193
- listener.flows.set(state, { providerId, timer });
235
+ listener.flows.set(state, { providerId, paths, timer, inFlight: false });
194
236
  }
195
237
 
196
238
  #forget(port: number, listener: PortListener, state: string): void {
@@ -250,9 +292,6 @@ export class OAuthCallbackListener {
250
292
  endpoint: LoopbackCallbackEndpoint,
251
293
  ): Promise<Response> {
252
294
  const url = new URL(request.url);
253
- if (url.pathname !== endpoint.path) {
254
- return new Response("Not found", { status: 404 });
255
- }
256
295
  const listener = this.#ports.get(endpoint.port);
257
296
  if (listener === undefined) {
258
297
  return new Response("OAuth session not found or expired.", { status: 400 });
@@ -278,7 +317,7 @@ export class OAuthCallbackListener {
278
317
  { status: 400, headers: { "content-type": "text/html; charset=utf-8" } },
279
318
  );
280
319
  }
281
- if (code.length === 0) {
320
+ if (code.length === 0 && !url.searchParams.has("login_option")) {
282
321
  return new Response("Missing authorization code.", { status: 400 });
283
322
  }
284
323
  // `state` is the correlation key and the replay guard. An authorization
@@ -297,14 +336,45 @@ export class OAuthCallbackListener {
297
336
  if (matched === undefined) {
298
337
  return new Response("OAuth session not found or expired.", { status: 400 });
299
338
  }
300
- // Claim the flow before awaiting: a browser that retries the redirect must
301
- // not start a second exchange with a code that is already being spent.
339
+ // Some providers (Kiro's portal) can land the callback on any of several
340
+ // registered paths — `/oauth/callback` or `/signin/callback`. Accept the
341
+ // path the flow registered for, not only the advertised default.
342
+ if (!matched.paths.includes(url.pathname)) {
343
+ return new Response("Not found", { status: 404 });
344
+ }
345
+ if (matched.inFlight) {
346
+ return new Response("OAuth callback is already being processed.", { status: 409 });
347
+ }
348
+ // Claim while the exchange is in flight. A multi-step flow (Kiro OIDC) may
349
+ // return a redirect and reuse this listener/state for its next callback.
350
+ matched.inFlight = true;
302
351
  const matchedState = state.length > 0 ? state : this.#stateOf(listener, matched);
303
- if (matchedState !== undefined) this.#forget(endpoint.port, listener, matchedState);
304
- let result: { ok: boolean; message: string };
352
+ let result: {
353
+ ok: boolean;
354
+ message: string;
355
+ redirectUrl?: string;
356
+ continueState?: string;
357
+ };
358
+ // Some authorization servers (Kiro's portal) append provider-specific query
359
+ // params — `login_option` names the social/enterprise family that approved.
360
+ // The exchange needs them to rebuild the exact redirect URI the portal saw,
361
+ // and the OIDC families carry `issuer_url`/`idc_region`/`client_id`/`scopes`
362
+ // that the continuation flow needs. Forward every param other than the ones
363
+ // this handler already consumed.
364
+ const callbackParams: Record<string, string> = {};
365
+ for (const [key, value] of url.searchParams) {
366
+ if (key === "code" || key === "state" || key === "error" || key === "error_description") continue;
367
+ callbackParams[key] = value;
368
+ }
305
369
  try {
306
- result = await this.#completer.complete(matched.providerId, code, state);
370
+ result = await this.#completer.complete(
371
+ matched.providerId,
372
+ code,
373
+ state,
374
+ Object.keys(callbackParams).length > 0 ? callbackParams : undefined,
375
+ );
307
376
  } catch (failure) {
377
+ if (matchedState !== undefined) this.#forget(endpoint.port, listener, matchedState);
308
378
  const message = failure instanceof Error ? failure.message : "OAuth login failed";
309
379
  log.error(`[oauth] callback completion failed for ${matched.providerId}: ${message}`);
310
380
  return new Response(
@@ -312,6 +382,18 @@ export class OAuthCallbackListener {
312
382
  { status: 500, headers: { "content-type": "text/html; charset=utf-8" } },
313
383
  );
314
384
  }
385
+ if (result.redirectUrl !== undefined) {
386
+ // Phase 1 of a two-phase login (Kiro OIDC): the flow is still pending, so
387
+ // continue by sending the browser to the phase-2 authorize URL. The phase-2
388
+ // authorize URL carries a fresh state (`continueState`), which the completer
389
+ // already registered under — keep the current matcher alive for it.
390
+ matched.inFlight = false;
391
+ return new Response(null, {
392
+ status: 302,
393
+ headers: { location: result.redirectUrl },
394
+ });
395
+ }
396
+ if (matchedState !== undefined) this.#forget(endpoint.port, listener, matchedState);
315
397
  if (!result.ok) {
316
398
  return new Response(
317
399
  `<!doctype html><html><body><p>OAuth login failed: ${escapeHtml(result.message)}</p></body></html>`,
@@ -171,7 +171,7 @@ export class DrizzleOAuthAccountStore implements OAuthAccountStore {
171
171
  * server the redirect arrives at; `callback-listener.ts` is what makes those
172
172
  * the same endpoint.
173
173
  */
174
- export interface PersistedOAuthAccount {
174
+ interface PersistedOAuthAccount {
175
175
  readonly accountId: string;
176
176
  }
177
177
 
@@ -212,9 +212,20 @@ async function requireClient(
212
212
  }
213
213
 
214
214
  /** The result of exchanging one authorization code, as the caller should report it. */
215
- export interface OAuthCompleteOutcome {
215
+ interface OAuthCompleteOutcome {
216
216
  readonly ok: boolean;
217
217
  readonly message: string;
218
+ /**
219
+ * When set, the exchange is not finished: the browser must be redirected to
220
+ * this URL to continue (Kiro's OIDC families need a second authorization
221
+ * leg after the portal callback).
222
+ */
223
+ readonly redirectUrl?: string;
224
+ /**
225
+ * New callback state the phase-2 authorize URL carries (Kiro mints a fresh
226
+ * state for the IdC leg; the console retains the pending flow under it).
227
+ */
228
+ readonly continueState?: string;
218
229
  }
219
230
 
220
231
  /**
@@ -231,29 +242,41 @@ export async function completeLogin(
231
242
  providerId: string,
232
243
  code: string,
233
244
  state: string,
245
+ callbackParams?: Readonly<Record<string, string>>,
234
246
  ): Promise<OAuthCompleteOutcome> {
235
247
  // An empty `state` means the authorization server does not echo one
236
248
  // (OpenRouter omits it from the callback entirely); correlate by provider in
237
249
  // that case, and by state otherwise so the stronger check stays in force
238
250
  // wherever the server supports it.
251
+ //
252
+ // The flow is peeked, not consumed, until the exchange succeeds: consuming
253
+ // first means a failed exchange (a transient upstream blip, a paste that
254
+ // missed a parameter) burns the state and every retry answers "unknown or
255
+ // expired state". The state is deleted only after the account persists.
239
256
  const flow = state
240
- ? await config.oauthFlowStore.consumePending(state)
257
+ ? await config.oauthFlowStore.peekPending(state)
241
258
  : await config.oauthFlowStore.consumePendingByProvider(providerId);
242
259
  if (!flow || flow.providerId !== providerId) {
243
260
  return { ok: false, message: "unknown or expired state" };
244
261
  }
245
- // The flow is spent, so its listener registration has nothing left to wait
246
- // for; releasing it here keeps a manual paste from leaving the port bound.
247
- // Stateless callbacks arrive with `""`, which matches no filed state —
248
- // release by provider so the OpenRouter port does not linger until TTL.
249
- if (state.length > 0) config.callbackListener.release(flow.redirectUri, state);
250
- else config.callbackListener.releaseProvider(flow.redirectUri, providerId);
262
+ // Release the listener registration for this flow. The pending state itself
263
+ // stays available so a manual re-paste can retry the exchange. The release is
264
+ // deferred until after the exchange for two-phase flows: Kiro's OIDC portal
265
+ // callback (phase 1) must keep the loopback listener alive so the second
266
+ // authorization leg can land on it.
251
267
  const client = await requireClient(config.providerRegistry, providerId);
252
268
  if (!client.exchangeCode) {
253
269
  return { ok: false, message: "provider does not support browser authorization" };
254
270
  }
271
+ const releaseListener = () => {
272
+ if (state.length > 0) config.callbackListener.release(flow.redirectUri, state);
273
+ else config.callbackListener.releaseProvider(flow.redirectUri, providerId);
274
+ };
255
275
  try {
256
- const result = await client.exchangeCode(code, flow.codeVerifier, flow.redirectUri, state);
276
+ const result = await client.exchangeCode(code, flow.codeVerifier, flow.redirectUri, state, {
277
+ ...(callbackParams === undefined ? {} : { parameters: callbackParams }),
278
+ ...(flow.providerState === undefined ? {} : { providerState: flow.providerState }),
279
+ });
257
280
  const tokenCheck = await validateIssuedAccessToken(
258
281
  result.access,
259
282
  providerJwtVerification(providerId),
@@ -270,9 +293,51 @@ export async function completeLogin(
270
293
  label: result.accountLabel ?? flow.accountLabel,
271
294
  ...result,
272
295
  });
296
+ // The exchange succeeded and the account is persisted; now the flow is
297
+ // spent. Consuming it here (peeked above) frees the state and the provider
298
+ // pointer so a replayed callback cannot re-create the account.
299
+ releaseListener();
300
+ if (state.length > 0) await config.oauthFlowStore.consumePending(state);
301
+ else await config.oauthFlowStore.consumePendingByProvider(providerId);
273
302
  await config.snapshotInvalidator?.invalidate();
274
303
  return { ok: true, message: `${providerId} account connected. You may close this tab.` };
275
304
  } catch (error) {
305
+ // Kiro's OIDC families: the portal callback carries no code — only
306
+ // `login_option` + issuer/region. The client responded with the second-leg
307
+ // authorize URL plus a fresh PKCE verifier and the OIDC registration.
308
+ // Rewrite the pending flow so phase 2 exchanges against that verifier, keep
309
+ // the listener alive, and tell the caller to redirect the browser there.
310
+ // dynamic-import: B-02 lazy graph — 58KB kiro OIDC path, only for Kiro flows.
311
+ // The kiro-oauth graph is 58KB, so it is imported lazily here — only the
312
+ // Kiro OIDC path needs the error class — instead of on the console boot
313
+ // graph (B-02).
314
+ const { KiroOidcPendingError } = await import(
315
+ "../../../providers/integrations/kiro/kiro-oauth"
316
+ );
317
+ if (error instanceof KiroOidcPendingError) {
318
+ // Phase 1 of Kiro's OIDC portal login: the portal callback carried no
319
+ // code — only `login_option` + issuer/region. The client answered with the
320
+ // phase-2 authorize URL, a fresh PKCE verifier, and the OIDC client
321
+ // registration. Mirror the 1.4.62 IDE: the second leg uses a *new* state
322
+ // (not the portal's), so mint it here, keep the pending flow reachable
323
+ // under both states, and tell the caller which state to expect back.
324
+ const continueState = error.continueState;
325
+ if (state.length > 0 && continueState.length > 0) {
326
+ await config.oauthFlowStore.savePending(continueState, {
327
+ ...flow,
328
+ codeVerifier: error.codeVerifier,
329
+ providerState: error.providerState,
330
+ });
331
+ config.callbackListener.track(flow.redirectUri, state, continueState);
332
+ }
333
+ return {
334
+ ok: false,
335
+ message: error.message,
336
+ redirectUrl: error.authorizeUrl,
337
+ continueState,
338
+ };
339
+ }
340
+ releaseListener();
276
341
  // Upstream token-endpoint bodies can echo credentials or sensitive
277
342
  // diagnostics — the browser gets a generic failure; the bounded detail
278
343
  // stays server-side in the console log.
@@ -351,7 +416,7 @@ function failurePage(message: string, wantsJson: boolean): Response {
351
416
  );
352
417
  }
353
418
 
354
- export function createOAuthLoginOperations(config: OAuthLoginConfig) {
419
+ function createOAuthLoginOperations(config: OAuthLoginConfig) {
355
420
  const operations = {
356
421
  async beginAuthorize(
357
422
  access: AccessDecision | undefined,
@@ -437,8 +502,15 @@ export function createOAuthLoginOperations(config: OAuthLoginConfig) {
437
502
  code: string,
438
503
  state: string,
439
504
  wantsJson = false,
505
+ callbackParams?: Readonly<Record<string, string>>,
440
506
  ): Promise<Response> {
441
- const outcome = await completeLogin(config, providerId, code, state);
507
+ const outcome = await completeLogin(config, providerId, code, state, callbackParams);
508
+ if (outcome.redirectUrl !== undefined) {
509
+ return new Response(null, {
510
+ status: 302,
511
+ headers: { location: outcome.redirectUrl },
512
+ });
513
+ }
442
514
  return outcome.ok ? successPage(providerId, wantsJson) : failurePage(outcome.message, wantsJson);
443
515
  },
444
516
  async handleCallbackError(
@@ -670,7 +742,14 @@ export function createOAuthLoginRoutes(config: OAuthLoginConfig): AnyElysia {
670
742
  // `state` is absent for authorization servers that do not echo it
671
743
  // (OpenRouter). The flow is then correlated by provider instead — one
672
744
  // browser login is in flight per provider at a time.
673
- return factory.handleCallback(params.providerId, code, state, wantsJson);
745
+ const loginOption = typeof query["login_option"] === "string" ? query["login_option"] : undefined;
746
+ return factory.handleCallback(
747
+ params.providerId,
748
+ code,
749
+ state,
750
+ wantsJson,
751
+ loginOption === undefined ? undefined : { login_option: loginOption },
752
+ );
674
753
  })
675
754
  .post(
676
755
  "/:providerId/oauth/device/start",
@@ -1,10 +1,17 @@
1
1
  import { t } from "elysia";
2
2
  import type { Elysia } from "elysia";
3
- import { and, eq, isNull } from "drizzle-orm";
3
+ import { and, eq, inArray, isNull } from "drizzle-orm";
4
4
  import { adminAuditLog, providerAccounts } from "../../persistence/schema";
5
5
  import { requireGlobalAdmin } from "../shared/errors";
6
- import { GLOBAL_QUOTA_LENS, invalidateQuotaCache } from "../../providers/quota/cache";
7
- import { invalidateCredentialCache } from "../../providers/operations/provider-credential-service";
6
+ import {
7
+ GLOBAL_QUOTA_LENS,
8
+ invalidateQuotaCache,
9
+ invalidateQuotaCaches,
10
+ } from "../../providers/quota/cache";
11
+ import {
12
+ invalidateCredentialCache,
13
+ invalidateCredentialCaches,
14
+ } from "../../providers/operations/provider-credential-service";
8
15
  import {
9
16
  QUOTA_REFRESH_TIMEOUT_MS,
10
17
  loadQuotaTarget,
@@ -119,6 +126,37 @@ export function registerAccountQuotaGlobalRoutes(
119
126
  });
120
127
  return { success: true };
121
128
  })
129
+ .post("/global/accounts/batch-delete", {
130
+ body: t.Object({
131
+ accountIds: t.Array(t.String()),
132
+ }),
133
+ }, async ({ request, body }) => {
134
+ const access = requireGlobalAdmin(accessResolver(request));
135
+ const accountIds = [...new Set(body.accountIds)];
136
+ if (accountIds.length === 0) {
137
+ return { ok: true, deleted: [], missing: [] };
138
+ }
139
+ // One DELETE statement scoped to shared accounts only: a tenant-owned row
140
+ // (`tenant_id IS NOT NULL`) is never matchable by a global admin, so it
141
+ // lands in `missing`.
142
+ const deleted = await db
143
+ .delete(providerAccounts)
144
+ .where(and(inArray(providerAccounts.id, accountIds), isNull(providerAccounts.tenantId)))
145
+ .returning({ id: providerAccounts.id });
146
+ const deletedIds = deleted.map((row) => row.id);
147
+ const deletedSet = new Set(deletedIds);
148
+ const missing = accountIds.filter((id) => !deletedSet.has(id));
149
+ invalidateCredentialCaches(deletedIds);
150
+ await invalidateQuotaCaches(GLOBAL_QUOTA_LENS, deletedIds, redis);
151
+ await snapshotInvalidator?.invalidate();
152
+ await db.insert(adminAuditLog).values({
153
+ actor: access.admissionIdentity,
154
+ action: "provider_account.global.batch_deleted",
155
+ target: "global",
156
+ detail: { deleted: deletedIds, missing },
157
+ });
158
+ return { ok: true, deleted: deletedIds, missing };
159
+ })
122
160
  .post("/global/accounts/:id/quota/refresh", async ({ request, params, set }) => {
123
161
  const access = requireGlobalAdmin(accessResolver(request));
124
162
  const target = await loadQuotaTarget(db, params.id, null);