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
@@ -118,7 +118,7 @@ function freezeList(
118
118
  return arr;
119
119
  }
120
120
 
121
- export function freezeSnapshot(s: ApiKeyAuthorizationSnapshot): ApiKeyAuthorizationSnapshot {
121
+ function freezeSnapshot(s: ApiKeyAuthorizationSnapshot): ApiKeyAuthorizationSnapshot {
122
122
  const model_list = freezeList(s.model_list);
123
123
  const model_access_mode: ApiKeyModelAccessMode =
124
124
  s.model_access_mode === "blacklist" ? "blacklist" : "whitelist";
@@ -363,6 +363,12 @@ export async function resolveApiKeyAuthorization(
363
363
  // keep the issued row values forever. Reading the parent here makes denylist /
364
364
  // one-time / recurring edits apply on the next cache miss.
365
365
  const policy = parent ?? row;
366
+ // Share-template concurrency scope decides whether the family shares one
367
+ // admission counter (default `family`) or each recipient has its own
368
+ // (`per_key`). In `family` mode the child participates in the parent's
369
+ // counter namespace; in `per_key` mode the child is its own admission
370
+ // identity and carries its own cap.
371
+ const perKeyConcurrency = parent !== undefined && row.concurrencyScope === "per_key";
366
372
  // Only a `share` parent can have children, and children are the only thing
367
373
  // the SUM below can find. Every other key ran an aggregate that was certain
368
374
  // to return 0 — one extra Postgres round-trip on the auth path for nothing.
@@ -381,7 +387,11 @@ export async function resolveApiKeyAuthorization(
381
387
  api_key_id: row.id,
382
388
  tenant_id: row.tenantId,
383
389
  // Family quota / concurrency share one admission counter namespace so two
384
- // recipients cannot each burn a full lifetime/daily budget.
390
+ // recipients cannot each burn a full lifetime/daily budget. In `per_key`
391
+ // concurrency mode the child keeps its own cap and counter for
392
+ // *concurrency* only — token budgets (daily/monthly/lifetime) stay
393
+ // family-wide on the parent, so the admission identity is always the
394
+ // parent's.
385
395
  ...(parent ? { admission_identity: parent.id } : {}),
386
396
  ...(policy.modelList ? { model_list: policy.modelList as string[] } : {}),
387
397
  model_access_mode: policy.modelAccessMode ?? "whitelist",
@@ -393,7 +403,11 @@ export async function resolveApiKeyAuthorization(
393
403
  ...(policy.monthlyTokenLimit != null ? { monthly_tokens: policy.monthlyTokenLimit } : {}),
394
404
  ...(policy.lifetimeTokenBudget != null ? { lifetime_token_budget: policy.lifetimeTokenBudget } : {}),
395
405
  lifetime_tokens_consumed: consumed,
396
- ...(policy.maxConcurrentRequests != null ? { max_concurrent: policy.maxConcurrentRequests } : {}),
406
+ // `per_key` mode: the child carries its own cap (copied at issue); family
407
+ // mode inherits the parent's cap.
408
+ ...((perKeyConcurrency ? row.maxConcurrentRequests : policy.maxConcurrentRequests) != null
409
+ ? { max_concurrent: perKeyConcurrency ? row.maxConcurrentRequests : policy.maxConcurrentRequests }
410
+ : {}),
397
411
  scopes,
398
412
  });
399
413
 
@@ -21,7 +21,7 @@ import { getHeader as readRecordHeader } from "../protocol/canonical-model";
21
21
  * `id` is the stable token an operator stores in a key's denylist, so it is
22
22
  * part of the persisted contract and must not be renamed casually.
23
23
  */
24
- export interface ClientRouterDefinition {
24
+ interface ClientRouterDefinition {
25
25
  readonly id: string;
26
26
  /** Human label for the dashboard. */
27
27
  readonly label: string;
@@ -40,7 +40,7 @@ export interface ClientRouterDefinition {
40
40
  * safe for a uniquely named header; a header whose value distinguishes it must
41
41
  * name a matcher instead.
42
42
  */
43
- export interface ClientRouterSignal {
43
+ interface ClientRouterSignal {
44
44
  /** Header name, lowercase. */
45
45
  readonly field: string;
46
46
  /** Human description shown when the signal matches. */
@@ -114,14 +114,14 @@ export const CLIENT_ROUTERS: readonly ClientRouterDefinition[] = [
114
114
  const LEGACY_CLIENT_ROUTER_IDS: Readonly<Record<string, string>> = { omniroute: "9router" };
115
115
 
116
116
  /** A signal that matched, with the product it belongs to. */
117
- export interface MatchedClientSignal {
117
+ interface MatchedClientSignal {
118
118
  readonly field: string;
119
119
  readonly description: string;
120
120
  /** The observed value, capped, for the operator's audit trail. */
121
121
  readonly value: string;
122
122
  }
123
123
 
124
- export interface ClientRouterMatch {
124
+ interface ClientRouterMatch {
125
125
  readonly routerId: string;
126
126
  readonly label: string;
127
127
  readonly signals: readonly MatchedClientSignal[];
@@ -22,7 +22,7 @@ import { isRecord } from "../protocol/codecs";
22
22
  const TEXT_DECODER = new TextDecoder();
23
23
 
24
24
  /** Decodes a stored secret: UTF-8 bytes or a string, trimmed. */
25
- export function decodeSecretText(secret: Uint8Array | string): string {
25
+ function decodeSecretText(secret: Uint8Array | string): string {
26
26
  const raw = typeof secret === "string" ? secret : TEXT_DECODER.decode(secret);
27
27
  return raw.trim();
28
28
  }
@@ -48,7 +48,7 @@ function fieldString(record: Record<string, unknown>, key: string): string {
48
48
  }
49
49
 
50
50
  /** `{accessToken, orgId?}` — the Kilo Code envelope (org scoping is optional). */
51
- export interface KiloEnvelope {
51
+ interface KiloEnvelope {
52
52
  readonly accessToken: string;
53
53
  /** Organization the gateway scopes requests to; absent when the profile omits one. */
54
54
  readonly orgId?: string;
@@ -79,7 +79,7 @@ export function decodeKiloCredential(secret: Uint8Array | string): KiloEnvelope
79
79
  }
80
80
 
81
81
  /** `{oauthAccessToken, apiKey}` — the Muse Code (Meta) envelope. */
82
- export interface MuseEnvelope {
82
+ interface MuseEnvelope {
83
83
  readonly oauthAccessToken: string;
84
84
  readonly apiKey: string;
85
85
  }
@@ -16,7 +16,7 @@ import { isRecord } from "../protocol/codecs";
16
16
  * but cannot read the cookie value, so it cannot forge the header.
17
17
  */
18
18
  export const CSRF_COOKIE_NAME = "csrf_token";
19
- export const CSRF_HEADER_NAME = "x-csrf-token";
19
+ const CSRF_HEADER_NAME = "x-csrf-token";
20
20
 
21
21
  /**
22
22
  * Name of the session cookie whose presence gates CSRF validation in
@@ -30,7 +30,7 @@ export const SESSION_COOKIE_NAME = "session_token";
30
30
  * `SessionCookiePolicy` structurally satisfies this (extra fields are fine),
31
31
  * so call sites keep passing their existing policy object unchanged.
32
32
  */
33
- export interface CsrfCookiePolicy {
33
+ interface CsrfCookiePolicy {
34
34
  readonly maxAge: number;
35
35
  readonly sameSite: "Lax" | "Strict" | "None";
36
36
  readonly secure: boolean;
@@ -3,185 +3,25 @@
3
3
  import { isIP } from "node:net";
4
4
  import type { TrustedProxyBoundary } from "../config/network";
5
5
  import { log } from "../observability/logger";
6
+ import {
7
+ cidrContainsAddress,
8
+ ipv4ToNumber,
9
+ ipv6ToBigInt,
10
+ isLoopbackAddress,
11
+ unwrapMappedIpv4,
12
+ } from "../network/ip-utils";
6
13
 
7
- function ipv4ToNumber(address: string): number | undefined {
8
- const parts = address.split(".");
9
- if (parts.length !== 4 || parts.some((part) => !/^\d+$/.test(part))) return undefined;
10
- const octets = parts.map(Number);
11
- if (octets.some((part) => part < 0 || part > 255)) return undefined;
12
- const [a, b, c, d] = octets;
13
- if (a === undefined || b === undefined || c === undefined || d === undefined) return undefined;
14
- return ((a * 256 + b) * 256 + c) * 256 + d;
15
- }
16
-
17
- function ipv6ToBigInt(address: string): bigint | undefined {
18
- const value = address.toLowerCase().split("%", 1)[0] ?? "";
19
- if (value.includes(".")) return undefined;
20
- const halves = value.split("::");
21
- if (halves.length > 2) return undefined;
22
- const left = halves[0] ? halves[0].split(":") : [];
23
- const right = halves.length === 2 && halves[1] ? halves[1].split(":") : [];
24
- if (halves.length === 1 && left.length !== 8) return undefined;
25
- const missing = 8 - left.length - right.length;
26
- if (missing < 0 || (halves.length === 2 && missing < 1)) return undefined;
27
- const groups = [...left, ...Array.from({ length: missing }, () => "0"), ...right];
28
- if (groups.length !== 8 || groups.some((part) => !/^[0-9a-f]{1,4}$/.test(part)))
29
- return undefined;
30
- let result = 0n;
31
- for (const group of groups) result = (result << 16n) | BigInt(Number.parseInt(group, 16));
32
- return result;
33
- }
34
-
35
- /**
36
- * Unwraps an IPv4-mapped IPv6 address to plain IPv4.
37
- *
38
- * Bun reports an IPv4 loopback peer as `::ffff:127.0.0.1` on Windows, and
39
- * `isIP` classifies it as IPv6 — so `ipv6ToBigInt` (which bails on a dotted
40
- * quad) never matched `127.0.0.1/32`. The trust check therefore failed for the
41
- * exact peer `TRUSTED_PROXY_CIDRS=127.0.0.1/32` is meant to cover, forwarded
42
- * headers were ignored, and every cloudflared request stored the loopback peer
43
- * as the client address. Accepts both the dotted and the hex (`::ffff:7f00:1`)
44
- * spelling.
45
- */
46
- function unwrapMappedIpv4(address: string): string {
47
- const value = address.toLowerCase().split("%", 1)[0] ?? "";
48
- // Recognize the mapped range STRUCTURALLY, not by its compressed spelling. The
49
- // old `startsWith("::ffff:")` test missed any form that writes the leading zero
50
- // groups out — `0:0:0:0:0:ffff:7f00:1` is the same host as `::ffff:127.0.0.1`,
51
- // but it failed the prefix test, fell through to `ipv6ToBigInt`, and was treated
52
- // as an unrelated IPv6 address. The trust check then refused a peer the allowlist
53
- // covered, and `canonicalClientIpKey` minted a second key for one host — which is
54
- // stored on issued child keys for consistent usage reporting and audit.
55
- //
56
- // A mapped address is `::ffff:<32 bits>`, i.e. the first 80 bits are zero and the
57
- // next 16 are `0xffff`. Matching that is the same test `isIP` uses internally.
58
- const tail = mappedIpv4Tail(value);
59
- if (tail === undefined) return address;
60
- // `mappedIpv4Tail` always returns two hex groups, so the dotted and hex
61
- // spellings have already converged by here.
62
- const groups = tail.split(":");
63
- if (groups.length !== 2) return address;
64
- const high = Number.parseInt(groups[0] ?? "", 16);
65
- const low = Number.parseInt(groups[1] ?? "", 16);
66
- if (!Number.isInteger(high) || !Number.isInteger(low) || high < 0 || high > 0xffff || low < 0 || low > 0xffff)
67
- return address;
68
- return `${(high >> 8) & 0xff}.${high & 0xff}.${(low >> 8) & 0xff}.${low & 0xff}`;
69
- }
70
-
71
- /**
72
- * The 32-bit tail of an IPv4-mapped IPv6 address, or `undefined` when `value` is
73
- * not one. Accepts every spelling of the same address: `::ffff:1.2.3.4`,
74
- * `::ffff:102:304`, `0:0:0:0:0:ffff:102:304`, and `::ffff:0:102:304`.
75
- *
76
- * Works by normalizing to exactly eight 16-bit groups first. That is the only way
77
- * to compare spellings: a dotted tail is written as ONE colon-separated token but
78
- * occupies TWO group slots, so any check that counts `split(":")` entries gets the
79
- * position of the `ffff` marker wrong.
80
- */
81
- function mappedIpv4Tail(value: string): string | undefined {
82
- const groups = normalizeIpv6Groups(value);
83
- if (groups === undefined) return undefined;
84
- // The first five groups (80 bits) must be zero, and the sixth must be `ffff`.
85
- for (let index = 0; index < 5; index += 1) {
86
- if (groups[index] !== "0") return undefined;
87
- }
88
- if (groups[5] !== "ffff") return undefined;
89
- const high = groups[6];
90
- const low = groups[7];
91
- if (high === undefined || low === undefined) return undefined;
92
- return `${high}:${low}`;
93
- }
94
-
95
- /**
96
- * An IPv6 literal as exactly eight lowercase 16-bit hex groups, or `undefined`
97
- * when it is not one. Expands a `::` run and converts a trailing dotted quad into
98
- * the two groups it represents.
99
- */
100
- function normalizeIpv6Groups(value: string): string[] | undefined {
101
- const text = value.trim();
102
- if (text.length === 0) return undefined;
103
- const doubleIndex = text.indexOf("::");
104
- // More than one `::` is invalid, as is a `::` inside a group.
105
- if (doubleIndex !== text.lastIndexOf("::")) return undefined;
106
-
107
- const headText = doubleIndex >= 0 ? text.slice(0, doubleIndex) : text;
108
- const tailText = doubleIndex >= 0 ? text.slice(doubleIndex + 2) : "";
109
- const head = headText.length > 0 ? headText.split(":") : [];
110
- const tail = tailText.length > 0 ? tailText.split(":") : [];
111
-
112
- // A dotted quad is a single token that fills two groups.
113
- const expandDotted = (tokens: string[]): string[] | undefined => {
114
- const last = tokens[tokens.length - 1];
115
- if (last === undefined || !last.includes(".")) return tokens;
116
- const quad = last.split(".");
117
- if (quad.length !== 4) return undefined;
118
- const octets = quad.map((part) => Number(part));
119
- if (octets.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) return undefined;
120
- const [a, b, c, d] = octets;
121
- if (a === undefined || b === undefined || c === undefined || d === undefined) return undefined;
122
- const high = ((a << 8) | b).toString(16);
123
- const low = ((c << 8) | d).toString(16);
124
- return [...tokens.slice(0, -1), high, low];
125
- };
126
-
127
- const expandedHead = expandDotted(head);
128
- const expandedTail = expandDotted(tail);
129
- if (expandedHead === undefined || expandedTail === undefined) return undefined;
130
-
131
- let full: string[];
132
- if (doubleIndex >= 0) {
133
- const missing = 8 - expandedHead.length - expandedTail.length;
134
- // `::` must stand for at least one group, so exactly-8 without it is invalid.
135
- if (missing < 1) return undefined;
136
- full = [...expandedHead, ...Array.from({ length: missing }, () => "0"), ...expandedTail];
137
- } else {
138
- full = expandedHead;
139
- }
140
- if (full.length !== 8) return undefined;
141
- const normalized: string[] = [];
142
- for (const group of full) {
143
- if (!/^[0-9a-f]{1,4}$/.test(group)) return undefined;
144
- normalized.push(Number.parseInt(group, 16).toString(16));
145
- }
146
- return normalized;
147
- }
14
+ export { isLoopbackAddress };
148
15
 
149
16
  function matchesCidr(address: string, cidr: string): boolean {
150
- const [network, rawPrefix] = cidr.split("/");
151
- if (!network) return false;
152
- // A bare address (`"127.0.0.1"`) has no prefix and means "this exact host". A
153
- // present-but-blank prefix (`"10.0.0.0/"`) must NOT be treated as /0: `Number("")`
154
- // is 0, and /0 matches every address — which made `isTrustedProxyPeer` true for
155
- // ANY peer, so the gateway believed a caller's own `X-Forwarded-For` and every
156
- // per-address decision downstream (abuse throttling, model bans, the audit
157
- // trail) read a forged value.
158
- //
159
- // Requiring a run of digits also refuses the exotic spellings `Number` accepts
160
- // for zero (`-0`, `+0`, `0x0`, `0b0`, `0o0`, `0.0`), all of which are typos
161
- // rather than a deliberate "trust everyone".
162
- const prefixText = rawPrefix === undefined ? undefined : rawPrefix.trim();
163
- if (prefixText !== undefined && !/^\d+$/.test(prefixText)) return false;
164
- // A mapped peer and a plain-IPv4 allowlist entry describe the same host.
165
- const subject = unwrapMappedIpv4(address);
166
- const base = unwrapMappedIpv4(network);
167
- const family = isIP(subject);
168
- if (family === 4) {
169
- const value = ipv4ToNumber(subject);
170
- const networkValue = ipv4ToNumber(base);
171
- if (value === undefined || networkValue === undefined) return false;
172
- const prefix = prefixText === undefined ? 32 : Number(prefixText);
173
- if (!Number.isInteger(prefix) || prefix < 0 || prefix > 32) return false;
174
- const mask = prefix === 0 ? 0 : (0xffffffff << (32 - prefix)) >>> 0;
175
- return ((value >>> 0) & mask) === ((networkValue >>> 0) & mask);
176
- }
177
- if (family !== 6) return false;
178
- const value = ipv6ToBigInt(subject);
179
- const networkValue = ipv6ToBigInt(base);
180
- if (value === undefined || networkValue === undefined) return false;
181
- const prefix = prefixText === undefined ? 128 : Number(prefixText);
182
- if (!Number.isInteger(prefix) || prefix < 0 || prefix > 128) return false;
183
- const shift = BigInt(128 - prefix);
184
- return (value >> shift) === (networkValue >> shift);
17
+ // `unwrapMapped: true`: a mapped peer and a plain-IPv4 allowlist entry
18
+ // describe the same host (Bun reports the IPv4 loopback peer as
19
+ // `::ffff:127.0.0.1` on Windows). `requirePrefix: false`: a bare address
20
+ // (`"127.0.0.1"`) means "this exact host".
21
+ return cidrContainsAddress(address, cidr, {
22
+ requirePrefix: false,
23
+ unwrapMapped: true,
24
+ });
185
25
  }
186
26
 
187
27
  function isTrustedPeer(peer: string, boundary: TrustedProxyBoundary): boolean {
@@ -206,20 +46,7 @@ const CLIENT_IDENTITY_HEADERS = [
206
46
 
207
47
  /**
208
48
  * True when `address` is a loopback address.
209
- *
210
- * Structural, not a string-prefix test: `127.0.0.1`, `::1`, and the mapped
211
- * `::ffff:127.0.0.1` a Windows/Bun peer arrives as are all the same host, and
212
- * a prefix test would miss the mapped form — the exact shape that hid this
213
- * class of bug before.
214
49
  */
215
- export function isLoopbackAddress(address: string): boolean {
216
- const normalized = unwrapMappedIpv4(address.trim().split("%", 1)[0] ?? "");
217
- if (normalized === "::1") return true;
218
- if (isIP(normalized) !== 4) return false;
219
- const value = ipv4ToNumber(normalized);
220
- // 127.0.0.0/8 — the whole loopback block, not just 127.0.0.1.
221
- return value !== undefined && ((value >>> 0) & 0xff000000) === (127 << 24);
222
- }
223
50
 
224
51
  /**
225
52
  * Whether a peer may vouch for forwarded headers *for client-address
@@ -24,7 +24,7 @@ import type { ApiKeyModelAccessMode } from "../persistence/schema";
24
24
  export type ModelRejectionReason = "model-denied" | "model-not-allowed";
25
25
 
26
26
  /** The subset of an authorization snapshot this rule reads. */
27
- export interface ModelAccessPolicy {
27
+ interface ModelAccessPolicy {
28
28
  readonly model_access_mode?: ApiKeyModelAccessMode | null | undefined;
29
29
  readonly model_list?: readonly string[] | ReadonlySet<string> | null | undefined;
30
30
  }
@@ -21,6 +21,24 @@ export const X_FRAME_OPTIONS = "DENY";
21
21
  */
22
22
  export const BADGE_IMAGE_ORIGIN = "https://img.shields.io";
23
23
 
24
+ /**
25
+ * Invariant: upstream response headers are NEVER forwarded wholesale to
26
+ * callers. There is no denylist-and-forward step on the egress path — the
27
+ * success projection (`proxySuccessHeaders` in
28
+ * `transport/dispatch/upstream.ts`) rebuilds the caller-facing header set from
29
+ * scratch (`cache-control: no-store`, `x-request-id`, and
30
+ * `GATEWAY_SECURITY_HEADERS` below), so credential-bearing upstream fields
31
+ * (`set-cookie`, `www-authenticate`, `proxy-authorization`) and hop-by-hop
32
+ * headers can never leak through by omission from a strip list.
33
+ *
34
+ * Any future feature that wants to pass upstream response fields to a caller
35
+ * must copy only the specific content-bearing headers it needs — do not
36
+ * reintroduce a "forward everything except" filter. A module implementing
37
+ * exactly that (`src/network/response-headers.ts` + `stripResponseHeaders`)
38
+ * existed once and was deleted as dead code (see git history for it), because
39
+ * every egress path had already moved to the rebuild-from-scratch projection.
40
+ */
41
+
24
42
  /**
25
43
  * Security headers on every `/v1` response, whichever path produced it.
26
44
  *
@@ -0,0 +1,47 @@
1
+ /**
2
+ * InvokeGuard — a process-local foreground-activity gauge (G-03).
3
+ *
4
+ * Every foreground request path signals its activity here: the console control
5
+ * plane bumps the counter for the duration of each request, and the proxy data
6
+ * plane exposes its in-flight count through `setProxyBusySource`. The scheduled
7
+ * worker registry (and the sweep runner between waves) consults
8
+ * `foregroundBusy()` before starting background work, so a page open / an
9
+ * invoke in progress is never forced to share the single event loop with a
10
+ * heavy sweep.
11
+ *
12
+ * The defer policy is at the call sites (scheduler: bounded pause before a
13
+ * tick; sweep runner: bounded pause between waves). This module only owns the
14
+ * gauge — it never decides when to pause.
15
+ */
16
+
17
+ /** Console request counter. Bumped per in-flight console request. */
18
+ let consoleActivity = 0;
19
+
20
+ /** Optional live source of "the proxy plane is busy" (the in-flight store). */
21
+ let proxyBusySource: (() => boolean) | undefined;
22
+
23
+ /** Registers the proxy plane's busy source (wired once from the app root). */
24
+ export function setProxyBusySource(source: () => boolean): void {
25
+ proxyBusySource = source;
26
+ }
27
+
28
+ /** Marks a foreground console request; returns the matching end callback. */
29
+ export function beginConsoleActivity(): () => void {
30
+ consoleActivity += 1;
31
+ let ended = false;
32
+ return () => {
33
+ if (ended) return;
34
+ ended = true;
35
+ consoleActivity = Math.max(0, consoleActivity - 1);
36
+ };
37
+ }
38
+
39
+ /** Live foreground-request count (console counter + proxy source if wired). */
40
+ export function foregroundActivityCount(): number {
41
+ return consoleActivity + (proxyBusySource?.() === true ? 1 : 0);
42
+ }
43
+
44
+ /** True when any foreground work is in flight right now. */
45
+ export function foregroundBusy(): boolean {
46
+ return consoleActivity > 0 || (proxyBusySource?.() ?? false);
47
+ }
@@ -54,12 +54,12 @@ export interface CanonicalAdapter {
54
54
  */
55
55
 
56
56
  /** One native service: the path it is served at and the kind its models carry. */
57
- export interface NativeServiceRoute {
57
+ interface NativeServiceRoute {
58
58
  readonly serviceKind: Exclude<ServiceKind, "llm">;
59
59
  readonly path: string;
60
60
  }
61
61
 
62
- export const NATIVE_SERVICES: readonly NativeServiceRoute[] = [
62
+ const NATIVE_SERVICES: readonly NativeServiceRoute[] = [
63
63
  { serviceKind: "systemone", path: "/v1/systemone" },
64
64
  { serviceKind: "websearch", path: "/v1/search" },
65
65
  { serviceKind: "imageGeneration", path: "/v1/images/generations" },
@@ -75,7 +75,7 @@ export const NATIVE_SERVICE_PATHS: readonly string[] = NATIVE_SERVICES.map(
75
75
  );
76
76
 
77
77
  /** Whether `path` is served by a native service route. */
78
- export function isNativeServicePath(path: string | undefined): boolean {
78
+ function isNativeServicePath(path: string | undefined): boolean {
79
79
  return path !== undefined && NATIVE_SERVICE_PATHS.includes(path);
80
80
  }
81
81
 
@@ -97,6 +97,15 @@ export function isNativeServicePath(path: string | undefined): boolean {
97
97
  */
98
98
  export const TOKEN_COUNT_PATH = "/v1/messages/count_tokens";
99
99
 
100
+ /**
101
+ * The native Codex compaction route (`POST /v1/responses/compact`).
102
+ *
103
+ * Not a native service (it has no `serviceKind` of its own — Codex rows are
104
+ * `llm`-adjacent), so it is declared separately from `NATIVE_SERVICE_PATHS`,
105
+ * but its body is equally opaque and must skip the canonical chat stage.
106
+ */
107
+ export const RESPONSES_COMPACT_PATH = "/v1/responses/compact";
108
+
100
109
  /**
101
110
  * Whether `path` reads a JSON body that is *not* a canonical chat request.
102
111
  *
@@ -105,7 +114,7 @@ export const TOKEN_COUNT_PATH = "/v1/messages/count_tokens";
105
114
  * will either reject a body the upstream accepts or plan it twice.
106
115
  */
107
116
  export function isOpaqueBodyRoute(path: string | undefined): boolean {
108
- return isNativeServicePath(path) || path === TOKEN_COUNT_PATH;
117
+ return isNativeServicePath(path) || path === TOKEN_COUNT_PATH || path === RESPONSES_COMPACT_PATH;
109
118
  }
110
119
 
111
120
  /** The route path serving `serviceKind`, when one is declared. */
@@ -16,6 +16,7 @@ import { finalizeRequestTelemetry } from "../guards/error-lifecycle";
16
16
  import { debitCachedQuota, quotaLens } from "../../providers/quota/cache";
17
17
  import { BUDDY_PROVIDER_IDS } from "../../providers/provider-metadata";
18
18
  import { log } from "../../observability/logger";
19
+ import { modelsDevCatalog } from "../../providers/discovery/models-dev-catalog";
19
20
  import {
20
21
  disablePoolForProxyHttpStatus,
21
22
  recordPoolDispatchOutcome,
@@ -394,7 +395,7 @@ let captureFailureReported = false;
394
395
  * deployment condition that does not change per request, so repeating it for
395
396
  * every request would bury the rest of the log without adding information.
396
397
  */
397
- export function reportCaptureFailure(error: unknown): void {
398
+ function reportCaptureFailure(error: unknown): void {
398
399
  if (captureFailureReported) return;
399
400
  captureFailureReported = true;
400
401
  const directory = payloadDirectory();
@@ -711,6 +712,32 @@ async function debitBuddySpend(completion: AttemptCompletion): Promise<void> {
711
712
  }
712
713
  }
713
714
 
715
+ /**
716
+ * Whether a completed attempt is a "degenerate stop": output far smaller than
717
+ * the model's ceiling, ended by an ordinary `stop` rather than `length`.
718
+ *
719
+ * This is the signature of a model truncating itself on a cached prefix — the
720
+ * pattern that surfaced as short, cut-off chat replies that the upstream still
721
+ * reported as a clean stop. It is measured so the operator can see how often
722
+ * it happens instead of eyeballing request detail.
723
+ */
724
+ export function isDegenerateStop(
725
+ usage: UsageRecord | undefined,
726
+ outputLimit: number | undefined,
727
+ ): boolean {
728
+ if (usage === undefined) return false;
729
+ const output = usage.output_tokens;
730
+ const reasoning = typeof usage.reasoning_tokens === "number" ? usage.reasoning_tokens : 0;
731
+ const totalOutput = output + reasoning;
732
+ if (totalOutput === 0 || totalOutput > 512) return false;
733
+ // A model with no meaningful ceiling cannot be "far smaller than it can
734
+ // produce", so the flag is only meaningful when a ceiling exists.
735
+ if (!(typeof outputLimit === "number" && Number.isFinite(outputLimit) && outputLimit > 0)) {
736
+ return false;
737
+ }
738
+ return totalOutput <= Math.max(1, Math.floor(outputLimit / 10));
739
+ }
740
+
714
741
  export async function completeAttempt(
715
742
  state: ProxyRequestState,
716
743
  completion: AttemptCompletion,
@@ -761,6 +788,15 @@ export async function completeAttempt(
761
788
  ...(completion.firstContentDeltaAtMs === undefined ? {} : { firstContentDeltaAtMs: completion.firstContentDeltaAtMs }),
762
789
  ...(completion.lastEventAtMs === undefined ? {} : { lastEventAtMs: completion.lastEventAtMs }),
763
790
  ...(completion.reportedModel === undefined ? {} : { reportedModel: completion.reportedModel }),
791
+ ...(completion.status === "completed" &&
792
+ isDegenerateStop(
793
+ completion.usage,
794
+ completion.providerId !== undefined && completion.modelId !== undefined
795
+ ? (modelsDevCatalog.resolve(completion.providerId, completion.modelId)?.outputLimit ?? undefined)
796
+ : undefined,
797
+ )
798
+ ? { degenerateStop: true }
799
+ : {}),
764
800
  };
765
801
  if (completion.terminal) state.completed = true;
766
802
  // Usage reconciliation is bookkeeping, and it is the last thing standing
@@ -785,27 +821,39 @@ export async function completeAttempt(
785
821
  // advisory: a failed report must never re-enter the retry path (a retry
786
822
  // would see `completed` and skip completion entirely), so it is swallowed.
787
823
  if (completion.status !== "cancelled" && !disableProxy) {
788
- // DEPLOYMENT_DISABLED and similar infra errors are proxy pool failures,
789
- // not account quota. Classify with scope pool so account stays healthy.
790
- const msg = completion.error instanceof Error ? completion.error.message.toLowerCase() : "";
791
- const isProxyDeploymentError = msg.includes("deployment_disabled") || (msg.includes("sin1::") && msg.includes("deployment"));
792
- const evidence = isProxyDeploymentError
793
- ? { scope: "pool" as const, origin: "network" as const }
794
- : { ...classifyUpstreamFailure(completion.error) };
795
- try {
796
- await reportAttemptOutcome(completion.db, {
797
- accountId: completion.accountId,
798
- ...(completion.modelId === undefined ? {} : { modelId: completion.modelId }),
799
- ...(completion.status === "failed"
800
- ? {
801
- error: completion.error,
802
- evidence,
803
- }
804
- : {}),
805
- ...(completion.snapshotService ? { snapshotService: completion.snapshotService } : {}),
806
- });
807
- } catch {
808
- // Swallowed: health reporting is non-critical bookkeeping.
824
+ // Account health records only failures that are evidence about the
825
+ // provider's own account: upstream verdicts (quota, auth, model
826
+ // capacity, rate limits), or credential-scoped refusals from the
827
+ // upstream. Local rejections — API-key admission (`max_concurrent`,
828
+ // rpm/token budgets), routing capacity, pool selection, request
829
+ // preparation — are decisions this gateway made about its own limits;
830
+ // they say nothing about the account and must not cool it or write a
831
+ // phantom `logged -> cooldown` transition. Success always reports so a
832
+ // cooling account recovers.
833
+ const succeeded = completion.status === "completed";
834
+ const gatewayError = completion.error instanceof GatewayError ? completion.error : undefined;
835
+ const upstreamVerdict = gatewayError?.origin === "upstream";
836
+ const isProxyDeploymentError =
837
+ completion.error instanceof Error ? completion.error.message.toLowerCase().includes("deployment_disabled") || (completion.error.message.toLowerCase().includes("sin1::") && completion.error.message.toLowerCase().includes("deployment")) : false;
838
+ if (succeeded || isProxyDeploymentError || upstreamVerdict) {
839
+ const evidence = isProxyDeploymentError
840
+ ? { scope: "pool" as const, origin: "network" as const }
841
+ : { ...classifyUpstreamFailure(completion.error) };
842
+ try {
843
+ await reportAttemptOutcome(completion.db, {
844
+ accountId: completion.accountId,
845
+ ...(completion.modelId === undefined ? {} : { modelId: completion.modelId }),
846
+ ...(completion.status === "failed"
847
+ ? {
848
+ error: completion.error,
849
+ evidence,
850
+ }
851
+ : {}),
852
+ ...(completion.snapshotService ? { snapshotService: completion.snapshotService } : {}),
853
+ });
854
+ } catch {
855
+ // Swallowed: health reporting is non-critical bookkeeping.
856
+ }
809
857
  }
810
858
  }
811
859
  if (completion.networkPoolId && completion.status !== "cancelled") {
@@ -61,6 +61,27 @@ function comboCandidateKey(candidate: RouteCandidate): string {
61
61
  ].join(":");
62
62
  }
63
63
 
64
+ /**
65
+ * True when the rejection is about the API key or the gateway's own policy,
66
+ * not about the provider account that was dialled.
67
+ *
68
+ * These are decided before the upstream is ever called, so every candidate in
69
+ * the plan fails identically; walking the rest of the list would just burn
70
+ * attempts and write phantom account-health rows (a local `capacity_exhausted`
71
+ * used to read as a provider `rate_limit_transient` cooldown).
72
+ */
73
+ function isClientScopedRejection(error: unknown): boolean {
74
+ if (!(error instanceof GatewayError)) return false;
75
+ return (
76
+ error.code === "capacity_exhausted" ||
77
+ error.code === "tenant_capacity_exhausted" ||
78
+ error.code === "admission_unavailable" ||
79
+ error.code === "model_not_allowed" ||
80
+ error.code === "ambiguous_model" ||
81
+ error.code === "route_unavailable"
82
+ );
83
+ }
84
+
64
85
  /**
65
86
  * Which combo member a candidate belongs to.
66
87
  *
@@ -402,6 +423,7 @@ export async function runAttemptLoop<TResult, TAdapter>(
402
423
  }
403
424
  const terminalAttempt =
404
425
  cancelled ||
426
+ isClientScopedRejection(error) ||
405
427
  !isRetryableFailure(error) ||
406
428
  (index === candidates.length - 1 && !poolScopedFailure);
407
429
  // Track only the most recent fired exchange; the array this replaced