cartethyia 2.5.9 → 2.6.0

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 (386) hide show
  1. package/.env.example +15 -42
  2. package/README.md +11 -10
  3. package/dist/dashboard/assets/{BannedPage-Brvuj_Eb.js → BannedPage-l6XMnfr9.js} +1 -1
  4. package/dist/dashboard/assets/BannedPage-l6XMnfr9.js.gz +0 -0
  5. package/dist/dashboard/assets/{CliToolDetailPage-CqfXtI3y.js → CliToolDetailPage-DwOzAW45.js} +1 -1
  6. package/dist/dashboard/assets/CliToolDetailPage-DwOzAW45.js.gz +0 -0
  7. package/dist/dashboard/assets/{CliToolsPage-GQeS91bg.js → CliToolsPage-BT3Up3QA.js} +1 -1
  8. package/dist/dashboard/assets/CliToolsPage-BT3Up3QA.js.gz +0 -0
  9. package/dist/dashboard/assets/{CombosPage-BDRsTXBB.js → CombosPage-6kYIUpzR.js} +1 -1
  10. package/dist/dashboard/assets/CombosPage-6kYIUpzR.js.gz +0 -0
  11. package/dist/dashboard/assets/{ConsoleLogPage-zP8AFHX0.js → ConsoleLogPage-zRnj1mlR.js} +1 -1
  12. package/dist/dashboard/assets/ConsoleLogPage-zRnj1mlR.js.gz +0 -0
  13. package/dist/dashboard/assets/{CustomizationPage-BmfrNZ14.js → CustomizationPage-C7m-H1Rz.js} +1 -1
  14. package/dist/dashboard/assets/CustomizationPage-C7m-H1Rz.js.gz +0 -0
  15. package/dist/dashboard/assets/HealthEventsModal-C8MR_BdB.js +1 -0
  16. package/dist/dashboard/assets/HealthEventsModal-C8MR_BdB.js.gz +0 -0
  17. package/dist/dashboard/assets/{LoginPage-DoreIgm8.js → LoginPage-CbphLvjg.js} +1 -1
  18. package/dist/dashboard/assets/LoginPage-CbphLvjg.js.gz +0 -0
  19. package/dist/dashboard/assets/{ModelPicker-C7pDb41R.js → ModelPicker-DVny4S5_.js} +1 -1
  20. package/dist/dashboard/assets/ModelPicker-DVny4S5_.js.gz +0 -0
  21. package/dist/dashboard/assets/OverviewPage-DABu2Wgt.js +1 -0
  22. package/dist/dashboard/assets/OverviewPage-DABu2Wgt.js.gz +0 -0
  23. package/dist/dashboard/assets/ProviderDetailPage-C15x9q13.js +3 -0
  24. package/dist/dashboard/assets/ProviderDetailPage-C15x9q13.js.gz +0 -0
  25. package/dist/dashboard/assets/{ProvidersPage-BnoPgA7w.js → ProvidersPage-Dfb2896L.js} +1 -1
  26. package/dist/dashboard/assets/ProvidersPage-Dfb2896L.js.gz +0 -0
  27. package/dist/dashboard/assets/{ProxyPage-Bq_TGgdb.js → ProxyPage-BaCIdAUt.js} +1 -1
  28. package/dist/dashboard/assets/ProxyPage-BaCIdAUt.js.gz +0 -0
  29. package/dist/dashboard/assets/QuotaPage-BsdoCIw1.js +1 -0
  30. package/dist/dashboard/assets/QuotaPage-BsdoCIw1.js.gz +0 -0
  31. package/dist/dashboard/assets/SettingsPage-KVJFh3Ip.js +1 -0
  32. package/dist/dashboard/assets/SettingsPage-KVJFh3Ip.js.gz +0 -0
  33. package/dist/dashboard/assets/{SetupPage-Day3mCOn.js → SetupPage-D4g6vOGc.js} +1 -1
  34. package/dist/dashboard/assets/SetupPage-D4g6vOGc.js.gz +0 -0
  35. package/dist/dashboard/assets/{SortableList-DQxllnB5.js → SortableList-C93i14BD.js} +1 -1
  36. package/dist/dashboard/assets/SortableList-C93i14BD.js.gz +0 -0
  37. package/dist/dashboard/assets/{StudioPage-uMExoWu1.js → StudioPage-SgQpymjm.js} +1 -1
  38. package/dist/dashboard/assets/{StudioPage-uMExoWu1.js.gz → StudioPage-SgQpymjm.js.gz} +0 -0
  39. package/dist/dashboard/assets/{UsagePage-DXa1oR-4.js → UsagePage-BFME-eba.js} +2 -2
  40. package/dist/dashboard/assets/UsagePage-BFME-eba.js.gz +0 -0
  41. package/dist/dashboard/assets/{api-keys-DQYLQxpW.js → api-keys-C23S9sgp.js} +1 -1
  42. package/dist/dashboard/assets/api-keys-C23S9sgp.js.gz +0 -0
  43. package/dist/dashboard/assets/{app-BIt446yE.js → app-2KhtHCv9.js} +1 -1
  44. package/dist/dashboard/assets/{app-BIt446yE.js.gz → app-2KhtHCv9.js.gz} +0 -0
  45. package/dist/dashboard/assets/app-CEeaMHOy.css +2 -0
  46. package/dist/dashboard/assets/app-CEeaMHOy.css.gz +0 -0
  47. package/dist/dashboard/assets/{assert-C9FD_JwJ.js → assert-DU6qrMA_.js} +1 -1
  48. package/dist/dashboard/assets/assert-DU6qrMA_.js.gz +0 -0
  49. package/dist/dashboard/assets/{badge-CR72dasT.js → badge-CKsF123d.js} +1 -1
  50. package/dist/dashboard/assets/badge-CKsF123d.js.gz +0 -0
  51. package/dist/dashboard/assets/{banner-CI9_eSfO.js → banner-8j7uChA9.js} +1 -1
  52. package/dist/dashboard/assets/banner-8j7uChA9.js.gz +0 -0
  53. package/dist/dashboard/assets/{card-DS7CQEsO.js → card-CdsSlWA3.js} +1 -1
  54. package/dist/dashboard/assets/card-CdsSlWA3.js.gz +0 -0
  55. package/dist/dashboard/assets/{cli-tools-CNqzrfdb.js → cli-tools-BaFUuFFF.js} +1 -1
  56. package/dist/dashboard/assets/cli-tools-BaFUuFFF.js.gz +0 -0
  57. package/dist/dashboard/assets/{clipboard-button-611pSsaX.js → clipboard-button-uL5aYLfW.js} +1 -1
  58. package/dist/dashboard/assets/clipboard-button-uL5aYLfW.js.gz +0 -0
  59. package/dist/dashboard/assets/{dialog-k37exjNt.js → dialog-C07hr5_G.js} +1 -1
  60. package/dist/dashboard/assets/dialog-C07hr5_G.js.gz +0 -0
  61. package/dist/dashboard/assets/{drawer-CX7N8ucR.js → drawer-BPO1TcKH.js} +1 -1
  62. package/dist/dashboard/assets/drawer-BPO1TcKH.js.gz +0 -0
  63. package/dist/dashboard/assets/{entry-CTP8AyiS.js → entry-Bz_CtDfC.js} +1 -1
  64. package/dist/dashboard/assets/entry-Bz_CtDfC.js.gz +0 -0
  65. package/dist/dashboard/assets/entry-CwPRKk0d.js +4 -0
  66. package/dist/dashboard/assets/entry-CwPRKk0d.js.gz +0 -0
  67. package/dist/dashboard/assets/entry-GJRbmrpA.js +1 -0
  68. package/dist/dashboard/assets/entry-GJRbmrpA.js.gz +0 -0
  69. package/dist/dashboard/assets/{github-badge-sltk1qUi.js → github-badge-DmQreSDd.js} +1 -1
  70. package/dist/dashboard/assets/github-badge-DmQreSDd.js.gz +0 -0
  71. package/dist/dashboard/assets/{index-z3zDOSMM.js → index-Bblu6erR.js} +2 -2
  72. package/dist/dashboard/assets/index-Bblu6erR.js.gz +0 -0
  73. package/dist/dashboard/assets/{inline-BhY2sSnO.js → inline-C9dCYaBZ.js} +1 -1
  74. package/dist/dashboard/assets/inline-C9dCYaBZ.js.gz +0 -0
  75. package/dist/dashboard/assets/{input-V1umFiBk.js → input-CjCnXweJ.js} +1 -1
  76. package/dist/dashboard/assets/input-CjCnXweJ.js.gz +0 -0
  77. package/dist/dashboard/assets/{layout-BBOYycyS.js → layout-V6y8DAkl.js} +1 -1
  78. package/dist/dashboard/assets/layout-V6y8DAkl.js.gz +0 -0
  79. package/dist/dashboard/assets/{list-row-1zzZYM0B.js → list-row-CM1AaG_y.js} +1 -1
  80. package/dist/dashboard/assets/list-row-CM1AaG_y.js.gz +0 -0
  81. package/dist/dashboard/assets/{live-BivGfuUs.js → live-ta6GMb-8.js} +1 -1
  82. package/dist/dashboard/assets/live-ta6GMb-8.js.gz +0 -0
  83. package/dist/dashboard/assets/{model-bans-CmffjLuG.js → model-bans-BmYTmbAk.js} +1 -1
  84. package/dist/dashboard/assets/model-bans-BmYTmbAk.js.gz +0 -0
  85. package/dist/dashboard/assets/{mutation-46P0oWMe.js → mutation-g_fduiNw.js} +1 -1
  86. package/dist/dashboard/assets/mutation-g_fduiNw.js.gz +0 -0
  87. package/dist/dashboard/assets/{numbers-DkfMR5Qs.js → numbers-BOevJzdz.js} +1 -1
  88. package/dist/dashboard/assets/numbers-BOevJzdz.js.gz +0 -0
  89. package/dist/dashboard/assets/{query-keys-BFhGpwR1.js → query-keys-UETqfytp.js} +1 -1
  90. package/dist/dashboard/assets/query-keys-UETqfytp.js.gz +0 -0
  91. package/dist/dashboard/assets/{routing-CAb4F37R.js → routing-BPUo-W8d.js} +1 -1
  92. package/dist/dashboard/assets/routing-BPUo-W8d.js.gz +0 -0
  93. package/dist/dashboard/assets/{select-C6MMiaio.js → select-C980c4cz.js} +1 -1
  94. package/dist/dashboard/assets/select-C980c4cz.js.gz +0 -0
  95. package/dist/dashboard/assets/{settings-DNX9sydi.js → settings--3hxSyry.js} +1 -1
  96. package/dist/dashboard/assets/settings--3hxSyry.js.gz +0 -0
  97. package/dist/dashboard/assets/{stack-BHTfXdz_.js → stack-_gfTdLVB.js} +1 -1
  98. package/dist/dashboard/assets/stack-_gfTdLVB.js.gz +0 -0
  99. package/dist/dashboard/assets/{state-mPDRlQkl.js → state-2c5V4zo7.js} +1 -1
  100. package/dist/dashboard/assets/state-2c5V4zo7.js.gz +0 -0
  101. package/dist/dashboard/assets/{switch-DOmJxg7H.js → switch-Dkke7Naf.js} +1 -1
  102. package/dist/dashboard/assets/switch-Dkke7Naf.js.gz +0 -0
  103. package/dist/dashboard/assets/{useMutation-Dem0lXQj.js → useMutation-EDFRkn1i.js} +1 -1
  104. package/dist/dashboard/assets/useMutation-EDFRkn1i.js.gz +0 -0
  105. package/dist/dashboard/index.html +1 -1
  106. package/dist/dashboard/index.html.gz +0 -0
  107. package/dist/dashboard/precompressed-assets.json +51 -51
  108. package/migrations/README.md +52 -52
  109. package/migrations/full-postgres/0020_add_telemetry_event_analytics.sql +77 -0
  110. package/migrations/full-postgres/0021_add_api_key_concurrency_tolerance.sql +13 -0
  111. package/migrations/full-postgres/0022_add_inferhub_config_key.sql +17 -0
  112. package/migrations/full-postgres/0023_drop_api_key_concurrency_tolerance.sql +13 -0
  113. package/migrations/lite-sqlite3/0020_add_telemetry_event_analytics.sql +69 -0
  114. package/migrations/lite-sqlite3/0021_add_api_key_concurrency_tolerance.sql +13 -0
  115. package/migrations/lite-sqlite3/0022_add_inferhub_config_key.sql +17 -0
  116. package/migrations/lite-sqlite3/0023_drop_api_key_concurrency_tolerance.sql +13 -0
  117. package/package.json +96 -97
  118. package/scripts/cli/admin.ts +126 -125
  119. package/scripts/cli/autostart.ts +237 -237
  120. package/scripts/cli/background.ts +120 -110
  121. package/scripts/cli/bin/carte.cjs +8 -8
  122. package/scripts/cli/bin/cartethyia.cjs +167 -167
  123. package/scripts/cli/console-menu.ts +308 -275
  124. package/scripts/cli/controller.ts +145 -0
  125. package/scripts/cli/doctor.ts +1 -1
  126. package/scripts/cli/env.ts +99 -99
  127. package/scripts/cli/gateway.ts +264 -261
  128. package/scripts/{health.ts → cli/health.ts} +4 -4
  129. package/scripts/cli/instance.ts +234 -224
  130. package/scripts/cli/main.ts +188 -164
  131. package/scripts/cli/platform.ts +64 -64
  132. package/scripts/cli/prompt.ts +275 -107
  133. package/scripts/cli/services.ts +88 -88
  134. package/scripts/cli/setup.ts +445 -445
  135. package/scripts/cli/start.ts +85 -82
  136. package/scripts/cli/startup-signal.ts +23 -23
  137. package/scripts/cli/telemetry.ts +70 -0
  138. package/scripts/cli/tray/index.ts +263 -263
  139. package/scripts/cli/tray/windows.ps1 +107 -107
  140. package/scripts/cli/tray.ts +192 -208
  141. package/scripts/cli/update.ts +39 -39
  142. package/scripts/cli/version.ts +67 -67
  143. package/scripts/cli/wordmark.ts +58 -58
  144. package/src/boot/app.ts +16 -1
  145. package/src/boot/dependencies.ts +128 -25
  146. package/src/config/package-paths.ts +24 -0
  147. package/src/config/resources.ts +137 -0
  148. package/src/config/runtime.ts +133 -76
  149. package/src/config/spec.ts +30 -29
  150. package/src/config/telemetry.ts +0 -25
  151. package/src/console/api-keys/bucket-spend.ts +14 -17
  152. package/src/console/api-keys/contracts.ts +410 -410
  153. package/src/console/api-keys/routes.ts +12 -0
  154. package/src/console/audit/store.ts +2 -3
  155. package/src/console/auth/setup-service.ts +8 -1
  156. package/src/console/backup/routes.ts +7 -0
  157. package/src/console/backup/service.ts +463 -463
  158. package/src/console/backup/store.ts +16 -11
  159. package/src/console/backup/validate.ts +1 -4
  160. package/src/console/cli-tools/fs-ops.ts +18 -2
  161. package/src/console/data-management/contracts.ts +1 -8
  162. package/src/console/data-management/operations.ts +61 -48
  163. package/src/console/data-management/routes.ts +4 -2
  164. package/src/console/domain-registration.ts +25 -3
  165. package/src/console/inferhub/client.ts +139 -0
  166. package/src/console/inferhub/key-store.ts +129 -0
  167. package/src/console/inferhub/normalize.ts +347 -0
  168. package/src/console/inferhub/routes.ts +411 -0
  169. package/src/console/inferhub/store.ts +109 -0
  170. package/src/console/inferhub/types.ts +183 -0
  171. package/src/console/observability/contracts.ts +2 -41
  172. package/src/console/observability/store.ts +176 -371
  173. package/src/console/providers/catalog/public-model-store.ts +0 -0
  174. package/src/console/providers/catalog/store.ts +23 -36
  175. package/src/console/quota/account-quota-shared.ts +52 -52
  176. package/src/console/quota/account-quota-tenant.ts +16 -24
  177. package/src/console/quota/refresh.ts +441 -441
  178. package/src/console/remote-support/service.ts +270 -114
  179. package/src/console/routing/pools/import-parse.ts +206 -206
  180. package/src/console/routing/pools/operations.ts +543 -543
  181. package/src/console/routing/pools/probe-verdict.ts +132 -132
  182. package/src/console/routing/pools/routes.ts +181 -181
  183. package/src/console/routing/pools/store.ts +1 -1
  184. package/src/console/settings/contracts.ts +42 -40
  185. package/src/console/settings/store.ts +25 -25
  186. package/src/console/share/share-stats.ts +22 -4
  187. package/src/console/share/share-usage.ts +30 -4
  188. package/src/console/shared/cursor.ts +15 -0
  189. package/src/console/updates/repository-updates.ts +40 -39
  190. package/src/console/updates/routes.ts +12 -0
  191. package/src/main.ts +13 -5
  192. package/src/network/pool/resolver.ts +1104 -1104
  193. package/src/observability/client-name.ts +22 -0
  194. package/src/observability/event-log.ts +5 -29
  195. package/src/observability/event-query.ts +690 -0
  196. package/src/observability/log-ring.ts +26 -2
  197. package/src/observability/log-sanitize.ts +118 -0
  198. package/src/observability/logger.ts +17 -13
  199. package/src/observability/metrics.ts +35 -0
  200. package/src/observability/payload-capture.ts +114 -41
  201. package/src/observability/payload-store.ts +249 -24
  202. package/src/observability/raw-event-writer.ts +118 -0
  203. package/src/observability/runtime-metrics.ts +71 -1
  204. package/src/observability/telemetry-buffer.ts +100 -17
  205. package/src/observability/telemetry-reconciliation.ts +417 -0
  206. package/src/persistence/boot.ts +20 -9
  207. package/src/persistence/db-pg.ts +13 -0
  208. package/src/persistence/db-sqlite.ts +569 -497
  209. package/src/persistence/dialect.ts +3 -2
  210. package/src/persistence/migrate.ts +181 -181
  211. package/src/persistence/schema.ts +1496 -1406
  212. package/src/persistence/shared-cache.ts +5 -1
  213. package/src/persistence/stores/api-key-store.ts +581 -581
  214. package/src/persistence/stores/telemetry-store.ts +124 -63
  215. package/src/protocol/reasoning-text.ts +30 -0
  216. package/src/protocol/registry.ts +2 -0
  217. package/src/protocol/response/chat.ts +33 -7
  218. package/src/protocol/response/codex.ts +14 -2
  219. package/src/protocol/response/messages.ts +5 -2
  220. package/src/providers/authentication/oauth-flow-store.ts +3 -10
  221. package/src/providers/bundled-discovery-providers.ts +122 -122
  222. package/src/providers/default-registry.ts +14 -0
  223. package/src/providers/discovery/catalog/catalog-blob.ts +18 -18
  224. package/src/providers/discovery/models-dev-catalog.ts +354 -354
  225. package/src/providers/discovery/probe-phases.ts +43 -7
  226. package/src/providers/discovery/probing-service.ts +70 -32
  227. package/src/providers/integrations/antigravity/antigravity.ts +9 -2
  228. package/src/providers/integrations/buddy/buddy-catalog-shared.ts +85 -17
  229. package/src/providers/integrations/buddy/buddy-chat-shared.ts +934 -934
  230. package/src/providers/integrations/buddy/buddy-checkin.ts +5 -9
  231. package/src/providers/integrations/buddy/buddy-discovery-shared.ts +3 -7
  232. package/src/providers/integrations/buddy/buddy-quota-shared.ts +3 -1
  233. package/src/providers/integrations/buddy/capabilities.ts +4 -0
  234. package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +57 -49
  235. package/src/providers/integrations/buddy/codebuddy/provider.ts +180 -180
  236. package/src/providers/integrations/buddy/workbuddy/provider.ts +206 -206
  237. package/src/providers/integrations/buddy/workbuddy/quota.ts +161 -161
  238. package/src/providers/integrations/claude/claude-quota.ts +2 -2
  239. package/src/providers/integrations/claude/claude-user-profiles.ts +3 -2
  240. package/src/providers/integrations/claude/claude.ts +3 -4
  241. package/src/providers/integrations/cline/cline.ts +94 -8
  242. package/src/providers/integrations/codex/codex.ts +61 -37
  243. package/src/providers/integrations/cursor/cursor-catalog.ts +368 -368
  244. package/src/providers/integrations/cursor/cursor-http2.ts +300 -300
  245. package/src/providers/integrations/cursor/cursor-identity.ts +2 -4
  246. package/src/providers/integrations/cursor/cursor-request.ts +11 -0
  247. package/src/providers/integrations/cursor/cursor.ts +29 -17
  248. package/src/providers/integrations/cursor/protocol/framing.ts +129 -129
  249. package/src/providers/integrations/cursor/protocol/messages.ts +2380 -2380
  250. package/src/providers/integrations/cursor/protocol/struct.ts +305 -305
  251. package/src/providers/integrations/cursor/protocol/tool-canonical.ts +275 -0
  252. package/src/providers/integrations/github/github.ts +3 -1
  253. package/src/providers/integrations/grok/grok-quota.ts +279 -279
  254. package/src/providers/integrations/grok/grok-ratelimit-store.ts +73 -73
  255. package/src/providers/integrations/grok/grok-ratelimit.ts +218 -218
  256. package/src/providers/integrations/hermes.ts +7 -1
  257. package/src/providers/integrations/inferhub.ts +201 -73
  258. package/src/providers/integrations/kilo/kilo.ts +152 -152
  259. package/src/providers/integrations/kiro/kiro-machine-id.ts +2 -4
  260. package/src/providers/integrations/opencode/provider.ts +2 -0
  261. package/src/providers/integrations/openrouter/provider.ts +129 -129
  262. package/src/providers/integrations/qoder/qoder-quota.ts +193 -192
  263. package/src/providers/integrations/qoder/qoder-realm.ts +106 -106
  264. package/src/providers/integrations/trae/trae-headers.ts +17 -48
  265. package/src/providers/integrations/trae/trae-oauth.ts +4 -26
  266. package/src/providers/integrations/trae/trae-quota.ts +389 -384
  267. package/src/providers/integrations/trae/trae-realm.ts +15 -2
  268. package/src/providers/integrations/trae/trae.ts +22 -24
  269. package/src/providers/integrations/xiaomi-mimo/think-stream.ts +100 -24
  270. package/src/providers/integrations/zcode/oauth.ts +1 -1
  271. package/src/providers/operations/account-health-service.ts +605 -279
  272. package/src/providers/provider-metadata.ts +12 -1
  273. package/src/providers/provider-model-definition.ts +373 -373
  274. package/src/providers/provider-probe.ts +3 -0
  275. package/src/providers/quota/quota-contracts.ts +3 -7
  276. package/src/providers/search/search-provider.ts +2 -6
  277. package/src/security/admission/contracts.ts +10 -0
  278. package/src/security/admission/in-memory-store.ts +89 -14
  279. package/src/security/admission/lua.ts +191 -182
  280. package/src/security/admission/redis-store.ts +30 -0
  281. package/src/security/admission/service.ts +18 -0
  282. package/src/security/api-key-auth-cache.ts +96 -0
  283. package/src/security/api-key-auth.ts +11 -100
  284. package/src/security/client-router-fingerprint.ts +3 -7
  285. package/src/security/ip-boundary.ts +7 -3
  286. package/src/shared/coerce.ts +67 -0
  287. package/src/shared/hash.ts +6 -0
  288. package/src/shared/time.ts +7 -0
  289. package/src/transport/dispatch/attempt-finalize.ts +26 -138
  290. package/src/transport/dispatch/attempt-loop.ts +103 -81
  291. package/src/transport/dispatch/capture-policy.ts +78 -0
  292. package/src/transport/dispatch/proxy-request.ts +4 -2
  293. package/src/transport/dispatch/streaming/encoder.ts +111 -5
  294. package/src/transport/dispatch/streaming/stream.ts +19 -2
  295. package/src/transport/dispatch/streaming/thinking-loop.ts +300 -0
  296. package/src/transport/failure-policy.ts +27 -0
  297. package/src/transport/guards/abuse-guard.ts +2 -2
  298. package/src/transport/guards/auth-guard.ts +17 -0
  299. package/src/transport/guards/body-policy.ts +3 -2
  300. package/src/transport/guards/concurrency-guard.ts +89 -13
  301. package/src/transport/guards/early-rejection-capture.ts +43 -3
  302. package/src/transport/guards/error-lifecycle.ts +10 -2
  303. package/src/transport/guards/guard-errors.ts +23 -0
  304. package/src/transport/guards/headroom-guard.ts +41 -7
  305. package/src/transport/guards/pipeline.ts +24 -0
  306. package/src/transport/guards/request-context.ts +2 -1
  307. package/src/transport/routing/admission.ts +51 -9
  308. package/src/transport/routing/bounded-lru.ts +49 -0
  309. package/src/transport/routing/candidate-index.ts +88 -0
  310. package/src/transport/routing/reservations.ts +121 -7
  311. package/src/transport/routing/round-robin.ts +10 -22
  312. package/src/transport/routing/route-catalog.ts +26 -13
  313. package/src/transport/routing/route-model.ts +22 -2
  314. package/src/transport/routing/router.ts +53 -31
  315. package/src/transport/surface/chat/encode.ts +2 -3
  316. package/src/transport/surface/chat/parse.ts +2 -1
  317. package/src/transport/surface/dialects.ts +11 -3
  318. package/src/transport/surface/messages/encode.ts +2 -1
  319. package/src/transport/surface/messages/parse.ts +3 -1
  320. package/src/transport/surface/messages/stream.ts +2 -6
  321. package/src/workers/maintenance-lease.ts +200 -0
  322. package/src/workers/scheduler.ts +101 -1
  323. package/dist/dashboard/assets/BannedPage-Brvuj_Eb.js.gz +0 -0
  324. package/dist/dashboard/assets/CliToolDetailPage-CqfXtI3y.js.gz +0 -0
  325. package/dist/dashboard/assets/CliToolsPage-GQeS91bg.js.gz +0 -0
  326. package/dist/dashboard/assets/CombosPage-BDRsTXBB.js.gz +0 -0
  327. package/dist/dashboard/assets/ConsoleLogPage-zP8AFHX0.js.gz +0 -0
  328. package/dist/dashboard/assets/CustomizationPage-BmfrNZ14.js.gz +0 -0
  329. package/dist/dashboard/assets/HealthEventsModal-Dn3M6WZc.js +0 -1
  330. package/dist/dashboard/assets/HealthEventsModal-Dn3M6WZc.js.gz +0 -0
  331. package/dist/dashboard/assets/LoginPage-DoreIgm8.js.gz +0 -0
  332. package/dist/dashboard/assets/ModelPicker-C7pDb41R.js.gz +0 -0
  333. package/dist/dashboard/assets/OverviewPage-CvvD1Ugk.js +0 -1
  334. package/dist/dashboard/assets/OverviewPage-CvvD1Ugk.js.gz +0 -0
  335. package/dist/dashboard/assets/ProviderDetailPage-Clbn1gsW.js +0 -3
  336. package/dist/dashboard/assets/ProviderDetailPage-Clbn1gsW.js.gz +0 -0
  337. package/dist/dashboard/assets/ProvidersPage-BnoPgA7w.js.gz +0 -0
  338. package/dist/dashboard/assets/ProxyPage-Bq_TGgdb.js.gz +0 -0
  339. package/dist/dashboard/assets/QuotaPage-D9DHXvom.js +0 -1
  340. package/dist/dashboard/assets/QuotaPage-D9DHXvom.js.gz +0 -0
  341. package/dist/dashboard/assets/SettingsPage-7xV8WkWL.js +0 -1
  342. package/dist/dashboard/assets/SettingsPage-7xV8WkWL.js.gz +0 -0
  343. package/dist/dashboard/assets/SetupPage-Day3mCOn.js.gz +0 -0
  344. package/dist/dashboard/assets/SortableList-DQxllnB5.js.gz +0 -0
  345. package/dist/dashboard/assets/UsagePage-DXa1oR-4.js.gz +0 -0
  346. package/dist/dashboard/assets/api-keys-DQYLQxpW.js.gz +0 -0
  347. package/dist/dashboard/assets/app-CX_Z8cMs.css +0 -2
  348. package/dist/dashboard/assets/app-CX_Z8cMs.css.gz +0 -0
  349. package/dist/dashboard/assets/assert-C9FD_JwJ.js.gz +0 -0
  350. package/dist/dashboard/assets/badge-CR72dasT.js.gz +0 -0
  351. package/dist/dashboard/assets/banner-CI9_eSfO.js.gz +0 -0
  352. package/dist/dashboard/assets/card-DS7CQEsO.js.gz +0 -0
  353. package/dist/dashboard/assets/cli-tools-CNqzrfdb.js.gz +0 -0
  354. package/dist/dashboard/assets/clipboard-button-611pSsaX.js.gz +0 -0
  355. package/dist/dashboard/assets/dialog-k37exjNt.js.gz +0 -0
  356. package/dist/dashboard/assets/drawer-CX7N8ucR.js.gz +0 -0
  357. package/dist/dashboard/assets/entry-B9Xvtnfj.js +0 -1
  358. package/dist/dashboard/assets/entry-B9Xvtnfj.js.gz +0 -0
  359. package/dist/dashboard/assets/entry-ByMGkR3C.js +0 -4
  360. package/dist/dashboard/assets/entry-ByMGkR3C.js.gz +0 -0
  361. package/dist/dashboard/assets/entry-CTP8AyiS.js.gz +0 -0
  362. package/dist/dashboard/assets/github-badge-sltk1qUi.js.gz +0 -0
  363. package/dist/dashboard/assets/index-z3zDOSMM.js.gz +0 -0
  364. package/dist/dashboard/assets/inline-BhY2sSnO.js.gz +0 -0
  365. package/dist/dashboard/assets/input-V1umFiBk.js.gz +0 -0
  366. package/dist/dashboard/assets/layout-BBOYycyS.js.gz +0 -0
  367. package/dist/dashboard/assets/list-row-1zzZYM0B.js.gz +0 -0
  368. package/dist/dashboard/assets/live-BivGfuUs.js.gz +0 -0
  369. package/dist/dashboard/assets/model-bans-CmffjLuG.js.gz +0 -0
  370. package/dist/dashboard/assets/mutation-46P0oWMe.js.gz +0 -0
  371. package/dist/dashboard/assets/numbers-DkfMR5Qs.js.gz +0 -0
  372. package/dist/dashboard/assets/query-keys-BFhGpwR1.js.gz +0 -0
  373. package/dist/dashboard/assets/routing-CAb4F37R.js.gz +0 -0
  374. package/dist/dashboard/assets/select-C6MMiaio.js.gz +0 -0
  375. package/dist/dashboard/assets/settings-DNX9sydi.js.gz +0 -0
  376. package/dist/dashboard/assets/stack-BHTfXdz_.js.gz +0 -0
  377. package/dist/dashboard/assets/state-mPDRlQkl.js.gz +0 -0
  378. package/dist/dashboard/assets/switch-DOmJxg7H.js.gz +0 -0
  379. package/dist/dashboard/assets/useMutation-Dem0lXQj.js.gz +0 -0
  380. package/scripts/build/aot.ts +0 -143
  381. package/scripts/build/build.ts +0 -24
  382. package/scripts/build/compile.ts +0 -88
  383. package/scripts/build/restart.ts +0 -120
  384. package/scripts/build/start.ts +0 -25
  385. package/src/observability/event-compaction.ts +0 -317
  386. package/src/observability/event-day-index.ts +0 -440
