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
@@ -9,6 +9,7 @@
9
9
  * back to the prebuilt base catalog; capability does not — see `defineModel`.
10
10
  */
11
11
  import type { ServiceKind, WireFamily } from "../protocol/canonical-model";
12
+ import type { DialectId } from "../protocol/translation/dialect/types";
12
13
  import { modelsDevCatalog } from "./discovery/models-dev-catalog";
13
14
 
14
15
  /** Billing metadata for one provider model. */
@@ -53,8 +54,22 @@ export interface ModelDefinition {
53
54
  readonly reasoning: boolean;
54
55
  /** Explicitly supported reasoning effort scale (e.g. `["minimal", "low", "medium", "high", "xhigh"]`). */
55
56
  readonly reasoningEfforts?: readonly string[];
57
+ /**
58
+ * Optional thinking/effort override for this row. When present, the ladder
59
+ * resolver prefers these efforts and transport mode over the rule table.
60
+ * Compile-time only — no DB column (see `thinking-ladder.ts`).
61
+ */
62
+ readonly thinking?: {
63
+ readonly efforts?: readonly string[];
64
+ readonly mode?: "effort" | "budget" | "anthropic-adaptive" | "google-level";
65
+ };
56
66
  readonly toolCall: boolean;
57
67
  readonly cost: ModelCostDefinition;
68
+ /**
69
+ * Explicit wire dialect for this row. Absent = resolver fallback (heuristic
70
+ * + family default) — see `src/protocol/translation/dialect/`.
71
+ */
72
+ readonly dialect?: DialectId;
58
73
  /**
59
74
  * Whether the provider serves this row on a free plan tier.
60
75
  *
@@ -85,7 +100,7 @@ export interface ModelDefinition {
85
100
  * on a free account while `gpt-5.6-luna` bills `x0.14` and is refused. The
86
101
  * only signal is the refusal itself: `11158 "model [X] is not allowed for
87
102
  * user type [personal_free_user]"`. See
88
- * `.skills/cartethyia-reverse-engineering/references/codebuddy-dispatch-headers.md`.
103
+ * `documentation/research/codebuddy/2026-10-19-codebuddy-chat-dispatch-headers.md`.
89
104
  *
90
105
  * Optional, and absence is "tier unknown" rather than "served on free". Read
91
106
  * it as `=== true`; a UI that shows a tier must treat undefined as unknown,
@@ -168,6 +183,8 @@ export interface ModelHelperRow {
168
183
  readonly output?: readonly ModelDefinition["modalities"]["output"][number][];
169
184
  /** Explicitly supported reasoning effort scale. */
170
185
  readonly reasoningEfforts?: readonly string[];
186
+ /** Optional thinking/effort override; see `ModelDefinition.thinking`. */
187
+ readonly thinking?: ModelDefinition["thinking"];
171
188
  readonly toolCall?: boolean;
172
189
  /**
173
190
  * Marks a row served by a free plan tier. Such rows bill zero per token
@@ -186,6 +203,8 @@ export interface ModelHelperRow {
186
203
  readonly freeTier?: boolean;
187
204
  /** Whether the provider refuses this row on a free plan. See `ModelDefinition.proOnly`. */
188
205
  readonly proOnly?: boolean;
206
+ /** Explicit wire dialect override for this row. See `ModelDefinition.dialect`. */
207
+ readonly dialect?: DialectId;
189
208
  }
190
209
 
191
210
  function defaultEndpoint(wireFamily: ModelDefinition["wireFamily"]): string {
@@ -283,10 +302,12 @@ export function defineModel(row: ModelHelperRow): ModelDefinition {
283
302
  },
284
303
  reasoning: row.reasoning ?? false,
285
304
  ...(row.reasoningEfforts !== undefined ? { reasoningEfforts: row.reasoningEfforts } : {}),
305
+ ...(row.thinking === undefined ? {} : { thinking: row.thinking }),
286
306
  toolCall: row.toolCall ?? true,
287
307
  cost,
288
308
  ...(row.freeTier === undefined ? {} : { freeTier: row.freeTier }),
289
309
  ...(row.proOnly === undefined ? {} : { proOnly: row.proOnly }),
310
+ ...(row.dialect === undefined ? {} : { dialect: row.dialect }),
290
311
  ...(limitsEstimated ? { limitsEstimated: true } : {}),
291
312
  };
292
313
  }
@@ -12,6 +12,7 @@
12
12
  * report a pool as refused for a reason replacing the proxy cannot fix.
