cartethyia 2.7.4 → 2.7.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 (595) hide show
  1. package/.env.example +5 -0
  2. package/README.md +18 -6
  3. package/dist/dashboard/assets/BannedPage-B1N603cM.js +1 -0
  4. package/dist/dashboard/assets/BannedPage-B1N603cM.js.gz +0 -0
  5. package/dist/dashboard/assets/CliToolDetailPage-9q4_4NoK.js +3 -0
  6. package/dist/dashboard/assets/CliToolDetailPage-9q4_4NoK.js.gz +0 -0
  7. package/dist/dashboard/assets/CliToolsPage-DwshGtK-.js +1 -0
  8. package/dist/dashboard/assets/CliToolsPage-DwshGtK-.js.gz +0 -0
  9. package/dist/dashboard/assets/CombosPage-D762MIkp.js +1 -0
  10. package/dist/dashboard/assets/CombosPage-D762MIkp.js.gz +0 -0
  11. package/dist/dashboard/assets/ConsoleLogPage-KRYjHZsX.js +1 -0
  12. package/dist/dashboard/assets/ConsoleLogPage-KRYjHZsX.js.gz +0 -0
  13. package/dist/dashboard/assets/CustomizationPage-DXsxk5uM.js +1 -0
  14. package/dist/dashboard/assets/CustomizationPage-DXsxk5uM.js.gz +0 -0
  15. package/dist/dashboard/assets/HealthEventsModal-BEjAjD4F.js +1 -0
  16. package/dist/dashboard/assets/HealthEventsModal-BEjAjD4F.js.gz +0 -0
  17. package/dist/dashboard/assets/InjectSystemPromptPanel-d5v3K_Yc.js +899 -0
  18. package/dist/dashboard/assets/InjectSystemPromptPanel-d5v3K_Yc.js.gz +0 -0
  19. package/dist/dashboard/assets/LoginPage-Ccv4tjNS.js +1 -0
  20. package/dist/dashboard/assets/LoginPage-Ccv4tjNS.js.gz +0 -0
  21. package/dist/dashboard/assets/{ModelPicker-BMS2t8BA.js → ModelPicker-C6WlpU5E.js} +1 -1
  22. package/dist/dashboard/assets/ModelPicker-C6WlpU5E.js.gz +0 -0
  23. package/dist/dashboard/assets/OverviewPage-DN9fYNWA.js +1 -0
  24. package/dist/dashboard/assets/OverviewPage-DN9fYNWA.js.gz +0 -0
  25. package/dist/dashboard/assets/ProviderDetailPage-BY0ieUgz.js +5 -0
  26. package/dist/dashboard/assets/ProviderDetailPage-BY0ieUgz.js.gz +0 -0
  27. package/dist/dashboard/assets/ProviderIcon-jjqXJDA3.js +1 -0
  28. package/dist/dashboard/assets/ProviderIcon-jjqXJDA3.js.gz +0 -0
  29. package/dist/dashboard/assets/ProvidersPage-DNsbfea2.js +1 -0
  30. package/dist/dashboard/assets/ProvidersPage-DNsbfea2.js.gz +0 -0
  31. package/dist/dashboard/assets/{ProxyPage-lsrx7NYg.js → ProxyPage-DxjvUzSD.js} +1 -1
  32. package/dist/dashboard/assets/ProxyPage-DxjvUzSD.js.gz +0 -0
  33. package/dist/dashboard/assets/QuotaPage-OaHScjpX.js +1 -0
  34. package/dist/dashboard/assets/QuotaPage-OaHScjpX.js.gz +0 -0
  35. package/dist/dashboard/assets/SettingsPage-BW4AKj2H.js +1 -0
  36. package/dist/dashboard/assets/SettingsPage-BW4AKj2H.js.gz +0 -0
  37. package/dist/dashboard/assets/SetupPage-44BZrxFC.js +1 -0
  38. package/dist/dashboard/assets/SetupPage-44BZrxFC.js.gz +0 -0
  39. package/dist/dashboard/assets/{StudioPage-Cq5tW7hW.js → StudioPage-DRPavCdU.js} +4 -4
  40. package/dist/dashboard/assets/StudioPage-DRPavCdU.js.gz +0 -0
  41. package/dist/dashboard/assets/ToolIcon-CfeLA-Z_.js +1 -0
  42. package/dist/dashboard/assets/ToolIcon-CfeLA-Z_.js.gz +0 -0
  43. package/dist/dashboard/assets/UsagePage-B7M7LvMM.js +56 -0
  44. package/dist/dashboard/assets/UsagePage-B7M7LvMM.js.gz +0 -0
  45. package/dist/dashboard/assets/api-keys-KvX83mRe.js +1 -0
  46. package/dist/dashboard/assets/api-keys-KvX83mRe.js.gz +0 -0
  47. package/dist/dashboard/assets/app-DRnQLhsc.js +9 -0
  48. package/dist/dashboard/assets/app-DRnQLhsc.js.gz +0 -0
  49. package/dist/dashboard/assets/app-mweRcsKp.css +2 -0
  50. package/dist/dashboard/assets/app-mweRcsKp.css.gz +0 -0
  51. package/dist/dashboard/assets/assert-CidQVoNo.js +1 -0
  52. package/dist/dashboard/assets/assert-CidQVoNo.js.gz +0 -0
  53. package/dist/dashboard/assets/{badge-D9CS6dZk.js → badge-BaX2GTrn.js} +1 -1
  54. package/dist/dashboard/assets/badge-BaX2GTrn.js.gz +0 -0
  55. package/dist/dashboard/assets/{banner-DympBNq4.js → banner-DFH4Bkte.js} +1 -1
  56. package/dist/dashboard/assets/banner-DFH4Bkte.js.gz +0 -0
  57. package/dist/dashboard/assets/brand/login_register.jpg +0 -0
  58. package/dist/dashboard/assets/brand/login_register_initial.jpg +0 -0
  59. package/dist/dashboard/assets/brand/og_bansos.jpg +0 -0
  60. package/dist/dashboard/assets/brand/og_image.jpg +0 -0
  61. package/dist/dashboard/assets/{card-ibRWU_uT.js → card-CxDw0lK6.js} +1 -1
  62. package/dist/dashboard/assets/card-CxDw0lK6.js.gz +0 -0
  63. package/dist/dashboard/assets/chevron-left-Bf6CaTSJ.js +1 -0
  64. package/dist/dashboard/assets/chevron-left-Bf6CaTSJ.js.gz +0 -0
  65. package/dist/dashboard/assets/circle-question-mark-Cz4un8sH.js +1 -0
  66. package/dist/dashboard/assets/circle-question-mark-Cz4un8sH.js.gz +0 -0
  67. package/dist/dashboard/assets/cli-tools-Bwv5-zGZ.js +1 -0
  68. package/dist/dashboard/assets/cli-tools-Bwv5-zGZ.js.gz +0 -0
  69. package/dist/dashboard/assets/{clipboard-button-DJm-9pz7.js → clipboard-button-DfS1ntiq.js} +1 -1
  70. package/dist/dashboard/assets/clipboard-button-DfS1ntiq.js.gz +0 -0
  71. package/dist/dashboard/assets/dialog-BK-NRr5u.js +1 -0
  72. package/dist/dashboard/assets/dialog-BK-NRr5u.js.gz +0 -0
  73. package/dist/dashboard/assets/{drawer-CqGYeOrd.js → drawer-QqPul6sC.js} +1 -1
  74. package/dist/dashboard/assets/drawer-QqPul6sC.js.gz +0 -0
  75. package/dist/dashboard/assets/entry-BMlYJVFj.js +1 -0
  76. package/dist/dashboard/assets/entry-BMlYJVFj.js.gz +0 -0
  77. package/dist/dashboard/assets/entry-D6WvojeD.js +4 -0
  78. package/dist/dashboard/assets/entry-D6WvojeD.js.gz +0 -0
  79. package/dist/dashboard/assets/entry-Dmx7gBiN.js +1 -0
  80. package/dist/dashboard/assets/entry-Dmx7gBiN.js.gz +0 -0
  81. package/dist/dashboard/assets/{formatters-D-MTJxMr.js → formatters-u7JPB5BX.js} +1 -1
  82. package/dist/dashboard/assets/formatters-u7JPB5BX.js.gz +0 -0
  83. package/dist/dashboard/assets/house-h9AZlJuT.js +1 -0
  84. package/dist/dashboard/assets/house-h9AZlJuT.js.gz +0 -0
  85. package/dist/dashboard/assets/{index-DBNPOfhV.js → index-Ko4dLRnV.js} +2 -2
  86. package/dist/dashboard/assets/index-Ko4dLRnV.js.gz +0 -0
  87. package/dist/dashboard/assets/{inline-BT0zntEr.js → inline-CHJQwxLa.js} +1 -1
  88. package/dist/dashboard/assets/inline-CHJQwxLa.js.gz +0 -0
  89. package/dist/dashboard/assets/{input-Da2ZViWg.js → input-BlmlizVV.js} +1 -1
  90. package/dist/dashboard/assets/input-BlmlizVV.js.gz +0 -0
  91. package/dist/dashboard/assets/{layout-BXNeLw6M.js → layout-Bz93I-yE.js} +1 -1
  92. package/dist/dashboard/assets/layout-Bz93I-yE.js.gz +0 -0
  93. package/dist/dashboard/assets/{list-row-BycOrrIV.js → list-row-Ml16KCfu.js} +1 -1
  94. package/dist/dashboard/assets/list-row-Ml16KCfu.js.gz +0 -0
  95. package/dist/dashboard/assets/live-7_QpnYGu.js +1 -0
  96. package/dist/dashboard/assets/live-7_QpnYGu.js.gz +0 -0
  97. package/dist/dashboard/assets/{mutation-Rr0TIvcj.js → mutation-GU7XdEjz.js} +1 -1
  98. package/dist/dashboard/assets/mutation-GU7XdEjz.js.gz +0 -0
  99. package/dist/dashboard/assets/network-CuMDaTzT.js +1 -0
  100. package/dist/dashboard/assets/network-CuMDaTzT.js.gz +0 -0
  101. package/dist/dashboard/assets/numbers-CoqpaDMx.js +2 -0
  102. package/dist/dashboard/assets/numbers-CoqpaDMx.js.gz +0 -0
  103. package/dist/dashboard/assets/pagination-B5aOoweZ.js +1 -0
  104. package/dist/dashboard/assets/pagination-B5aOoweZ.js.gz +0 -0
  105. package/dist/dashboard/assets/providers/zai.webp +0 -0
  106. package/dist/dashboard/assets/providers/zcode.webp +0 -0
  107. package/dist/dashboard/assets/{query-keys-BloKtaXJ.js → query-keys-YSoQOUh6.js} +1 -1
  108. package/dist/dashboard/assets/{query-keys-BloKtaXJ.js.gz → query-keys-YSoQOUh6.js.gz} +0 -0
  109. package/dist/dashboard/assets/quota-DJ1QlAOq.js +1 -0
  110. package/dist/dashboard/assets/quota-DJ1QlAOq.js.gz +0 -0
  111. package/dist/dashboard/assets/routing-fnCsxgNO.js +1 -0
  112. package/dist/dashboard/assets/routing-fnCsxgNO.js.gz +0 -0
  113. package/dist/dashboard/assets/{select-BIYIBkMU.js → select-BHqpd-bg.js} +2 -2
  114. package/dist/dashboard/assets/select-BHqpd-bg.js.gz +0 -0
  115. package/dist/dashboard/assets/settings-BxslsErn.js +1 -0
  116. package/dist/dashboard/assets/settings-BxslsErn.js.gz +0 -0
  117. package/dist/dashboard/assets/shield-BMQNxORh.js +1 -0
  118. package/dist/dashboard/assets/shield-BMQNxORh.js.gz +0 -0
  119. package/dist/dashboard/assets/{stack-DfgnFDWY.js → stack-j7cXjgYX.js} +1 -1
  120. package/dist/dashboard/assets/stack-j7cXjgYX.js.gz +0 -0
  121. package/dist/dashboard/assets/{state-CkfdYWel.js → state-DHmEnLa2.js} +1 -1
  122. package/dist/dashboard/assets/state-DHmEnLa2.js.gz +0 -0
  123. package/dist/dashboard/assets/{switch-SYm237HG.js → switch-DmZcyTIC.js} +1 -1
  124. package/dist/dashboard/assets/switch-DmZcyTIC.js.gz +0 -0
  125. package/dist/dashboard/assets/{useMutation-BQqCRNIi.js → useMutation-CbCP4w1o.js} +1 -1
  126. package/dist/dashboard/assets/useMutation-CbCP4w1o.js.gz +0 -0
  127. package/dist/dashboard/console-backgrounds.jpg +0 -0
  128. package/dist/dashboard/console-backgrounds_initial.jpg +0 -0
  129. package/dist/dashboard/index.html +2 -2
  130. package/dist/dashboard/index.html.gz +0 -0
  131. package/dist/dashboard/precompressed-assets.json +60 -59
  132. package/migrations/full-postgres/0036_system_prompt_presets_refs.sql +10 -0
  133. package/migrations/full-postgres/0037_share_concurrency_scope.sql +3 -0
  134. package/migrations/full-postgres/0038_add_accounts_quota_snapshot.sql +7 -0
  135. package/migrations/full-postgres/0039_add_telemetry_provider_model_indexes.sql +13 -0
  136. package/migrations/full-postgres/0040_add_models_dialect.sql +3 -0
  137. package/migrations/full-postgres/0041_add_daily_checkin_ledger_account_fk.sql +7 -0
  138. package/migrations/full-postgres/0042_add_telemetry_degenerate_stop.sql +14 -0
  139. package/migrations/lite-sqlite3/0036_system_prompt_presets_refs.sql +14 -0
  140. package/migrations/lite-sqlite3/0037_share_concurrency_scope.sql +3 -0
  141. package/migrations/lite-sqlite3/0038_add_accounts_quota_snapshot.sql +4 -0
  142. package/migrations/lite-sqlite3/0039_add_telemetry_provider_model_indexes.sql +6 -0
  143. package/migrations/lite-sqlite3/0040_add_models_dialect.sql +3 -0
  144. package/migrations/lite-sqlite3/0041_add_daily_checkin_ledger_account_fk.sql +26 -0
  145. package/migrations/lite-sqlite3/0042_add_telemetry_degenerate_stop.sql +12 -0
  146. package/package.json +4 -6
  147. package/scripts/cli/update.ts +15 -41
  148. package/src/boot/app.ts +58 -2
  149. package/src/boot/dependencies.ts +794 -732
  150. package/src/boot/lifecycle.ts +11 -12
  151. package/src/config/cooldowns.ts +34 -55
  152. package/src/config/network.ts +1 -1
  153. package/src/config/package-paths.ts +1 -1
  154. package/src/config/providers.ts +19 -14
  155. package/src/config/resources.ts +0 -5
  156. package/src/config/runtime.ts +2 -5
  157. package/src/config/spec.ts +33 -6
  158. package/src/config/usage.ts +27 -0
  159. package/src/console/api-keys/ban-constants.ts +38 -0
  160. package/src/console/api-keys/contracts.ts +71 -62
  161. package/src/console/api-keys/routes.ts +56 -24
  162. package/src/console/auth/access.ts +4 -4
  163. package/src/console/auth/account-reset.ts +1 -1
  164. package/src/console/auth/audit-recorder.ts +1 -1
  165. package/src/console/auth/lockout.ts +7 -3
  166. package/src/console/auth/row-coercion.ts +57 -12
  167. package/src/console/auth/session-resolver.ts +1 -1
  168. package/src/console/auth/session-service.ts +8 -2
  169. package/src/console/auth/session.ts +37 -31
  170. package/src/console/auth/setup-service.ts +4 -3
  171. package/src/console/auth/user-store.ts +1 -1
  172. package/src/console/backup/9router-map.ts +0 -3
  173. package/src/console/backup/contracts.ts +9 -9
  174. package/src/console/backup/export-endpoint.ts +5 -6
  175. package/src/console/backup/nine-router.ts +1 -7
  176. package/src/console/backup/routes.ts +29 -33
  177. package/src/console/backup/service.ts +468 -468
  178. package/src/console/backup/store.ts +47 -29
  179. package/src/console/backup/validate.ts +11 -1
  180. package/src/console/cli-tools/contracts.ts +43 -299
  181. package/src/console/cli-tools/fs-ops.ts +29 -0
  182. package/src/console/cli-tools/injectors/claude.ts +3 -3
  183. package/src/console/cli-tools/injectors/codex.ts +181 -181
  184. package/src/console/cli-tools/injectors/driver.ts +150 -247
  185. package/src/console/cli-tools/injectors/kilo.ts +146 -21
  186. package/src/console/cli-tools/injectors/opencode.ts +176 -153
  187. package/src/console/cli-tools/routes.ts +7 -1
  188. package/src/console/cli-tools/service.ts +8 -7
  189. package/src/console/cli-tools/store.ts +2 -2
  190. package/src/console/console-router.ts +32 -0
  191. package/src/console/dashboard-assets.ts +9 -9
  192. package/src/console/domain-registration.ts +16 -2
  193. package/src/console/inferhub/client.ts +0 -7
  194. package/src/console/inferhub/key-store.ts +1 -1
  195. package/src/console/inferhub/normalize.ts +2 -2
  196. package/src/console/inferhub/routes.ts +1 -1
  197. package/src/console/inferhub/types.ts +1 -1
  198. package/src/console/observability/contracts.ts +7 -1
  199. package/src/console/observability/live.ts +31 -18
  200. package/src/console/observability/logs.ts +1 -1
  201. package/src/console/observability/sse.ts +62 -8
  202. package/src/console/observability/store.ts +23 -4
  203. package/src/console/providers/catalog/catalog-projections.ts +1 -1
  204. package/src/console/providers/catalog/contracts.ts +39 -0
  205. package/src/console/providers/catalog/model-operations.ts +1 -1
  206. package/src/console/providers/catalog/provider-operations.ts +95 -17
  207. package/src/console/providers/catalog/public-model-store.ts +0 -0
  208. package/src/console/providers/catalog/routes.ts +27 -3
  209. package/src/console/providers/catalog/store.ts +147 -8
  210. package/src/console/providers/detail/routes.ts +1 -1
  211. package/src/console/providers/oauth/callback-listener.ts +100 -18
  212. package/src/console/providers/oauth/routes.ts +92 -13
  213. package/src/console/quota/account-quota-global.ts +41 -3
  214. package/src/console/quota/account-quota-shared.ts +10 -3
  215. package/src/console/quota/account-quota-tenant.ts +259 -66
  216. package/src/console/quota/daily-growth-job.ts +308 -0
  217. package/src/console/quota/native-identity.ts +2 -2
  218. package/src/console/quota/quota-view.ts +1 -1
  219. package/src/console/remote-support/routes.ts +1 -1
  220. package/src/console/remote-support/service.ts +2 -2
  221. package/src/console/routing/model/contracts.ts +1 -1
  222. package/src/console/routing/pools/contracts.ts +2 -2
  223. package/src/console/routing/pools/import-parse.ts +3 -3
  224. package/src/console/routing/pools/relay-deploy.ts +3 -3
  225. package/src/console/settings/contracts.ts +29 -2
  226. package/src/console/settings/store.ts +20 -0
  227. package/src/console/share/share-router.ts +100 -32
  228. package/src/console/share/share-stats.ts +11 -13
  229. package/src/console/share/share-usage.ts +1 -1
  230. package/src/console/shared/errors.ts +2 -2
  231. package/src/console/shared/object.ts +18 -0
  232. package/src/console/shared/read-through-cache.ts +4 -4
  233. package/src/console/shared/transaction.ts +14 -0
  234. package/src/console/system-prompt/contracts.ts +89 -12
  235. package/src/console/system-prompt/routes.ts +98 -45
  236. package/src/console/updates/apply-update.ts +244 -0
  237. package/src/console/updates/routes.ts +252 -97
  238. package/src/main.ts +11 -5
  239. package/src/network/http2-bidi.ts +1 -1
  240. package/src/network/ip-utils.ts +298 -0
  241. package/src/network/outbound-fetch.ts +1 -1
  242. package/src/network/pool/agent.ts +7 -6
  243. package/src/network/pool/byte-accounting.ts +1 -1
  244. package/src/network/pool/resolver.ts +16 -12
  245. package/src/network/pool/selector.ts +150 -136
  246. package/src/network/pool-health-machine.ts +1 -1
  247. package/src/network/rate-limit-verdict.ts +1 -1
  248. package/src/network/retry.ts +1 -1
  249. package/src/network/ssrf.ts +20 -71
  250. package/src/observability/event-log.ts +9 -2
  251. package/src/observability/event-query.ts +61 -291
  252. package/src/observability/log-sanitize.ts +1 -1
  253. package/src/observability/metrics.ts +4 -4
  254. package/src/observability/payload-capture.ts +6 -5
  255. package/src/observability/payload-store.ts +1 -1
  256. package/src/observability/runtime-metrics.ts +1 -1
  257. package/src/observability/telemetry-buffer.ts +19 -3
  258. package/src/observability/telemetry-reconciliation.ts +3 -3
  259. package/src/observability/token-speed.ts +1 -1
  260. package/src/persistence/connection-url.ts +1 -1
  261. package/src/persistence/db-handle.ts +1 -1
  262. package/src/persistence/db-pg.ts +2 -2
  263. package/src/persistence/db-sqlite.ts +3 -3
  264. package/src/persistence/dialect.ts +5 -1
  265. package/src/persistence/readiness.ts +1 -1
  266. package/src/persistence/redis.ts +1 -1
  267. package/src/persistence/schema.ts +121 -40
  268. package/src/persistence/shared-cache.ts +2 -2
  269. package/src/persistence/sort-order.ts +1 -1
  270. package/src/persistence/stores/api-key-store.ts +29 -2
  271. package/src/persistence/stores/share-store.ts +20 -8
  272. package/src/persistence/system-prompt-store.ts +63 -18
  273. package/src/persistence/tenant-preferences.ts +1 -1
  274. package/src/persistence/ttl-cache.ts +3 -3
  275. package/src/persistence/usage-flush.ts +7 -2
  276. package/src/protocol/canonical-model.ts +1 -1
  277. package/src/protocol/codecs.ts +6 -10
  278. package/src/protocol/dsml.ts +8 -125
  279. package/src/protocol/image-clamp.ts +2 -2
  280. package/src/protocol/reasoning-text.ts +1 -1
  281. package/src/protocol/reasoning.ts +7 -52
  282. package/src/protocol/registry.ts +50 -15
  283. package/src/protocol/request/chat.ts +46 -19
  284. package/src/protocol/request/codex.ts +1 -1
  285. package/src/protocol/request/messages.ts +46 -14
  286. package/src/protocol/request/responses.ts +19 -10
  287. package/src/protocol/response/chat.ts +25 -10
  288. package/src/protocol/response/codex.ts +1 -1
  289. package/src/protocol/response/messages.ts +1 -1
  290. package/src/protocol/response/omp-tool-call-markup.ts +1 -1
  291. package/src/protocol/response/responses.ts +1 -1
  292. package/src/protocol/sse-decoder.ts +50 -1
  293. package/src/protocol/stop-reason.ts +2 -2
  294. package/src/protocol/think-split.ts +347 -0
  295. package/src/protocol/translation/attachment-integrity.ts +3 -3
  296. package/src/protocol/translation/cache-controls.ts +8 -20
  297. package/src/protocol/translation/capabilities.ts +6 -6
  298. package/src/protocol/translation/dialect/deepseek.ts +10 -0
  299. package/src/protocol/translation/dialect/inband.ts +39 -0
  300. package/src/protocol/translation/dialect/model-thinking.ts +89 -0
  301. package/src/protocol/translation/dialect/qwen.ts +114 -0
  302. package/src/protocol/translation/dialect/resolver.ts +83 -0
  303. package/src/protocol/translation/dialect/rules.ts +93 -0
  304. package/src/protocol/translation/dialect/thinking-ladder.ts +245 -0
  305. package/src/protocol/translation/dialect/types.ts +58 -0
  306. package/src/protocol/translation/quirks.ts +18 -4
  307. package/src/protocol/translation/thinking.ts +12 -252
  308. package/src/protocol/translation/tool-id.ts +3 -3
  309. package/src/protocol/translation/tool-repair.ts +1 -1
  310. package/src/protocol/upstream/post-json.ts +6 -1
  311. package/src/providers/adapters/compatible-adapter.ts +71 -4
  312. package/src/providers/authentication/jwt-validator.ts +0 -5
  313. package/src/providers/authentication/oauth-client.ts +254 -253
  314. package/src/providers/authentication/oauth-flow-store.ts +42 -7
  315. package/src/providers/bundled-discovery-providers.ts +6 -30
  316. package/src/providers/configured-openai-providers.ts +82 -97
  317. package/src/providers/default-registry.ts +35 -37
  318. package/src/providers/discovery/catalog/catalog-format.ts +13 -2
  319. package/src/providers/discovery/discovery-types.ts +0 -1
  320. package/src/providers/discovery/openai-model-discovery.ts +2 -2
  321. package/src/providers/discovery/probe-phases.ts +48 -13
  322. package/src/providers/discovery/probing-service.ts +6 -8
  323. package/src/providers/integrations/agentrouter/agentrouter.ts +12 -2
  324. package/src/providers/integrations/antigravity/antigravity-oauth.ts +0 -1
  325. package/src/providers/integrations/antigravity/antigravity-protocol.ts +88 -28
  326. package/src/providers/integrations/antigravity/antigravity-quota.ts +18 -6
  327. package/src/providers/integrations/antigravity/antigravity.ts +22 -13
  328. package/src/providers/integrations/buddy/buddy-catalog-shared.ts +1 -1
  329. package/src/providers/integrations/buddy/buddy-chat-shared.ts +20 -4
  330. package/src/providers/integrations/buddy/buddy-cli-headers.ts +19 -52
  331. package/src/providers/integrations/buddy/buddy-identity.ts +86 -0
  332. package/src/providers/integrations/buddy/buddy-oauth-shared.ts +1 -1
  333. package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +1 -2
  334. package/src/providers/integrations/buddy/codebuddy/provider.ts +4 -15
  335. package/src/providers/integrations/buddy/codebuddy/shared.ts +30 -9
  336. package/src/providers/integrations/buddy/workbuddy/provider.ts +5 -9
  337. package/src/providers/integrations/buddy/workbuddy/shared.ts +21 -3
  338. package/src/providers/integrations/claude/claude-cch.ts +148 -148
  339. package/src/providers/integrations/claude/fallback-credit.ts +3 -7
  340. package/src/providers/integrations/cline/cline.ts +1 -1
  341. package/src/providers/integrations/codex/codex-device-code.ts +101 -101
  342. package/src/providers/integrations/codex/codex-oauth.ts +0 -1
  343. package/src/providers/integrations/commandcode/commandcode.ts +75 -115
  344. package/src/providers/integrations/commandcode/quota.ts +131 -51
  345. package/src/providers/integrations/cursor/cursor-http2.ts +0 -3
  346. package/src/providers/integrations/cursor/cursor-quota.ts +2 -6
  347. package/src/providers/integrations/cursor/cursor-request.ts +0 -87
  348. package/src/providers/integrations/cursor/cursor-shared.ts +0 -4
  349. package/src/providers/integrations/cursor/cursor.ts +35 -3
  350. package/src/providers/integrations/cursor/protocol/framing.ts +0 -56
  351. package/src/providers/integrations/cursor/protocol/messages.ts +0 -96
  352. package/src/providers/integrations/factory/factory.ts +9 -9
  353. package/src/providers/integrations/github/github-discovery.ts +70 -12
  354. package/src/providers/integrations/github/github-oauth.ts +184 -14
  355. package/src/providers/integrations/github/github.ts +134 -26
  356. package/src/providers/integrations/grok/grok-oauth.ts +261 -261
  357. package/src/providers/integrations/grok/grok-quota-frame.ts +1 -1
  358. package/src/providers/integrations/grok/grok-quota.ts +16 -7
  359. package/src/providers/integrations/grok/grok-ratelimit-store.ts +0 -5
  360. package/src/providers/integrations/grok/grok-ratelimit.ts +3 -0
  361. package/src/providers/integrations/grok/grok.ts +86 -36
  362. package/src/providers/integrations/kilo/kilo-oauth.ts +1 -1
  363. package/src/providers/integrations/kiro/kiro-cache.ts +31 -0
  364. package/src/providers/integrations/kiro/kiro-catalog.ts +31 -5
  365. package/src/providers/integrations/kiro/kiro-discovery.ts +47 -43
  366. package/src/providers/integrations/kiro/kiro-machine-id.ts +6 -3
  367. package/src/providers/integrations/kiro/kiro-oauth.ts +523 -255
  368. package/src/providers/integrations/kiro/kiro-quota.ts +35 -47
  369. package/src/providers/integrations/kiro/kiro-request.ts +8 -11
  370. package/src/providers/integrations/kiro/kiro-stream.ts +13 -0
  371. package/src/providers/integrations/kiro/kiro.ts +80 -68
  372. package/src/providers/integrations/muse/muse-oauth.ts +0 -1
  373. package/src/providers/integrations/muse/muse-quota.ts +0 -4
  374. package/src/providers/integrations/opencode/fingerprint.ts +7 -5
  375. package/src/providers/integrations/opencode/provider.ts +17 -24
  376. package/src/providers/integrations/qoder/qoder-catalog.ts +94 -11
  377. package/src/providers/integrations/qoder/qoder-oauth.ts +165 -25
  378. package/src/providers/integrations/qoder/qoder-quota.ts +48 -6
  379. package/src/providers/integrations/qoder/qoder-realm.ts +10 -31
  380. package/src/providers/integrations/qoder/qoder-sse.ts +159 -0
  381. package/src/providers/integrations/qoder/qoder-umid.ts +21 -25
  382. package/src/providers/integrations/qoder/qoder-virtual.ts +12 -0
  383. package/src/providers/integrations/qoder/qoder.ts +422 -71
  384. package/src/providers/integrations/shared/connect.ts +0 -54
  385. package/src/providers/integrations/shared/hash.ts +15 -0
  386. package/src/providers/integrations/shared/session-cache.ts +136 -0
  387. package/src/providers/integrations/trae/trae-cache.ts +7 -87
  388. package/src/providers/integrations/trae/trae-catalog.ts +1 -6
  389. package/src/providers/integrations/trae/trae-protocol.ts +17 -8
  390. package/src/providers/integrations/trae/trae-realm.ts +0 -7
  391. package/src/providers/integrations/trae/trae.ts +51 -8
  392. package/src/providers/integrations/xai/xai-oauth.ts +5 -5
  393. package/src/providers/integrations/zcode-start-plan/gifts.ts +265 -0
  394. package/src/providers/integrations/zcode-start-plan/oauth.ts +154 -0
  395. package/src/providers/integrations/zcode-start-plan/provider.ts +131 -0
  396. package/src/providers/integrations/zcode-start-plan/quota.ts +152 -0
  397. package/src/providers/integrations/zcode-start-plan/zcode_prompt.json +10 -0
  398. package/src/providers/operations/account-error-classification.ts +198 -133
  399. package/src/providers/operations/account-health-service.ts +31 -39
  400. package/src/providers/operations/account-reset-service.ts +58 -1
  401. package/src/providers/operations/byok-wire-profile.ts +19 -15
  402. package/src/providers/operations/cli-platform.ts +31 -5
  403. package/src/providers/operations/client-version-resolver.ts +4 -6
  404. package/src/providers/operations/client-versions.ts +10 -7
  405. package/src/providers/operations/provider-catalog-seeder.ts +2 -0
  406. package/src/providers/operations/provider-catalog-service.ts +18 -2
  407. package/src/providers/operations/provider-credential-service.ts +19 -2
  408. package/src/providers/provider-contracts.ts +18 -7
  409. package/src/providers/provider-metadata.ts +17 -2
  410. package/src/providers/provider-model-definition.ts +22 -1
  411. package/src/providers/provider-probe.ts +4 -2
  412. package/src/providers/provider-usage.ts +50 -19
  413. package/src/providers/quota/cache.ts +29 -0
  414. package/src/providers/quota/quota-contracts.ts +4 -0
  415. package/src/providers/quota/refresh.ts +27 -4
  416. package/src/providers/quota/snapshot.ts +94 -0
  417. package/src/providers/search/search-provider.ts +1 -3
  418. package/src/providers/search/search-providers.ts +2 -1
  419. package/src/security/abuse.ts +4 -4
  420. package/src/security/admission/service.ts +13 -6
  421. package/src/security/admission/ttl.ts +61 -61
  422. package/src/security/api-key-auth.ts +17 -3
  423. package/src/security/client-router-fingerprint.ts +4 -4
  424. package/src/security/credential-codec.ts +3 -3
  425. package/src/security/csrf.ts +2 -2
  426. package/src/security/ip-boundary.ts +16 -189
  427. package/src/security/model-access-rule.ts +1 -1
  428. package/src/security/outbound-headers.ts +18 -0
  429. package/src/shared/invoke-guard.ts +47 -0
  430. package/src/transport/contracts.ts +13 -4
  431. package/src/transport/dispatch/attempt-finalize.ts +70 -22
  432. package/src/transport/dispatch/attempt-loop.ts +22 -0
  433. package/src/transport/dispatch/capture-policy.ts +34 -37
  434. package/src/transport/dispatch/empty-stream.ts +23 -20
  435. package/src/transport/dispatch/generation.ts +25 -45
  436. package/src/transport/dispatch/native-shared.ts +127 -0
  437. package/src/transport/dispatch/proxy-request.ts +3 -1
  438. package/src/transport/dispatch/responses-compact.ts +25 -46
  439. package/src/transport/dispatch/streaming/encoder.ts +18 -72
  440. package/src/transport/dispatch/streaming/finalize.ts +2 -2
  441. package/src/transport/dispatch/streaming/stream.ts +7 -3
  442. package/src/transport/dispatch/streaming/thinking-loop.ts +1 -1
  443. package/src/transport/dispatch/systemone.ts +20 -48
  444. package/src/transport/dispatch/token-count.ts +41 -38
  445. package/src/transport/dispatch/websearch.ts +19 -45
  446. package/src/transport/drain-endpoint.ts +1 -1
  447. package/src/transport/failure-policy.ts +21 -1
  448. package/src/transport/guards/auth-guard.ts +0 -6
  449. package/src/transport/guards/body-policy.ts +3 -3
  450. package/src/transport/guards/error-lifecycle.ts +4 -0
  451. package/src/transport/guards/headroom-guard.ts +1 -1
  452. package/src/transport/guards/pipeline.ts +1 -1
  453. package/src/transport/guards/request-context.ts +12 -17
  454. package/src/transport/request/preparer.ts +286 -7
  455. package/src/transport/request/rtk/compress-request.ts +3 -3
  456. package/src/transport/request/rtk/rtk-constants.ts +2 -2
  457. package/src/transport/request/rtk/rtk-filters.ts +8 -8
  458. package/src/transport/request/state.ts +7 -13
  459. package/src/transport/request/system-prompt-injection.ts +74 -11
  460. package/src/transport/request/tenant-preferences.ts +22 -0
  461. package/src/transport/routing/candidate-index.ts +2 -2
  462. package/src/transport/routing/round-robin.ts +1 -1
  463. package/src/transport/routing/route-catalog.ts +17 -18
  464. package/src/transport/routing/route-model.ts +126 -33
  465. package/src/transport/routing/router.ts +159 -37
  466. package/src/transport/shutdown-notice.ts +2 -2
  467. package/src/transport/surface/adapters.ts +3 -3
  468. package/src/transport/surface/chat/encode.ts +7 -7
  469. package/src/transport/surface/chat/parse.ts +7 -7
  470. package/src/transport/surface/completion.ts +2 -2
  471. package/src/transport/surface/content-parts.ts +1 -1
  472. package/src/transport/surface/dialects.ts +2 -2
  473. package/src/transport/surface/messages/parse.ts +3 -3
  474. package/src/transport/surface/reasoning-summary.ts +9 -14
  475. package/src/transport/surface/responses/adapter.ts +1 -1
  476. package/src/transport/surface/responses/errors.ts +0 -9
  477. package/src/transport/vision-handoff-cache.ts +58 -0
  478. package/src/transport/vision-handoff.ts +431 -0
  479. package/src/workers/account-identity-sweep.ts +2 -2
  480. package/src/workers/buddy-growth-tasks.ts +392 -346
  481. package/src/workers/daily-checkin.ts +102 -49
  482. package/src/workers/maintenance-lease.ts +2 -35
  483. package/src/workers/oauth-refresh-sweep.ts +8 -1
  484. package/src/workers/quota-refresh-sweep.ts +120 -82
  485. package/src/workers/scheduler.ts +67 -4
  486. package/src/workers/sweep-runner.ts +24 -5
  487. package/src/workers/zcode-gift-claim.ts +108 -0
  488. package/dist/dashboard/assets/AccountCooldown-Bjo92Qat.js +0 -1
  489. package/dist/dashboard/assets/AccountCooldown-Bjo92Qat.js.gz +0 -0
  490. package/dist/dashboard/assets/BannedPage-ChG5svFb.js +0 -1
  491. package/dist/dashboard/assets/BannedPage-ChG5svFb.js.gz +0 -0
  492. package/dist/dashboard/assets/CliToolDetailPage-BIcWQun7.js +0 -1
  493. package/dist/dashboard/assets/CliToolDetailPage-BIcWQun7.js.gz +0 -0
  494. package/dist/dashboard/assets/CliToolsPage-Bf2VelDe.js +0 -1
  495. package/dist/dashboard/assets/CliToolsPage-Bf2VelDe.js.gz +0 -0
  496. package/dist/dashboard/assets/CombosPage-DCgm5g6g.js +0 -1
  497. package/dist/dashboard/assets/CombosPage-DCgm5g6g.js.gz +0 -0
  498. package/dist/dashboard/assets/ConsoleLogPage-a8mD0QF1.js +0 -1
  499. package/dist/dashboard/assets/ConsoleLogPage-a8mD0QF1.js.gz +0 -0
  500. package/dist/dashboard/assets/CustomizationPage-ggeMSjte.js +0 -1
  501. package/dist/dashboard/assets/CustomizationPage-ggeMSjte.js.gz +0 -0
  502. package/dist/dashboard/assets/HealthEventsModal-DzSWzE_j.js +0 -1
  503. package/dist/dashboard/assets/HealthEventsModal-DzSWzE_j.js.gz +0 -0
  504. package/dist/dashboard/assets/LoginPage-U6AKaJcF.js +0 -1
  505. package/dist/dashboard/assets/LoginPage-U6AKaJcF.js.gz +0 -0
  506. package/dist/dashboard/assets/ModelPicker-BMS2t8BA.js.gz +0 -0
  507. package/dist/dashboard/assets/OverviewPage-D2D4IiHl.js +0 -1
  508. package/dist/dashboard/assets/OverviewPage-D2D4IiHl.js.gz +0 -0
  509. package/dist/dashboard/assets/ProviderDetailPage-CrNf7j_X.js +0 -5
  510. package/dist/dashboard/assets/ProviderDetailPage-CrNf7j_X.js.gz +0 -0
  511. package/dist/dashboard/assets/ProviderIcon-FB6yS4v1.js +0 -1
  512. package/dist/dashboard/assets/ProviderIcon-FB6yS4v1.js.gz +0 -0
  513. package/dist/dashboard/assets/ProvidersPage-BSDbquc1.js +0 -1
  514. package/dist/dashboard/assets/ProvidersPage-BSDbquc1.js.gz +0 -0
  515. package/dist/dashboard/assets/ProxyPage-lsrx7NYg.js.gz +0 -0
  516. package/dist/dashboard/assets/QuotaPage-L0skzML8.js +0 -1
  517. package/dist/dashboard/assets/QuotaPage-L0skzML8.js.gz +0 -0
  518. package/dist/dashboard/assets/SettingsPage-CmAbjRHW.js +0 -1
  519. package/dist/dashboard/assets/SettingsPage-CmAbjRHW.js.gz +0 -0
  520. package/dist/dashboard/assets/SetupPage-f0DE1h0-.js +0 -1
  521. package/dist/dashboard/assets/SetupPage-f0DE1h0-.js.gz +0 -0
  522. package/dist/dashboard/assets/SortableList-ClzpwyET.js +0 -1
  523. package/dist/dashboard/assets/SortableList-ClzpwyET.js.gz +0 -0
  524. package/dist/dashboard/assets/StudioPage-Cq5tW7hW.js.gz +0 -0
  525. package/dist/dashboard/assets/ToolIcon-K1ckcDKg.js +0 -1
  526. package/dist/dashboard/assets/ToolIcon-K1ckcDKg.js.gz +0 -0
  527. package/dist/dashboard/assets/UsagePage-Dfc9rWQn.js +0 -56
  528. package/dist/dashboard/assets/UsagePage-Dfc9rWQn.js.gz +0 -0
  529. package/dist/dashboard/assets/api-keys-C38iXoO4.js +0 -1
  530. package/dist/dashboard/assets/api-keys-C38iXoO4.js.gz +0 -0
  531. package/dist/dashboard/assets/app-BayTTy01.js +0 -9
  532. package/dist/dashboard/assets/app-BayTTy01.js.gz +0 -0
  533. package/dist/dashboard/assets/app-a159k0ub.css +0 -2
  534. package/dist/dashboard/assets/app-a159k0ub.css.gz +0 -0
  535. package/dist/dashboard/assets/assert-DbRA8EG2.js +0 -15
  536. package/dist/dashboard/assets/assert-DbRA8EG2.js.gz +0 -0
  537. package/dist/dashboard/assets/badge-D9CS6dZk.js.gz +0 -0
  538. package/dist/dashboard/assets/banner-DympBNq4.js.gz +0 -0
  539. package/dist/dashboard/assets/card-ibRWU_uT.js.gz +0 -0
  540. package/dist/dashboard/assets/cli-tools-63qBGaRw.js +0 -1
  541. package/dist/dashboard/assets/cli-tools-63qBGaRw.js.gz +0 -0
  542. package/dist/dashboard/assets/clipboard-button-DJm-9pz7.js.gz +0 -0
  543. package/dist/dashboard/assets/dialog-8fiE6tec.js +0 -1
  544. package/dist/dashboard/assets/dialog-8fiE6tec.js.gz +0 -0
  545. package/dist/dashboard/assets/drawer-CqGYeOrd.js.gz +0 -0
  546. package/dist/dashboard/assets/entry-BSAsYsmV.js +0 -1
  547. package/dist/dashboard/assets/entry-BSAsYsmV.js.gz +0 -0
  548. package/dist/dashboard/assets/entry-CiIQULZD.js +0 -4
  549. package/dist/dashboard/assets/entry-CiIQULZD.js.gz +0 -0
  550. package/dist/dashboard/assets/entry-Daz0lJ-W.js +0 -1
  551. package/dist/dashboard/assets/entry-Daz0lJ-W.js.gz +0 -0
  552. package/dist/dashboard/assets/formatters-D-MTJxMr.js.gz +0 -0
  553. package/dist/dashboard/assets/github-badge-CPK47xQx.js +0 -1
  554. package/dist/dashboard/assets/github-badge-CPK47xQx.js.gz +0 -0
  555. package/dist/dashboard/assets/index-DBNPOfhV.js.gz +0 -0
  556. package/dist/dashboard/assets/inline-BT0zntEr.js.gz +0 -0
  557. package/dist/dashboard/assets/input-Da2ZViWg.js.gz +0 -0
  558. package/dist/dashboard/assets/layout-BXNeLw6M.js.gz +0 -0
  559. package/dist/dashboard/assets/list-row-BycOrrIV.js.gz +0 -0
  560. package/dist/dashboard/assets/live-DCQNZpF7.js +0 -1
  561. package/dist/dashboard/assets/live-DCQNZpF7.js.gz +0 -0
  562. package/dist/dashboard/assets/mutation-Rr0TIvcj.js.gz +0 -0
  563. package/dist/dashboard/assets/network-9Yj2ZyLo.js +0 -1
  564. package/dist/dashboard/assets/network-9Yj2ZyLo.js.gz +0 -0
  565. package/dist/dashboard/assets/numbers-CEagj6ZZ.js +0 -2
  566. package/dist/dashboard/assets/numbers-CEagj6ZZ.js.gz +0 -0
  567. package/dist/dashboard/assets/providers/zai-coding-plan.svg +0 -6
  568. package/dist/dashboard/assets/providers/zai-coding-plan.svg.gz +0 -0
  569. package/dist/dashboard/assets/quota-Caw7wqci.js +0 -1
  570. package/dist/dashboard/assets/quota-Caw7wqci.js.gz +0 -0
  571. package/dist/dashboard/assets/routing-CO9zCUus.js +0 -1
  572. package/dist/dashboard/assets/routing-CO9zCUus.js.gz +0 -0
  573. package/dist/dashboard/assets/select-BIYIBkMU.js.gz +0 -0
  574. package/dist/dashboard/assets/settings-jS3-PNSA.js +0 -1
  575. package/dist/dashboard/assets/settings-jS3-PNSA.js.gz +0 -0
  576. package/dist/dashboard/assets/stack-DfgnFDWY.js.gz +0 -0
  577. package/dist/dashboard/assets/state-CkfdYWel.js.gz +0 -0
  578. package/dist/dashboard/assets/stream-retry-5QnVuSSo.js +0 -1
  579. package/dist/dashboard/assets/stream-retry-5QnVuSSo.js.gz +0 -0
  580. package/dist/dashboard/assets/switch-SYm237HG.js.gz +0 -0
  581. package/dist/dashboard/assets/useMutation-BQqCRNIi.js.gz +0 -0
  582. package/src/console/cli-tools/injectors/cline.ts +0 -110
  583. package/src/console/cli-tools/injectors/copilot.ts +0 -108
  584. package/src/console/cli-tools/injectors/cowork.ts +0 -64
  585. package/src/console/cli-tools/injectors/deepseek-tui.ts +0 -85
  586. package/src/console/cli-tools/injectors/droid.ts +0 -96
  587. package/src/console/cli-tools/injectors/grok-build.ts +0 -70
  588. package/src/console/cli-tools/injectors/hermes.ts +0 -101
  589. package/src/console/cli-tools/injectors/jcode.ts +0 -115
  590. package/src/console/cli-tools/injectors/openclaw.ts +0 -161
  591. package/src/network/response-headers.ts +0 -39
  592. package/src/protocol/response/dsml.ts +0 -38
  593. package/src/providers/integrations/xiaomi-mimo/think-stream.ts +0 -229
  594. /package/dist/dashboard/assets/brand/{og_bansos.webp → og_bansos_initial.webp} +0 -0
  595. /package/dist/dashboard/assets/brand/{og_image.webp → og_image_initial.webp} +0 -0
