cartethyia 2.5.3 → 2.5.6

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 (456) hide show
  1. package/.env.example +13 -4
  2. package/README.md +2 -2
  3. package/dist/dashboard/assets/{BannedPage-hfCzi-Ux.js → BannedPage-CADDq4IU.js} +1 -1
  4. package/dist/dashboard/assets/BannedPage-CADDq4IU.js.gz +0 -0
  5. package/dist/dashboard/assets/CliToolDetailPage-_z1hID7E.js +1 -0
  6. package/dist/dashboard/assets/CliToolDetailPage-_z1hID7E.js.gz +0 -0
  7. package/dist/dashboard/assets/{CliToolsPage-DEDyKBlC.js → CliToolsPage-DuO1Z7dq.js} +1 -1
  8. package/dist/dashboard/assets/CliToolsPage-DuO1Z7dq.js.gz +0 -0
  9. package/dist/dashboard/assets/CombosPage-DNEPi3xU.js +1 -0
  10. package/dist/dashboard/assets/CombosPage-DNEPi3xU.js.gz +0 -0
  11. package/dist/dashboard/assets/{ConsoleLogPage-bsLuZmYH.js → ConsoleLogPage-BVdpfiz1.js} +1 -1
  12. package/dist/dashboard/assets/ConsoleLogPage-BVdpfiz1.js.gz +0 -0
  13. package/dist/dashboard/assets/{CustomizationPage-_UWhwziJ.js → CustomizationPage-BR5HHvbi.js} +1 -1
  14. package/dist/dashboard/assets/CustomizationPage-BR5HHvbi.js.gz +0 -0
  15. package/dist/dashboard/assets/HealthEventsModal-mUfcknqm.js +1 -0
  16. package/dist/dashboard/assets/HealthEventsModal-mUfcknqm.js.gz +0 -0
  17. package/dist/dashboard/assets/{LoginPage-DDVIfZGW.js → LoginPage-V5faV9V2.js} +1 -1
  18. package/dist/dashboard/assets/LoginPage-V5faV9V2.js.gz +0 -0
  19. package/dist/dashboard/assets/ModelPicker-C7pDb41R.js +1 -0
  20. package/dist/dashboard/assets/ModelPicker-C7pDb41R.js.gz +0 -0
  21. package/dist/dashboard/assets/OverviewPage-BDu20Jdx.js +1 -0
  22. package/dist/dashboard/assets/OverviewPage-BDu20Jdx.js.gz +0 -0
  23. package/dist/dashboard/assets/ProviderDetailPage-Dllr_aFO.js +3 -0
  24. package/dist/dashboard/assets/ProviderDetailPage-Dllr_aFO.js.gz +0 -0
  25. package/dist/dashboard/assets/ProvidersPage-uPiGVTTK.js +1 -0
  26. package/dist/dashboard/assets/ProvidersPage-uPiGVTTK.js.gz +0 -0
  27. package/dist/dashboard/assets/ProxyPage-BaUkO-4O.js +7 -0
  28. package/dist/dashboard/assets/ProxyPage-BaUkO-4O.js.gz +0 -0
  29. package/dist/dashboard/assets/QuotaPage-o0IFAsFb.js +1 -0
  30. package/dist/dashboard/assets/QuotaPage-o0IFAsFb.js.gz +0 -0
  31. package/dist/dashboard/assets/SettingsPage-CYWqbXjp.js +1 -0
  32. package/dist/dashboard/assets/SettingsPage-CYWqbXjp.js.gz +0 -0
  33. package/dist/dashboard/assets/{SetupPage-D6pmQ86L.js → SetupPage-UB690SE2.js} +1 -1
  34. package/dist/dashboard/assets/SetupPage-UB690SE2.js.gz +0 -0
  35. package/dist/dashboard/assets/{SortableList-DWvKiuSl.js → SortableList-BLujdX7N.js} +1 -1
  36. package/dist/dashboard/assets/SortableList-BLujdX7N.js.gz +0 -0
  37. package/dist/dashboard/assets/{StudioPage-CmkUrgdj.js → StudioPage-D5C2raAc.js} +3 -3
  38. package/dist/dashboard/assets/StudioPage-D5C2raAc.js.gz +0 -0
  39. package/dist/dashboard/assets/UsagePage-8uIcxa1W.js +56 -0
  40. package/dist/dashboard/assets/UsagePage-8uIcxa1W.js.gz +0 -0
  41. package/dist/dashboard/assets/api-keys-DQYLQxpW.js +1 -0
  42. package/dist/dashboard/assets/api-keys-DQYLQxpW.js.gz +0 -0
  43. package/dist/dashboard/assets/app-CX_Z8cMs.css +2 -0
  44. package/dist/dashboard/assets/app-CX_Z8cMs.css.gz +0 -0
  45. package/dist/dashboard/assets/{assert-NHwYq95t.js → assert-C9FD_JwJ.js} +1 -1
  46. package/dist/dashboard/assets/assert-C9FD_JwJ.js.gz +0 -0
  47. package/dist/dashboard/assets/{badge-6uzDDvqs.js → badge-CR72dasT.js} +1 -1
  48. package/dist/dashboard/assets/badge-CR72dasT.js.gz +0 -0
  49. package/dist/dashboard/assets/{banner-4_WNg2WZ.js → banner-CI9_eSfO.js} +1 -1
  50. package/dist/dashboard/assets/banner-CI9_eSfO.js.gz +0 -0
  51. package/dist/dashboard/assets/{card--AtFedAq.js → card-DS7CQEsO.js} +1 -1
  52. package/dist/dashboard/assets/card-DS7CQEsO.js.gz +0 -0
  53. package/dist/dashboard/assets/{cli-tools-B3e7dx3N.js → cli-tools-CNqzrfdb.js} +1 -1
  54. package/dist/dashboard/assets/cli-tools-CNqzrfdb.js.gz +0 -0
  55. package/dist/dashboard/assets/{clipboard-button-NreUx6c8.js → clipboard-button-611pSsaX.js} +1 -1
  56. package/dist/dashboard/assets/clipboard-button-611pSsaX.js.gz +0 -0
  57. package/dist/dashboard/assets/{fingerprint-pattern-BWuHtbW4.js → concurrency-BDF0ok7y.js} +1 -1
  58. package/dist/dashboard/assets/concurrency-BDF0ok7y.js.gz +0 -0
  59. package/dist/dashboard/assets/database-Dz9N3zGd.js +1 -0
  60. package/dist/dashboard/assets/database-Dz9N3zGd.js.gz +0 -0
  61. package/dist/dashboard/assets/dialog-k37exjNt.js +1 -0
  62. package/dist/dashboard/assets/dialog-k37exjNt.js.gz +0 -0
  63. package/dist/dashboard/assets/drawer-CX7N8ucR.js +1 -0
  64. package/dist/dashboard/assets/drawer-CX7N8ucR.js.gz +0 -0
  65. package/dist/dashboard/assets/entry-B9Xvtnfj.js +1 -0
  66. package/dist/dashboard/assets/entry-B9Xvtnfj.js.gz +0 -0
  67. package/dist/dashboard/assets/entry-CTP8AyiS.js +1 -0
  68. package/dist/dashboard/assets/entry-CTP8AyiS.js.gz +0 -0
  69. package/dist/dashboard/assets/entry-DxBHnauy.js +4 -0
  70. package/dist/dashboard/assets/entry-DxBHnauy.js.gz +0 -0
  71. package/dist/dashboard/assets/{github-badge-D_eM3IgG.js → github-badge-sltk1qUi.js} +1 -1
  72. package/dist/dashboard/assets/github-badge-sltk1qUi.js.gz +0 -0
  73. package/dist/dashboard/assets/{index-BydyKaDz.js → index-CmxZr14F.js} +2 -2
  74. package/dist/dashboard/assets/index-CmxZr14F.js.gz +0 -0
  75. package/dist/dashboard/assets/{inline-BcNGF8Gr.js → inline-BhY2sSnO.js} +1 -1
  76. package/dist/dashboard/assets/inline-BhY2sSnO.js.gz +0 -0
  77. package/dist/dashboard/assets/{input-BG41et9G.js → input-V1umFiBk.js} +1 -1
  78. package/dist/dashboard/assets/input-V1umFiBk.js.gz +0 -0
  79. package/dist/dashboard/assets/layout-BBOYycyS.js +1 -0
  80. package/dist/dashboard/assets/layout-BBOYycyS.js.gz +0 -0
  81. package/dist/dashboard/assets/{list-row-Gh_W4OR_.js → list-row-1zzZYM0B.js} +1 -1
  82. package/dist/dashboard/assets/list-row-1zzZYM0B.js.gz +0 -0
  83. package/dist/dashboard/assets/{live-BJGYRBnR.js → live-BivGfuUs.js} +1 -1
  84. package/dist/dashboard/assets/live-BivGfuUs.js.gz +0 -0
  85. package/dist/dashboard/assets/model-bans-CmffjLuG.js +1 -0
  86. package/dist/dashboard/assets/model-bans-CmffjLuG.js.gz +0 -0
  87. package/dist/dashboard/assets/{mutation-BkoLM5Ba.js → mutation-46P0oWMe.js} +1 -1
  88. package/dist/dashboard/assets/mutation-46P0oWMe.js.gz +0 -0
  89. package/dist/dashboard/assets/numbers-DkfMR5Qs.js +2 -0
  90. package/dist/dashboard/assets/numbers-DkfMR5Qs.js.gz +0 -0
  91. package/dist/dashboard/assets/{query-keys-D7wX2vM5.js → query-keys-BFhGpwR1.js} +1 -1
  92. package/dist/dashboard/assets/query-keys-BFhGpwR1.js.gz +0 -0
  93. package/dist/dashboard/assets/{routing-CcusqXA7.js → routing-CAb4F37R.js} +1 -1
  94. package/dist/dashboard/assets/routing-CAb4F37R.js.gz +0 -0
  95. package/dist/dashboard/assets/{select-BO6h8yNv.js → select-BYR_5K2c.js} +2 -2
  96. package/dist/dashboard/assets/select-BYR_5K2c.js.gz +0 -0
  97. package/dist/dashboard/assets/{settings-mpmXTgUa.js → settings-DNX9sydi.js} +1 -1
  98. package/dist/dashboard/assets/settings-DNX9sydi.js.gz +0 -0
  99. package/dist/dashboard/assets/{stack-DwLmjUFK.js → stack-BHTfXdz_.js} +1 -1
  100. package/dist/dashboard/assets/stack-BHTfXdz_.js.gz +0 -0
  101. package/dist/dashboard/assets/{state-DYdQmdAl.js → state-mPDRlQkl.js} +1 -1
  102. package/dist/dashboard/assets/state-mPDRlQkl.js.gz +0 -0
  103. package/dist/dashboard/assets/switch-DOmJxg7H.js +1 -0
  104. package/dist/dashboard/assets/switch-DOmJxg7H.js.gz +0 -0
  105. package/dist/dashboard/assets/terminal-0_IjPWTk.js +1 -0
  106. package/dist/dashboard/assets/terminal-0_IjPWTk.js.gz +0 -0
  107. package/dist/dashboard/assets/tokens-COzario9.js +1 -0
  108. package/dist/dashboard/assets/tokens-COzario9.js.gz +0 -0
  109. package/dist/dashboard/assets/use-modal-focus-By7oJoGd.js +1 -0
  110. package/dist/dashboard/assets/use-modal-focus-By7oJoGd.js.gz +0 -0
  111. package/dist/dashboard/assets/{useMutation-w4u4CVue.js → useMutation-Dem0lXQj.js} +1 -1
  112. package/dist/dashboard/assets/useMutation-Dem0lXQj.js.gz +0 -0
  113. package/dist/dashboard/assets/zap-C4EeVW4q.js +1 -0
  114. package/dist/dashboard/assets/zap-C4EeVW4q.js.gz +0 -0
  115. package/dist/dashboard/donation/qris-risun-marke.png +0 -0
  116. package/dist/dashboard/index.html +1 -1
  117. package/dist/dashboard/index.html.gz +0 -0
  118. package/dist/dashboard/precompressed-assets.json +57 -57
  119. package/migrations/README.md +52 -52
  120. package/migrations/lite-sqlite3/0000_baseline.sql +2 -2
  121. package/package.json +96 -95
  122. package/scripts/cli/admin.ts +125 -0
  123. package/scripts/cli/autostart.ts +237 -0
  124. package/scripts/cli/{ui/background.ts → background.ts} +1 -1
  125. package/scripts/cli/{command/console-menu.ts → console-menu.ts} +275 -248
  126. package/scripts/cli/doctor.ts +37 -0
  127. package/scripts/cli/{support/env.ts → env.ts} +99 -99
  128. package/scripts/cli/{gateway/spawn.ts → gateway.ts} +112 -24
  129. package/scripts/cli/instance.ts +224 -0
  130. package/scripts/cli/main.ts +88 -18
  131. package/scripts/cli/{support/platform.ts → platform.ts} +64 -64
  132. package/scripts/cli/port.ts +100 -0
  133. package/scripts/cli/{support/services.ts → services.ts} +88 -88
  134. package/scripts/cli/{command/setup.ts → setup.ts} +35 -34
  135. package/scripts/cli/{command/start.ts → start.ts} +10 -5
  136. package/scripts/cli/{support/startup-signal.ts → startup-signal.ts} +23 -23
  137. package/scripts/cli/tray/index.ts +62 -12
  138. package/scripts/cli/{command/tray.ts → tray.ts} +208 -208
  139. package/scripts/cli/update.ts +40 -0
  140. package/scripts/cli/version.ts +67 -0
  141. package/scripts/cli/{ui/wordmark.ts → wordmark.ts} +58 -58
  142. package/src/boot/app.ts +11 -7
  143. package/src/boot/dependencies.ts +50 -19
  144. package/src/boot/timeout.ts +2 -13
  145. package/src/config/cooldowns.ts +186 -138
  146. package/src/config/network.ts +8 -14
  147. package/src/config/providers.ts +7 -10
  148. package/src/config/runtime.ts +76 -76
  149. package/src/config/security.ts +15 -0
  150. package/src/config/spec.ts +52 -7
  151. package/src/config/telemetry.ts +26 -0
  152. package/src/config/timeouts.ts +0 -6
  153. package/src/console/admin/model-abuse/contracts.ts +28 -6
  154. package/src/console/admin/model-abuse/routes.ts +56 -3
  155. package/src/console/api-keys/bucket-spend.ts +15 -4
  156. package/src/console/api-keys/contracts.ts +410 -414
  157. package/src/console/api-keys/routes.ts +881 -880
  158. package/src/console/auth/credentials.ts +3 -3
  159. package/src/console/auth/setup-service.ts +2 -2
  160. package/src/console/backup/contracts.ts +15 -2
  161. package/src/console/backup/export-endpoint.ts +6 -2
  162. package/src/console/backup/nine-router.ts +2 -5
  163. package/src/console/backup/service.ts +4 -0
  164. package/src/console/backup/store.ts +192 -29
  165. package/src/console/backup/validate.ts +3 -45
  166. package/src/console/cli-tools/fs-ops.ts +487 -487
  167. package/src/console/console-router-guards.ts +1 -1
  168. package/src/console/console-router.ts +8 -0
  169. package/src/console/data-management/contracts.ts +100 -0
  170. package/src/console/data-management/operations.ts +234 -0
  171. package/src/console/data-management/routes.ts +103 -0
  172. package/src/console/domain-registration.ts +45 -1
  173. package/src/console/observability/contracts.ts +524 -485
  174. package/src/console/observability/live.ts +22 -0
  175. package/src/console/observability/store.ts +1138 -1018
  176. package/src/console/observability/usage-dimensions.ts +4 -4
  177. package/src/console/providers/catalog/catalog-projections.ts +6 -7
  178. package/src/console/providers/catalog/contracts.ts +6 -0
  179. package/src/console/providers/catalog/model-operations.ts +5 -2
  180. package/src/console/providers/catalog/provider-operations.ts +24 -4
  181. package/src/console/providers/catalog/public-model-store.ts +8 -11
  182. package/src/console/providers/catalog/routes.ts +7 -0
  183. package/src/console/providers/catalog/store.ts +1553 -1532
  184. package/src/console/providers/oauth/routes.ts +8 -12
  185. package/src/console/quota/account-quota-shared.ts +52 -39
  186. package/src/console/quota/account-quota-tenant.ts +6 -6
  187. package/src/console/quota/quota-view.ts +1 -1
  188. package/src/console/quota/refresh.ts +441 -429
  189. package/src/console/remote-support/contracts.ts +61 -0
  190. package/src/console/remote-support/routes.ts +65 -0
  191. package/src/console/remote-support/service.ts +207 -0
  192. package/src/console/response-compression.ts +0 -3
  193. package/src/console/routing/pools/import-parse.ts +206 -0
  194. package/src/console/routing/pools/operations.ts +543 -501
  195. package/src/console/routing/pools/probe-result.ts +3 -2
  196. package/src/console/routing/pools/probe-verdict.ts +132 -132
  197. package/src/console/routing/pools/routes.ts +181 -164
  198. package/src/console/routing/pools/store.ts +8 -8
  199. package/src/console/settings/contracts.ts +95 -2
  200. package/src/console/settings/store.ts +81 -3
  201. package/src/console/shared/cursor.ts +8 -8
  202. package/src/console/shared/errors.ts +1 -3
  203. package/src/console/updates/routes.ts +16 -0
  204. package/src/main.ts +10 -0
  205. package/src/network/http2-bidi.ts +2 -4
  206. package/src/network/pool/agent.ts +478 -478
  207. package/src/network/pool/selector.ts +5 -4
  208. package/src/network/retry.ts +1 -2
  209. package/src/network/ssrf.ts +5 -4
  210. package/src/observability/event-compaction.ts +317 -0
  211. package/src/observability/event-day-index.ts +440 -0
  212. package/src/observability/event-log.ts +155 -6
  213. package/src/observability/logger.ts +6 -6
  214. package/src/observability/payload-capture.ts +158 -33
  215. package/src/observability/payload-store.ts +82 -61
  216. package/src/observability/redaction.ts +5 -6
  217. package/src/observability/request-notify.ts +43 -0
  218. package/src/observability/telemetry-status.ts +8 -9
  219. package/src/observability/token-speed.ts +5 -4
  220. package/src/persistence/db-mode.ts +7 -7
  221. package/src/persistence/health-event-flush.ts +36 -0
  222. package/src/persistence/readiness.ts +1 -1
  223. package/src/persistence/schema.ts +75 -29
  224. package/src/persistence/shared-cache.ts +1 -2
  225. package/src/persistence/stores/api-key-store.ts +18 -6
  226. package/src/persistence/stores/share-store.ts +4 -3
  227. package/src/persistence/stores/telemetry-store.ts +33 -11
  228. package/src/protocol/canonical-model.ts +11 -3
  229. package/src/protocol/codecs.ts +139 -6
  230. package/src/protocol/gateway-error.ts +6 -4
  231. package/src/protocol/image-clamp.ts +19 -1
  232. package/src/protocol/registry.ts +8 -0
  233. package/src/protocol/request/chat.ts +21 -1
  234. package/src/protocol/request/codex.ts +10 -1
  235. package/src/protocol/request/gemini.ts +9 -2
  236. package/src/protocol/request/messages.ts +18 -7
  237. package/src/protocol/request/responses.ts +9 -1
  238. package/src/protocol/response/chat.ts +34 -4
  239. package/src/protocol/response/codex.ts +7 -4
  240. package/src/protocol/response/dsml.ts +140 -28
  241. package/src/protocol/response/responses.ts +11 -5
  242. package/src/protocol/stream-error-frames.ts +4 -5
  243. package/src/providers/adapters/compatible-adapter.ts +3 -0
  244. package/src/providers/authentication/jwt-validator.ts +9 -10
  245. package/src/providers/authentication/oauth-refresh-service.ts +9 -11
  246. package/src/providers/bundled-discovery-providers.ts +122 -119
  247. package/src/providers/configured-openai-providers.ts +4 -4
  248. package/src/providers/default-registry.ts +5 -4
  249. package/src/providers/discovery/catalog/catalog-blob.ts +18 -18
  250. package/src/providers/discovery/catalog/catalog-format.ts +3 -6
  251. package/src/providers/discovery/models-dev-catalog.ts +354 -354
  252. package/src/providers/discovery/openai-model-discovery.ts +5 -0
  253. package/src/providers/discovery/openrouter-free-roster.ts +15 -17
  254. package/src/providers/discovery/probe-phases.ts +4 -4
  255. package/src/providers/discovery/probe-wire.ts +6 -13
  256. package/src/providers/discovery/probing-service.ts +1117 -1045
  257. package/src/providers/integrations/alibabacp/alibabacp.ts +1 -1
  258. package/src/providers/integrations/antigravity/antigravity-quota.ts +5 -7
  259. package/src/providers/integrations/buddy/buddy-chat-shared.ts +23 -2
  260. package/src/providers/integrations/buddy/buddy-checkin.ts +1 -4
  261. package/src/providers/integrations/buddy/buddy-cli-headers.ts +23 -6
  262. package/src/providers/integrations/buddy/buddy-conversation-headers.ts +0 -1
  263. package/src/providers/integrations/buddy/buddy-identity.ts +52 -0
  264. package/src/providers/integrations/buddy/buddy-media.ts +2 -2
  265. package/src/providers/integrations/buddy/buddy-usage-store.ts +3 -4
  266. package/src/providers/integrations/buddy/codebuddy/quota.ts +2 -13
  267. package/src/providers/integrations/buddy/codebuddy/shared.ts +118 -115
  268. package/src/providers/integrations/buddy/workbuddy/quota.ts +0 -11
  269. package/src/providers/integrations/buddy/workbuddy/shared.ts +5 -2
  270. package/src/providers/integrations/claude/claude.ts +608 -608
  271. package/src/providers/integrations/cline/cline-quota.ts +27 -20
  272. package/src/providers/integrations/cline/cline.ts +537 -539
  273. package/src/providers/integrations/codex/codex-headers.ts +25 -39
  274. package/src/providers/integrations/codex/codex-oauth.ts +1 -1
  275. package/src/providers/integrations/codex/codex.ts +2 -29
  276. package/src/providers/integrations/commandcode/commandcode.ts +674 -675
  277. package/src/providers/integrations/commandcode/discovery.ts +117 -116
  278. package/src/providers/integrations/cursor/cursor-auth.ts +418 -418
  279. package/src/providers/integrations/cursor/cursor-quota.ts +309 -0
  280. package/src/providers/integrations/cursor/cursor-request.ts +616 -471
  281. package/src/providers/integrations/cursor/cursor-session-manager.ts +319 -281
  282. package/src/providers/integrations/cursor/cursor.ts +1333 -960
  283. package/src/providers/integrations/cursor/protocol/messages.ts +25 -0
  284. package/src/providers/integrations/factory/factory-models.ts +3 -5
  285. package/src/providers/integrations/factory/factory.ts +8 -1
  286. package/src/providers/integrations/gemini.ts +6 -1
  287. package/src/providers/integrations/grok/grok-install-id.ts +2 -3
  288. package/src/providers/integrations/grok/grok-quota.ts +279 -177
  289. package/src/providers/integrations/grok/grok-ratelimit-store.ts +73 -0
  290. package/src/providers/integrations/grok/grok-ratelimit.ts +218 -0
  291. package/src/providers/integrations/grok/grok.ts +6 -7
  292. package/src/providers/integrations/hermes.ts +14 -18
  293. package/src/providers/integrations/kiro/kiro-discovery.ts +1 -1
  294. package/src/providers/integrations/kiro/kiro.ts +1 -1
  295. package/src/providers/integrations/mistral.ts +6 -6
  296. package/src/providers/integrations/opencode/fingerprint.ts +3 -3
  297. package/src/providers/integrations/opencode/provider.ts +19 -20
  298. package/src/providers/integrations/qoder/qoder-catalog.ts +450 -450
  299. package/src/providers/integrations/qoder/qoder-oauth.ts +388 -310
  300. package/src/providers/integrations/qoder/qoder-quota.ts +192 -0
  301. package/src/providers/integrations/qoder/qoder.ts +884 -865
  302. package/src/providers/integrations/shared/connect.ts +6 -6
  303. package/src/providers/integrations/trae/trae-catalog.ts +1 -2
  304. package/src/providers/integrations/trae/trae-quota.ts +384 -0
  305. package/src/providers/integrations/trae/trae.ts +2 -2
  306. package/src/providers/integrations/zcode/oauth.ts +6 -1
  307. package/src/providers/operations/account-health-service.ts +75 -27
  308. package/src/providers/operations/account-reset-service.ts +1 -1
  309. package/src/providers/operations/client-versions.ts +6 -6
  310. package/src/providers/operations/custom-cli-headers.ts +3 -4
  311. package/src/providers/operations/provider-catalog-seeder.ts +95 -95
  312. package/src/providers/operations/provider-catalog-service.ts +3 -4
  313. package/src/providers/provider-contracts.ts +7 -0
  314. package/src/providers/provider-metadata.ts +10 -8
  315. package/src/providers/provider-registry.ts +6 -7
  316. package/src/providers/provider-usage.ts +1 -1
  317. package/src/providers/quota/quota-support.ts +8 -0
  318. package/src/security/abuse.ts +15 -10
  319. package/src/security/access-control.ts +144 -146
  320. package/src/security/admission/contracts.ts +11 -0
  321. package/src/security/admission/in-memory-store.ts +4 -3
  322. package/src/security/admission/lease-sweep.ts +6 -6
  323. package/src/security/admission/reasons.ts +5 -1
  324. package/src/security/admission/redis-store.ts +211 -210
  325. package/src/security/admission/service.ts +4 -0
  326. package/src/security/api-key-auth.ts +27 -17
  327. package/src/security/ip-boundary.ts +155 -15
  328. package/src/security/model-abuse.ts +366 -94
  329. package/src/security/outbound-headers.ts +3 -4
  330. package/src/shared/sleep.ts +4 -0
  331. package/src/transport/dispatch/attempt-finalize.ts +47 -5
  332. package/src/transport/dispatch/attempt-loop.ts +531 -530
  333. package/src/transport/dispatch/generation.ts +6 -0
  334. package/src/transport/dispatch/proxy-request.ts +24 -1
  335. package/src/transport/dispatch/ratelimit-capture.ts +37 -0
  336. package/src/transport/dispatch/stream-bridge.ts +1 -3
  337. package/src/transport/dispatch/streaming/finalize.ts +1 -1
  338. package/src/transport/dispatch/streaming/stream.ts +19 -6
  339. package/src/transport/dispatch/upstream.ts +9 -0
  340. package/src/transport/failure-policy.ts +162 -8
  341. package/src/transport/guards/auth-guard.ts +7 -4
  342. package/src/transport/guards/concurrency-guard.ts +0 -5
  343. package/src/transport/guards/error-lifecycle.ts +13 -6
  344. package/src/transport/guards/pipeline.ts +15 -3
  345. package/src/transport/guards/request-context.ts +8 -2
  346. package/src/transport/request/inflight.ts +5 -6
  347. package/src/transport/request/preparer.ts +9 -8
  348. package/src/transport/request/state.ts +21 -8
  349. package/src/transport/request/tenant-preferences.ts +60 -0
  350. package/src/transport/routing/admission.ts +4 -0
  351. package/src/transport/routing/route-catalog.ts +65 -10
  352. package/src/transport/routing/route-model.ts +46 -14
  353. package/src/transport/routing/router.ts +21 -14
  354. package/src/transport/surface/completion.ts +2 -3
  355. package/src/transport/surface/dialects.ts +5 -6
  356. package/src/transport/surface/responses/encode.ts +38 -0
  357. package/src/transport/surface/responses/parse.ts +4 -0
  358. package/src/transport/translation/capabilities.ts +2 -3
  359. package/src/transport/translation/quirks.ts +3 -3
  360. package/src/workers/buddy-growth-tasks.ts +5 -10
  361. package/src/workers/daily-checkin.ts +17 -23
  362. package/src/workers/quota-refresh-sweep.ts +26 -26
  363. package/dist/dashboard/assets/BannedPage-hfCzi-Ux.js.gz +0 -0
  364. package/dist/dashboard/assets/CliToolDetailPage-DLGqYEwI.js +0 -1
  365. package/dist/dashboard/assets/CliToolDetailPage-DLGqYEwI.js.gz +0 -0
  366. package/dist/dashboard/assets/CliToolsPage-DEDyKBlC.js.gz +0 -0
  367. package/dist/dashboard/assets/CombosPage-CfuYiQ8Z.js +0 -1
  368. package/dist/dashboard/assets/CombosPage-CfuYiQ8Z.js.gz +0 -0
  369. package/dist/dashboard/assets/ConsoleLogPage-bsLuZmYH.js.gz +0 -0
  370. package/dist/dashboard/assets/CustomizationPage-_UWhwziJ.js.gz +0 -0
  371. package/dist/dashboard/assets/HealthEventsModal-Cjfc5WFS.js +0 -1
  372. package/dist/dashboard/assets/HealthEventsModal-Cjfc5WFS.js.gz +0 -0
  373. package/dist/dashboard/assets/LoginPage-DDVIfZGW.js.gz +0 -0
  374. package/dist/dashboard/assets/ModelPicker-CWkTUtpX.js +0 -1
  375. package/dist/dashboard/assets/ModelPicker-CWkTUtpX.js.gz +0 -0
  376. package/dist/dashboard/assets/OverviewPage-B53YYMng.js +0 -1
  377. package/dist/dashboard/assets/OverviewPage-B53YYMng.js.gz +0 -0
  378. package/dist/dashboard/assets/ProviderDetailPage-B0KaAQf0.js +0 -3
  379. package/dist/dashboard/assets/ProviderDetailPage-B0KaAQf0.js.gz +0 -0
  380. package/dist/dashboard/assets/ProvidersPage-Dx31DgIO.js +0 -1
  381. package/dist/dashboard/assets/ProvidersPage-Dx31DgIO.js.gz +0 -0
  382. package/dist/dashboard/assets/ProxyPage-BpnR4kXT.js +0 -7
  383. package/dist/dashboard/assets/ProxyPage-BpnR4kXT.js.gz +0 -0
  384. package/dist/dashboard/assets/QuotaPage-BpOQCnun.js +0 -1
  385. package/dist/dashboard/assets/QuotaPage-BpOQCnun.js.gz +0 -0
  386. package/dist/dashboard/assets/SettingsPage-DV1eo2J_.js +0 -1
  387. package/dist/dashboard/assets/SettingsPage-DV1eo2J_.js.gz +0 -0
  388. package/dist/dashboard/assets/SetupPage-D6pmQ86L.js.gz +0 -0
  389. package/dist/dashboard/assets/SortableList-DWvKiuSl.js.gz +0 -0
  390. package/dist/dashboard/assets/StudioPage-CmkUrgdj.js.gz +0 -0
  391. package/dist/dashboard/assets/UsagePage-B47uOydr.js +0 -56
  392. package/dist/dashboard/assets/UsagePage-B47uOydr.js.gz +0 -0
  393. package/dist/dashboard/assets/api-keys-uNywYlfv.js +0 -1
  394. package/dist/dashboard/assets/api-keys-uNywYlfv.js.gz +0 -0
  395. package/dist/dashboard/assets/app-DxivpfYG.css +0 -2
  396. package/dist/dashboard/assets/app-DxivpfYG.css.gz +0 -0
  397. package/dist/dashboard/assets/assert-NHwYq95t.js.gz +0 -0
  398. package/dist/dashboard/assets/badge-6uzDDvqs.js.gz +0 -0
  399. package/dist/dashboard/assets/banner-4_WNg2WZ.js.gz +0 -0
  400. package/dist/dashboard/assets/card--AtFedAq.js.gz +0 -0
  401. package/dist/dashboard/assets/cli-tools-B3e7dx3N.js.gz +0 -0
  402. package/dist/dashboard/assets/clipboard-button-NreUx6c8.js.gz +0 -0
  403. package/dist/dashboard/assets/dialog-BtpsJU98.js +0 -1
  404. package/dist/dashboard/assets/dialog-BtpsJU98.js.gz +0 -0
  405. package/dist/dashboard/assets/download-A0SW9OCW.js +0 -1
  406. package/dist/dashboard/assets/download-A0SW9OCW.js.gz +0 -0
  407. package/dist/dashboard/assets/drawer-WwwtzihF.js +0 -1
  408. package/dist/dashboard/assets/drawer-WwwtzihF.js.gz +0 -0
  409. package/dist/dashboard/assets/entry-CkryrIea.js +0 -4
  410. package/dist/dashboard/assets/entry-CkryrIea.js.gz +0 -0
  411. package/dist/dashboard/assets/entry-TJIye_jT.js +0 -1
  412. package/dist/dashboard/assets/entry-TJIye_jT.js.gz +0 -0
  413. package/dist/dashboard/assets/entry-jy9_4-Ke.js +0 -1
  414. package/dist/dashboard/assets/entry-jy9_4-Ke.js.gz +0 -0
  415. package/dist/dashboard/assets/fingerprint-pattern-BWuHtbW4.js.gz +0 -0
  416. package/dist/dashboard/assets/github-badge-D_eM3IgG.js.gz +0 -0
  417. package/dist/dashboard/assets/index-BydyKaDz.js.gz +0 -0
  418. package/dist/dashboard/assets/inline-BcNGF8Gr.js.gz +0 -0
  419. package/dist/dashboard/assets/input-BG41et9G.js.gz +0 -0
  420. package/dist/dashboard/assets/layout-C8mer_Ak.js +0 -1
  421. package/dist/dashboard/assets/layout-C8mer_Ak.js.gz +0 -0
  422. package/dist/dashboard/assets/list-row-Gh_W4OR_.js.gz +0 -0
  423. package/dist/dashboard/assets/live-BJGYRBnR.js.gz +0 -0
  424. package/dist/dashboard/assets/model-bans-DeVLDA7I.js +0 -1
  425. package/dist/dashboard/assets/model-bans-DeVLDA7I.js.gz +0 -0
  426. package/dist/dashboard/assets/mutation-BkoLM5Ba.js.gz +0 -0
  427. package/dist/dashboard/assets/numbers-Bs4LOOuf.js +0 -2
  428. package/dist/dashboard/assets/numbers-Bs4LOOuf.js.gz +0 -0
  429. package/dist/dashboard/assets/query-keys-D7wX2vM5.js.gz +0 -0
  430. package/dist/dashboard/assets/routing-CcusqXA7.js.gz +0 -0
  431. package/dist/dashboard/assets/select-BO6h8yNv.js.gz +0 -0
  432. package/dist/dashboard/assets/settings-mpmXTgUa.js.gz +0 -0
  433. package/dist/dashboard/assets/sparkles-BRaRUxAU.js +0 -1
  434. package/dist/dashboard/assets/sparkles-BRaRUxAU.js.gz +0 -0
  435. package/dist/dashboard/assets/stack-DwLmjUFK.js.gz +0 -0
  436. package/dist/dashboard/assets/state-DYdQmdAl.js.gz +0 -0
  437. package/dist/dashboard/assets/switch-CidSfbG7.js +0 -1
  438. package/dist/dashboard/assets/switch-CidSfbG7.js.gz +0 -0
  439. package/dist/dashboard/assets/terminal-Bd4ee8TA.js +0 -1
  440. package/dist/dashboard/assets/terminal-Bd4ee8TA.js.gz +0 -0
  441. package/dist/dashboard/assets/tokens-DDxMbSRL.js +0 -1
  442. package/dist/dashboard/assets/tokens-DDxMbSRL.js.gz +0 -0
  443. package/dist/dashboard/assets/use-presence-BSZEBx7U.js +0 -1
  444. package/dist/dashboard/assets/use-presence-BSZEBx7U.js.gz +0 -0
  445. package/dist/dashboard/assets/useMutation-w4u4CVue.js.gz +0 -0
  446. package/scripts/cli/command/admin.ts +0 -65
  447. package/scripts/cli/command/doctor.ts +0 -257
  448. package/scripts/cli/command/update.ts +0 -23
  449. package/scripts/cli/gateway/drain-token.ts +0 -78
  450. package/scripts/cli/gateway/instance-lock.ts +0 -105
  451. package/scripts/cli/gateway/stop.ts +0 -51
  452. package/scripts/cli/support/bun.ts +0 -27
  453. package/scripts/cli/support/version.ts +0 -37
  454. /package/dist/dashboard/assets/{app-CafWtVXA.js → app-BIt446yE.js} +0 -0
  455. /package/dist/dashboard/assets/{app-CafWtVXA.js.gz → app-BIt446yE.js.gz} +0 -0
  456. /package/scripts/cli/{ui/prompt.ts → prompt.ts} +0 -0