13
13
  */
14
14
  import { encodeWireRequest } from "../protocol/registry";
15
+ import { resolveDialect } from "../protocol/translation/dialect/resolver";
15
16
  import type { CanonicalRequest } from "../protocol/canonical-model";
16
17
  import type { ProviderDispatchContext } from "./provider-contracts";
17
18
  import { providerBaseUrl, providerRateLimitIsIpScoped } from "./provider-metadata";
@@ -60,7 +61,7 @@ function probeRequest(): CanonicalRequest {
60
61
  stream: true,
61
62
  source_surface: "chat",
62
63
  generation_controls: {},
63
- } as unknown as CanonicalRequest;
64
+ };
64
65
  }
65
66
 
66
67
  /**
@@ -98,6 +99,7 @@ export async function buildProviderProbeRequest(
98
99
  ...(spec.chatTokenLimitField === undefined
99
100
  ? {}
100
101
  : { chatTokenLimitField: spec.chatTokenLimitField }),
102
+ dialectPolicy: resolveDialect(PROBE_MODEL, providerId, "chat"),
101
103
  });
102
104
  // The adapter's own hooks build the wire body; without running them the
103
105
  // probe would omit the agent-tool fingerprint and be refused on a healthy
@@ -116,7 +118,7 @@ export async function buildProviderProbeRequest(
116
118
  );
117
119
  const headers = (await spec.buildExtraHeaders?.(probeContext(providerId), source)) ?? {};
118
120
  return {
119
- url: `${providerBaseUrl(providerId).replace(/\/+$/, "")}/inference/openai/v1/chat/completions`,
121
+ url: `${providerBaseUrl(providerId)}/inference/openai/v1/chat/completions`,
120
122
  headers: { "content-type": "application/json", ...headers },
121
123
  body: payload,
122
124
  };
@@ -1,4 +1,5 @@
1
1
  import type { UsageRecord } from "../protocol/canonical-model";
2
+ import { resolveUsageInputMode } from "../config/usage";
2
3
  export interface OpenAiUsage {
3
4
  prompt_tokens: number;
4
5
  completion_tokens: number;
@@ -235,10 +236,21 @@ export function normalizeUsage(input: Record<string, unknown>): UsageRecord {
235
236
  const reportedTotal = typeof input.total_tokens === "number" ? input.total_tokens : null;
236
237
  const inputAlreadyIncludesCache =
237
238
  reportedTotal !== null && reportedTotal === input_tokens + output_tokens;
238
- const hasAnthropicCacheShape = anthropic_cache_read !== null && !inputAlreadyIncludesCache;
239
+ // `inclusive` (default) folds cache into input so every provider reads alike:
240
+ // IN is always the whole prompt and CACHED its subset. Anthropic's raw shape
241
+ // is fresh-only with cache separate — and its `total_tokens` equals
242
+ // input+output by contract, so the `inputAlreadyIncludesCache` heuristic
243
+ // would otherwise mistake it for already-inclusive and never add the cache,
244
+ // under-reporting prompt by the entire cached span. `as-is` keeps the raw
245
+ // upstream semantics (and the legacy heuristic for already-inclusive bridges).
246
+ const accountInputInclusive = resolveUsageInputMode() === "inclusive";
247
+ const cacheIsSeparate = anthropic_cache_read !== null;
248
+ const shouldFoldCache = accountInputInclusive
249
+ ? cacheIsSeparate
250
+ : cacheIsSeparate && !inputAlreadyIncludesCache;
239
251
  let uncached_input_tokens: number | "unavailable" = "unavailable";
240
252
  if (typeof cached_input_tokens === "number") {
241
- if (hasAnthropicCacheShape) {
253
+ if (shouldFoldCache) {
242
254
  uncached_input_tokens = input_tokens;
243
255
  input_tokens +=
244
256
  cached_input_tokens + (typeof cache_write_tokens === "number" ? cache_write_tokens : 0);
@@ -246,7 +258,7 @@ export function normalizeUsage(input: Record<string, unknown>): UsageRecord {
246
258
  uncached_input_tokens = Math.max(0, input_tokens - Math.min(input_tokens, cached_input_tokens));
247
259
  cached_input_tokens = Math.min(input_tokens, cached_input_tokens);
248
260
  }
249
- } else if (hasAnthropicCacheShape && typeof cache_write_tokens === "number") {
261
+ } else if (shouldFoldCache && typeof cache_write_tokens === "number") {
250
262
  input_tokens += cache_write_tokens;
251
263
  }
252
264
 
@@ -436,28 +448,47 @@ export function usageToMessagesWire(usage: UsageRecord | undefined): Record<stri
436
448
  * and event shapes — only the key lookup lives here, so a new upstream key
437
449
  * is added once and every reader picks it up.
438
450
  */
