cartethyia 2.7.3 → 2.7.4

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 (352) hide show
  1. package/dist/dashboard/assets/{BannedPage-2TJD37Hb.js → BannedPage-ChG5svFb.js} +1 -1
  2. package/dist/dashboard/assets/BannedPage-ChG5svFb.js.gz +0 -0
  3. package/dist/dashboard/assets/{CliToolDetailPage-xefJNRmJ.js → CliToolDetailPage-BIcWQun7.js} +1 -1
  4. package/dist/dashboard/assets/CliToolDetailPage-BIcWQun7.js.gz +0 -0
  5. package/dist/dashboard/assets/{CliToolsPage-Rj9iHQ3F.js → CliToolsPage-Bf2VelDe.js} +1 -1
  6. package/dist/dashboard/assets/CliToolsPage-Bf2VelDe.js.gz +0 -0
  7. package/dist/dashboard/assets/{CombosPage-JEbYJFxw.js → CombosPage-DCgm5g6g.js} +1 -1
  8. package/dist/dashboard/assets/CombosPage-DCgm5g6g.js.gz +0 -0
  9. package/dist/dashboard/assets/ConsoleLogPage-a8mD0QF1.js +1 -0
  10. package/dist/dashboard/assets/ConsoleLogPage-a8mD0QF1.js.gz +0 -0
  11. package/dist/dashboard/assets/CustomizationPage-ggeMSjte.js +1 -0
  12. package/dist/dashboard/assets/CustomizationPage-ggeMSjte.js.gz +0 -0
  13. package/dist/dashboard/assets/HealthEventsModal-DzSWzE_j.js +1 -0
  14. package/dist/dashboard/assets/HealthEventsModal-DzSWzE_j.js.gz +0 -0
  15. package/dist/dashboard/assets/{LoginPage-C4w7vea-.js → LoginPage-U6AKaJcF.js} +1 -1
  16. package/dist/dashboard/assets/LoginPage-U6AKaJcF.js.gz +0 -0
  17. package/dist/dashboard/assets/{ModelPicker-BtPcAfot.js → ModelPicker-BMS2t8BA.js} +1 -1
  18. package/dist/dashboard/assets/ModelPicker-BMS2t8BA.js.gz +0 -0
  19. package/dist/dashboard/assets/OverviewPage-D2D4IiHl.js +1 -0
  20. package/dist/dashboard/assets/OverviewPage-D2D4IiHl.js.gz +0 -0
  21. package/dist/dashboard/assets/ProviderDetailPage-CrNf7j_X.js +5 -0
  22. package/dist/dashboard/assets/ProviderDetailPage-CrNf7j_X.js.gz +0 -0
  23. package/dist/dashboard/assets/ProvidersPage-BSDbquc1.js +1 -0
  24. package/dist/dashboard/assets/ProvidersPage-BSDbquc1.js.gz +0 -0
  25. package/dist/dashboard/assets/ProxyPage-lsrx7NYg.js +9 -0
  26. package/dist/dashboard/assets/ProxyPage-lsrx7NYg.js.gz +0 -0
  27. package/dist/dashboard/assets/QuotaPage-L0skzML8.js +1 -0
  28. package/dist/dashboard/assets/QuotaPage-L0skzML8.js.gz +0 -0
  29. package/dist/dashboard/assets/SettingsPage-CmAbjRHW.js +1 -0
  30. package/dist/dashboard/assets/SettingsPage-CmAbjRHW.js.gz +0 -0
  31. package/dist/dashboard/assets/{SetupPage-BQS0Rx_u.js → SetupPage-f0DE1h0-.js} +1 -1
  32. package/dist/dashboard/assets/SetupPage-f0DE1h0-.js.gz +0 -0
  33. package/dist/dashboard/assets/{SortableList-BAL5npBC.js → SortableList-ClzpwyET.js} +1 -1
  34. package/dist/dashboard/assets/SortableList-ClzpwyET.js.gz +0 -0
  35. package/dist/dashboard/assets/{StudioPage-TP7voAQ0.js → StudioPage-Cq5tW7hW.js} +3 -3
  36. package/dist/dashboard/assets/StudioPage-Cq5tW7hW.js.gz +0 -0
  37. package/dist/dashboard/assets/UsagePage-Dfc9rWQn.js +56 -0
  38. package/dist/dashboard/assets/UsagePage-Dfc9rWQn.js.gz +0 -0
  39. package/dist/dashboard/assets/api-keys-C38iXoO4.js +1 -0
  40. package/dist/dashboard/assets/api-keys-C38iXoO4.js.gz +0 -0
  41. package/dist/dashboard/assets/app-a159k0ub.css +2 -0
  42. package/dist/dashboard/assets/app-a159k0ub.css.gz +0 -0
  43. package/dist/dashboard/assets/assert-DbRA8EG2.js +15 -0
  44. package/dist/dashboard/assets/assert-DbRA8EG2.js.gz +0 -0
  45. package/dist/dashboard/assets/{badge-B_DMpMJN.js → badge-D9CS6dZk.js} +1 -1
  46. package/dist/dashboard/assets/badge-D9CS6dZk.js.gz +0 -0
  47. package/dist/dashboard/assets/{banner-DMrMPmth.js → banner-DympBNq4.js} +1 -1
  48. package/dist/dashboard/assets/banner-DympBNq4.js.gz +0 -0
  49. package/dist/dashboard/assets/{card-Be5Uzamx.js → card-ibRWU_uT.js} +1 -1
  50. package/dist/dashboard/assets/card-ibRWU_uT.js.gz +0 -0
  51. package/dist/dashboard/assets/cli-tools-63qBGaRw.js +1 -0
  52. package/dist/dashboard/assets/cli-tools-63qBGaRw.js.gz +0 -0
  53. package/dist/dashboard/assets/{clipboard-button-DOfrVmsi.js → clipboard-button-DJm-9pz7.js} +1 -1
  54. package/dist/dashboard/assets/clipboard-button-DJm-9pz7.js.gz +0 -0
  55. package/dist/dashboard/assets/{dialog-C9omdjQt.js → dialog-8fiE6tec.js} +1 -1
  56. package/dist/dashboard/assets/dialog-8fiE6tec.js.gz +0 -0
  57. package/dist/dashboard/assets/{drawer-Bm8WKaGY.js → drawer-CqGYeOrd.js} +1 -1
  58. package/dist/dashboard/assets/drawer-CqGYeOrd.js.gz +0 -0
  59. package/dist/dashboard/assets/{entry-q5HwZ8nN.js → entry-BSAsYsmV.js} +1 -1
  60. package/dist/dashboard/assets/entry-BSAsYsmV.js.gz +0 -0
  61. package/dist/dashboard/assets/entry-CiIQULZD.js +4 -0
  62. package/dist/dashboard/assets/entry-CiIQULZD.js.gz +0 -0
  63. package/dist/dashboard/assets/entry-Daz0lJ-W.js +1 -0
  64. package/dist/dashboard/assets/entry-Daz0lJ-W.js.gz +0 -0
  65. package/dist/dashboard/assets/{formatters-BTScrQpe.js → formatters-D-MTJxMr.js} +1 -1
  66. package/dist/dashboard/assets/formatters-D-MTJxMr.js.gz +0 -0
  67. package/dist/dashboard/assets/{github-badge-Orjuzemr.js → github-badge-CPK47xQx.js} +1 -1
  68. package/dist/dashboard/assets/github-badge-CPK47xQx.js.gz +0 -0
  69. package/dist/dashboard/assets/{index-cPrX5vn6.js → index-DBNPOfhV.js} +2 -2
  70. package/dist/dashboard/assets/index-DBNPOfhV.js.gz +0 -0
  71. package/dist/dashboard/assets/{inline-Dvux8w-0.js → inline-BT0zntEr.js} +1 -1
  72. package/dist/dashboard/assets/inline-BT0zntEr.js.gz +0 -0
  73. package/dist/dashboard/assets/{input-CU1Rgf3p.js → input-Da2ZViWg.js} +1 -1
  74. package/dist/dashboard/assets/input-Da2ZViWg.js.gz +0 -0
  75. package/dist/dashboard/assets/{layout-C_7A6H55.js → layout-BXNeLw6M.js} +1 -1
  76. package/dist/dashboard/assets/layout-BXNeLw6M.js.gz +0 -0
  77. package/dist/dashboard/assets/{list-row-CzFvogbj.js → list-row-BycOrrIV.js} +1 -1
  78. package/dist/dashboard/assets/list-row-BycOrrIV.js.gz +0 -0
  79. package/dist/dashboard/assets/live-DCQNZpF7.js +1 -0
  80. package/dist/dashboard/assets/live-DCQNZpF7.js.gz +0 -0
  81. package/dist/dashboard/assets/minimize-2-BTmMTK4o.js +1 -0
  82. package/dist/dashboard/assets/minimize-2-BTmMTK4o.js.gz +0 -0
  83. package/dist/dashboard/assets/{mutation-JJGQm8TD.js → mutation-Rr0TIvcj.js} +1 -1
  84. package/dist/dashboard/assets/mutation-Rr0TIvcj.js.gz +0 -0
  85. package/dist/dashboard/assets/network-9Yj2ZyLo.js +1 -0
  86. package/dist/dashboard/assets/network-9Yj2ZyLo.js.gz +0 -0
  87. package/dist/dashboard/assets/{numbers-BNDZnsUE.js → numbers-CEagj6ZZ.js} +2 -2
  88. package/dist/dashboard/assets/numbers-CEagj6ZZ.js.gz +0 -0
  89. package/dist/dashboard/assets/{query-keys-CLaNz8Xz.js → query-keys-BloKtaXJ.js} +1 -1
  90. package/dist/dashboard/assets/query-keys-BloKtaXJ.js.gz +0 -0
  91. package/dist/dashboard/assets/quota-Caw7wqci.js +1 -0
  92. package/dist/dashboard/assets/quota-Caw7wqci.js.gz +0 -0
  93. package/dist/dashboard/assets/routing-CO9zCUus.js +1 -0
  94. package/dist/dashboard/assets/routing-CO9zCUus.js.gz +0 -0
  95. package/dist/dashboard/assets/{select-B3iTetHN.js → select-BIYIBkMU.js} +2 -2
  96. package/dist/dashboard/assets/select-BIYIBkMU.js.gz +0 -0
  97. package/dist/dashboard/assets/settings-jS3-PNSA.js +1 -0
  98. package/dist/dashboard/assets/settings-jS3-PNSA.js.gz +0 -0
  99. package/dist/dashboard/assets/{stack-KrOrm7JZ.js → stack-DfgnFDWY.js} +1 -1
  100. package/dist/dashboard/assets/stack-DfgnFDWY.js.gz +0 -0
  101. package/dist/dashboard/assets/{state-DLS7IDMy.js → state-CkfdYWel.js} +1 -1
  102. package/dist/dashboard/assets/state-CkfdYWel.js.gz +0 -0
  103. package/dist/dashboard/assets/{switch-42ItvbAs.js → switch-SYm237HG.js} +1 -1
  104. package/dist/dashboard/assets/switch-SYm237HG.js.gz +0 -0
  105. package/dist/dashboard/assets/{useMutation-Dmnfh1Ue.js → useMutation-BQqCRNIi.js} +1 -1
  106. package/dist/dashboard/assets/useMutation-BQqCRNIi.js.gz +0 -0
  107. package/dist/dashboard/index.html +2 -2
  108. package/dist/dashboard/index.html.gz +0 -0
  109. package/dist/dashboard/precompressed-assets.json +55 -54
  110. package/migrations/full-postgres/0027_qoder_identity_genuine_seed.sql +9 -10
  111. package/migrations/full-postgres/0032_add_provider_pool_accounts.sql +53 -0
  112. package/migrations/full-postgres/0033_add_api_key_ban.sql +9 -0
  113. package/migrations/full-postgres/0034_add_api_key_ban_until.sql +7 -0
  114. package/migrations/full-postgres/0035_align_pool_account_status_enum.sql +21 -0
  115. package/migrations/lite-sqlite3/0027_qoder_identity_genuine_seed.sql +9 -10
  116. package/migrations/lite-sqlite3/0032_add_provider_pool_accounts.sql +44 -0
  117. package/migrations/lite-sqlite3/0033_add_api_key_ban.sql +8 -0
  118. package/migrations/lite-sqlite3/0034_add_api_key_ban_until.sql +5 -0
  119. package/migrations/lite-sqlite3/0035_align_pool_account_status_enum.sql +6 -0
  120. package/package.json +2 -1
  121. package/src/boot/app.ts +9 -8
  122. package/src/config/network.ts +7 -3
  123. package/src/console/api-keys/contracts.ts +52 -0
  124. package/src/console/api-keys/routes.ts +93 -4
  125. package/src/console/audit/routes.ts +3 -3
  126. package/src/console/auth/account-reset.ts +4 -4
  127. package/src/console/auth/session.ts +3 -3
  128. package/src/console/backup/contracts.ts +2 -0
  129. package/src/console/backup/export-endpoint.ts +6 -6
  130. package/src/console/backup/routes.ts +3 -3
  131. package/src/console/cli-tools/routes.ts +3 -3
  132. package/src/console/console-router-guards.ts +2 -2
  133. package/src/console/console-router.ts +4 -4
  134. package/src/console/data-management/routes.ts +3 -3
  135. package/src/console/domain-registration.ts +2 -2
  136. package/src/console/observability/contracts.ts +4 -0
  137. package/src/console/observability/live.ts +3 -3
  138. package/src/console/observability/logs.ts +3 -3
  139. package/src/console/observability/performance.ts +3 -3
  140. package/src/console/observability/routes.ts +3 -3
  141. package/src/console/observability/store.ts +16 -16
  142. package/src/console/providers/catalog/contracts.ts +43 -22
  143. package/src/console/providers/catalog/provider-operations.ts +17 -2
  144. package/src/console/providers/catalog/routes.ts +3 -3
  145. package/src/console/providers/catalog/store-helpers.ts +349 -326
  146. package/src/console/providers/catalog/store.ts +1523 -1292
  147. package/src/console/providers/detail/routes.ts +3 -3
  148. package/src/console/providers/oauth/routes.ts +3 -3
  149. package/src/console/quota/account-quota-tenant.ts +26 -1
  150. package/src/console/quota/account-test-egress.ts +4 -4
  151. package/src/console/routing/model/routes.ts +3 -3
  152. package/src/console/routing/pools/contracts.ts +11 -8
  153. package/src/console/routing/pools/operations.ts +14 -7
  154. package/src/console/routing/pools/probe-verdict.ts +8 -3
  155. package/src/console/routing/pools/relay-deploy.ts +9 -9
  156. package/src/console/routing/pools/routes.ts +3 -3
  157. package/src/console/routing/pools/store.ts +21 -1
  158. package/src/console/settings/routes.ts +3 -3
  159. package/src/console/share/share-router.ts +12 -12
  160. package/src/console/share/share-stats.ts +105 -35
  161. package/src/console/shared/read-through-cache.ts +38 -2
  162. package/src/console/updates/routes.ts +7 -4
  163. package/src/network/outbound-fetch.ts +75 -4
  164. package/src/network/pool/resolver.ts +39 -39
  165. package/src/network/pool-cooldown-store.ts +71 -16
  166. package/src/network/pool-health-machine.ts +21 -1
  167. package/src/network/rate-limit-verdict.ts +68 -0
  168. package/src/network/ssrf.ts +4 -4
  169. package/src/observability/event-query.ts +320 -16
  170. package/src/observability/telemetry-status.ts +1 -1
  171. package/src/persistence/db-sqlite.ts +7 -2
  172. package/src/persistence/readiness.ts +18 -6
  173. package/src/persistence/schema.ts +66 -7
  174. package/src/persistence/stores/api-key-store.ts +99 -3
  175. package/src/persistence/stores/telemetry-store.ts +1 -1
  176. package/src/protocol/codecs.ts +6 -6
  177. package/src/protocol/gateway-error.ts +6 -3
  178. package/src/protocol/image-generation.ts +2 -2
  179. package/src/protocol/response/chat.ts +2 -2
  180. package/src/protocol/response/codex.ts +1 -1
  181. package/src/protocol/response/gemini.ts +1 -1
  182. package/src/protocol/response/messages.ts +5 -5
  183. package/src/protocol/response/responses.ts +2 -2
  184. package/src/protocol/sse-decoder.ts +14 -3
  185. package/src/protocol/translation/capabilities.ts +2 -2
  186. package/src/protocol/upstream/send-claude-messages.ts +3 -3
  187. package/src/providers/adapters/base-adapter.ts +1 -1
  188. package/src/providers/adapters/compatible-adapter.ts +2 -2
  189. package/src/providers/authentication/oauth-refresh-service.ts +2 -2
  190. package/src/providers/configured-provider.ts +1 -1
  191. package/src/providers/discovery/probe-phases.ts +2 -2
  192. package/src/providers/discovery/probing-service.ts +12 -12
  193. package/src/providers/integrations/agentrouter/agentrouter.ts +2 -3
  194. package/src/providers/integrations/alibabacp/alibabacp.ts +2 -2
  195. package/src/providers/integrations/anthropic.ts +4 -11
  196. package/src/providers/integrations/antigravity/antigravity.ts +13 -24
  197. package/src/providers/integrations/buddy/buddy-catalog-shared.ts +1 -1
  198. package/src/providers/integrations/buddy/buddy-discovery-shared.ts +37 -6
  199. package/src/providers/integrations/buddy/buddy-identity.ts +22 -1
  200. package/src/providers/integrations/buddy/buddy-media.ts +16 -16
  201. package/src/providers/integrations/buddy/codebuddy/provider.ts +1 -1
  202. package/src/providers/integrations/buddy/workbuddy/provider.ts +1 -1
  203. package/src/providers/integrations/claude/claude-betas.ts +1 -1
  204. package/src/providers/integrations/claude/claude-credentials.ts +1 -1
  205. package/src/providers/integrations/claude/claude-slow-lane-retry.ts +1 -1
  206. package/src/providers/integrations/claude/claude.ts +3 -10
  207. package/src/providers/integrations/cline/cline.ts +1 -1
  208. package/src/providers/integrations/cloudflareai/cloudflareai.ts +4 -7
  209. package/src/providers/integrations/codex/codex-errors.ts +1 -1
  210. package/src/providers/integrations/codex/codex.ts +14 -15
  211. package/src/providers/integrations/commandcode/commandcode.ts +4 -4
  212. package/src/providers/integrations/commandcode/quota.ts +5 -1
  213. package/src/providers/integrations/cursor/cursor-http2.ts +11 -11
  214. package/src/providers/integrations/cursor/cursor.ts +15 -15
  215. package/src/providers/integrations/factory/factory.ts +4 -4
  216. package/src/providers/integrations/gemini.ts +11 -11
  217. package/src/providers/integrations/github/github.ts +6 -6
  218. package/src/providers/integrations/grok/grok.ts +2 -6
  219. package/src/providers/integrations/inferhub.ts +4 -11
  220. package/src/providers/integrations/kimi/kimi.ts +4 -11
  221. package/src/providers/integrations/kiro/kiro.ts +10 -10
  222. package/src/providers/integrations/muse/muse.ts +6 -9
  223. package/src/providers/integrations/perplexity.ts +2 -2
  224. package/src/providers/integrations/qoder/qoder-catalog.ts +47 -9
  225. package/src/providers/integrations/qoder/qoder-checkin.ts +26 -8
  226. package/src/providers/integrations/qoder/qoder-native-identity-store.ts +9 -8
  227. package/src/providers/integrations/qoder/qoder-realm.ts +15 -0
  228. package/src/providers/integrations/qoder/qoder-umid.ts +12 -13
  229. package/src/providers/integrations/qoder/qoder-virtual.ts +2 -2
  230. package/src/providers/integrations/qoder/qoder.ts +188 -22
  231. package/src/providers/integrations/tokenharbor.ts +2 -2
  232. package/src/providers/integrations/trae/trae.ts +6 -9
  233. package/src/providers/integrations/zcode/oauth.ts +1 -1
  234. package/src/providers/operations/provider-catalog-service.ts +4 -4
  235. package/src/providers/operations/provider-credential-service.ts +16 -4
  236. package/src/providers/operations/upstream-deadline.ts +3 -3
  237. package/src/providers/provider-contracts.ts +47 -3
  238. package/src/providers/provider-credential-resolver.ts +1 -1
  239. package/src/providers/provider-registry.ts +4 -4
  240. package/src/providers/quota/quota-contracts.ts +24 -10
  241. package/src/providers/quota/refresh.ts +2 -2
  242. package/src/providers/search/search-provider.ts +5 -5
  243. package/src/security/abuse.ts +4 -4
  244. package/src/security/admission/reasons.ts +17 -17
  245. package/src/security/api-key-auth.ts +4 -4
  246. package/src/shared/coerce.ts +19 -0
  247. package/src/transport/dispatch/attempt-finalize.ts +4 -3
  248. package/src/transport/dispatch/attempt-loop.ts +10 -7
  249. package/src/transport/dispatch/empty-stream.ts +6 -6
  250. package/src/transport/dispatch/generation.ts +9 -9
  251. package/src/transport/dispatch/leases.ts +8 -8
  252. package/src/transport/dispatch/proxy-request.ts +7 -7
  253. package/src/transport/dispatch/responses-compact.ts +11 -11
  254. package/src/transport/dispatch/retry-policy.ts +4 -67
  255. package/src/transport/dispatch/stream-bridge.ts +1 -1
  256. package/src/transport/dispatch/streaming/finalize.ts +9 -8
  257. package/src/transport/dispatch/systemone.ts +8 -8
  258. package/src/transport/dispatch/token-count.ts +9 -9
  259. package/src/transport/dispatch/websearch.ts +6 -6
  260. package/src/transport/drain-endpoint.ts +2 -2
  261. package/src/transport/failure-policy.ts +13 -6
  262. package/src/transport/guards/auth-guard.ts +6 -5
  263. package/src/transport/guards/body-policy.ts +4 -4
  264. package/src/transport/guards/error-lifecycle.ts +18 -3
  265. package/src/transport/guards/guard-errors.ts +7 -3
  266. package/src/transport/guards/headroom-guard.ts +1 -1
  267. package/src/transport/guards/pipeline.ts +6 -6
  268. package/src/transport/guards/readiness-guard.ts +16 -2
  269. package/src/transport/guards/request-context.ts +3 -3
  270. package/src/transport/request/preparer.ts +20 -20
  271. package/src/transport/request/state.ts +4 -7
  272. package/src/transport/routing/route-catalog.ts +97 -2
  273. package/src/transport/routing/route-model.ts +7 -7
  274. package/src/transport/routing/router.ts +2 -2
  275. package/src/transport/shutdown-notice.ts +19 -7
  276. package/src/transport/surface/completion.ts +1 -1
  277. package/src/transport/surface/messages/encode.ts +1 -1
  278. package/dist/dashboard/assets/BannedPage-2TJD37Hb.js.gz +0 -0
  279. package/dist/dashboard/assets/CliToolDetailPage-xefJNRmJ.js.gz +0 -0
  280. package/dist/dashboard/assets/CliToolsPage-Rj9iHQ3F.js.gz +0 -0
  281. package/dist/dashboard/assets/CombosPage-JEbYJFxw.js.gz +0 -0
  282. package/dist/dashboard/assets/ConsoleLogPage-C3xx_inM.js +0 -1
  283. package/dist/dashboard/assets/ConsoleLogPage-C3xx_inM.js.gz +0 -0
  284. package/dist/dashboard/assets/CustomizationPage-Dw306gJU.js +0 -1
  285. package/dist/dashboard/assets/CustomizationPage-Dw306gJU.js.gz +0 -0
  286. package/dist/dashboard/assets/HealthEventsModal-jaBo1TWV.js +0 -1
  287. package/dist/dashboard/assets/HealthEventsModal-jaBo1TWV.js.gz +0 -0
  288. package/dist/dashboard/assets/LoginPage-C4w7vea-.js.gz +0 -0
  289. package/dist/dashboard/assets/ModelPicker-BtPcAfot.js.gz +0 -0
  290. package/dist/dashboard/assets/OverviewPage-CGC5gNs1.js +0 -1
  291. package/dist/dashboard/assets/OverviewPage-CGC5gNs1.js.gz +0 -0
  292. package/dist/dashboard/assets/ProviderDetailPage-DQcthNGt.js +0 -4
  293. package/dist/dashboard/assets/ProviderDetailPage-DQcthNGt.js.gz +0 -0
  294. package/dist/dashboard/assets/ProvidersPage-0OsXRGXe.js +0 -1
  295. package/dist/dashboard/assets/ProvidersPage-0OsXRGXe.js.gz +0 -0
  296. package/dist/dashboard/assets/ProxyPage-DdPc8slb.js +0 -9
  297. package/dist/dashboard/assets/ProxyPage-DdPc8slb.js.gz +0 -0
  298. package/dist/dashboard/assets/QuotaPage-Ci5O4BnH.js +0 -1
  299. package/dist/dashboard/assets/QuotaPage-Ci5O4BnH.js.gz +0 -0
  300. package/dist/dashboard/assets/SettingsPage-CDZjTt87.js +0 -1
  301. package/dist/dashboard/assets/SettingsPage-CDZjTt87.js.gz +0 -0
  302. package/dist/dashboard/assets/SetupPage-BQS0Rx_u.js.gz +0 -0
  303. package/dist/dashboard/assets/SortableList-BAL5npBC.js.gz +0 -0
  304. package/dist/dashboard/assets/StudioPage-TP7voAQ0.js.gz +0 -0
  305. package/dist/dashboard/assets/UsagePage-Cj_fEzhg.js +0 -56
  306. package/dist/dashboard/assets/UsagePage-Cj_fEzhg.js.gz +0 -0
  307. package/dist/dashboard/assets/api-keys-MbI7e7rt.js +0 -1
  308. package/dist/dashboard/assets/api-keys-MbI7e7rt.js.gz +0 -0
  309. package/dist/dashboard/assets/app-B2vyPoII.css +0 -2
  310. package/dist/dashboard/assets/app-B2vyPoII.css.gz +0 -0
  311. package/dist/dashboard/assets/assert-C0YYuZJ4.js +0 -1
  312. package/dist/dashboard/assets/assert-C0YYuZJ4.js.gz +0 -0
  313. package/dist/dashboard/assets/badge-B_DMpMJN.js.gz +0 -0
  314. package/dist/dashboard/assets/banner-DMrMPmth.js.gz +0 -0
  315. package/dist/dashboard/assets/card-Be5Uzamx.js.gz +0 -0
  316. package/dist/dashboard/assets/cli-tools-muFRphDu.js +0 -1
  317. package/dist/dashboard/assets/cli-tools-muFRphDu.js.gz +0 -0
  318. package/dist/dashboard/assets/clipboard-button-DOfrVmsi.js.gz +0 -0
  319. package/dist/dashboard/assets/dialog-C9omdjQt.js.gz +0 -0
  320. package/dist/dashboard/assets/drawer-Bm8WKaGY.js.gz +0 -0
  321. package/dist/dashboard/assets/entry-DS90gUks.js +0 -1
  322. package/dist/dashboard/assets/entry-DS90gUks.js.gz +0 -0
  323. package/dist/dashboard/assets/entry-j40LJotO.js +0 -4
  324. package/dist/dashboard/assets/entry-j40LJotO.js.gz +0 -0
  325. package/dist/dashboard/assets/entry-q5HwZ8nN.js.gz +0 -0
  326. package/dist/dashboard/assets/formatters-BTScrQpe.js.gz +0 -0
  327. package/dist/dashboard/assets/github-badge-Orjuzemr.js.gz +0 -0
  328. package/dist/dashboard/assets/index-cPrX5vn6.js.gz +0 -0
  329. package/dist/dashboard/assets/inline-Dvux8w-0.js.gz +0 -0
  330. package/dist/dashboard/assets/input-CU1Rgf3p.js.gz +0 -0
  331. package/dist/dashboard/assets/layout-C_7A6H55.js.gz +0 -0
  332. package/dist/dashboard/assets/list-row-CzFvogbj.js.gz +0 -0
  333. package/dist/dashboard/assets/live-KaG15HLQ.js +0 -1
  334. package/dist/dashboard/assets/live-KaG15HLQ.js.gz +0 -0
  335. package/dist/dashboard/assets/minimize-2-D-MN8W8a.js +0 -1
  336. package/dist/dashboard/assets/minimize-2-D-MN8W8a.js.gz +0 -0
  337. package/dist/dashboard/assets/mutation-JJGQm8TD.js.gz +0 -0
  338. package/dist/dashboard/assets/numbers-BNDZnsUE.js.gz +0 -0
  339. package/dist/dashboard/assets/query-keys-CLaNz8Xz.js.gz +0 -0
  340. package/dist/dashboard/assets/quota-DZGyp0N5.js +0 -1
  341. package/dist/dashboard/assets/quota-DZGyp0N5.js.gz +0 -0
  342. package/dist/dashboard/assets/routing-BEH32QOC.js +0 -1
  343. package/dist/dashboard/assets/routing-BEH32QOC.js.gz +0 -0
  344. package/dist/dashboard/assets/select-B3iTetHN.js.gz +0 -0
  345. package/dist/dashboard/assets/settings-Dt3Eqbnq.js +0 -1
  346. package/dist/dashboard/assets/settings-Dt3Eqbnq.js.gz +0 -0
  347. package/dist/dashboard/assets/stack-KrOrm7JZ.js.gz +0 -0
  348. package/dist/dashboard/assets/state-DLS7IDMy.js.gz +0 -0
  349. package/dist/dashboard/assets/switch-42ItvbAs.js.gz +0 -0
  350. package/dist/dashboard/assets/useMutation-Dmnfh1Ue.js.gz +0 -0
  351. /package/dist/dashboard/assets/{app-wvwCi4Eh.js → app-BayTTy01.js} +0 -0
  352. /package/dist/dashboard/assets/{app-wvwCi4Eh.js.gz → app-BayTTy01.js.gz} +0 -0