@@ -4,7 +4,7 @@ import { GatewayError } from "../../protocol/gateway-error";
4
4
  import { assertBoundedJsonDepth } from "../../protocol/codecs";
5
5
  import type { ProxyRequestStateStore } from "../request/state";
6
6
  import { fastPathname } from "../pathname";
7
- import { NATIVE_SERVICE_PATHS, TOKEN_COUNT_PATH } from "../contracts";
7
+ import { NATIVE_SERVICE_PATHS, RESPONSES_COMPACT_PATH, TOKEN_COUNT_PATH } from "../contracts";
8
8
  import { bodyTooLargeError } from "./guard-errors";
9
9
 
10
10
  /** Whether this request is a gateway (`/v1/*`) request. */
@@ -33,7 +33,7 @@ const PROXY_JSON_ROUTES = [
33
33
  "/v1/chat/completions",
34
34
  "/v1/responses",
35
35
  "/v1/completions",
36
- "/v1/responses/compact",
36
+ RESPONSES_COMPACT_PATH,
37
37
  "/v1/messages",
38
38
  // Reads a JSON body through the ingress policy but skips the canonical chat
39
39
  // stage — see `isOpaqueBodyRoute`. Listed here because this table must mirror
@@ -62,7 +62,7 @@ export function isJsonProxyRoutePath(path: string): boolean {
62
62
  return isJsonProxyRoute(path);
63
63
  }
64
64
 
65
- export interface IngressPolicyOptions {
65
+ interface IngressPolicyOptions {
66
66
  readonly maxBodyBytes?: number;
67
67
  }
68
68
 
@@ -312,6 +312,7 @@ function requestLogMetadata(
312
312
  cachedInputTokens: state.outcome.usage.cached_input_tokens,
313
313
  reasoningTokens: state.outcome.usage.reasoning_tokens,
314
314
  estimatedCost: state.outcome.usage.estimated_cost,
315
+ ...(state.outcome.degenerateStop ? { degenerateStop: true } : {}),
315
316
  },
316
317
  }
317
318
  : {}),