@@ -1,4 +1,4 @@
1
- import { appendFile, mkdir, readdir, rm } from "node:fs/promises";
1
+ import { readdir, rm } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
  import { sql } from "drizzle-orm";
4
4
  import { affectedRows, currentTimestamp, laterOf } from "../dialect";
@@ -11,7 +11,8 @@ import {
11
11
  } from "../schema";
12
12
  import { isGatewayError } from "../../observability/telemetry-status";
13
13
  import { TELEMETRY_EVENT_DIR } from "../../observability/event-log";
14
- import { dayEventFileName, utcDay } from "../../observability/event-day-index";
14
+ import { RawTelemetryEventWriter } from "../../observability/raw-event-writer";
15
+ import { hourStamp } from "../../shared/time";
15
16
 
16
17
  interface UsageTotalDelta {
17
18
  readonly tenantId: string;
@@ -71,94 +72,153 @@ function aggregateUsage(rows: readonly (typeof telemetryEvents.$inferInsert)[])
71
72
  const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
72
73
 
73
74
  /**
74
- * Deletes whole days past the retention cutoff: the event file and its index
75
- * together.
75
+ * Deletes hourly event files whose whole hour is older than the retention
76
+ * cutoff.
76
77
  *
77
- * The day is the retention unit now, so both go at once. Removing one without
78
- * the other is the failure this avoids — an index with no events reports totals
79
- * for requests that no longer exist, and events with no index are invisible to
80
- * every usage read.
81
- *
82
- * The cutoff is a whole day, so today's file — still being appended to — is
83
- * never a candidate. Comparing filenames hour by hour would match a day file's
84
- * `.events` suffix against an hour stamp and could drop the open file.
78
+ * The CURRENT and PREVIOUS hour are never touched. The current one is still
79
+ * being appended; the previous one is why retention cannot be an hour
80
+ * boundary: a batch flushed just after a boundary still carries events from
81
+ * the hour that just ended, and the writer files them under that hour. Under
82
+ * multi-process/multi-writer operation a file for the previous hour can also
83
+ * still be open, so deleting it would either fail (Windows) or unlink a file
84
+ * another writer has open (POSIX). Files are named `<hour>-<uuid>.jsonl` now,
85
+ * so an hour is complete only once it is two hours old.
85
86
  */
86
87
  async function pruneEventFiles(before: Date): Promise<void> {
87
88
  const files = await readdir(TELEMETRY_EVENT_DIR).catch(() => [] as string[]);
89
+ // Two hours back: the current hour is open, the previous one can be too.
90
+ const floor = new Date(Math.min(before.getTime(), Date.now() - 2 * 3_600_000));
91
+ const cutoff = hourStamp(floor);
92
+ for (const file of files) {
93
+ if (!file.endsWith(".jsonl") || file.endsWith(".events.jsonl")) continue;
94
+ const hour = file.slice(0, 13);
95
+ if (hour < cutoff) await rm(join(TELEMETRY_EVENT_DIR, file), { force: true });
96
+ }
97
+ // Read-only legacy day logs are pruned only after their whole UTC day has
98
+ // aged out; index files are never consulted for usage anymore.
88
99
  const cutoffDay = before.toISOString().slice(0, 10);
89
100
  for (const file of files) {
90
- const day = file.slice(0, 10);
91
- if (day.length !== 10 || day >= cutoffDay) continue;
92
- if (!file.endsWith(".events.jsonl") && !file.endsWith(".index.json")) continue;
101
+ if (!file.endsWith(".events.jsonl") || file.slice(0, 10) >= cutoffDay) continue;
93
102
  await rm(join(TELEMETRY_EVENT_DIR, file), { force: true });
94
103
  }
95
104
  }
96
105
 
97
106
  /**
98
- * Appends one JSON line per event to the day's event file.
99
- *
100
- * One file per UTC day, appended to and never rewritten while the day is open.
101
- * A file per `windowMinutes` window — 96 a day at the default — would have to be
102
- * found, opened and parsed one by one on every usage query, and which of those
103
- * files was still being written could only be inferred from the name, which is
104
- * what lets the open one be deleted mid-write.
105
- *
106
- * A single growing file has no such ambiguity, and the day index tracks how much
107
- * of it has been counted by byte offset.
107
+ * Appends one JSON line per event. One file per hour, so retention is a file
108
+ * delete and no request is replaced by a compacted summary.
108
109
  */
109
- async function appendEvents(rows: readonly (typeof telemetryEvents.$inferInsert)[]): Promise<void> {
110
- await mkdir(TELEMETRY_EVENT_DIR, { recursive: true });
111
- const file = join(TELEMETRY_EVENT_DIR, dayEventFileName(utcDay(new Date())));
112
- const newline = String.fromCharCode(10);
113
- const body = rows.map((row) => JSON.stringify(row)).join(newline) + newline;
114
- await appendFile(file, body, "utf8");
115
- }
116
-
117
110
  export class DrizzleTelemetryStore {
118
- constructor(private readonly db: CartethyiaDatabase) {}
111
+ private readonly eventWriter: RawTelemetryEventWriter;
119
112
 
120
- /** Inserts events and their durable identity aggregates atomically. */
113
+ constructor(
114
+ private readonly db: CartethyiaDatabase,
115
+ eventWriter = new RawTelemetryEventWriter(TELEMETRY_EVENT_DIR),
116
+ ) {
117
+ this.eventWriter = eventWriter;
118
+ }
119
+
120
+ /**
121
+ * Applies usage deltas inside an enclosing transaction.
122
+ *
123
+ * Takes the transaction handle, not this.db: `insertEvents` runs the event
124
+ * insert and the aggregate upsert in ONE transaction, so usage is charged
125
+ * from the rows actually inserted — never from rows that `ON CONFLICT DO
126
+ * NOTHING` skipped. The deltas are computed by the caller from the
127
+ * inserted-rows result for exactly that reason.
128
+ */
129
+ private async applyUsageDeltas(
130
+ tx: Parameters<Parameters<CartethyiaDatabase["transaction"]>[0]>[0],
131
+ deltas: readonly UsageTotalDelta[],
132
+ ): Promise<void> {
133
+ if (deltas.length === 0) return;
134
+ // Deterministic order so two processes racing the same identities lock
135
+ // rows in the same order and cannot deadlock.
136
+ const ordered = [...deltas].sort((left, right) =>
137
+ left.tenantId.localeCompare(right.tenantId)
138
+ || left.identityType.localeCompare(right.identityType)
139
+ || left.entityId.localeCompare(right.entityId));
140
+ const now = new Date();
141
+ await tx
142
+ .insert(telemetryUsageTotals)
143
+ .values(ordered.map((delta) => ({
144
+ ...delta,
145
+ requests: delta.requests,
146
+ errors: delta.errors,
147
+ inputTokens: delta.inputTokens,
148
+ outputTokens: delta.outputTokens,
149
+ lastUsedAt: now,
150
+ })))
151
+ .onConflictDoUpdate({
152
+ target: [
153
+ telemetryUsageTotals.tenantId,
154
+ telemetryUsageTotals.identityType,
155
+ telemetryUsageTotals.entityId,
156
+ ],
157
+ set: {
158
+ requests: sql`${telemetryUsageTotals.requests} + excluded.requests`,
159
+ errors: sql`${telemetryUsageTotals.errors} + excluded.errors`,
160
+ inputTokens: sql`${telemetryUsageTotals.inputTokens} + excluded.input_tokens`,
161
+ outputTokens: sql`${telemetryUsageTotals.outputTokens} + excluded.output_tokens`,
162
+ lastUsedAt: laterOf(sql`${telemetryUsageTotals.lastUsedAt}`, sql`excluded.last_used_at`, this.db),
163
+ updatedAt: currentTimestamp(),
164
+ },
165
+ });
166
+ }
167
+
168
+ /** Writes raw events and maintains their durable identity aggregates. */
121
169
  async insertEvents(rows: Array<typeof telemetryEvents.$inferInsert>): Promise<void> {
122
170
  if (rows.length === 0) return;
123
171
  // The file accepts anything, so a malformed row would be written and then
124
172
  // retried forever without ever failing. Rejecting it here keeps the buffer's
125
173
  // drop-and-count contract: one bad row fails the batch, once.
174
+ const unique: Array<typeof telemetryEvents.$inferInsert> = [];
175
+ const ids = new Set<string>();
126
176
  for (const row of rows) {
127
177
  if (!UUID_RE.test(row.tenantId)) {
128
178
  throw new Error("telemetry event rejected: tenantId is not a uuid");
129
179
  }
180
+ // In-batch dedupe on the stable id: keep the first occurrence. A batch
181
+ // containing the same request twice (a buffer bug or a replayed
182
+ // enqueue) writes one row and charges once, instead of relying on the
183
+ // DB to skip the second insert silently.
184
+ if (row.id !== undefined) {
185
+ if (ids.has(row.id)) continue;
186
+ ids.add(row.id);
187
+ }
188
+ unique.push(row);
130
189
  }
131
- const deltas = aggregateUsage(rows);
132
- // The per-request row goes to an append-only file, not to Postgres. The
133
- // database keeps only the durable aggregate below, so a request never
134
- // adds a row to the largest table. Readers of recent requests read the
135
- // file; readers of long-term totals read the aggregate.
190
+ // The raw append happens FIRST: the file is the at-least-once mirror. A
191
+ // crash after the append but before the SQL commit is recovered by
192
+ // `carte telemetry reconcile` (the backfill skips rows the unique index
193
+ // already holds). A raw append failure rejects the whole store call so
194
+ // the buffer retries the batch; nothing is committed, nothing charged.
195
+ await this.eventWriter.append(unique);
196
+ // One transaction: insert (skip conflicts), then charge usage from the
197
+ // rows actually inserted. A retried drain or a cross-process race
198
+ // converges to one row and one charge; no compensation transactions.
136
199
  await this.db.transaction(async (tx) => {
137
- if (deltas.length === 0) return;
138
- const now = new Date();
139
- await tx
140
- .insert(telemetryUsageTotals)
141
- .values(deltas.map((delta) => ({ ...delta, lastUsedAt: now })))
142
- .onConflictDoUpdate({
143
- target: [
144
- telemetryUsageTotals.tenantId,
145
- telemetryUsageTotals.identityType,
146
- telemetryUsageTotals.entityId,
147
- ],
148
- set: {
149
- requests: sql`${telemetryUsageTotals.requests} + excluded.requests`,
150
- errors: sql`${telemetryUsageTotals.errors} + excluded.errors`,
151
- inputTokens: sql`${telemetryUsageTotals.inputTokens} + excluded.input_tokens`,
152
- outputTokens: sql`${telemetryUsageTotals.outputTokens} + excluded.output_tokens`,
153
- lastUsedAt: laterOf(sql`${telemetryUsageTotals.lastUsedAt}`, sql`excluded.last_used_at`),
154
- updatedAt: currentTimestamp(),
155
- },
200
+ const inserted = await tx
201
+ .insert(telemetryEvents)
202
+ .values(unique)
203
+ .onConflictDoNothing({
204
+ target: [telemetryEvents.tenantId, telemetryEvents.requestId],
205
+ })
206
+ .returning({
207
+ tenantId: telemetryEvents.tenantId,
208
+ requestId: telemetryEvents.requestId,
209
+ accountId: telemetryEvents.accountId,
210
+ apiKeyId: telemetryEvents.apiKeyId,
211
+ status: telemetryEvents.status,
212
+ httpStatus: telemetryEvents.httpStatus,
213
+ inputTokens: telemetryEvents.inputTokens,
214
+ outputTokens: telemetryEvents.outputTokens,
156
215
  });
216
+ // Usage is charged only from what actually landed. `returning` on
217
+ // `ON CONFLICT DO NOTHING` yields exactly the inserted rows on both
218
+ // backends; skipped conflicts contribute nothing.
219
+ const deltas = aggregateUsage(inserted);
220
+ await this.applyUsageDeltas(tx, deltas);
157
221
  });
158
- // After the aggregate commits, so a rejected batch is retried once, not
159
- // appended once per attempt. The file is the record of requests whose
160
- // totals were actually counted.
161
- await appendEvents(rows);
162
222
  }
163
223
 
164
224
  /** Inserts one redacted payload and returns its generated identity. */
@@ -225,6 +285,7 @@ export class DrizzleTelemetryStore {
225
285
  * the payload sweeper's `cleanupExpired`.
226
286
  */
227
287
  async pruneTelemetry(before: Date): Promise<void> {
288
+ await this.eventWriter.idle();
228
289
  await pruneEventFiles(before);
229
290
  const MAX_BATCHES_PER_SWEEP = 40;
230
291
  for (let batch = 0; batch < MAX_BATCHES_PER_SWEEP; batch += 1) {
@@ -0,0 +1,30 @@
1
+ /** The reasoning-shaped part this reads. Structural, so any reasoning part —
2
+ * including a `ContentPart` narrowed to `kind: "reasoning"` — is accepted. */
3
+ export interface ReasoningTextSource {
4
+ readonly payload?: unknown;
5
+ readonly summary?: string;
6
+ readonly opaque?: boolean;
7
+ }
8
+
9
+ /**
10
+ * The one reader for visible reasoning text.
11
+ *
12
+ * A reasoning part carries two distinct fields: `payload` (the readable
13
+ * reasoning stream — Responses `reasoning_text`, Chat `reasoning_content`) and
14
+ * `summary` (Responses `reasoning_summary_text`, a digest of the same thought).
15
+ * Two call sites used to read them in opposite orders, so a part that populates
16
+ * both rendered different text per surface — and a part that populated both
17
+ * with the *same* string was emitted twice by surfaces that read each field.
18
+ *
19
+ * Precedence is `payload` → `summary`: `payload` is the stream, `summary` is a
20
+ * digest of it, and when only one is present it is the one to show. Opaque
21
+ * parts (redacted thinking, encrypted reasoning) have no readable text at all.
22
+ */
23
+ export function reasoningTextOf(part: ReasoningTextSource): string | undefined {
24
+ if (part.opaque === true) return undefined;
25
+ const payload = part.payload;
26
+ if (typeof payload === "string" && payload.length > 0) return payload;
27
+ const summary = part.summary;
28
+ if (typeof summary === "string" && summary.length > 0) return summary;
29
+ return undefined;
30
+ }
@@ -110,6 +110,8 @@ async function loadCodec(wireFamily: WireFamily): Promise<CodecModule> {
110
110
  const cached = codecCache.get(wireFamily);
111
111
  if (cached !== undefined) return cached;
112
112
  const promise = (async (): Promise<CodecModule> => {
113
+ // dynamic-import: codec picked by wire family at request time; static
114
+ // imports here would couple every request path to every codec module.
113
115
  switch (wireFamily) {
114
116
  case "chat":
115
117
  return (await import("./request/chat")).chatCodecModule;
@@ -258,6 +258,10 @@ export async function* decodeChatSseStream(
258
258
  // Single emission ledger: one entry per logical call.
259
259
  const ledger = createToolEmitLedger();
260
260
  const think = new MimoThinkSplitter();
261
+ // Separate splitter for the reasoning channel: `reasoning_content` is its own
262
+ // stream, and interleaving it into the answer splitter would let a tag in one
263
+ // channel swallow the other's text.
264
+ const reasoningThink = new MimoThinkSplitter();
261
265
  const declaredToolNames = declaredToolNamesForRequest(request);
262
266
  const callableToolNames = callableToolNamesForRequest(request);
263
267
  const contentMarkup =
@@ -344,13 +348,22 @@ export async function* decodeChatSseStream(
344
348
  const reasoningMarkupResult =
345
349
  typeof reasoningRaw === "string" ? reasoningMarkup?.push(reasoningRaw) : undefined;
346
350
  // Strip stray `<think>` tags the upstream double-wrapped inside its own
347
- // reasoning stream (buddy's hy4-preview-f leaks them per-delta). Done
348
- // per delta rather than once at the end because an open-ended tag that
349
- // spans multiple deltas is a splitter job, not this one.
350
- const reasoningText =
351
+ // reasoning stream (buddy's hy4-preview-f leaks them per-delta). This
352
+ // is the splitter's job, not `stripThinkingTags`'s: a stateless strip
353
+ // cannot see a tag split across two deltas, so `"<thi"` was emitted
354
+ // verbatim and only the following `</think>` removed.
355
+ const reasoningSplit =
351
356
  typeof reasoningRaw === "string"
352
- ? stripThinkingTags(reasoningMarkupResult?.text ?? reasoningRaw)
353
- : reasoningRaw;
357
+ ? reasoningThink.push(reasoningMarkupResult?.text ?? reasoningRaw)
358
+ : undefined;
359
+ // Both sides are reasoning. The splitter routes text outside a
360
+ // `<think>` span to its `.text` side, but this field is the reasoning
361
+ // stream in its entirety: untagged prose here is still thought, not
362
+ // answer. Taking only `.reasoning` dropped every un-tagged delta.
363
+ const reasoningText =
364
+ reasoningSplit === undefined
365
+ ? undefined
366
+ : `${reasoningSplit.reasoning}${reasoningSplit.text}`;
354
367
  if (typeof reasoningText === "string" && reasoningText.length > 0) {
355
368
  yield {
356
369
  type: "content_delta",
@@ -510,7 +523,9 @@ export async function* decodeChatSseStream(
510
523
  }
511
524
  const reasoningMarkupTail = reasoningMarkup?.flush();
512
525
  if (reasoningMarkupTail?.text) {
513
- const reasoningText = stripThinkingTags(reasoningMarkupTail.text);
526
+ const split = reasoningThink.push(reasoningMarkupTail.text);
527
+ // Both sides are reasoning: see the per-delta branch above.
528
+ const reasoningText = `${split.reasoning}${split.text}`;
514
529
  if (reasoningText.length > 0) {
515
530
  yield {
516
531
  type: "content_delta",
@@ -519,6 +534,17 @@ export async function* decodeChatSseStream(
519
534
  } as CanonicalEvent;
520
535
  }
521
536
  }
537
+ // Release anything the reasoning splitter held for a tag that never
538
+ // completed, before `think.flush()` and the terminal.
539
+ const reasoningRest = reasoningThink.flush();
540
+ const reasoningTail = `${reasoningRest.reasoning}${reasoningRest.text}`;
541
+ if (reasoningTail.length > 0) {
542
+ yield {
543
+ type: "content_delta",
544
+ sequence_number: seq++,
545
+ content: { kind: "reasoning", payload: null, summary: reasoningTail },
546
+ } as CanonicalEvent;
547
+ }
522
548
  const rest = think.flush();
523
549
  if (rest.reasoning.length > 0) {
524
550
  yield {
@@ -629,11 +629,23 @@ export class CodexStreamFrameProcessor {
629
629
  if (streamError !== undefined) throw streamError;
630
630
  this.terminalStatus = "failed";
631
631
  } else if (typeof json["delta"] === "string" && json["delta"].length > 0) {
632
+ // An unrecognized frame carrying a bare `delta` used to be forwarded as
633
+ // answer text. Reasoning frames that fall through the chain above — the
634
+ // last `else if` is the catch-all for every type this decoder does not
635
+ // name — then leaked into the answer verbatim. Keeping the payload
636
+ // visible as an extension part is the unknown-event convention the
637
+ // Responses stream already uses (`responses:<type>`), so the frame stays
638
+ // in telemetry without ever reaching the answer. Dropping it instead
639
+ // would silently discard real content.
632
640
  events.push({
633
641
  type: "content_delta",
634
642
  sequence_number: this.#seq++,
635
- content: { kind: "text", text: json["delta"] as string },
636
- } as unknown as CanonicalEvent);
643
+ content: {
644
+ kind: "extension",
645
+ name: "codex:untyped_delta",
646
+ payload: json["delta"] as string,
647
+ },
648
+ } as CanonicalEvent);
637
649
  } else {
638
650
  const choices = json["choices"] as
639
651
  | Array<Record<string, unknown>>
@@ -92,8 +92,12 @@ export function parseClaudeContent(value: unknown, isOAuth = false): ContentPart
92
92
  const thinking = stringValue(value.thinking) ? value.thinking : "";
93
93
  return {
94
94
  kind: "reasoning",
95
+ // `thinking` is the readable reasoning stream, not a digest of itself.
96
+ // Putting it in both `payload` and `summary` made the Responses encoder
97
+ // emit the same text twice — once as `reasoning_text` and again as
98
+ // `reasoning_summary_text` — the same bug already fixed on the Codex
99
+ // surface (see `reasoningEvent` in `response/codex.ts`).
95
100
  payload: thinking,
96
- summary: thinking,
97
101
  ...(stringValue(value.signature) ? { signature: value.signature } : {}),
98
102
  };
99
103
  }
@@ -375,7 +379,6 @@ function streamEventToCanonical(
375
379
  content: {
376
380
  kind: "reasoning",
377
381
  payload: delta.thinking,
378
- summary: delta.thinking,
379
382
  },
380
383
  });
381
384
  }
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { createHash, randomBytes } from "node:crypto";
4
4
  import { base64url, decodeJwt } from "jose";
5
+ import { asRecord as record, asTrimmedString as nonEmptyTrimmedString } from "../../shared/coerce";
5
6
  import type { RedisClient } from "../../persistence/redis";
6
7
  import { TtlCache } from "../../persistence/ttl-cache";
7
8
 
@@ -268,18 +269,10 @@ export function devicePollBackoff(
268
269
  }
269
270
 
270
271
  /** Trimmed non-empty string, or `undefined` for anything else. */
271
- export function nonEmptyTrimmedString(value: unknown): string | undefined {
272
- if (typeof value !== "string") return undefined;
273
- const trimmed = value.trim();
274
- return trimmed.length > 0 ? trimmed : undefined;
275
- }
272
+ export { nonEmptyTrimmedString };
276
273
 
277
274
  /** Object (non-array) narrowing for untyped upstream JSON. */
278
- export function record(value: unknown): Record<string, unknown> | undefined {
279
- return value !== null && typeof value === "object" && !Array.isArray(value)
280
- ? (value as Record<string, unknown>)
281
- : undefined;
282
- }
275
+ export { record };
283
276
 
284
277
 
285
278
  interface PkcePair {