@@ -17,6 +17,10 @@ import { type TelemetryEventRecord } from "../../observability/event-log";
17
17
  import {
18
18
  queryTelemetryEvents as querySqlEvents,
19
19
  sumTelemetryTokens,
20
+ familyKpiTotals,
21
+ familyWindowRequests,
22
+ familyHourlyTelemetry,
23
+ familyIpBreakdown,
20
24
  } from "../../observability/event-query";
21
25
  import type { CartethyiaDatabase } from "../../persistence/db-handle";
22
26
  import { apiKeys } from "../../persistence/schema";
@@ -72,6 +76,12 @@ export interface ShareTopClientIp {
72
76
  * the tool rather than the machine.
73
77
  */
74
78
  readonly clientType: string | null;
79
+ /**
80
+ * True when this address made a request within {@link ACTIVE_IP_WINDOW_MS}.
81
+ * A recipient watching the link can tell which addresses are still using it
82
+ * versus ones that stopped days ago.
83
+ */
84
+ readonly active: boolean;
75
85
  }
76
86
 
77
87
  export interface ShareFamilyStats {
@@ -111,6 +121,24 @@ export interface ShareStatsPort {
111
121
 
112
122
  const HOUR_MS = 3_600_000;
113
123
 
124
+ /**
125
+ * How old an address's last request may be and still count as active on the
126
+ * share page. 24 hours matches the page's own "last 24 hours" framing: an IP
127
+ * that made a request since yesterday is "active", one that stopped before
128
+ * that window is "inactive".
129
+ */
130
+ const ACTIVE_IP_WINDOW_MS = 24 * HOUR_MS;
131
+
132
+ /**
133
+ * Recency window for the client-IP ranking's *work set*: the GROUP BY only
134
+ * scans this slice of telemetry instead of the whole retained window every
135
+ * tick. Within the window, addresses are ranked by request volume (highest
136
+ * first), and older addresses fall out of the ranking entirely. 7 days keeps
137
+ * the ranking useful while staying orders of magnitude smaller than the
138
+ * retained window.
139
+ */
140
+ const IP_WINDOW_MS = 7 * 24 * HOUR_MS;
141
+
114
142
  /**
115
143
  * Names the client behind a request from its User-Agent.
116
144
  *
@@ -154,7 +182,7 @@ interface FamilyRollup {
154
182
  totals: ShareFamilyTotals;
155
183
  byHour: Map<string, { requests: number; tokens: number }>;
156
184
  models: Map<string, { modelId: string; requests: number; tokens: number; rateSum: number; rateCount: number; ttfbSum: number; ttfbCount: number }>;
157
- ips: Map<string, { ip: string; requests: number; tokens: number; lastSeenAt: string | null; userAgent: string | null }>;
185
+ ips: Map<string, { ip: string; requests: number; tokens: number; lastSeenAt: string | null; userAgent: string | null; active: boolean }>;
158
186
  }
159
187
 
160
188
  /**
@@ -163,7 +191,7 @@ interface FamilyRollup {
163
191
  */
164
192
  function rollupFamily(
165
193
  events: readonly TelemetryEventRecord[],
166
- bounds: { dayStart: number; monthStart: number; lastHourStart: number; windowStart: number; allowed: ReadonlySet<string> | null },
194
+ bounds: { dayStart: number; monthStart: number; lastHourStart: number; windowStart: number; activeIpCutoff: number; allowed: ReadonlySet<string> | null },
167
195
  ): FamilyRollup {
168
196
  const totals = { requests: 0, errors: 0, inputTokens: 0, outputTokens: 0, totalTokens: 0, lastHourRequests: 0, todayTokens: 0, monthTokens: 0 };
169
197
  const byHour = new Map<string, { requests: number; tokens: number }>();
@@ -196,13 +224,14 @@ function rollupFamily(
196
224
  models.set(event.requestedModel, model);
197
225
  }
198
226
  if (event.clientIp !== null) {
199
- const ip = ips.get(event.clientIp) ?? { ip: event.clientIp, requests: 0, tokens: 0, lastSeenAt: null, userAgent: null };
227
+ const ip = ips.get(event.clientIp) ?? { ip: event.clientIp, requests: 0, tokens: 0, lastSeenAt: null, userAgent: null, active: false };
200
228
  ip.requests += 1;
201
229
  ip.tokens += tokens;
202
230
  // Events arrive newest first, so the first one seen is the most recent.
203
231
  if (ip.lastSeenAt === null) {
204
232
  ip.lastSeenAt = event.createdAt;
205
233
  ip.userAgent = event.userAgent;
234
+ ip.active = at >= bounds.activeIpCutoff;
206
235
  }
207
236
  ips.set(event.clientIp, ip);
208
237
  }
@@ -244,17 +273,18 @@ const HOURS_WINDOW = 24;
244
273
  /**
245
274
  * Cache lifetime for one family's rollup.
246
275
  *
247
- * Matched to the share page's SSE tick, which was the point of the old 2 s
248
- * value — but at 2 s the cache expired on every tick, so every single push was
249
- * a full parse of the retained telemetry window. A share page's numbers move
250
- * when a request does, not every 2 s; 10 s still reads as live, and cuts the
251
- * parse rate by five.
276
+ * Longer than the SSE tick (30s) so most ticks are served from cache. The
277
+ * cold-compute path reads the family's telemetry windows (24h chart, 7d IP
278
+ * ranking), which on a busy family is the single most expensive thing the
279
+ * share page does; a 5-minute TTL caps that to once per family per 5
280
+ * minutes while the page still reads as live — KPI numbers move with
281
+ * traffic, not on a per-minute cadence.
252
282
  *
253
283
  * Declared here rather than imported from the router so this port does not
254
- * depend on it. `SHARE_STATS_STREAM_INTERVAL_MS` must be raised with it, or
255
- * the two drift apart and the cache expires between ticks for no reason.
284
+ * depend on it. `SHARE_STATS_STREAM_INTERVAL_MS` must stay below it, or the
285
+ * tick and cache drift apart and every tick bypasses the cache.
256
286
  */
257
- export const SHARE_STATS_CACHE_TTL_SECONDS = 10;
287
+ export const SHARE_STATS_CACHE_TTL_SECONDS = 300;
258
288
 
259
289
  /**
260
290
  * Cache key for one family's rollup. The family is named by its template key
@@ -318,17 +348,23 @@ async function computeFamilyStatsBody(
318
348
  const hourStart = utcHourStart(now);
319
349
  const lastHourStart = new Date(now.getTime() - HOUR_MS);
320
350
  const windowStart = new Date(hourStart.getTime() - (HOURS_WINDOW - 1) * HOUR_MS);
351
+ // The IP breakdown is bounded to this recency window: a family's "who is
352
+ // using the link" answer only needs addresses that were active recently,
353
+ // and an unbounded GROUP BY over every retained event is the one remaining
354
+ // full-window scan on the share page.
355
+ const ipWindowStart = new Date(now.getTime() - IP_WINDOW_MS);
321
356
 
322
- // All-time and today's/month's spend come from SQL aggregates, not from the
323
- // bounded page below. The page is capped so a very large family cannot pull
324
- // the whole table, which means it cannot carry the all-time total. Lifetime,
325
- // daily, and monthly are therefore separate bounded sums over the retained
326
- // SQL window (each is a single `SUM` aggregate, not a row materialization).
327
- // The page's events feed only the hourly chart and the two ranked tables.
328
- const [totalTokens, todayTokens, monthTokens, template] = await Promise.all([
329
- sumTelemetryTokens(db, tenantId, { familyKeyId: templateKeyId, since: new Date(0) }),
357
+ // All-time family KPI comes from the durable rollup
358
+ // (`telemetry_usage_totals`, charged at write time), not from re-aggregating
359
+ // the entire retained telemetry window on every tick: the share page's tick
360
+ // would otherwise read every event a busy family ever produced just to draw
361
+ // four numbers. Today's/month's spend and the last-hour count are bounded
362
+ // window reads over the time index, which stay small.
363
+ const [kpi, todayTokens, monthTokens, lastHourRequests, template] = await Promise.all([
364
+ familyKpiTotals(db, tenantId, templateKeyId),
330
365
  sumTelemetryTokens(db, tenantId, { familyKeyId: templateKeyId, since: dayStart }),
331
366
  sumTelemetryTokens(db, tenantId, { familyKeyId: templateKeyId, since: monthStart }),
367
+ familyWindowRequests(db, tenantId, templateKeyId, lastHourStart),
332
368
  db
333
369
  .select({ lifetime: apiKeys.lifetimeTokensConsumed })
334
370
  .from(apiKeys)
@@ -342,8 +378,9 @@ async function computeFamilyStatsBody(
342
378
  // across the overview, the share dialog, and this page.
343
379
  const templateLifetime = Number(template[0]?.lifetime ?? 0);
344
380
 
345
- // The hourly chart covers the last 24 hours, so the page only needs events
346
- // in that window — not the whole retained history. Bounded and small.
381
+ // The ranked tables need real events, bounded to the 24-hour window. The
382
+ // totals and hourly chart are computed in SQL, so the page cap here only
383
+ // shapes the model/IP rankings — never the KPI numbers.
347
384
  const page = await querySqlEvents(db, {
348
385
  tenantId,
349
386
  familyApiKeyId: templateKeyId,
@@ -356,6 +393,7 @@ async function computeFamilyStatsBody(
356
393
  monthStart: monthStart.getTime(),
357
394
  lastHourStart: lastHourStart.getTime(),
358
395
  windowStart: windowStart.getTime(),
396
+ activeIpCutoff: now.getTime() - ACTIVE_IP_WINDOW_MS,
359
397
  allowed: allowedSet,
360
398
  });
361
399
  rollup.totals = {
@@ -363,14 +401,28 @@ async function computeFamilyStatsBody(
363
401
  // The template counter is the seed on the running total, not a distinct
364
402
  // bucket: daily/monthly windows are telemetry-only (the counter has no
365
403
  // time axis), so those keep their own sums.
366
- totalTokens: templateLifetime + totalTokens,
404
+ totalTokens: templateLifetime + kpi.tokens,
367
405
  todayTokens,
368
406
  monthTokens,
407
+ // Lifetime counts come from the durable rollup, not the page: a family
408
+ // busier than the page cap must not freeze at 500 requests/errors.
409
+ requests: kpi.requests,
410
+ errors: kpi.errors,
411
+ lastHourRequests,
369
412
  };
370
413
 
371
- // Fill the 24 buckets in JS: only hours that saw traffic are present, and
372
- // a chart drawn from sparse rows would silently compress the axis.
373
- const byHour = rollup.byHour;
414
+ // The hourly chart is a SQL aggregate over the whole family window, so a
415
+ // busy family's bars stay true. Buckets are filled in JS because only hours
416
+ // that saw traffic are present; a chart drawn from sparse rows would
417
+ // silently compress the axis.
418
+ const bucketSeconds = HOUR_MS / 1000;
419
+ const sqlHourly = await familyHourlyTelemetry(db, tenantId, templateKeyId, windowStart, bucketSeconds);
420
+ const byHour = new Map(
421
+ sqlHourly.map((bucket) => [
422
+ new Date(bucket.at).toISOString(),
423
+ { requests: bucket.requests, tokens: bucket.inputTokens + bucket.outputTokens },
424
+ ]),
425
+ );
374
426
  const hourly: ShareHourlyBucket[] = [];
375
427
  for (let index = 0; index < HOURS_WINDOW; index += 1) {
376
428
  const hour = new Date(windowStart.getTime() + index * HOUR_MS).toISOString();
@@ -378,6 +430,33 @@ async function computeFamilyStatsBody(
378
430
  hourly.push({ hour, requests: bucket?.requests ?? 0, tokens: bucket?.tokens ?? 0 });
379
431
  }
380
432
 
433
+ // Client IPs come from a SQL aggregate over a bounded recent window, ranked
434
+ // by request volume — so a family's "who is using the link most" answer
435
+ // stays cheap instead of scanning the whole retained telemetry on every
436
+ // tick. `clientType` is best-effort from the page's events (the most recent
437
+ // 24 hours), which carry the User-Agent.
438
+ const ipRows = await familyIpBreakdown(db, tenantId, templateKeyId, TOP_IPS_LIMIT, ipWindowStart);
439
+ const clientTypeByIp = new Map<string, string | null>();
440
+ for (const event of events) {
441
+ if (event.clientIp !== null && !clientTypeByIp.has(event.clientIp)) {
442
+ clientTypeByIp.set(event.clientIp, clientTypeFromUserAgent(event.userAgent));
443
+ }
444
+ }
445
+ const activeIpCutoff = now.getTime() - ACTIVE_IP_WINDOW_MS;
446
+ const clientIps = ipRows.map((row) => {
447
+ const lastSeenAt =
448
+ row.lastSeenAt instanceof Date ? row.lastSeenAt : new Date(String(row.lastSeenAt ?? ""));
449
+ const lastSeenMs = Number.isFinite(lastSeenAt.getTime()) ? lastSeenAt.getTime() : 0;
450
+ return {
451
+ ip: maskClientIp(row.ip),
452
+ requests: row.requests,
453
+ tokens: row.tokens,
454
+ lastSeenAt: lastSeenMs > 0 ? lastSeenAt.toISOString() : null,
455
+ clientType: clientTypeByIp.get(row.ip) ?? null,
456
+ active: lastSeenMs >= activeIpCutoff,
457
+ };
458
+ });
459
+
381
460
  return {
382
461
  totals: rollup.totals,
383
462
  hourly,
@@ -391,15 +470,6 @@ async function computeFamilyStatsBody(
391
470
  avgTokensPerSec: row.rateCount > 0 ? row.rateSum / row.rateCount : null,
392
471
  avgTtfbMs: row.ttfbCount > 0 ? Math.round(row.ttfbSum / row.ttfbCount) : null,
393
472
  })),
394
- clientIps: [...rollup.ips.values()]
395
- .sort((a, b) => b.requests - a.requests)
396
- .slice(0, TOP_IPS_LIMIT)
397
- .map((row) => ({
398
- ip: maskClientIp(row.ip),
399
- requests: row.requests,
400
- tokens: row.tokens,
401
- lastSeenAt: row.lastSeenAt,
402
- clientType: clientTypeFromUserAgent(row.userAgent),
403
- })),
473
+ clientIps,
404
474
  };
405
475
  }
@@ -63,6 +63,34 @@ export function etagKey(key: string): string {
63
63
  return `${key}:etag`;
64
64
  }
65
65
 
66
+ /**
67
+ * In-memory fallback store used when no Redis client is configured (the
68
+ * embedded lite backend). Keyed by the same cache key, storing the value and
69
+ * its validator with a TTL. Bounded so a busy gateway cannot grow it without
70
+ * limit.
71
+ */
72
+ const LOCAL_CACHE_MAX_ENTRIES = 2000;
73
+ const localCache = new Map<string, { at: number; value: string; etag: string }>();
74
+
75
+ function localGet(key: string): { value: string; etag: string } | undefined {
76
+ const entry = localCache.get(key);
77
+ if (entry === undefined) return undefined;
78
+ if (Date.now() - entry.at >= READ_THROUGH_DEFAULT_TTL_SECONDS * 1000) {
79
+ localCache.delete(key);
80
+ return undefined;
81
+ }
82
+ return { value: entry.value, etag: entry.etag };
83
+ }
84
+
85
+ function localSet(key: string, value: string, etag: string): void {
86
+ if (localCache.size >= LOCAL_CACHE_MAX_ENTRIES) {
87
+ // Evict the oldest entry (Map iterates in insertion order).
88
+ const oldest = localCache.keys().next();
89
+ if (!oldest.done) localCache.delete(oldest.value);
90
+ }
91
+ localCache.set(key, { at: Date.now(), value, etag });
92
+ }
93
+
66
94
  /**
67
95
  * Reads through the cache and returns a strong validator alongside the value.
68
96
  *
@@ -74,7 +102,8 @@ export function etagKey(key: string): string {
74
102
  * With no cache configured (`client === undefined`) the value is computed and
75
103
  * the validator is still returned, so conditional requests keep working for
76
104
  * the length of the client's own polling loop; only the cross-instance sharing
77
- * is lost.
105
+ * is lost. A process-local Map serves the same TTL when Redis is absent, so a
106
+ * poller still gets cache hits.
78
107
  */
79
108
  export async function cachedReadWithEtag<T>(
80
109
  client: ReadThroughCacheClient | undefined,
@@ -83,8 +112,15 @@ export async function cachedReadWithEtag<T>(
83
112
  ttlSeconds: number = READ_THROUGH_DEFAULT_TTL_SECONDS,
84
113
  ): Promise<CachedResult<T>> {
85
114
  if (client === undefined) {
115
+ const entry = localGet(key);
116
+ if (entry !== undefined) {
117
+ return { value: JSON.parse(entry.value) as T, etag: entry.etag };
118
+ }
86
119
  const fresh = await compute();
87
- return { value: fresh, etag: etagFor(JSON.stringify(fresh) ?? "null") };
120
+ const bytes = JSON.stringify(fresh) ?? "null";
121
+ const etag = etagFor(bytes);
122
+ localSet(key, bytes, etag);
123
+ return { value: fresh, etag };
88
124
  }
89
125
  try {
90
126
  const hit = await client.get(key);
@@ -1,4 +1,4 @@
1
- import { Elysia } from "elysia";
1
+ import { Elysia, type AnyElysia } from "elysia";
2
2
  import { spawn } from "node:child_process";
3
3
  import { consoleErrorHandler, requireTenantScope } from "../shared/errors";
4
4
  import { consoleSseResponse, createConsoleSseStream } from "../observability/sse";
@@ -7,7 +7,10 @@ import type { ConsoleAccessResolver } from "../auth/access";
7
7
  import { fetchLatestRelease, type LatestRelease } from "./repository-updates";
8
8
  import { isNewerRelease } from "./version-compare";
9
9
 
10
- const MEMORY_SAMPLE_INTERVAL_MS = 10_000;
10
+ // Memory is sampled per visible tab: ten seconds per stream made a console
11
+ // with many open tabs wake up constantly for numbers nobody is reading. 30 s
12
+ // is still responsive for a memory warning while cutting that work by three.
13
+ const MEMORY_SAMPLE_INTERVAL_MS = 30_000;
11
14
  const UPDATE_SAMPLE_INTERVAL_MS = 60_000;
12
15
  const HIGH_MEMORY_PERCENT = 85;
13
16
  const HIGH_MEMORY_CLEAR_PERCENT = 80;
@@ -94,7 +97,7 @@ export function createUpdateRoutes(config: {
94
97
  readonly accessResolver: ConsoleAccessResolver;
95
98
  readonly observabilityStore: ObservabilityStore;
96
99
  readonly fetchRelease?: () => Promise<LatestRelease | null>;
97
- }): Elysia {
100
+ }): AnyElysia {
98
101
  const fetchRelease = config.fetchRelease ?? fetchLatestRelease;
99
102
  return new Elysia()
100
103
  .error(consoleErrorHandler("Update check failed"))
@@ -199,5 +202,5 @@ export function createUpdateRoutes(config: {
199
202
  };
200
203
  }),
201
204
  );
202
- }) as unknown as Elysia;
205
+ });
203
206
  }