@@ -385,6 +386,9 @@ function requestTelemetryEvent(
385
386
  ...(state.payloadCaptureState === undefined
386
387
  ? {}
387
388
  : { payloadCaptureState: state.payloadCaptureState }),
389
+ // Degenerate stop (short output ended by an ordinary `stop`) — measured
390
+ // so a remote operator can spot the pattern in the request list.
391
+ ...(state.outcome?.degenerateStop ? { degenerateStop: true } : {}),
388
392
  };
389
393
  }
390
394
 
@@ -31,7 +31,7 @@ const SHED_MARKER_TTL_SECONDS = 5;
31
31
  const SHED_MARKER_KEY = "gateway:shed:v1";
32
32
  const SHED_RETRY_MS = 1_000;
33
33
 
34
- export interface HeadroomGuardDeps {
34
+ interface HeadroomGuardDeps {
35
35
  readonly store: ProxyRequestStateStore;
36
36
  /**
37
37
  * The last sampled memory state, published once per sample by
@@ -35,7 +35,7 @@ import {
35
35
  } from "../../observability/payload-capture";
36
36
 
37
37
  /** Shared state/context carried by every ordered transport stage. */
38
- export interface TransportPipelineContext {
38
+ interface TransportPipelineContext {
39
39
  readonly db: CartethyiaDatabase;
40
40
  readonly stateStore: ProxyRequestStateStore;
41
41
  readonly surfaceRegistry: SurfaceAdapterRegistry;
@@ -24,7 +24,7 @@ interface RequestApp {
24
24
  ): RequestApp;
25
25
  }
26
26
 
27
- export interface RequestContextDeps {
27
+ interface RequestContextDeps {
28
28
  readonly stateStore: ProxyRequestStateStore;
29
29
  readonly clock?: () => number;
30
30
  /** Explicit deadline override; defaults to `CARTETHYIA_UPSTREAM_TIMEOUT_MS` (120s). */
@@ -148,8 +148,7 @@ export function createCanonicalRequestMiddleware(deps: {
148
148
  // the JSON routes table, so the body-policy predicate covers it.
149
149
  // Native routes (compact, System One) read a body but are never parsed
150
150
  // into a canonical request — their bodies are opaque by contract.
151
- if (!isJsonProxyRoutePath(path) || path === "/v1/responses/compact" || isOpaqueBodyRoute(path))
152
- return;
151
+ if (!isJsonProxyRoutePath(path) || isOpaqueBodyRoute(path)) return;
153
152
  // Model discovery and other GET/multimodal routes must not go through canonical parsing.
154
153
  const state = deps.stateStore.require(request);
155
154
  // Single-read invariant: the ingress policy middleware already decoded the
@@ -221,25 +220,21 @@ export function createProxyRoutePreparationMiddleware(deps: {
221
220
  if (!path.startsWith("/v1/")) return;
222
221
  if (request.method === "GET" || request.method === "HEAD") return;
223
222
  const isJsonRoute = isJsonProxyRoutePath(path);
224
- if (!isJsonRoute || path === "/v1/responses/compact" || isOpaqueBodyRoute(path)) return;
223
+ if (!isJsonRoute || isOpaqueBodyRoute(path)) return;
225
224
  const state = deps.stateStore.require(request);
226
225
  if (!state.authorization || !state.canonicalRequest)
227
226
  throw new GatewayError("route_unavailable", 503, "The request was not authenticated and parsed, so this route cannot be served (request {requestId}). Resend the request. The bouncer stepped out — resend shortly 🚪");
228
227
  // The key rides along so a strike is charged to the caller that made it.
229
228
  // Without it, one recipient of a shared link could escalate a ban onto
230
- try {
231
- state.preparedRequest = await deps.preparer.prepare({
232
- canonicalRequest: state.canonicalRequest,
233
- authorization: state.authorization,
234
- deadlineMs: state.deadlineMs,
235
- signal: state.abortController.signal,
236
- ...(state.clientUserAgent === undefined
237
- ? {}
238
- : { clientUserAgent: state.clientUserAgent }),
239
- });
240
- } catch (error) {
241
- throw error;
242
- }
229
+ state.preparedRequest = await deps.preparer.prepare({
230
+ canonicalRequest: state.canonicalRequest,
231
+ authorization: state.authorization,
232
+ deadlineMs: state.deadlineMs,
233
+ signal: state.abortController.signal,
234
+ ...(state.clientUserAgent === undefined
235
+ ? {}
236
+ : { clientUserAgent: state.clientUserAgent }),
237
+ });
243
238
  })
244
239
  .as("plugin");
245
240
  }
@@ -1,12 +1,14 @@
1
1
  import { GatewayError } from "../../protocol/gateway-error";
2
- import type { CanonicalRequest, ServiceKind } from "../../protocol/canonical-model";
2
+ import type { CanonicalRequest, CanonicalMessage, ServiceKind, ContentPart } from "../../protocol/canonical-model";
3
3
  import { toolResultParts } from "../../protocol/canonical-model";
4
4
  import type { ApiKeyAdmissionService } from "../../security/admission/service";
5
+ import type { VisionHandoffCall, VisionHandoffResult } from "../vision-handoff";
6
+ import { isImageOrPlaceholderPart, lastImageCarryingUserTurnIndex } from "../vision-handoff";
5
7
  import type { CallerIdentity } from "../../security/access-control";
6
8
  import type { RouteCandidate as RouteCandidate, InMemoryRouteSnapshotService, RoutePlan } from "../routing/route-model";
7
9
  import type { RouteContinuationAffinity } from "../routing/route-model";
8
10
  import { resolveAliasTarget, type RoutingEngine } from "../routing/router";
9
- import { projectForRoute, routeCapabilitiesFor } from "../../protocol/translation/capabilities";
11
+ import { projectForRoute, routeCapabilitiesFor, deriveRequiredCapabilities } from "../../protocol/translation/capabilities";
10
12
  import type { RequiredCapability } from "../../protocol/translation/capabilities";
11
13
  import { isModelAllowed, type ResolvedApiKey } from "../../security/api-key-auth";
12
14
  import { allowsCliToolMappings } from "../../security/cli-client-fingerprint";
@@ -36,6 +38,128 @@ function requestNeedsToolRepair(request: CanonicalRequest): boolean {
36
38
  return false;
37
39
  }
38
40
 
41
+ /**
42
+ * The text that replaces an image part after a successful vision handoff.
43
+ * The description is wrapped so the receiving model understands it is an
44
+ * automated description of an image the user attached — not prose the user
45
+ * typed — which is what made it answer as if it had no image at all.
46
+ */
47
+ function visionDescriptionText(description: string): string {
48
+ return `[The user attached an image. A vision model described it for you: ${description}]`;
49
+ }
50
+
51
+ /**
52
+ * Replaces every image part (or its `[image]` degradation placeholder) in the
53
+ * request with a single text part carrying the vision-handoff description.
54
+ * Returns `null` when the request has neither image parts nor placeholders.
55
+ * Only user-message parts are replaced; system/tool-result images are left
56
+ * untouched.
57
+ */
58
+ function replaceImagePartsWithText(
59
+ request: CanonicalRequest,
60
+ description: string,
61
+ ): CanonicalRequest | null {
62
+ const activeIndex = lastImageCarryingUserTurnIndex(request);
63
+ const strip = (parts: readonly ContentPart[]): readonly ContentPart[] => {
64
+ const hasImage = parts.some(isImageOrPlaceholderPart);
65
+ if (!hasImage) return parts;
66
+ return parts.flatMap((part): ContentPart[] =>
67
+ isImageOrPlaceholderPart(part)
68
+ ? [{ kind: "text", text: visionDescriptionText(description) }]
69
+ : [part],
70
+ );
71
+ };
72
+ const nextMessages = request.messages.map((message, i) => {
73
+ if (message.role !== "user") return message;
74
+ // The active turn (the one the handoff actually described) gets the
75
+ // description; older image-carrying history turns are dropped — they are
76
+ // not part of the current context the vision model saw, and leaving a
77
+ // stale image on a text-only route would risk an upstream rejection.
78
+ if (i === activeIndex) {
79
+ return { ...message, content: strip(message.content) };
80
+ }
81
+ const hasImage = message.content.some(isImageOrPlaceholderPart);
82
+ if (!hasImage) return message;
83
+ return { ...message, content: message.content.filter((part) => !isImageOrPlaceholderPart(part)) };
84
+ });
85
+ const unchangedMessages = nextMessages.every(
86
+ (message, i) => message === request.messages[i],
87
+ );
88
+ if (unchangedMessages) return null;
89
+ return { ...request, messages: nextMessages };
90
+ }
91
+
92
+ /**
93
+ * When a handoff request is a giant session (opencode CLI sessions routinely
94
+ * carry hundreds of tool-result turns), the text-only request that goes to the
95
+ * main model is still enormous: the handoff only replaced the image parts, so
96
+ * the full history — including 700+ tool transcripts — is forwarded verbatim.
97
+ * Upstreams reject or reset such payloads (502/ECONNRESET), which is the
98
+ * "broken on large sessions" report: a small fresh chat works, a session with
99
+ * a large context dies even though the handoff itself is just a description.
100
+ *
101
+ * The handoff describes *an image the user just attached* — it does not need
102
+ * the rest of the session. For a giant request this pass returns only the
103
+ * system prompt (when present) plus the active image-carrying turn; the whole
104
+ * history is dropped so the payload stays small and upstreams do not reset it.
105
+ * Small requests (< the thresholds) pass through untouched.
106
+ */
107
+ export function condenseHandoffRequest(request: CanonicalRequest): CanonicalRequest {
108
+ const activeIndex = lastImageCarryingUserTurnIndex(request);
109
+ if (activeIndex < 0) return request;
110
+
111
+ const totalMessages = request.messages.length;
112
+ let totalChars = 0;
113
+ for (const message of request.messages) totalChars += messageCharSize(message);
114
+ if (totalMessages <= HANDOFF_CONDENSE_MESSAGE_THRESHOLD && totalChars <= HANDOFF_CONDENSE_CHARS_THRESHOLD) {
115
+ return request;
116
+ }
117
+
118
+ const kept: CanonicalMessage[] = [];
119
+ for (let i = 0; i < request.messages.length; i += 1) {
120
+ const message = request.messages[i]!;
121
+ if (message.role === "system") {
122
+ kept.push(message);
123
+ continue;
124
+ }
125
+ if (i === activeIndex) {
126
+ kept.push(message);
127
+ // Anything after the active turn stays after it (rare, but preserved).
128
+ kept.push(...request.messages.slice(activeIndex + 1));
129
+ break;
130
+ }
131
+ }
132
+ // Never return a request that lost the very turn we are answering.
133
+ if (!kept.includes(request.messages[activeIndex]!)) return request;
134
+ return { ...request, messages: kept };
135
+ }
136
+
137
+ const HANDOFF_CONDENSE_MESSAGE_THRESHOLD = 100;
138
+ const HANDOFF_CONDENSE_CHARS_THRESHOLD = 200_000;
139
+
140
+ function messageCharSize(message: CanonicalMessage): number {
141
+ let size = 0;
142
+ for (const part of message.content) {
143
+ if (part.kind === "text" || part.kind === "refusal") {
144
+ size += part.text.length;
145
+ } else if (part.kind === "toolCall") {
146
+ size += part.name.length;
147
+ try {
148
+ size += JSON.stringify(part.arguments).length;
149
+ } catch {
150
+ size += 64;
151
+ }
152
+ } else {
153
+ try {
154
+ size += JSON.stringify((part as { data?: unknown }).data ?? "").length;
155
+ } catch {
156
+ size += 64;
157
+ }
158
+ }
159
+ }
160
+ return size;
161
+ }
162
+
39
163
  /** Prefix of a provider-continuation tool-call id the gateway itself minted. */
40
164
  const CURSOR_CALL_PREFIX = "call_cursor_";
41
165
 
@@ -46,7 +170,7 @@ const CURSOR_CALL_PREFIX = "call_cursor_";
46
170
  * Any other id belongs to a conversation the client owns; it is not a
47
171
  * continuation and must not influence routing.
48
172
  */
49
- export function continuationCallIds(request: CanonicalRequest): readonly string[] {
173
+ function continuationCallIds(request: CanonicalRequest): readonly string[] {
50
174
  const ids: string[] = [];
51
175
  for (const message of request.messages) {
52
176
  if (message.role !== "tool" && message.role !== "user") continue;
@@ -70,7 +194,7 @@ export function continuationCallIds(request: CanonicalRequest): readonly string[
70
194
  * Returns `undefined` when the request carries no continuation ids — the
71
195
  * ordinary routing path.
72
196
  */
73
- export function resolveContinuationAffinity(
197
+ function resolveContinuationAffinity(
74
198
  request: CanonicalRequest,
75
199
  authorization: ResolvedApiKey,
76
200
  ): RouteContinuationAffinity | undefined {
@@ -122,6 +246,46 @@ export function resolveContinuationAffinity(
122
246
  return undefined;
123
247
  }
124
248
 
249
+ /**
250
+ * True when every continuation call id this request carries is answered by a
251
+ * matching `toolCall` block in the same conversation — i.e. the request is a
252
+ * complete replay, not a tail continuation still owed to a live Run.
253
+ */
254
+ function isCompleteContinuationReplay(request: CanonicalRequest, ids: readonly string[]): boolean {
255
+ const calledIds = new Set<string>();
256
+ for (const message of request.messages) {
257
+ for (const part of message.content) {
258
+ if (part.kind === "toolCall") calledIds.add(part.call_id);
259
+ }
260
+ }
261
+ return ids.every((id) => calledIds.has(id));
262
+ }
263
+
264
+ /**
265
+ * Resolves a request's pending tool results to the one account that owns them,
266
+ * with rebaseability computed from the conversation shape.
267
+ *
268
+ * A hard pin is only safe when the request is still owed to a live Run. But an
269
+ * account that has since become unroutable (quota exhausted, cooling) must not
270
+ * turn a complete replay into a hard 400: the whole recorded conversation is
271
+ * present, so the router may fall through to normal routing and the Cursor
272
+ * adapter rebases it into a fresh Run on whichever account serves the model.
273
+ * That is what makes switching provider/model mid-conversation work.
274
+ */
275
+ function resolveContinuationAffinityWithRebase(
276
+ request: CanonicalRequest,
277
+ authorization: ResolvedApiKey,
278
+ ): RouteContinuationAffinity | undefined {
279
+ const affinity = resolveContinuationAffinity(request, authorization);
280
+ if (affinity === undefined) return undefined;
281
+ const ids = continuationCallIds(request);
282
+ if (affinity.rebaseable) return affinity;
283
+ return {
284
+ ...affinity,
285
+ rebaseable: isCompleteContinuationReplay(request, ids),
286
+ };
287
+ }
288
+
125
289
  /** Looks up one pending call across every provider that records continuations. */
126
290
  function lookupPendingCall(
127
291
  callId: string,
@@ -170,15 +334,29 @@ export interface PreparedNativeRequest {
170
334
  readonly admissionService: ApiKeyAdmissionService;
171
335
  }
172
336
 
173
- export interface ProxyRequestPreparerDeps {
337
+ interface ProxyRequestPreparerDeps {
174
338
  readonly snapshotService: InMemoryRouteSnapshotService;
175
339
  readonly routingEngine: RoutingEngine;
176
340
  readonly admissionService: ApiKeyAdmissionService;
177
341
  }
178
342
 
179
343
  export class ProxyRequestPreparer {
344
+ /**
345
+ * Vision-handoff runner, injected by boot once the request state store and
346
+ * the full dispatch dependency surface exist. Absent (or when the feature is
347
+ * not configured) means the legacy `[image]` placeholder path.
348
+ */
349
+ #runVisionHandoff: ((input: VisionHandoffCall) => Promise<readonly VisionHandoffResult[]>) | undefined;
350
+
180
351
  constructor(private readonly deps: ProxyRequestPreparerDeps) {}
181
352
 
353
+ /** Injects the vision-handoff runner (called once from boot wiring). */
354
+ bindVisionHandoff(
355
+ runner: (input: VisionHandoffCall) => Promise<readonly VisionHandoffResult[]>,
356
+ ): void {
357
+ this.#runVisionHandoff = runner;
358
+ }
359
+
182
360
  async prepare(input: {
183
361
  readonly canonicalRequest: CanonicalRequest;
184
362
  readonly authorization: ResolvedApiKey;
@@ -186,6 +364,8 @@ export class ProxyRequestPreparer {
186
364
  readonly signal?: AbortSignal;
187
365
  /** Inbound `User-Agent`; gates remote CLI remaps so short slots stay tool-local. */
188
366
  readonly clientUserAgent?: string;
367
+ /** Suppresses nested vision handoff; internal sub-requests set this. */
368
+ readonly disableVisionHandoff?: boolean;
189
369
  }): Promise<PreparedProxyRequest> {
190
370
  const { canonicalRequest: initialRequest, authorization, signal } = input;
191
371
  if (signal?.aborted)
@@ -266,7 +446,7 @@ export class ProxyRequestPreparer {
266
446
  // degradation could narrow the candidate set past it. This is also the
267
447
  // point where a mismatched or expired call id becomes a 400 the caller can
268
448
  // act on, instead of a dispatch that silently starts a new upstream turn.
269
- const continuationAffinity = resolveContinuationAffinity(request, authorization);
449
+ const continuationAffinity = resolveContinuationAffinityWithRebase(requestWithAttachments, authorization);
270
450
  // Capability-aware routing: derive requirements BEFORE planning so the
271
451
  // router filters candidates against snapshot profiles. When nothing
272
452
  // supports the full request, degrade (same greedy order) and re-plan
@@ -296,9 +476,108 @@ export class ProxyRequestPreparer {
296
476
  continuationAffinity,
297
477
  );
298
478
  } catch (error) {
299
- if (error instanceof GatewayError && error.code === "capability_unsupported") continue;
479
+ // `plan()` collapses "no candidate supports this capability" into
480
+ // `accounts_unavailable` (503), not `capability_unsupported` — a
481
+ // candidate that exists but cannot carry the required capability is
482
+ // evaluated ineligible, and an all-ineligible set reads as "no usable
483
+ // account". Both mean "try the next, more degraded variant" here.
484
+ // `accounts_unavailable` is swallowed on the pristine variant (so a
485
+ // genuinely unhealthy pool falls through to a degraded plan) and on any
486
+ // variant that still carries requirements (a capability the route
487
+ // lacks, not a dead pool). Only a fully degraded variant — nothing left
488
+ // to drop — that still refuses means the pool itself is unusable, and
489
+ // that 503 must surface.
490
+ if (
491
+ error instanceof GatewayError &&
492
+ (error.code === "capability_unsupported" ||
493
+ (error.code === "accounts_unavailable" &&
494
+ (variant.degraded.length === 0 || variant.required.length > 0)))
495
+ ) {
496
+ continue;
497
+ }
300
498
  throw error;
301
499
  }
500
+ // Vision handoff: when the winning route cannot carry images (the image
501
+ // capability was degraded), try to describe the images with a configured
502
+ // vision model and plan the resulting text-only request instead. This
503
+ // only runs when the selected model is NOT image-capable — a vision route
504
+ // that plans without degradation keeps the real image. The handoff is
505
+ // best-effort; any failure keeps the legacy `[image]` placeholder plan.
506
+ if (
507
+ variant.degraded.includes("image") &&
508
+ input.disableVisionHandoff !== true &&
509
+ this.#runVisionHandoff !== undefined
510
+ ) {
511
+ try {
512
+ const handedOff = await this.#runVisionHandoff({
513
+ canonicalRequest: requestWithAttachments,
514
+ authorization,
515
+ deadlineMs: input.deadlineMs,
516
+ ...(input.signal === undefined ? {} : { signal: input.signal }),
517
+ ...(input.clientUserAgent === undefined ? {} : { clientUserAgent: input.clientUserAgent }),
518
+ });
519
+ if (handedOff.length > 0) {
520
+ const description = handedOff.map((result) => result.description).join("\n");
521
+ // Replace on the already-degraded variant so earlier degradations
522
+ // (tools, reasoning, …) survive alongside the handoff — a chain
523
+ // variant carries them, and rewriting from the pristine request
524
+ // would reintroduce capabilities the winning route lacks.
525
+ // Condense first (while the active turn still carries its image, so
526
+ // it can be located): a session with hundreds of tool-result turns
527
+ // forwards a multi-megabyte body that upstreams reset or reject
528
+ // (502 / ECONNRESET) — the "broken on large sessions" report. The
529
+ // handoff only needs to describe the attached image, so the giant
530
+ // history is dropped entirely, leaving system + active turn.
531
+ const condensedBase = condenseHandoffRequest(variant.request);
532
+ const replaced = replaceImagePartsWithText(condensedBase, description);
533
+ if (replaced !== null) {
534
+ const requiredAfter = deriveRequiredCapabilities(replaced);
535
+ try {
536
+ const handoffPlan = await this.deps.routingEngine.plan(
537
+ request.model,
538
+ snapshot,
539
+ authorization.tenantId,
540
+ requiredAfter,
541
+ allowCliMappings,
542
+ authorization.cliMappingOwnerId ?? authorization.id,
543
+ continuationAffinity,
544
+ );
545
+ plan = handoffPlan;
546
+ variantRequest = replaced;
547
+ degraded = [
548
+ ...degraded,
549
+ ...variant.degraded.filter(
550
+ (cap) => cap !== "image" && !degraded.includes(cap),
551
+ ),
552
+ ];
553
+ break;
554
+ } catch (error) {
555
+ if (
556
+ error instanceof GatewayError &&
557
+ (error.code === "capability_unsupported" ||
558
+ error.code === "accounts_unavailable")
559
+ ) {
560
+ // Re-plan failed without the image requirement; keep the
561
+ // degraded `[image]` plan captured above. `accounts_unavailable`
562
+ // is also swallowed: the text-only re-plan can be refused for
563
+ // the same health/pool reasons the original plan was, and the
564
+ // parent request must never fail because a best-effort
565
+ // handoff could not be re-planned.
566
+ } else {
567
+ throw error;
568
+ }
569
+ }
570
+ }
571
+ }
572
+ } catch (error) {
573
+ // Best-effort only: a handoff failure never fails the parent request.
574
+ log.warn("[vision-handoff] handoff failed; falling back to [image] placeholder", {
575
+ model: request.model,
576
+ tenantId: authorization.tenantId,
577
+ error: error instanceof Error ? error.message : String(error),
578
+ });
579
+ }
580
+ }
302
581
  variantRequest = variant.request;
303
582
  degraded = [...degraded, ...variant.degraded.filter((cap) => !degraded.includes(cap))];
304
583
  break;
@@ -26,7 +26,7 @@ import { autoDetectFilter, dedupLogFilter, safeApplyFilter, smartTruncateFilter
26
26
  import { RTK_RAW_CAP, rtkProfile, type RtkLevel } from "./rtk-constants";
27
27
  import { ponyTailPrompt, type PonyTailLevel } from "./ponytail-prompt";
28
28
 
29
- export interface CompressionOptions {
29
+ interface CompressionOptions {
30
30
  /** Compact bulky tool-result text in the message history. */
31
31
  readonly rtkPrune?: boolean;
32
32
  /** RTK strength; `full` when omitted. See {@link RtkLevel}. */
@@ -35,7 +35,7 @@ export interface CompressionOptions {
35
35
  readonly ponyTail?: PonyTailLevel | null | undefined;
36
36
  }
37
37
 
38
- export interface CompressionStats {
38
+ interface CompressionStats {
39
39
  /** Tool-result text parts actually shortened. */
40
40
  readonly prunedParts: number;
41
41
  /** Characters removed by RTK (>= 0). */
@@ -45,7 +45,7 @@ export interface CompressionStats {
45
45
  }
46
46
 
47
47
  /** Compresses one text blob with RTK at a strength level, or returns it unchanged. */
48
- export function compressToolText(text: string, level?: RtkLevel | null): string {
48
+ function compressToolText(text: string, level?: RtkLevel | null): string {
49
49
  const profile = rtkProfile(level);
50
50
  if (text.length < profile.minSize || text.length > RTK_RAW_CAP) return text;
51
51
  const filter = autoDetectFilter(text, { genericFallback: profile.genericFallback });
@@ -2,7 +2,7 @@
2
2
  // not env knobs: these are safety margins, not deployment settings.
3
3
 
4
4
  /** Skip blobs smaller than this — compressing tiny tool results only adds noise. */
5
- export const RTK_MIN_COMPRESS_SIZE = 500;
5
+ const RTK_MIN_COMPRESS_SIZE = 500;
6
6
  /** Never touch a blob larger than this; a multi-megabyte dump is not a tool result to prune. */
7
7
  export const RTK_RAW_CAP = 10 * 1024 * 1024;
8
8
  /** How many leading characters autodetect inspects when classifying a blob. */
@@ -22,7 +22,7 @@ export const RTK_DETECT_WINDOW = 1024;
22
22
  export const RTK_LEVELS = ["lite", "full", "ultra"] as const;
23
23
  export type RtkLevel = (typeof RTK_LEVELS)[number];
24
24
 
25
- export interface RtkProfile {
25
+ interface RtkProfile {
26
26
  /** Blobs smaller than this are left alone at this level. */
27
27
  readonly minSize: number;
28
28
  /** Whether the generic fallbacks (dedup-log, smart-truncate) may classify a blob. */
@@ -29,10 +29,10 @@ import {
29
29
  TREE_MAX_LINES,
30
30
  } from "./rtk-constants";
31
31
 
32
- export type RtkFilter = ((input: string) => string) & { filterName?: string };
32
+ type RtkFilter = ((input: string) => string) & { filterName?: string };
33
33
 
34
34
  /** `git diff` → per-file header, per-hunk truncation, +/- counts. */
35
- export const gitDiffFilter: RtkFilter = (diff) => {
35
+ const gitDiffFilter: RtkFilter = (diff) => {
36
36
  const result: string[] = [];
37
37
  let currentFile = "";
38
38
  let added = 0;
@@ -83,7 +83,7 @@ export const gitDiffFilter: RtkFilter = (diff) => {
83
83
  gitDiffFilter.filterName = "git-diff";
84
84
 
85
85
  /** `git log` → one line per commit, capped. */
86
- export const gitLogFilter: RtkFilter = (input) => {
86
+ const gitLogFilter: RtkFilter = (input) => {
87
87
  const lines = input.split("\n");
88
88
  const out: string[] = [];
89
89
  for (const line of lines) {
@@ -99,7 +99,7 @@ export const gitLogFilter: RtkFilter = (input) => {
99
99
  gitLogFilter.filterName = "git-log";
100
100
 
101
101
  /** `git status` → branch line, changed-file counts, file list capped. */
102
- export const gitStatusFilter: RtkFilter = (input) => {
102
+ const gitStatusFilter: RtkFilter = (input) => {
103
103
  const lines = input.split("\n");
104
104
  const out: string[] = [];
105
105
  let changed = 0;
@@ -119,7 +119,7 @@ export const gitStatusFilter: RtkFilter = (input) => {
119
119
  gitStatusFilter.filterName = "git-status";
120
120
 
121
121
  /** `grep`/`rg` dump (`file:line:content`) → grouped by file, capped per file. */
122
- export const grepFilter: RtkFilter = (input) => {
122
+ const grepFilter: RtkFilter = (input) => {
123
123
  const byFile = new Map<string, Array<[string, string]>>();
124
124
  let total = 0;
125
125
  for (const line of input.split("\n")) {
@@ -151,7 +151,7 @@ export const grepFilter: RtkFilter = (input) => {
151
151
  grepFilter.filterName = "grep";
152
152
 
153
153
  /** `find` dump (path-per-line) → grouped by parent directory, capped. */
154
- export const findFilter: RtkFilter = (input) => {
154
+ const findFilter: RtkFilter = (input) => {
155
155
  const byDir = new Map<string, string[]>();
156
156
  for (const line of input.split("\n")) {
157
157
  const path = line.trim();
@@ -177,7 +177,7 @@ export const findFilter: RtkFilter = (input) => {
177
177
  findFilter.filterName = "find";
178
178
 
179
179
  /** `ls -la` dump → directories and files with human sizes, noise dirs dropped. */
180
- export const lsFilter: RtkFilter = (input) => {
180
+ const lsFilter: RtkFilter = (input) => {
181
181
  const dirs: string[] = [];
182
182
  const files: Array<[string, number]> = [];
183
183
  const byExt = new Map<string, number>();
@@ -219,7 +219,7 @@ export const lsFilter: RtkFilter = (input) => {
219
219
  lsFilter.filterName = "ls";
220
220
 
221
221
  /** `tree` dump → collapsed, noise dirs dropped, capped. */
222
- export const treeFilter: RtkFilter = (input) => {
222
+ const treeFilter: RtkFilter = (input) => {
223
223
  const out: string[] = [];
224
224
  for (const line of input.split("\n")) {
225
225
  const trimmed = line.replace(/[│├└─]/g, " ").trim();
@@ -46,6 +46,12 @@ export interface ProxyRequestOutcome {
46
46
  readonly firstContentDeltaAtMs?: number;
47
47
  /** Last event timestamp (for generation duration calculation). */
48
48
  readonly lastEventAtMs?: number;
49
+ /**
50
+ * True for a completed request whose output was far smaller than the model's
51
+ * ceiling and which ended with an ordinary `stop` (a "degenerate stop").
52
+ * Set by completion bookkeeping; carried onto the telemetry row.
53
+ */
54
+ readonly degenerateStop?: boolean;
49
55
  }
50
56
  export interface ProxyRequestState {
51
57
  readonly requestId: string;
@@ -156,10 +162,9 @@ export interface ProxyRequestState {
156
162
  */
157
163
  extendDeadline(ms: number): void;
158
164
  cleanup(): void;
159
- addCleanup(cleanup: () => void): void;
160
165
  }
161
166
 
162
- export interface RequestTracker {
167
+ interface RequestTracker {
163
168
  track(id: string): void;
164
169
  untrack(id: string): void;
165
170
  }
@@ -316,7 +321,6 @@ export class ProxyRequestStateStore {
316
321
  else request.signal.addEventListener("abort", onAbort, { once: true });
317
322
  let cleaned = false;
318
323
  let providerFlightStarted = false;
319
- const cleanups: Array<() => void> = [];
320
324
  const armDeadline = (ms: number): void => {
321
325
  clearTimeout(timer);
322
326
  timer = setTimeout(() => {
@@ -390,22 +394,12 @@ export class ProxyRequestStateStore {
390
394
  request.signal.removeEventListener("abort", onAbort);
391
395
  } catch {
392
396
  }
393
- for (const cleanup of cleanups.splice(0)) {
394
- try {
395
- cleanup();
396
- } catch {
397
- }
398
- }
399
397
  try {
400
398
  abortController.abort(new DOMException("request complete", "AbortError"));
401
399
  } catch {
402
400
  }
403
401
  this.states.delete(request);
404
402
  },
405
- addCleanup: (cleanup) => {
406
- if (cleaned) cleanup();
407
- else cleanups.push(cleanup);
408
- },
409
403
  };
410
404
  this.liveControllers.set(state.requestId, abortController);
411
405
  this.tracker?.track(state.requestId);