439
- export const REASONING_TEXT_KEYS: readonly string[] = ["reasoning_content", "reasoning"];
451
+ export const REASONING_TEXT_KEYS: readonly string[] = [
452
+ "reasoning_content",
453
+ "reasoning",
454
+ "reasoning_summary",
455
+ "reasoning_summary_text",
456
+ "reasoning_text",
457
+ ];
458
+
459
+ /**
460
+ * Extracts readable text from one reasoning value. Accepts a plain string and
461
+ * the object shapes upstreams use for reasoning deltas: `{text}`,
462
+ * `{summary}`, `{delta}` (Responses-style), or an array of parts.
463
+ */
464
+ function reasoningTextValue(value: unknown): string | undefined {
465
+ if (typeof value === "string") return value;
466
+ if (Array.isArray(value)) {
467
+ let text = "";
468
+ for (const part of value) {
469
+ const piece = reasoningTextValue(part);
470
+ if (piece !== undefined) text += piece;
471
+ }
472
+ return text.length > 0 ? text : undefined;
473
+ }
474
+ if (typeof value !== "object" || value === null) return undefined;
475
+ const rec = value as Record<string, unknown>;
476
+ for (const key of ["text", "summary", "delta", "content"]) {
477
+ const piece = reasoningTextValue(rec[key]);
478
+ if (piece !== undefined) return piece;
479
+ }
480
+ return undefined;
481
+ }
440
482
 
441
483
  /** First string found under a known reasoning-text key, or undefined. */
442
484
  export function readReasoningText(source: Record<string, unknown>): string | undefined {
443
485
  for (const key of REASONING_TEXT_KEYS) {
444
- const value = source[key];
445
- if (typeof value === "string") return value;
486
+ const text = reasoningTextValue(source[key]);
487
+ if (text !== undefined) return text;
446
488
  }
447
489
  // Qwen/MiniMax-family `reasoning_details[]` parts carrying text fragments.
448
- const details = source["reasoning_details"];
449
- if (Array.isArray(details)) {
450
- const text = details
451
- .map((part) =>
452
- typeof part === "string"
453
- ? part
454
- : typeof (part as Record<string, unknown>)?.["text"] === "string"
455
- ? ((part as Record<string, unknown>)["text"] as string)
456
- : "",
457
- )
458
- .join("");
459
- if (text.length > 0) return text;
460
- }
490
+ const details = reasoningTextValue(source["reasoning_details"]);
491
+ if (details !== undefined) return details;
461
492
  return undefined;
462
493
  }
463
494
 
@@ -318,6 +318,35 @@ export async function invalidateQuotaCache(
318
318
  }
319
319
  }
320
320
 
321
+ /**
322
+ * Drops many accounts from one lens in a single pass.
323
+ *
324
+ * The batch-delete route clears several accounts' quota caches at once. The
325
+ * in-process tier is swept once (one `keys()` pass), and Redis is cleared with
326
+ * one pipeline, so the cost does not scale with per-account round trips.
327
+ */
328
+ export async function invalidateQuotaCaches(
329
+ lens: string,
330
+ accountIds: readonly string[],
331
+ redis: RedisClient | undefined,
332
+ ): Promise<void> {
333
+ if (accountIds.length === 0) return;
334
+ const keys = accountIds.map((accountId) => quotaKey(lens, accountId));
335
+ const keySet = new Set(keys);
336
+ for (const key of inMemoryQuotaCache.keys()) {
337
+ if (keySet.has(key)) inMemoryQuotaCache.delete(key);
338
+ }
339
+ if (redis === undefined) return;
340
+ try {
341
+ const pipeline = redis.pipeline();
342
+ for (const key of keys) pipeline.del(key);
343
+ await pipeline.exec().catch(() => undefined);
344
+ } catch (error) {
345
+ metrics.quota_cache_invalidate_failed.inc(1);
346
+ log.warn("quota cache batch invalidate failed", { lens, error: String(error) });
347
+ }
348
+ }
349
+
321
350
  /**
322
351
  * Drops one account from EVERY cache lens. Global accounts are shared, but a
323
352
  * release-N writer may have written the same account under a per-tenant lens,
@@ -18,6 +18,10 @@ export interface ProviderQuotaWindow {
18
18
  readonly remaining?: number | null;
19
19
  /** False means the timestamp is a hard expiry rather than a recurring reset. */