@@ -1,7 +1,7 @@
1
1
  // Validated fetch: SSRF-guarded outbound fetch with optional pool-agent support.
2
2
  import { GatewayError } from "../protocol/gateway-error";
3
- import { Agent as HttpAgent } from "node:http";
4
- import { Agent as HttpsAgent } from "node:https";
3
+ import { request as httpRequest, Agent as HttpAgent } from "node:http";
4
+ import { request as httpsRequest, Agent as HttpsAgent } from "node:https";
5
5
  import { Agent as UndiciAgent, fetch as undiciFetch, type UndiciRequestInit } from "undici";
6
6
  import { SocksProxyAgent } from "socks-proxy-agent";
7
7
  import { resolveAllAddresses, validateResolvedAddresses } from "./ssrf";
@@ -167,6 +167,66 @@ async function dial(
167
167
  });
168
168
  }
169
169
 
170
+ /**
171
+ * One raw GET/HEAD dial that carries a body.
172
+ *
173
+ * The runtime-native fetch (`Bun`/undici) rejects `GET`/`HEAD` requests with a
174
+ * body — "request with GET/HEAD method cannot have body" — but some upstreams
175
+ * validate a signature over the body on a GET (Qoder's `/model/list` does).
176
+ * This is the Node `http`/`https` path for exactly that case. It runs only
177
+ * after the SSRF resolve/validate pass and only when no pool/relay agent owns
178
+ * the dial, so the security boundary is unchanged.
179
+ */
180
+ function nodeDial(url: URL, init: RequestInit): Promise<Response> {
181
+ return new Promise((resolve, reject) => {
182
+ const mod = url.protocol === "http:" ? httpRequest : httpsRequest;
183
+ const headers: Record<string, string> = {};
184
+ {
185
+ const h = new Headers(init.headers);
186
+ h.forEach((value, key) => {
187
+ headers[key] = value;
188
+ });
189
+ }
190
+ let payload: Buffer | undefined;
191
+ const body = init.body;
192
+ if (typeof body === "string") {
193
+ payload = Buffer.from(body, "utf8");
194
+ } else if (Buffer.isBuffer(body)) {
195
+ payload = body;
196
+ } else if (body instanceof Uint8Array) {
197
+ payload = Buffer.from(body);
198
+ } else if (body !== null && body !== undefined) {
199
+ reject(new TypeError("nodeDial only supports string/Buffer/Uint8Array bodies"));
200
+ return;
201
+ }
202
+ if (payload !== undefined) headers["content-length"] = String(payload.byteLength);
203
+ const req = mod(
204
+ url,
205
+ {
206
+ method: (init.method ?? "GET").toUpperCase(),
207
+ headers,
208
+ ...(init.signal === undefined ? {} : { signal: init.signal as AbortSignal }),
209
+ },
210
+ (res) => {
211
+ const chunks: Buffer[] = [];
212
+ res.on("data", (c: Buffer) => chunks.push(c));
213
+ res.on("end", () => {
214
+ resolve(
215
+ new Response(Buffer.concat(chunks), {
216
+ status: res.statusCode ?? 200,
217
+ ...(res.statusMessage === undefined ? {} : { statusText: res.statusMessage }),
218
+ headers: res.headers as unknown as HeadersInit,
219
+ }),
220
+ );
221
+ });
222
+ },
223
+ );
224
+ req.on("error", reject);
225
+ if (payload !== undefined) req.write(payload);
226
+ req.end();
227
+ });
228
+ }
229
+
170
230
  export function createValidatedFetch(options: ValidatedFetchOptions = {}): ValidatedFetch {
171
231
  const policy = options.policy ?? {};
172
232
  const maxRedirects = options.maxRedirects ?? policy.maxRedirects ?? 3;
@@ -242,7 +302,18 @@ export function createValidatedFetch(options: ValidatedFetchOptions = {}): Valid
242
302
  // serverless front door refuses (400).
243
303
  response = await dial(relay.url, relay.init, fetchFn, undefined);
244
304
  } else {
245
- response = await dial(url, requestInit, fetchFn, proxyUrl, socksAgent ? socksDispatcherFor(socksAgent) : undefined, socksFetch);
305
+ const method = (requestInit.method ?? "GET").toUpperCase();
306
+ const hasBody = requestInit.body !== undefined && requestInit.body !== null;
307
+ // The runtime fetch cannot carry a body on GET/HEAD, but Qoder's
308
+ // model-list endpoint requires a signed GET body. When no pool/relay
309
+ // owns the dial and no injected test fetch is present, dial through the
310
+ // Node http(s) client (post SSRF validation). Every other case keeps
311
+ // the existing fetch path.
312
+ if ((method === "GET" || method === "HEAD") && hasBody && !socksAgent && options.fetchFn === undefined) {
313
+ response = await nodeDial(url, requestInit);
314
+ } else {
315
+ response = await dial(url, requestInit, fetchFn, proxyUrl, socksAgent ? socksDispatcherFor(socksAgent) : undefined, socksFetch);
316
+ }
246
317
  }
247
318
  if (response.status < 300 || response.status >= 400) {
248
319
  trackNetworkCall(url.hostname, performance.now() - startedAt);
@@ -264,7 +335,7 @@ export function createValidatedFetch(options: ValidatedFetchOptions = {}): Valid
264
335
  if (isCrossOriginRedirect && (response.status === 307 || response.status === 308)) {
265
336
  const method = (requestInit.method ?? "GET").toUpperCase();
266
337
  if (method !== "GET" && method !== "HEAD" && requestInit.body !== undefined) {
267
- throw new GatewayError("invalid_request", 400, "cross-origin redirect with body blocked");
338
+ throw new GatewayError("invalid_request", 400, "cross-origin redirect with body blocked (request {requestId}). Check the shape of what you sent — then resend 🪞");
268
339
  }
269
340
  }
270
341
  url = nextUrl;