@@ -34,6 +34,7 @@ import { ProxyRequestStateStore } from "../request/state";
34
34
  import { ProxyRequestPreparer } from "../request/preparer";
35
35
  import { completeAttempt, estimatedUsage } from "./attempt-finalize";
36
36
  import { runAttemptLoop } from "./attempt-loop";
37
+ import { captureProviderRateLimit } from "./ratelimit-capture";
37
38
 
38
39
  export interface GenerationHandlerDeps {
39
40
  readonly db: CartethyiaDatabase;
@@ -125,6 +126,11 @@ async function passThrough(
125
126
  },
126
127
  ): Promise<Response> {
127
128
  const usage = estimatedUsage(0, 0);
129
+ // The CLI bucket comes back on every upstream response, so this is the free
130
+ // place to learn it: the quota surface then reads the stored value instead of
131
+ // spending a request of its own. Both the manual account test and automatic
132
+ // routing land here, so a captured reading follows real traffic.
133
+ captureProviderRateLimit(args.candidate.provider_id, args.candidate.provider_account_id, upstream.headers);
128
134
  await completeAttempt(state, {
129
135
  status: "completed",
130
136
  providerId: args.candidate.provider_id,
@@ -15,7 +15,11 @@ import { messagesAdapter } from "../surface/messages/adapter";
15
15
  import { completionAdapter } from "../surface/completion";
16
16
  import { forwardedRequestHeaders, proxySuccessHeaders, buildUpstreamDispatchContext, reportedModelFrom } from "./upstream";
17
17
  import { resolvePromptCacheKey } from "../../providers/operations/session-resolution";
18
- import { applyTenantPreferences } from "../request/tenant-preferences";
18
+ import {
19
+ applyTenantPreferences,
20
+ applyTenantDeadlineOverride,
21
+ resolveTenantImageClampMaxBytes,
22
+ } from "../request/tenant-preferences";
19
23
  import { metrics } from "../../observability/metrics";
20
24
  import type { NetworkPoolSelector } from "../../network/pool/selector";
21
25
  import type { TelemetryBatchBuffer } from "../../observability/telemetry-buffer";
@@ -128,6 +132,22 @@ export async function handleProviderProxyRequest(
128
132
  if (!prepared)
129
133
  throw new GatewayError("admission_unavailable", 503, "The request was not prepared, so no route can be dialed. Resend the request.");
130
134
  const canonicalRequest = await applyTenantPreferences(prepared, deps.db);
135
+ // Per-tenant timeout, applied as soon as the tenant is known. The deadline
136
+ // was armed at admission with the deployment default; this re-arms it.
137
+ await applyTenantDeadlineOverride(
138
+ prepared.authorization.snapshot.tenant_id,
139
+ deps.db,
140
+ (ms) => state.extendDeadline(ms),
141
+ );
142
+ // Memoized on the request: the ceiling is a tenant setting, so it cannot
143
+ // change between failover attempts, and each wire encoder that clamps an
144
+ // image reads it from here instead of paying for a preference read per
145
+ // attempt.
146
+ const imageClampMaxBytes = await resolveTenantImageClampMaxBytes(
147
+ deps.db,
148
+ prepared.authorization.snapshot.tenant_id,
149
+ );
150
+ if (imageClampMaxBytes !== undefined) state.imageClampMaxBytes = imageClampMaxBytes;
131
151
  const candidates =
132
152
  prepared.eligibleRouteCandidates.length > 0 ? prepared.eligibleRouteCandidates : [prepared.candidate];
133
153
  const attemptCandidates = candidates;
@@ -226,6 +246,9 @@ export async function handleProviderProxyRequest(
226
246
  headers: inboundHeaders,
227
247
  requestId: state.requestId,
228
248
  requestIdentity: state,
249
+ ...(state.imageClampMaxBytes === undefined
250
+ ? {}
251
+ : { imageClampMaxBytes: state.imageClampMaxBytes }),
229
252
  ...(conversationAffinity ? { conversationAffinity } : {}),
230
253
  ...(candidate.user_agent === undefined ? {} : { userAgent: candidate.user_agent }),
231
254
  ...(deps.networkBindingFactory
@@ -0,0 +1,37 @@
1
+ // Files a provider's rate-limit headers against the account that earned them.
2
+ //
3
+ // Providers answer real traffic with the bucket they are metering — Grok's CLI
4
+ // plane returns `x-ratelimit-*` on every response — and that is a reading the
5
+ // gateway already paid for. Capturing it here is what lets the quota surface
6
+ // show a balance without spending a request of its own.
7
+ //
8
+ // Kept provider-scoped and fail-open: a provider with no bucket parser is
9
+ // skipped, and any throw is swallowed, because a bookkeeping miss must never
10
+ // take down a request that already succeeded.
11
+
12
+ import { rememberGrokRateLimit } from "../../providers/integrations/grok/grok-ratelimit-store";
13
+ import { readGrokRateLimitHeaders } from "../../providers/integrations/grok/grok-ratelimit";
14
+
15
+ /** Providers whose response headers carry a readable bucket. */
16
+ const GROK_PROVIDER_IDS: readonly string[] = ["grok"];
17
+
18
+ /**
19
+ * Stores whatever bucket the response carried, keyed by account.
20
+ *
21
+ * No account id means no home for the reading: a bucket filed under the wrong
22
+ * account is worse than none, since it would report another account's balance.
23
+ */
24
+ export function captureProviderRateLimit(
25
+ providerId: string,
26
+ accountId: string | undefined,
27
+ headers: { get(name: string): string | null },
28
+ ): void {
29
+ if (!accountId) return;
30
+ const id = providerId.toLowerCase();
31
+ if (!GROK_PROVIDER_IDS.includes(id)) return;
32
+ try {
33
+ rememberGrokRateLimit(accountId, readGrokRateLimitHeaders(headers));
34
+ } catch {
35
+ // Swallowed: advisory bookkeeping on a request that already succeeded.
36
+ }
37
+ }
@@ -44,9 +44,7 @@ export function createDispatchStreamEncoder(
44
44
  return {
45
45
  push: (event) => encoder.push(event).map((wire) => formatSseEvent(wire.type, wire)),
46
46
  finish: () => encoder.finish().map((wire) => formatSseEvent(wire.type, wire)),
47
- encodeError: ({ origin, code, message, details }) => [
48
- formatSseEvent("response.error", { type: "response.error", origin, code, message, details }),
49
- ],
47
+ encodeError: (input) => encoder.encodeError(input).map((wire) => formatSseEvent(wire.type, wire)),
50
48
  };
51
49
  }
52
50
  if (surface === "messages") {
@@ -154,7 +154,7 @@ export class StreamFinalizer {
154
154
  *
155
155
  * Once the upstream has produced anything — even surface-invisible
156
156
  * reasoning deltas — it is alive: the stall bound governs and the tighter
157
- * first-chunk bound no longer applies.
157
+ * first-chunk bound does not apply.
158
158
  *
159
159
  * The request deadline is a fixed ceiling measured from ingress, and a
160
160
  * stream that keeps sending past it is not stalled. A live WorkBuddy stream
@@ -49,6 +49,7 @@ import {
49
49
  resolveCaptureDepthForRequest,
50
50
  shouldCaptureProviderBodyForRequest,
51
51
  } from "../attempt-finalize";
52
+ import { resolveTenantImageClampMaxBytes } from "../../request/tenant-preferences";
52
53
  import { proxySuccessHeaders, buildUpstreamDispatchContext } from "../upstream";
53
54
  import type { AttemptLeaseOwnership } from "../leases";
54
55
  import type { ProviderContinuationScope } from "../../../providers/provider-contracts";
@@ -186,6 +187,16 @@ export async function dispatchStreamingAttempt(input: StreamingDispatchInput): P
186
187
  providerCaptureMaxBytes(captureDepth),
187
188
  )
188
189
  : undefined;
190
+ // Same memoized ceiling the non-streaming path resolves, read before the
191
+ // dispatch context is built so a stream gets the same limit as a normal
192
+ // request rather than silently falling back to the deployment default.
193
+ if (state.imageClampMaxBytes === undefined) {
194
+ const clampMaxBytes = await resolveTenantImageClampMaxBytes(
195
+ deps.db,
196
+ prepared.authorization.snapshot.tenant_id,
197
+ );
198
+ if (clampMaxBytes !== undefined) state.imageClampMaxBytes = clampMaxBytes;
199
+ }
189
200
  const dispatchContext = buildUpstreamDispatchContext({
190
201
  credential,
191
202
  deadline: state.deadlineMs,
@@ -193,6 +204,9 @@ export async function dispatchStreamingAttempt(input: StreamingDispatchInput): P
193
204
  headers: inboundHeaders,
194
205
  requestId: state.requestId,
195
206
  requestIdentity: state,
207
+ ...(state.imageClampMaxBytes === undefined
208
+ ? {}
209
+ : { imageClampMaxBytes: state.imageClampMaxBytes }),
196
210
  ...(conversationAffinity ? { conversationAffinity } : {}),
197
211
  ...(candidate.user_agent === undefined ? {} : { userAgent: candidate.user_agent }),
198
212
  ...(wrappedOutboundFetch ? { outboundFetch: wrappedOutboundFetch } : {}),
@@ -218,10 +232,9 @@ export async function dispatchStreamingAttempt(input: StreamingDispatchInput): P
218
232
  });
219
233
  // `ProviderAdapter.dispatch` is declared to return a non-nullable
220
234
  // `AsyncIterable`, so an adapter that returned `undefined` would be a
221
- // type violation rather than a state this code could handle. The guard
222
- // that used to sit here (`if (!iterable) throw ...`) was therefore
223
- // unreachable, and it advertised a contract the interface does not
224
- // have. Enforcing it belongs at the adapter boundary, not here.
235
+ // type violation rather than a state this code could handle — no null check
236
+ // is needed here, and adding one would advertise a contract the interface
237
+ // does not have. Enforcing it belongs at the adapter boundary.
225
238
  const createUpstreamIterable = () =>
226
239
  adapter.dispatch(
227
240
  dispatchRequest,
@@ -361,8 +374,8 @@ export async function dispatchStreamingAttempt(input: StreamingDispatchInput): P
361
374
  // reasoning item when the client is Chat Completions). Keep
362
375
  // pulling until at least one wire chunk is enqueued; returning
363
376
  // from pull with an empty queue can leave Web Streams waiting
364
- // forever, which is why real clients previously saw one
365
- // assistant header and then a 60s socket reset.
377
+ // forever — a client then sees one assistant header and then a
378
+ // 60s socket reset.
366
379
  for (;;) {
367
380
  // Re-arm on every upstream read: the watchdog fires only
368
381
  // when the provider stays silent longer than the bound.
@@ -56,6 +56,12 @@ export function buildUpstreamDispatchContext(input: {
56
56
  readonly continuationScope?: ProviderContinuationScope;
57
57
  /** Stable per-conversation cache affinity (preferred over random ids). */
58
58
  readonly conversationAffinity?: string;
59
+ /**
60
+ * Per-tenant inline-attachment ceiling in bytes, resolved once per request.
61
+ * Carried through dispatch because wire encoders clamp images but hold no
62
+ * database handle.
63
+ */
64
+ readonly imageClampMaxBytes?: number;
59
65
  /** Opaque identity shared by retries of one logical gateway request. */
60
66
  readonly requestIdentity?: object;
61
67
  }): Record<string, unknown> {
@@ -94,6 +100,9 @@ export function buildUpstreamDispatchContext(input: {
94
100
  ...(input.leaseOwnership ? { lease_ownership: input.leaseOwnership } : {}),
95
101
  ...(input.continuationScope ? { continuation_scope: input.continuationScope } : {}),
96
102
  ...(input.conversationAffinity ? { conversation_affinity: input.conversationAffinity } : {}),
103
+ ...(input.imageClampMaxBytes === undefined
104
+ ? {}
105
+ : { image_clamp_max_bytes: input.imageClampMaxBytes }),
97
106
  ...(input.requestIdentity ? { request_identity: input.requestIdentity } : {}),
98
107
  } as Record<string, unknown>;
99
108
  }
@@ -229,6 +229,35 @@ export function extractUpstreamMessage(body: unknown): string {
229
229
  * Reading only a nested string dropped the code for the second family
230
230
  * entirely, so the operator saw a bare message with no provider code.
231
231
  */
232
+ /**
233
+ * The provider code hidden inside one string value, or `undefined`.
234
+ *
235
+ * Recognizes both a nested JSON envelope written as an escaped string
236
+ * (`"{\"code\":\"11-128\"}"`) and the bare escaped form (`"\"code\\\":\\\"11-128\""`).
237
+ * Cheap by construction: the scan is a substring test, and only a string that
238
+ * looks like it carries a code is handed to `JSON.parse`, so a large message
239
+ * body costs one `includes` rather than a parse.
240
+ */
241
+ function parseEmbeddedCode(value: string): string | undefined {
242
+ if (!value.includes("code")) return undefined;
243
+ // An escaped envelope: try it as JSON first, then with its escapes collapsed.
244
+ for (const candidate of [value, value.replace(/\\+"/g, '"')]) {
245
+ const trimmed = candidate.trim();
246
+ if (!trimmed.startsWith("{") && !trimmed.startsWith('"')) continue;
247
+ let parsed: unknown;
248
+ try {
249
+ parsed = JSON.parse(trimmed);
250
+ } catch {
251
+ continue;
252
+ }
253
+ const code = upstreamProviderCode(parsed);
254
+ if (code !== undefined) return code;
255
+ }
256
+ // A bare `"code": "11-128"` fragment inside a longer message.
257
+ const match = value.match(/\\*["']?code\\*["']?\s*:\s*\\*["']?([A-Za-z0-9_.:-]{3,32})/i);
258
+ return match?.[1];
259
+ }
260
+
232
261
  export function upstreamProviderCode(body: unknown): string | undefined {
233
262
  if (!isRecord(body)) return undefined;
234
263
  const nested = isRecord(body.error) ? body.error : undefined;
@@ -236,6 +265,20 @@ export function upstreamProviderCode(body: unknown): string | undefined {
236
265
  for (const candidate of [ext?.code, nested?.code, body.code]) {
237
266
  if (typeof candidate === "string" && candidate.length > 0) return candidate;
238
267
  }
268
+ // A provider may embed its error envelope as an *escaped string* rather than a
269
+ // nested object — `"error": "{\"code\":\"11-128\",...}"` — so none of the
270
+ // candidates above is a record and the code is invisible. That is the shape
271
+ // the reference gateways detect, and it matters because the channel code is
272
+ // what decides whether an account is cooled: missing it, a blocked account
273
+ // stays in rotation failing every request.
274
+ for (const candidate of [ext, nested, body]) {
275
+ if (!isRecord(candidate)) continue;
276
+ for (const value of Object.values(candidate)) {
277
+ if (typeof value !== "string") continue;
278
+ const embedded = parseEmbeddedCode(value);
279
+ if (embedded !== undefined) return embedded;
280
+ }
281
+ }
239
282
  // A numeric code is still the provider's identifier; keep it as text so an
240
283
  // operator can search for it. The nested string above wins when both exist.
241
284
  const numeric = ext?.code ?? nested?.code ?? body.code;
@@ -364,6 +407,14 @@ export function classifyUpstreamFailure(error: unknown): UpstreamFailurePolicy {
364
407
  error.code === "model_not_found" ||
365
408
  error.code === "admission_unavailable" ||
366
409
  error.code === "capacity_exhausted" ||
410
+ // A provider max-inflight rejection: the plan's next sibling account may
411
+ // have a free slot, exactly as with an API-key concurrency rejection.
412
+ error.code === "provider_capacity_exhausted" ||
413
+ // The upstream accepted the request and is holding it in a queue. Another
414
+ // account of the same provider is very likely not queued, so failover is
415
+ // the faster answer — and if every account is queued, the caller still has
416
+ // the stated interval to wait for.
417
+ error.code === "upstream_queued" ||
367
418
  error.code === "accounts_unavailable" ||
368
419
  error.code === "accounts_rate_limited" ||
369
420
  error.code === "quota_exceeded" ||
@@ -499,8 +550,8 @@ export interface TerminalOutcome {
499
550
  * Derives a failed attempt's status, error category, and error origin **from
500
551
  * one decision**, so the three can never contradict each other.
501
552
  *
502
- * They used to be computed independently at each dispatch call site, and they
503
- * drifted: a request that failed on an upstream condition could be recorded
553
+ * Computing them independently at a dispatch call site lets them
554
+ * drift: a request that failed on an upstream condition could be recorded
504
555
  * `cancelled` (499) while its category said `transport_unavailable` and its
505
556
  * http status said 502 — a row no operator could trust. Bundling them here
506
557
  * makes the inconsistency unrepresentable: a cancel always carries
@@ -591,6 +642,88 @@ export function isContextLengthFailure(body: unknown): boolean {
591
642
  }
592
643
  return false;
593
644
  }
645
+ /**
646
+ * How long an upstream asked the caller to wait before retrying, in
647
+ * milliseconds, or `undefined` when the body states no duration.
648
+ *
649
+ * `retryAfterSeconds` is the field the queue-shaped refusals carry. It is read
650
+ * from a few plausible spellings because the upstreams that queue a request do
651
+ * not agree on one, and a queue with no duration still has to be reported as a
652
+ * queue — the caller's wait is then bounded by the caller, not by us.
653
+ */
654
+ export function queueRetryAfterMs(body: unknown): number | undefined {
655
+ if (!isRecord(body)) return undefined;
656
+ const envelope = isRecord(body.error) ? body.error : body;
657
+ for (const source of [envelope, body]) {
658
+ for (const key of ["retryAfterSeconds", "retry_after_seconds", "retryAfterMs", "retry_after"]) {
659
+ const raw = source[key];
660
+ if (typeof raw === "number" && Number.isFinite(raw) && raw > 0) {
661
+ return key.toLowerCase().endsWith("ms") ? Math.round(raw) : Math.round(raw * 1000);
662
+ }
663
+ if (typeof raw === "string") {
664
+ const value = Number(raw.trim());
665
+ if (Number.isFinite(value) && value > 0) {
666
+ return key.toLowerCase().endsWith("ms") ? Math.round(value) : Math.round(value * 1000);
667
+ }
668
+ }
669
+ }
670
+ }
671
+ return undefined;
672
+ }
673
+
674
+ /**
675
+ * True when an upstream error body says the request is **queued**, not refused.
676
+ *
677
+ * Qoder answers `403` with `{"code":"10605", "isQueued":true,
678
+ * "retryAfterSeconds":N}`. That is not a rejection — the request is accepted and
679
+ * will be served once capacity frees up — but it arrives on a status the
680
+ * gateway treats as a failure, so without this it surfaced as a 403 the client
681
+ * read as "you may not do this" and the turn ended. Reporting it as a queue
682
+ * lets the caller wait for the stated interval instead.
683
+ *
684
+ * Recognized conservatively: an explicit `isQueued` flag, the `10605` code, or
685
+ * a message naming a queue. A bare word like "queue" appearing anywhere in a
686
+ * long body is not enough on its own, which is why the message test requires
687
+ * the queue to be what the message is *about* rather than a word it contains.
688
+ */
689
+ export function isUpstreamQueued(body: unknown): boolean {
690
+ if (!isRecord(body)) return false;
691
+ const envelope = isRecord(body.error) ? body.error : body;
692
+ if (envelope.isQueued === true || body.isQueued === true) return true;
693
+ if (envelope.queued === true || body.queued === true) return true;
694
+ for (const candidate of [envelope.code, body.code]) {
695
+ if (typeof candidate === "string" && candidate.trim() === "10605") return true;
696
+ }
697
+ for (const candidate of [envelope.message, envelope.msg, body.message, body.msg]) {
698
+ if (typeof candidate !== "string") continue;
699
+ const lower = candidate.toLowerCase();
700
+ // "is queued" / "queued, retry in Ns" — the queue as the message's subject.
701
+ if (/\bqueued\b|\bis queue\b|resource_limit/.test(lower)) return true;
702
+ }
703
+ return false;
704
+ }
705
+
706
+ /** The error a queued upstream request becomes: retryable, with its wait. */
707
+ export function upstreamQueuedError(
708
+ providerId: string,
709
+ retryAfterMs: number | undefined,
710
+ detail: string,
711
+ ): GatewayError {
712
+ return new GatewayError(
713
+ "upstream_queued",
714
+ 503,
715
+ detail.length > 0
716
+ ? detail
717
+ : "The upstream has queued this request. Retry after the stated interval.",
718
+ {
719
+ providerId,
720
+ reason: "upstream-queued",
721
+ ...(retryAfterMs === undefined ? {} : { retryAfterMs, cooldownMs: retryAfterMs }),
722
+ },
723
+ "upstream",
724
+ );
725
+ }
726
+
594
727
  const QUOTA_ERROR_IDENTIFIERS: ReadonlySet<string> = new Set([
595
728
  "rate_limit_exceeded",
596
729
  "rate_limit_error",
@@ -815,12 +948,31 @@ export async function mapUpstreamHttpError(response: Response, providerId: strin
815
948
  parsedBody = undefined;
816
949
  }
817
950
  const identifier = upstreamErrorIdentifier(parsedBody) ?? providerCode;
818
- const classification = isContextLengthFailure(parsedBody)
819
- ? { code: "context_length_exceeded" as const, status: 413, origin: "upstream" as const }
820
- : classifyUpstreamError(response.status, identifier);
951
+ // A queued request is checked *before* the status table: the upstream accepted
952
+ // the request and will serve it, but says so on a failure status (Qoder's 403
953
+ // + 10605). Letting the table classify it first turned "wait, then retry"
954
+ // into a refusal the client could not distinguish from a permissions error.
955
+ const upstreamQueued = isUpstreamQueued(parsedBody);
956
+ // The body's stated interval wins; the header is only a fallback, because an
957
+ // upstream that queues a request usually states the wait in the body and
958
+ // leaves `Retry-After` off entirely.
959
+ const queuedWaitMs = upstreamQueued
960
+ ? (queueRetryAfterMs(parsedBody) ??
961
+ (parseUpstreamBackoff(response.headers) ?? undefined))
962
+ : undefined;
963
+ const classification = upstreamQueued
964
+ ? { code: "upstream_queued" as const, status: 503, origin: "upstream" as const }
965
+ : isContextLengthFailure(parsedBody)
966
+ ? { code: "context_length_exceeded" as const, status: 413, origin: "upstream" as const }
967
+ : classifyUpstreamError(response.status, identifier);
821
968
  const extracted = extractUpstreamMessage(parsedBody ?? text);
822
969
  const message = (extracted.length > 0 ? extracted : text.trim() || `Provider returned HTTP ${response.status}.`).slice(0, MAX_UPSTREAM_ERROR_BYTES).replace(/[\r\n]+/g, " ");
823
- const retryAfterMs = parseUpstreamBackoff(response.headers);
970
+ const headerWaitMs = parseUpstreamBackoff(response.headers);
971
+ // A queue carries its own interval, which is the one the caller must honour:
972
+ // it is what the upstream promised, while `Retry-After` may be absent or may
973
+ // describe a different window. `cooldownMs` rides along so the account-health
974
+ // layer can also read it.
975
+ const retryAfterMs = queuedWaitMs ?? headerWaitMs;
824
976
  const requestId = upstreamRequestId(response.headers);
825
977
  const reportedProviderCode = providerCode ?? identifier;
826
978
  throw new GatewayError(classification.code, classification.status, message, {
@@ -830,6 +982,9 @@ export async function mapUpstreamHttpError(response: Response, providerId: strin
830
982
  ...(requestId ? { upstreamRequestId: requestId } : {}),
831
983
  raw: text.slice(0, MAX_UPSTREAM_ERROR_BYTES),
832
984
  ...(retryAfterMs === null || retryAfterMs === undefined ? {} : { retryAfterMs }),
985
+ ...(queuedWaitMs !== undefined
986
+ ? { reason: "upstream-queued", cooldownMs: queuedWaitMs }
987
+ : {}),
833
988
  ...(classification.rateLimitScope
834
989
  ? { rateLimitScope: classification.rateLimitScope }
835
990
  : {}),
@@ -895,8 +1050,7 @@ export function isRetryableFailure(error: unknown): boolean {
895
1050
  * timeout.
896
1051
  *
897
1052
  * Owned here so the attempt loop, the empty-stream replay, and the Codex
898
- * retry budget cannot drift apart; each previously declared its own copy of
899
- * the constant.
1053
+ * retry budget cannot drift apart.
900
1054
  */
901
1055
  export const MIN_RETRY_BUDGET_MS = 15_000;
902
1056
 
@@ -90,14 +90,17 @@ export function createApiKeyAuthenticationMiddleware(deps: {
90
90
  );
91
91
  // Abuse ban: refuse before parse/prepare and before authorization is
92
92
  // recorded, so a banned caller cannot keep producing failed rows. The ban
93
- // is keyed on the client address, not the key: one key can be shared by
94
- // every recipient of a share link, so refusing the key would punish
95
- // callers that did nothing.
93
+ // is keyed on the client address *and* the API key: one key can be shared
94
+ // by every recipient of a share link, so refusing the address alone would
95
+ // punish callers that did nothing, while refusing the key alone would let
96
+ // the caller that probed mint a fresh one.
96
97
  if (deps.modelStrikes) {
97
98
  const state = deps.stateStore.get(request);
98
99
  const ip = state?.clientIdentity?.address;
99
100
  if (ip !== undefined) {
100
- const banned = await deps.modelStrikes.check({ ip }).catch(() => false);
101
+ const banned = await deps.modelStrikes
102
+ .check({ ip, apiKeyId: authorization.id })
103
+ .catch(() => false);
101
104
  if (banned) throw modelAbuseBannedError();
102
105
  }
103
106
  }
@@ -22,11 +22,6 @@ const PROCESS_CONCURRENCY_RETRY_MS = 3_000;
22
22
 
23
23
  let inFlight = 0;
24
24
 
25
- /** The current in-flight count. Exposed for tests; not a control surface. */
26
- export function processInFlight(): number {
27
- return inFlight;
28
- }
29
-
30
25
  export function createProcessConcurrencyMiddleware(): Elysia {
31
26
  return new Elysia()
32
27
  .beforeHandle(({ request }) => {
@@ -12,6 +12,7 @@ import { computeTokensPerSec } from "../../observability/token-speed";
12
12
  import { pushStructuredConsoleLog } from "../../observability/log-ring";
13
13
  import type { ConsoleLogLevel, ConsoleLogMetadata } from "../../observability/log-ring";
14
14
  import { isProxyDispatchRoute } from "./body-policy";
15
+ import { notifyRequestCompleted } from "../../observability/request-notify";
15
16
 
16
17
  interface ElysiaBuiltinError {
17
18
  readonly status?: number;
@@ -217,12 +218,12 @@ export function finalizeRequestTelemetry(
217
218
  return;
218
219
  }
219
220
 
220
- // An early rejection never reaches `completeAttempt`, which is the only
221
- // caller that used to write a payload frame — so a 400 from ingress, auth,
222
- // parse, or the preparer produced a request row whose four capture panels
223
- // were all empty, and the one case an operator most wants to read (what did
224
- // the client actually send?) had nothing to read. Capture the ingress body
225
- // and the error the client saw, keyed to the same request id as the row.
221
+ // An early rejection never reaches `completeAttempt`, the caller that writes
222
+ // a payload frame — so a 400 from ingress, auth, parse, or the preparer would
223
+ // produce a request row whose four capture panels were all empty, and the one
224
+ // case an operator most wants to read (what did the client actually send?)
225
+ // would have nothing to read. Capture the ingress body and the error the
226
+ // client saw, keyed to the same request id as the row.
226
227
  if (requestData === undefined && state.ingressBody !== undefined) {
227
228
  void captureEarlyRejectionPayload(state, authorization);
228
229
  }
@@ -235,6 +236,12 @@ export function finalizeRequestTelemetry(
235
236
  tokensPerSec,
236
237
  }),
237
238
  );
239
+ notifyRequestCompleted({
240
+ tenantId: authorization.tenantId,
241
+ requestId: state.requestId,
242
+ status,
243
+ httpStatus: requestStatus,
244
+ });
238
245
  }
239
246
 
240
247
  /** Console-log severity for a terminal request status. */
@@ -28,7 +28,12 @@ import type { CartethyiaDatabase } from "../../persistence/db-handle";
28
28
  import type { RedisClient } from "../../persistence/redis";
29
29
  import type { TelemetryBatchBuffer } from "../../observability/telemetry-buffer";
30
30
  import type { ModelStrikeService } from "../../security/model-abuse";
31
- import { TelemetryPayloadCapture, payloadCaptureKeepsBodies } from "../../observability/payload-capture";
31
+ import {
32
+ TelemetryPayloadCapture,
33
+ payloadCaptureKeepsBodies,
34
+ payloadRetentionMs,
35
+ resolvePayloadCapture,
36
+ } from "../../observability/payload-capture";
32
37
 
33
38
  /** Shared state/context carried by every ordered transport stage. */
34
39
  export interface TransportPipelineContext {
@@ -167,8 +172,15 @@ export function createTransportPipeline(context: TransportPipelineContext): Tran
167
172
  stateStore: context.stateStore,
168
173
  telemetryBuffer: context.telemetry,
169
174
  // One capture for the process, shared with every request state so an
170
- // early rejection can record its payload from the finalize hook.
171
- payloadCapture: new TelemetryPayloadCapture(context.db),
175
+ // early rejection can record its payload from the finalize hook. It
176
+ // resolves the tenant's retention itself because an early rejection has
177
+ // no preference memo on its state — nothing resolved one before the
178
+ // request was refused.
179
+ payloadCapture: new TelemetryPayloadCapture(context.db, (tenantId) =>
180
+ resolvePayloadCapture(context.db, tenantId).then(
181
+ (resolved) => (resolved.ok ? resolved.retentionMs : payloadRetentionMs()),
182
+ ),
183
+ ),
172
184
  });
173
185
  } else {
174
186
  registerRequestCleanup(rootApp, { stateStore: context.stateStore });
@@ -269,7 +269,13 @@ export function createProxyRoutePreparationMiddleware(deps: {
269
269
  throw new GatewayError("admission_unavailable", 503, "The request was not authenticated and parsed, so this route cannot be served. Resend the request.");
270
270
  const strikes = deps.modelStrikes;
271
271
  const ip = state.clientIdentity?.address;
272
- const strikeIdentity = strikes && ip !== undefined ? { strikes, ip } : undefined;
272
+ // The key rides along so a strike is charged to the caller that made it.
273
+ // Without it, one recipient of a shared link could escalate a ban onto
274
+ // every other caller behind the same address.
275
+ const strikeIdentity =
276
+ strikes && ip !== undefined
277
+ ? { strikes, ip, apiKeyId: state.authorization?.id ?? null }
278
+ : undefined;
273
279
  try {
274
280
  state.preparedRequest = await deps.preparer.prepare({
275
281
  canonicalRequest: state.canonicalRequest,
@@ -286,7 +292,7 @@ export function createProxyRoutePreparationMiddleware(deps: {
286
292
  // request, or any other failure is not the caller probing for models.
287
293
  if (strikeIdentity && isModelRejection(error)) {
288
294
  const outcome = await strikeIdentity.strikes
289
- .noteInvalid({ ip: strikeIdentity.ip })
295
+ .noteInvalid({ ip: strikeIdentity.ip, apiKeyId: strikeIdentity.apiKeyId })
290
296
  .catch(() => null);
291
297
  if (outcome?.banned === true) throw modelAbuseBannedError();
292
298
  if (outcome) {
@@ -4,12 +4,11 @@
4
4
  * provider-dispatch leases, keyed by `requestId`, carrying the client IP
5
5
  * captured at ingress and the hard deadline the flight must not outlive.
6
6
  *
7
- * This used to be a process-global module that kept its own counter alongside
8
- * the request state store's `liveControllers` map — two registries that could
9
- * disagree, and did: a flight the store had already torn down could stay
10
- * counted here forever because nothing tied the two together. The registry is
11
- * now *owned* by the `ProxyRequestStateStore` (`state.ts`), which is also the
12
- * one place that evicts a request, so a flight can no longer outlive its state.
7
+ * The registry is *owned* by the `ProxyRequestStateStore` (`state.ts`), which
8
+ * is also the one place that evicts a request, so a flight cannot outlive its
9
+ * state. A separate process-global counter alongside the store's
10
+ * `liveControllers` map would be a second registry able to disagree with it: a
11
+ * flight the store had already torn down would stay counted there forever.
13
12
  *
14
13
  * The store hands each entry an `expiresAtMs` (the request's hard deadline). A
15
14
  * periodic backstop sweep reads `overdue()` and force-releases any flight whose
@@ -85,12 +85,13 @@ export function resolveContinuationAffinity(
85
85
  // id replayed from another key or tenant resolves nothing.
86
86
  const record = lookupPendingCall(callId, scope);
87
87
  if (record === undefined) {
88
- throw new GatewayError(
89
- "invalid_request",
90
- 400,
91
- "This tool result refers to a Cursor tool call that is no longer pending. Resend the complete conversation.",
92
- { callId },
93
- );
88
+ // A replayed conversation carries every earlier tool result, and results
89
+ // previous legs already answered resolve nothing here — they are history,
90
+ // not the flight being continued. The request is a continuation only
91
+ // through the ids that still resolve; when none do, the ordinary routing
92
+ // path decides (a Cursor adapter may rebase the full history into a
93
+ // fresh Run).
94
+ continue;
94
95
  }
95
96
  if (affinity !== undefined && affinity.providerAccountId !== record.providerAccountId) {
96
97
  throw new GatewayError(
@@ -240,8 +241,8 @@ export class ProxyRequestPreparer {
240
241
  // than dispatched: a provider rejects the whole request for one corrupt
241
242
  // attachment, and the buddy family reports it with no field named, so the
242
243
  // caller gets a 400 it cannot act on. Running this before capability
243
- // derivation also means a request whose only image was defective no longer
244
- // requires the `image` capability — it plans like the text request it has
244
+ // derivation also means a request whose only image was defective does not
245
+ // require the `image` capability — it plans like the text request it has
245
246
  // become, instead of being routed to a vision route for nothing.
246
247
  const requestWithAttachments = dropCorruptAttachments(request).request;
247
248
  // Continuation affinity is resolved before anything else: a request