20
20
  readonly recurring?: boolean;
21
+ /** Exact routing model IDs this grant serves; absent means account-wide. */
22
+ readonly modelIds?: readonly string[];
23
+ /** Activation instant: future grants remain visible but cannot fund requests. */
24
+ readonly startsAt?: string | null;
21
25
  }
22
26
 
23
27
  export interface ProviderQuotaResult {
@@ -15,8 +15,21 @@ import { fetchProviderQuota } from "./quota-support";
15
15
  import { type FetchLike, type ProviderQuotaResult } from "./quota-contracts";
16
16
  import type { ProviderId } from "../provider-contracts";
17
17
  import type { ProviderRegistry } from "../provider-registry";
18
- import { GLOBAL_QUOTA_LENS, setCachedQuota } from "./cache";
18
+ import { quotaLens, setCachedQuota } from "./cache";
19
+ import { persistQuotaSnapshot } from "./snapshot";
19
20
  import { asJsonbRecord } from "../../shared/coerce";
21
+ import { KIRO_DEFAULT_LABELS } from "../integrations/kiro/kiro-oauth";
22
+
23
+ /**
24
+ * The stock Kiro fallback labels as a SQL literal list. Built once from the
25
+ * canonical set (`KIRO_DEFAULT_LABELS`) and escaped for the single-quote SQL
26
+ * literal; the values are compile-time constants, so this fragment is static.
27
+ */
28
+ const KIRO_DEFAULT_LABEL_IN = sql.raw(
29
+ [...KIRO_DEFAULT_LABELS]
30
+ .map((label) => `'${label.replace(/'/g, "''")}'`)
31
+ .join(", "),
32
+ );
20
33
 
21
34
  /** Why an account quota refresh failed; `null` means the fetch succeeded. */
22
35
  export type QuotaRefreshFailureCode = "missing_credential" | "quota_fetch_failed";
@@ -52,7 +65,7 @@ export interface QuotaRefreshDeps {
52
65
  */
53
66
  readonly resolveCredential: (providerId: ProviderId, accountId: string) => Promise<string>;
54
67
  /** Marks a stored api_key credential with the upstream-distinguishable envelope. */
55
- readonly markApiKeyCredential?: (providerId: string, credential: string) => string;
68
+ readonly markApiKeyCredential?: (providerId: string, credential: string) => Promise<string>;
56
69
  /** Invalidates the route snapshot when a stamped figure may exclude an account. */
57
70
  readonly snapshotInvalidator?: { invalidate(): unknown } | undefined;
58
71
  }
@@ -219,7 +232,7 @@ export interface QuotaRefreshTarget {
219
232
 
220
233
  /** Cache lens an account's quota belongs under. */
221
234
  export function targetLens(target: QuotaRefreshTarget): string {
222
- return target.tenantId ?? GLOBAL_QUOTA_LENS;
235
+ return quotaLens(target.tenantId);
223
236
  }
224
237
 
225
238
  const TARGET_COLUMNS = {
@@ -352,7 +365,7 @@ async function runQuotaRefresh(
352
365
  // collector to the key-only probe, and OAuth credentials carry no key.
353
366
  const marked =
354
367
  target.credentialKind === "api_key"
355
- ? (deps.markApiKeyCredential?.(target.providerId, credential) ?? credential)
368
+ ? (await deps.markApiKeyCredential?.(target.providerId, credential) ?? credential)
356
369
  : credential;
357
370
  try {
358
371
  const authState = asJsonbRecord(target.authState);
@@ -399,6 +412,15 @@ async function runQuotaRefresh(
399
412
  log.warn(`[quota] failed to record plan for account=${target.accountId}`, error as Error);
400
413
  }
401
414
  }
415
+ // Persist the whole successful read, not just the plan. The row survives a
416
+ // restart that empties the in-process cache, so a cold console open renders
417
+ // the last known balance instead of nothing (and the sweep refreshes it). A
418
+ // failed fetch leaves the previous snapshot in place — same reasoning as the
419
+ // plan column: failure is evidence about the network/credential, not that
420
+ // the account's balance changed.
421
+ if (quota.error === null) {
422
+ await persistQuotaSnapshot(deps.db, target.accountId, quota);
423
+ }
402
424
  // A quota surface that names the account adopts the identity — but only over
403
425
  // a default label. An operator's custom rename is never overwritten, and the
404
426
  // CASE keeps the read and the write in one statement so a concurrent rename
@@ -416,6 +438,7 @@ async function runQuotaRefresh(
416
438
  label: sql`CASE
417
439
  WHEN lower(trim(${providerAccounts.label})) = lower(trim(${providerAccounts.providerId})) THEN ${quota.accountLabel}
418
440
  WHEN ${providerAccounts.label} LIKE '%|%' THEN ${quota.accountLabel}
441
+ WHEN ${providerAccounts.label} IN (${KIRO_DEFAULT_LABEL_IN}) THEN ${quota.accountLabel}
419
442
  ELSE ${providerAccounts.label}
420
443
  END`,
421
444
  })
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Persisted quota snapshots.
3
+ *
4
+ * The in-process quota cache is a short TTL and empty on a cold start, so
5
+ * right after a restart the console would render no balance for every account
6
+ * until the sweep re-reads them — while Buddy's usage snapshots survived via a
7
+ * table. This is the generic counterpart: every provider's last successful
8
+ * quota read is persisted on the account row, and the console falls back to it
9
+ * when the cache is cold. A failed fetch leaves the previous snapshot in place,
10
+ * mirroring `quota_plan`.
11
+ */
12
+ import { eq, inArray } from "drizzle-orm";
13
+ import type { CartethyiaDatabase } from "../../persistence/db-handle";
14
+ import { providerAccounts, type ProviderQuotaResultSnapshot } from "../../persistence/schema";
15
+ import type { ProviderQuotaResult } from "./quota-contracts";
16
+
17
+ /**
18
+ * Parses the `quota_snapshot` column. Postgres' jsonb arrives as an object;
19
+ * SQLite's text-backed column arrives as a JSON string — accept both.
20
+ */
21
+ function parseSnapshot(value: unknown): ProviderQuotaResultSnapshot | null {
22
+ if (value === null || value === undefined) return null;
23
+ let root: unknown = value;
24
+ if (typeof value === "string") {
25
+ try {
26
+ root = JSON.parse(value);
27
+ } catch {
28
+ return null;
29
+ }
30
+ }
31
+ if (typeof root !== "object" || root === null || Array.isArray(root)) return null;
32
+ const rec = root as Record<string, unknown>;
33
+ const { quota, fetchedAt } = rec;
34
+ if (typeof quota !== "object" || quota === null || typeof fetchedAt !== "string") return null;
35
+ return { quota: quota as ProviderQuotaResult, fetchedAt };
36
+ }
37
+
38
+ /** Stores one successful quota read on the account row. Never throws. */
39
+ export async function persistQuotaSnapshot(
40
+ db: CartethyiaDatabase,
41
+ accountId: string,
42
+ quota: ProviderQuotaResult,
43
+ fetchedAt: Date = new Date(),
44
+ ): Promise<void> {
45
+ try {
46
+ await db
47
+ .update(providerAccounts)
48
+ .set({ quotaSnapshot: { quota, fetchedAt: fetchedAt.toISOString() } })
49
+ .where(eq(providerAccounts.id, accountId));
50
+ } catch {
51
+ // A snapshot that cannot be stored is a stale display, not an error the
52
+ // caller must handle.
53
+ }
54
+ }
55
+
56
+ /** Reads one account's last successful quota snapshot, if any. */
57
+ export async function readQuotaSnapshot(
58
+ db: CartethyiaDatabase,
59
+ accountId: string,
60
+ ): Promise<ProviderQuotaResultSnapshot | null> {
61
+ try {
62
+ const rows = await db
63
+ .select({ quotaSnapshot: providerAccounts.quotaSnapshot })
64
+ .from(providerAccounts)
65
+ .where(eq(providerAccounts.id, accountId))
66
+ .limit(1);
67
+ return parseSnapshot(rows[0]?.quotaSnapshot);
68
+ } catch {
69
+ return null;
70
+ }
71
+ }
72
+
73
+ /** Batch read for one page render: one query for every account. */
74
+ export async function readQuotaSnapshots(
75
+ db: CartethyiaDatabase,
76
+ accountIds: readonly string[],
77
+ ): Promise<Map<string, ProviderQuotaResultSnapshot>> {
78
+ const found = new Map<string, ProviderQuotaResultSnapshot>();
79
+ if (accountIds.length === 0) return found;
80
+ try {
81
+ const rows = await db
82
+ .select({ id: providerAccounts.id, quotaSnapshot: providerAccounts.quotaSnapshot })
83
+ .from(providerAccounts)
84
+ .where(inArray(providerAccounts.id, [...accountIds]));
85
+ for (const row of rows) {
86
+ const snapshot = parseSnapshot(row.quotaSnapshot);
87
+ if (snapshot) found.set(row.id, snapshot);
88
+ }
89
+ } catch {
90
+ // A read failure yields a partial map; callers already treat a missing
91
+ // entry as "no snapshot".
92
+ }
93
+ return found;
94
+ }
@@ -1,5 +1,3 @@
1
- import { asRecord } from "../../shared/coerce";
2
-
3
1
  /**
4
2
  * Declarative web-search provider adapters.
5
3
  *
@@ -161,4 +159,4 @@ function jsonFromEventStream(raw: string): unknown {
161
159
 
162
160
  }
163
161
 
164
- export { asRecord, stringField };
162
+ export { stringField };
@@ -9,7 +9,8 @@
9
9
  * normalization, which is what makes a provider's wire local to one spec.
10
10
  */
11
11
  import type { SearchParams, SearchProviderSpec, SearchRequestInit } from "./search-provider";
12
- import { asRecord, stringField } from "./search-provider";
12
+ import { stringField } from "./search-provider";
13
+ import { asRecord } from "../../shared/coerce";
13
14
 
14
15
  function domainFilterParams(filter: readonly string[] | undefined): {
15
16
  includes: string[];
@@ -57,7 +57,7 @@ const BAN_ROUTE_KEY = "#ban";
57
57
  * `banThreshold` is both the identity-wide escalation threshold and the
58
58
  * highest count either ring must keep observable.
59
59
  */
60
- export interface IpAbuseAttempt {
60
+ interface IpAbuseAttempt {
61
61
  readonly identity: string;
62
62
  readonly route: string;
63
63
  readonly now: number;
@@ -67,7 +67,7 @@ export interface IpAbuseAttempt {
67
67
  }
68
68
 
69
69
  /** What one admission step decided. */
70
- export interface IpAbuseOutcome {
70
+ interface IpAbuseOutcome {
71
71
  /** The identity was already banned; nothing was incremented. */
72
72
  readonly banned: boolean;
73
73
  /** Attempts now in this route's window, including this one. `0` when banned. */
@@ -115,13 +115,13 @@ export interface IpAbuseStore {
115
115
  checkAndRecord(attempt: IpAbuseAttempt): Promise<IpAbuseOutcome>;
116
116
  }
117
117
 
118
- export interface IpAbuseProtectionServiceOptions {
118
+ interface IpAbuseProtectionServiceOptions {
119
119
  readonly maxRequestsPerWindow?: number;
120
120
  readonly banThreshold?: number;
121
121
  readonly banDurationMs?: number;
122
122
  }
123
123
 
124
- export interface InMemoryIpAbuseStoreOptions {
124
+ interface InMemoryIpAbuseStoreOptions {
125
125
  readonly windowMs?: number;
126
126
  readonly clock?: () => number;
127
127
  /** Distinct (identity, route) keys tracked before the oldest is evicted. */
@@ -12,7 +12,7 @@ import { admissionMetricLabel, reasonToGatewayError } from "./reasons";
12
12
 
13
13
  /** Persists reconciled lifetime token consumption for an API key. Injected so
14
14
  * the in-memory admission service stays decoupled from Drizzle/Postgres. */
15
- export type LifetimeUsagePersister = (input: {
15
+ type LifetimeUsagePersister = (input: {
16
16
  apiKeyId: string;
17
17
  delta: number;
18
18
  }) => Promise<void>;
@@ -23,7 +23,7 @@ export type LifetimeUsagePersister = (input: {
23
23
  * counter store only when it is about to seed a missing lifetime counter, so
24
24
  * the ≤3s-stale snapshot value cannot freeze a low baseline in place. Injected
25
25
  * so the admission module stays decoupled from Drizzle/Postgres. */
26
- export type FreshLifetimeConsumedReader = (apiKeyId: string) => Promise<number | undefined>;
26
+ type FreshLifetimeConsumedReader = (apiKeyId: string) => Promise<number | undefined>;
27
27
 
28
28
 
29
29
  export class ApiKeyAdmissionService {
@@ -164,13 +164,20 @@ export class ApiKeyAdmissionService {
164
164
  const actual = usageTokens(usage);
165
165
  await this.store.reconcile(apiKeyId, estimatedTokens, actual, reservationId);
166
166
  finalized = true;
167
- // Persist against the authenticating key (`snapshot.api_key_id`), not
168
- // the admission identity. Share children share the parent's counter
169
- // namespace for enforcement, but attribution must still land on the
170
- // child row so recipient usage and sumChildrenConsumed stay correct.
167
+ // Persist the spend against BOTH the authenticating key and the family
168
+ // owner. The authenticating key (`snapshot.api_key_id`, the child for
169
+ // a share recipient) carries attribution so `sumChildrenConsumed` and
170
+ // recipient rows stay correct. The family owner (`admission_identity`,
171
+ // the template itself for a share child) is the live accumulator the
172
+ // console reads for total usage — without it the template column never
173
+ // moved and every figure depended on telemetry rows alone.
171
174
  if (this.persistLifetimeUsage) {
175
+ const ownerId = snapshot.admission_identity ?? snapshot.api_key_id;
172
176
  try {
173
177
  await this.persistLifetimeUsage({ apiKeyId: snapshot.api_key_id, delta: actual });
178
+ if (ownerId !== snapshot.api_key_id) {
179
+ await this.persistLifetimeUsage({ apiKeyId: ownerId, delta: actual });
180
+ }
174
181
  } catch {
175
182
  // Non-fatal: the transient counter already reflects reality; a
176
183
  // subsequent request will overwrite the row with the fresh count.
@@ -1,61 +1,61 @@
1
- /**
2
- * Lease and counter lifetimes, and the calendar bucket keys they key on.
3
- *
4
- * These live together because they are one arithmetic problem: a counter
5
- * must outlive the lease that charges it, and a lease key must outlive the
6
- * horizon the sweeper reads. Splitting them across files is how those
7
- * invariants silently break, so the reasoning stays in one place.
8
- */
9
- export const LEASE_TTL_MS = 3600_000;
10
-
11
- /**
12
- * The lease key's own Redis TTL is deliberately LONGER than the `expires_at`
13
- * horizon the lease hash records.
14
- *
15
- * `expires_at` is the moment the sweeper becomes allowed to reclaim the lease.
16
- * If the key TTL equalled that horizon, Redis would evict the hash at exactly
17
- * the moment `reapIfExpired` wanted to read it: `HGET state` would return nil,
18
- * the sweep would skip it, and a crash between reserve and release would strand
19
- * the held concurrency / tenant-concurrency slot for as long as the counter
20
- * keeps being refreshed — every later reserve re-arms the counter's own TTL, so
21
- * a key still in use never recovers its leaked slot. The grace window keeps the
22
- * hash readable for several sweep intervals past the horizon, so the recovery
23
- * path actually runs.
24
- *
25
- * Exported so the invariant is asserted directly: a lease key must outlive the
26
- * horizon it records by more than one sweep interval (`sweepLeases` runs every
27
- * 30s, wired in `runtime/dependencies.ts`).
28
- */
29
- const LEASE_TTL_GRACE_SECONDS = 120;
30
- /** Seconds after which a lease hash is reapable (`expires_at` horizon). */
31
- export const LEASE_REAP_HORIZON_SECONDS = Math.ceil(LEASE_TTL_MS / 1000);
32
- /** Seconds the lease hash itself lives; must exceed the reap horizon. */
33
- export const LEASE_KEY_TTL_SECONDS = LEASE_REAP_HORIZON_SECONDS + LEASE_TTL_GRACE_SECONDS;
34
-
35
- /**
36
- * TTLs for the token-budget counters, in seconds.
37
- *
38
- * Daily lives just past two days (a day bucket plus slack to cover a request
39
- * straddling midnight); monthly and lifetime share one 35-day window — long
40
- * enough that a month bucket is never evicted mid-month, and it is the *only*
41
- * thing bounding `admission:lifetime:*`, which has no natural bucket to expire
42
- * on. Every writer below must therefore carry its own TTL: a plain `SET`
43
- * without one creates a key that outlives every bucket it feeds.
44
- */
45
- export const DAILY_COUNTER_TTL_SECONDS = 172800;
46
- export const MONTHLY_COUNTER_TTL_SECONDS = 3024000;
47
- export const LIFETIME_COUNTER_TTL_SECONDS = 3024000;
48
- /** RPM window plus slack; concurrency slots are held for the request duration. */
49
- export const RPM_WINDOW_TTL_SECONDS = 65;
50
- // Must exceed the lease's own lifetime (`LEASE_TTL_MS` + grace), otherwise a
51
- // request held longer than this lets its concurrency counter expire underneath
52
- // a still-live lease and a later reserve INCRs from zero. Derived rather than
53
- // hard-coded so raising the lease TTL cannot silently reopen the hole.
54
- export const CONCURRENCY_TTL_SECONDS = LEASE_KEY_TTL_SECONDS;
55
-
56
- export function dailyBucket(now: number): string {
57
- return new Date(now).toISOString().slice(0, 10);
58
- }
59
- export function monthlyBucket(now: number): string {
60
- return new Date(now).toISOString().slice(0, 7);
61
- }
1
+ /**
2
+ * Lease and counter lifetimes, and the calendar bucket keys they key on.
3
+ *
4
+ * These live together because they are one arithmetic problem: a counter
5
+ * must outlive the lease that charges it, and a lease key must outlive the
6
+ * horizon the sweeper reads. Splitting them across files is how those
7
+ * invariants silently break, so the reasoning stays in one place.
8
+ */
9
+ export const LEASE_TTL_MS = 3600_000;
10
+
11
+ /**
12
+ * The lease key's own Redis TTL is deliberately LONGER than the `expires_at`
13
+ * horizon the lease hash records.
14
+ *
15
+ * `expires_at` is the moment the sweeper becomes allowed to reclaim the lease.
16
+ * If the key TTL equalled that horizon, Redis would evict the hash at exactly
17
+ * the moment `reapIfExpired` wanted to read it: `HGET state` would return nil,
18
+ * the sweep would skip it, and a crash between reserve and release would strand
19
+ * the held concurrency / tenant-concurrency slot for as long as the counter
20
+ * keeps being refreshed — every later reserve re-arms the counter's own TTL, so
21
+ * a key still in use never recovers its leaked slot. The grace window keeps the
22
+ * hash readable for several sweep intervals past the horizon, so the recovery
23
+ * path actually runs.
24
+ *
25
+ * Exported so the invariant is asserted directly: a lease key must outlive the
26
+ * horizon it records by more than one sweep interval (`sweepLeases` runs every
27
+ * 30s, wired in `runtime/dependencies.ts`).
28
+ */
29
+ const LEASE_TTL_GRACE_SECONDS = 120;
30
+ /** Seconds after which a lease hash is reapable (`expires_at` horizon). */
31
+ export const LEASE_REAP_HORIZON_SECONDS = Math.ceil(LEASE_TTL_MS / 1000);
32
+ /** Seconds the lease hash itself lives; must exceed the reap horizon. */
33
+ export const LEASE_KEY_TTL_SECONDS = LEASE_REAP_HORIZON_SECONDS + LEASE_TTL_GRACE_SECONDS;
34
+
35
+ /**
36
+ * TTLs for the token-budget counters, in seconds.
37
+ *
38
+ * Daily lives just past two days (a day bucket plus slack to cover a request
39
+ * straddling midnight); monthly and lifetime share one 35-day window — long
40
+ * enough that a month bucket is never evicted mid-month, and it is the *only*
41
+ * thing bounding `admission:lifetime:*`, which has no natural bucket to expire
42
+ * on. Every writer below must therefore carry its own TTL: a plain `SET`
43
+ * without one creates a key that outlives every bucket it feeds.
44
+ */
45
+ export const DAILY_COUNTER_TTL_SECONDS = 172800;
46
+ export const MONTHLY_COUNTER_TTL_SECONDS = 3024000;
47
+ export const LIFETIME_COUNTER_TTL_SECONDS = 3024000;
48
+ /** RPM window plus slack; concurrency slots are held for the request duration. */
49
+ export const RPM_WINDOW_TTL_SECONDS = 65;
50
+ // Must exceed the lease's own lifetime (`LEASE_TTL_MS` + grace), otherwise a
51
+ // request held longer than this lets its concurrency counter expire underneath
52
+ // a still-live lease and a later reserve INCRs from zero. Derived rather than
53
+ // hard-coded so raising the lease TTL cannot silently reopen the hole.
54
+ export const CONCURRENCY_TTL_SECONDS = LEASE_KEY_TTL_SECONDS;
55
+
56
+ export function dailyBucket(now: number): string {
57
+ return new Date(now).toISOString().slice(0, 10);
58
+ }
59
+ export function monthlyBucket(now: number): string {
60
+ return new Date(now).toISOString().slice(0, 7);
61
+ }