cartethyia 2.7.9 → 2.8.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 (375) hide show
  1. package/.env.example +3 -2
  2. package/dist/dashboard/assets/{BannedPage-BxoG1MrL.js → BannedPage-Cc0vKpMG.js} +1 -1
  3. package/dist/dashboard/assets/BannedPage-Cc0vKpMG.js.gz +0 -0
  4. package/dist/dashboard/assets/{CliToolDetailPage-ixwgKk53.js → CliToolDetailPage-Bqj2aa1T.js} +1 -1
  5. package/dist/dashboard/assets/CliToolDetailPage-Bqj2aa1T.js.gz +0 -0
  6. package/dist/dashboard/assets/{CliToolsPage-BmttNV3U.js → CliToolsPage-CXsaOAO2.js} +1 -1
  7. package/dist/dashboard/assets/CliToolsPage-CXsaOAO2.js.gz +0 -0
  8. package/dist/dashboard/assets/{CombosPage-BE3qwwfC.js → CombosPage-BeLpJ4ma.js} +1 -1
  9. package/dist/dashboard/assets/CombosPage-BeLpJ4ma.js.gz +0 -0
  10. package/dist/dashboard/assets/{ConsoleLogPage-CRZdmMIX.js → ConsoleLogPage-DnV0uZ3m.js} +1 -1
  11. package/dist/dashboard/assets/ConsoleLogPage-DnV0uZ3m.js.gz +0 -0
  12. package/dist/dashboard/assets/{CustomizationPage-Dv_FJ6Er.js → CustomizationPage-CBxAjo63.js} +1 -1
  13. package/dist/dashboard/assets/CustomizationPage-CBxAjo63.js.gz +0 -0
  14. package/dist/dashboard/assets/HealthEventsModal-B5yEE0k6.js +1 -0
  15. package/dist/dashboard/assets/HealthEventsModal-B5yEE0k6.js.gz +0 -0
  16. package/dist/dashboard/assets/{InjectSystemPromptPanel-BOoO8rpz.js → InjectSystemPromptPanel-BWFLECmz.js} +2 -2
  17. package/dist/dashboard/assets/InjectSystemPromptPanel-BWFLECmz.js.gz +0 -0
  18. package/dist/dashboard/assets/{LoginPage-ChJuNNVN.js → LoginPage-CgoPc5B9.js} +1 -1
  19. package/dist/dashboard/assets/LoginPage-CgoPc5B9.js.gz +0 -0
  20. package/dist/dashboard/assets/{ModelPicker-BZAJtPg1.js → ModelPicker-D9bFQern.js} +1 -1
  21. package/dist/dashboard/assets/ModelPicker-D9bFQern.js.gz +0 -0
  22. package/dist/dashboard/assets/{OverviewPage-BE40s3u5.js → OverviewPage-B_p4tL8k.js} +1 -1
  23. package/dist/dashboard/assets/OverviewPage-B_p4tL8k.js.gz +0 -0
  24. package/dist/dashboard/assets/ProviderDetailPage-rs5E2rxv.js +8 -0
  25. package/dist/dashboard/assets/ProviderDetailPage-rs5E2rxv.js.gz +0 -0
  26. package/dist/dashboard/assets/{ProviderIcon-CQ6bPUf_.js → ProviderIcon-D_0PiZtg.js} +1 -1
  27. package/dist/dashboard/assets/ProviderIcon-D_0PiZtg.js.gz +0 -0
  28. package/dist/dashboard/assets/ProvidersPage-CLI2bJTK.js +1 -0
  29. package/dist/dashboard/assets/ProvidersPage-CLI2bJTK.js.gz +0 -0
  30. package/dist/dashboard/assets/{ProxyPage-BJ87Pp0U.js → ProxyPage-BwNC_-Cx.js} +1 -1
  31. package/dist/dashboard/assets/ProxyPage-BwNC_-Cx.js.gz +0 -0
  32. package/dist/dashboard/assets/QuotaPage-DA-a1iSN.js +1 -0
  33. package/dist/dashboard/assets/QuotaPage-DA-a1iSN.js.gz +0 -0
  34. package/dist/dashboard/assets/{SettingsPage-C5HPWqEv.js → SettingsPage-CctTga2C.js} +1 -1
  35. package/dist/dashboard/assets/SettingsPage-CctTga2C.js.gz +0 -0
  36. package/dist/dashboard/assets/{SetupPage-CFNcATJJ.js → SetupPage-febLU4r1.js} +1 -1
  37. package/dist/dashboard/assets/SetupPage-febLU4r1.js.gz +0 -0
  38. package/dist/dashboard/assets/{StudioPage-YoD4tY5l.js → StudioPage-M6fvR5Wz.js} +1 -1
  39. package/dist/dashboard/assets/StudioPage-M6fvR5Wz.js.gz +0 -0
  40. package/dist/dashboard/assets/{UsagePage-DxjaAhb_.js → UsagePage-nyeuRqz9.js} +1 -1
  41. package/dist/dashboard/assets/UsagePage-nyeuRqz9.js.gz +0 -0
  42. package/dist/dashboard/assets/{api-keys-BTlkq_Uk.js → api-keys-BFOlvrJC.js} +1 -1
  43. package/dist/dashboard/assets/api-keys-BFOlvrJC.js.gz +0 -0
  44. package/dist/dashboard/assets/app-Dx2QgsNj.css +2 -0
  45. package/dist/dashboard/assets/app-Dx2QgsNj.css.gz +0 -0
  46. package/dist/dashboard/assets/assert-CqTLw5e7.js +1 -0
  47. package/dist/dashboard/assets/assert-CqTLw5e7.js.gz +0 -0
  48. package/dist/dashboard/assets/{badge-DdbT_7Am.js → badge-C83EqLB0.js} +1 -1
  49. package/dist/dashboard/assets/badge-C83EqLB0.js.gz +0 -0
  50. package/dist/dashboard/assets/{banner-CsGN-nbm.js → banner-D1xn7pIe.js} +1 -1
  51. package/dist/dashboard/assets/banner-D1xn7pIe.js.gz +0 -0
  52. package/dist/dashboard/assets/{card-mgZ_TLLZ.js → card-DU3Lq5Lm.js} +1 -1
  53. package/dist/dashboard/assets/card-DU3Lq5Lm.js.gz +0 -0
  54. package/dist/dashboard/assets/{cli-tools-A1-v8-03.js → cli-tools-Dw1U_drK.js} +1 -1
  55. package/dist/dashboard/assets/cli-tools-Dw1U_drK.js.gz +0 -0
  56. package/dist/dashboard/assets/{clipboard-button-CJ4Zty89.js → clipboard-button-CEL5Sg_J.js} +1 -1
  57. package/dist/dashboard/assets/clipboard-button-CEL5Sg_J.js.gz +0 -0
  58. package/dist/dashboard/assets/{dialog-B5L2jPQ_.js → dialog-BjFR5UHy.js} +1 -1
  59. package/dist/dashboard/assets/dialog-BjFR5UHy.js.gz +0 -0
  60. package/dist/dashboard/assets/{drawer-DkjUouAj.js → drawer-HvBW0AjI.js} +1 -1
  61. package/dist/dashboard/assets/drawer-HvBW0AjI.js.gz +0 -0
  62. package/dist/dashboard/assets/entry-BSu3gFlt.js +4 -0
  63. package/dist/dashboard/assets/entry-BSu3gFlt.js.gz +0 -0
  64. package/dist/dashboard/assets/{entry-B-LVLV6o.js → entry-CECVELyy.js} +1 -1
  65. package/dist/dashboard/assets/entry-CECVELyy.js.gz +0 -0
  66. package/dist/dashboard/assets/{entry-CAhdYl3B.js → entry-DYYofQ7W.js} +1 -1
  67. package/dist/dashboard/assets/entry-DYYofQ7W.js.gz +0 -0
  68. package/dist/dashboard/assets/{formatters-CrsdLVsn.js → formatters-DLW4i8zK.js} +1 -1
  69. package/dist/dashboard/assets/formatters-DLW4i8zK.js.gz +0 -0
  70. package/dist/dashboard/assets/{index-BU64Lqvp.js → index-1-fCqe64.js} +2 -2
  71. package/dist/dashboard/assets/index-1-fCqe64.js.gz +0 -0
  72. package/dist/dashboard/assets/{inline-B2aDLyyg.js → inline-DGvU2oLK.js} +1 -1
  73. package/dist/dashboard/assets/inline-DGvU2oLK.js.gz +0 -0
  74. package/dist/dashboard/assets/{input-BCVkdR9d.js → input-iNnO1X2K.js} +1 -1
  75. package/dist/dashboard/assets/input-iNnO1X2K.js.gz +0 -0
  76. package/dist/dashboard/assets/{layout-DNcAdMK-.js → layout-0q8UGypb.js} +1 -1
  77. package/dist/dashboard/assets/layout-0q8UGypb.js.gz +0 -0
  78. package/dist/dashboard/assets/{list-row-DgrIH0hw.js → list-row-C0w2bQjJ.js} +1 -1
  79. package/dist/dashboard/assets/list-row-C0w2bQjJ.js.gz +0 -0
  80. package/dist/dashboard/assets/{live-Cae01iu8.js → live-M5aJ_xCa.js} +1 -1
  81. package/dist/dashboard/assets/live-M5aJ_xCa.js.gz +0 -0
  82. package/dist/dashboard/assets/{network-CRib7H_2.js → network-BKeQGT3K.js} +1 -1
  83. package/dist/dashboard/assets/network-BKeQGT3K.js.gz +0 -0
  84. package/dist/dashboard/assets/{numbers-BKQE8Xpz.js → numbers-CivAk3da.js} +1 -1
  85. package/dist/dashboard/assets/numbers-CivAk3da.js.gz +0 -0
  86. package/dist/dashboard/assets/{pagination-ecPvG_8g.js → pagination-CpzvI5ep.js} +1 -1
  87. package/dist/dashboard/assets/pagination-CpzvI5ep.js.gz +0 -0
  88. package/dist/dashboard/assets/providers/zro.svg +3 -0
  89. package/dist/dashboard/assets/providers/zro.svg.gz +0 -0
  90. package/dist/dashboard/assets/quota-DiPD6UKH.js +1 -0
  91. package/dist/dashboard/assets/quota-DiPD6UKH.js.gz +0 -0
  92. package/dist/dashboard/assets/{routing-Ckm1UXuC.js → routing-BFQX2VBc.js} +1 -1
  93. package/dist/dashboard/assets/routing-BFQX2VBc.js.gz +0 -0
  94. package/dist/dashboard/assets/{select-ChoFWyKz.js → select-CXbck0jG.js} +2 -2
  95. package/dist/dashboard/assets/select-CXbck0jG.js.gz +0 -0
  96. package/dist/dashboard/assets/{settings-CKjFPlKW.js → settings-C_XvMHh9.js} +1 -1
  97. package/dist/dashboard/assets/settings-C_XvMHh9.js.gz +0 -0
  98. package/dist/dashboard/assets/{stack-DWCwU3fh.js → stack-DggRiCxE.js} +1 -1
  99. package/dist/dashboard/assets/stack-DggRiCxE.js.gz +0 -0
  100. package/dist/dashboard/assets/{state-Cg3VL5J_.js → state-ClFzfbYp.js} +1 -1
  101. package/dist/dashboard/assets/state-ClFzfbYp.js.gz +0 -0
  102. package/dist/dashboard/assets/{switch-Db70BMix.js → switch-CTIFxKqS.js} +1 -1
  103. package/dist/dashboard/assets/switch-CTIFxKqS.js.gz +0 -0
  104. package/dist/dashboard/assets/{useMutation-BqdE0LuM.js → useMutation-BClG3yWj.js} +1 -1
  105. package/dist/dashboard/assets/useMutation-BClG3yWj.js.gz +0 -0
  106. package/dist/dashboard/index.html +2 -2
  107. package/dist/dashboard/index.html.gz +0 -0
  108. package/dist/dashboard/precompressed-assets.json +54 -53
  109. package/migrations/full-postgres/0046_add_accounts_quota_plan_fetched_at.sql +7 -0
  110. package/migrations/lite-sqlite3/0046_add_accounts_quota_plan_fetched_at.sql +4 -0
  111. package/package.json +1 -1
  112. package/src/boot/app.ts +10 -13
  113. package/src/boot/dependencies.ts +1 -1
  114. package/src/boot/flush.ts +20 -0
  115. package/src/boot/lifecycle.ts +7 -5
  116. package/src/config/routing.ts +26 -0
  117. package/src/config/spec-metadata.ts +72 -0
  118. package/src/config/spec.ts +20 -0
  119. package/src/console/api-keys/contracts.ts +12 -1
  120. package/src/console/api-keys/routes.ts +40 -28
  121. package/src/console/auth/account-reset.ts +7 -9
  122. package/src/console/backup/contracts.ts +4 -0
  123. package/src/console/backup/export-endpoint.ts +7 -5
  124. package/src/console/cli-tools/service.ts +6 -12
  125. package/src/console/console-router-guards.ts +23 -16
  126. package/src/console/domain-registration.ts +0 -2
  127. package/src/console/inferhub/normalize.ts +24 -28
  128. package/src/console/inferhub/types.ts +3 -0
  129. package/src/console/ip-bans/routes.ts +7 -13
  130. package/src/console/observability/store.ts +0 -53
  131. package/src/console/providers/catalog/contracts.ts +2 -2
  132. package/src/console/providers/catalog/discover-constants.ts +6 -0
  133. package/src/console/providers/catalog/routes.ts +1 -0
  134. package/src/console/providers/catalog/store.ts +21 -16
  135. package/src/console/quota/account-quota-global.ts +14 -24
  136. package/src/console/quota/account-quota-shared.ts +49 -0
  137. package/src/console/quota/account-quota-tenant.ts +6 -18
  138. package/src/console/quota/quota-view.ts +19 -3
  139. package/src/console/routing/pools/operations.ts +31 -46
  140. package/src/console/shared/errors.ts +33 -6
  141. package/src/main.ts +2 -3
  142. package/src/network/pool/byte-accounting.ts +3 -2
  143. package/src/network/pool/selector.ts +182 -39
  144. package/src/network/pool-cooldown-store.ts +89 -14
  145. package/src/network/pool-health.ts +49 -61
  146. package/src/network/rate-limit-verdict.ts +1 -1
  147. package/src/network/ssrf.ts +6 -6
  148. package/src/observability/event-log.ts +7 -361
  149. package/src/observability/performance-metrics.ts +9 -0
  150. package/src/observability/runtime-metrics.ts +14 -9
  151. package/src/observability/telemetry-buffer.ts +15 -25
  152. package/src/observability/telemetry-reconciliation.ts +9 -37
  153. package/src/observability/telemetry-row.ts +125 -0
  154. package/src/persistence/db-mode.ts +4 -1
  155. package/src/persistence/dialect.ts +18 -0
  156. package/src/persistence/migrate.ts +6 -3
  157. package/src/persistence/schema.ts +8 -0
  158. package/src/persistence/ttl-cache.ts +20 -2
  159. package/src/protocol/codecs.ts +1 -1
  160. package/src/protocol/dsml.ts +34 -86
  161. package/src/protocol/markup-stream-parser.ts +195 -0
  162. package/src/protocol/reasoning-text.ts +19 -0
  163. package/src/protocol/request/chat.ts +10 -10
  164. package/src/protocol/request/gemini.ts +8 -2
  165. package/src/protocol/request/messages.ts +3 -4
  166. package/src/protocol/response/chat.ts +12 -20
  167. package/src/protocol/response/omp-tool-call-markup.ts +43 -86
  168. package/src/protocol/response/responses.ts +18 -2
  169. package/src/protocol/stop-reason.ts +23 -1
  170. package/src/protocol/think-split.ts +112 -0
  171. package/src/protocol/translation/dialect/qwen.ts +26 -58
  172. package/src/providers/adapters/compatible-adapter.ts +71 -12
  173. package/src/providers/authentication/oauth-client.ts +1 -1
  174. package/src/providers/authentication/oauth-device-flow.ts +98 -8
  175. package/src/providers/authentication/oauth-refresh-service.ts +30 -49
  176. package/src/providers/configured-provider.ts +148 -0
  177. package/src/providers/default-registry.ts +61 -36
  178. package/src/providers/discovery/probe-phases.ts +14 -4
  179. package/src/providers/discovery/probing-service.ts +15 -2
  180. package/src/providers/integrations/alibabacp/alibabacp-models.ts +1 -2
  181. package/src/providers/integrations/{dashscope-models.ts → alibabacp/dashscope-models.ts} +2 -2
  182. package/src/providers/integrations/antigravity/antigravity-protocol.ts +5 -1
  183. package/src/providers/integrations/buddy/buddy-chat-shared.ts +173 -17
  184. package/src/providers/integrations/buddy/buddy-identity.ts +19 -6
  185. package/src/providers/integrations/buddy/capabilities.ts +2 -2
  186. package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +3 -0
  187. package/src/providers/integrations/buddy/codebuddy/provider.ts +4 -0
  188. package/src/providers/integrations/buddy/codebuddy/shared.ts +37 -22
  189. package/src/providers/integrations/claude/claude-quota.ts +14 -17
  190. package/src/providers/integrations/cline/cline.ts +2 -2
  191. package/src/providers/integrations/commandcode/quota.ts +22 -28
  192. package/src/providers/integrations/cursor/cursor-auth.ts +0 -8
  193. package/src/providers/integrations/cursor/cursor-exec.ts +9 -2
  194. package/src/providers/integrations/cursor/cursor-request.ts +26 -12
  195. package/src/providers/integrations/cursor/cursor-shared.ts +7 -4
  196. package/src/providers/integrations/cursor/cursor.ts +89 -40
  197. package/src/providers/integrations/cursor/protocol/messages.ts +22 -29
  198. package/src/providers/integrations/cursor/protocol/protobuf-schema.ts +37 -0
  199. package/src/providers/integrations/devin/protocol/devin-proto.ts +1 -27
  200. package/src/providers/integrations/factory/factory-oauth.ts +17 -44
  201. package/src/providers/integrations/freebuff/assets/official-system-manager.txt +88 -0
  202. package/src/providers/integrations/freebuff/assets/official-system-worker.txt +57 -0
  203. package/src/providers/integrations/freebuff/assets/official-tools.json +1070 -0
  204. package/src/providers/integrations/freebuff/freebuff-catalog.ts +120 -0
  205. package/src/providers/integrations/freebuff/freebuff-constants.ts +1233 -0
  206. package/src/providers/integrations/freebuff/freebuff-discovery.ts +108 -0
  207. package/src/providers/integrations/freebuff/freebuff-oauth.ts +240 -0
  208. package/src/providers/integrations/freebuff/freebuff-quota.ts +260 -0
  209. package/src/providers/integrations/freebuff/freebuff-virtual.ts +89 -0
  210. package/src/providers/integrations/freebuff/freebuff.ts +682 -0
  211. package/src/providers/integrations/grok/grok-oauth.ts +48 -45
  212. package/src/providers/integrations/grok/grok-ratelimit-store.ts +10 -17
  213. package/src/providers/integrations/grok/grok-turn-index.ts +11 -16
  214. package/src/providers/integrations/{hermes.ts → hermes/hermes.ts} +9 -9
  215. package/src/providers/integrations/hermes/index.ts +2 -0
  216. package/src/providers/integrations/kiro/kiro-catalog.ts +17 -1
  217. package/src/providers/integrations/kiro/kiro-discovery.ts +10 -12
  218. package/src/providers/integrations/kiro/kiro-quota.ts +8 -4
  219. package/src/providers/integrations/kiro/kiro.ts +15 -19
  220. package/src/providers/integrations/ollamacloud/index.ts +2 -0
  221. package/src/providers/integrations/{ollamacloud-models.ts → ollamacloud/models.ts} +2 -2
  222. package/src/providers/integrations/{ollamacloud-quota.ts → ollamacloud/quota.ts} +3 -3
  223. package/src/providers/integrations/opencode/opencode-go-quota.ts +2 -8
  224. package/src/providers/integrations/perch/perch-credential.ts +25 -0
  225. package/src/providers/integrations/perch/perch-protocol.ts +0 -3
  226. package/src/providers/integrations/perch/perch-quota.ts +2 -16
  227. package/src/providers/integrations/perch/perch.ts +3 -13
  228. package/src/providers/integrations/qoder/claim-qoder-credits.ts +18 -59
  229. package/src/providers/integrations/qoder/qoder-adapter.ts +1 -12
  230. package/src/providers/integrations/qoder/qoder-auth.ts +101 -29
  231. package/src/providers/integrations/qoder/qoder-catalog.ts +20 -5
  232. package/src/providers/integrations/qoder/qoder-chat.ts +7 -21
  233. package/src/providers/integrations/qoder/qoder-checkin.ts +4 -4
  234. package/src/providers/integrations/qoder/qoder-grpc.ts +0 -1
  235. package/src/providers/integrations/qoder/qoder-ide.ts +0 -107
  236. package/src/providers/integrations/qoder/qoder.ts +0 -1
  237. package/src/providers/integrations/trae/trae-protocol.ts +9 -5
  238. package/src/providers/integrations/trae/trae-quota.ts +6 -28
  239. package/src/providers/integrations/trae/trae-realm.ts +38 -0
  240. package/src/providers/integrations/trae/trae.ts +7 -24
  241. package/src/providers/integrations/zcode-start-plan/provider.ts +226 -23
  242. package/src/providers/integrations/zcode-start-plan/quota.ts +19 -2
  243. package/src/providers/integrations/zro/zro.ts +204 -0
  244. package/src/providers/operations/account-error-classification.ts +199 -307
  245. package/src/providers/operations/account-health-service.ts +82 -1
  246. package/src/providers/operations/account-refresh-status.ts +22 -37
  247. package/src/providers/operations/account-tier.ts +15 -16
  248. package/src/providers/operations/client-version-resolver.ts +1 -1
  249. package/src/providers/operations/client-versions.ts +5 -51
  250. package/src/providers/operations/cooldown-rules.ts +11 -16
  251. package/src/providers/operations/{model-catalog-cache.ts → provider-caches.ts} +68 -16
  252. package/src/providers/operations/provider-catalog-service.ts +1 -1
  253. package/src/providers/operations/provider-credential-service.ts +102 -32
  254. package/src/providers/operations/provider-rules/github.ts +48 -0
  255. package/src/providers/operations/provider-rules/grok.ts +33 -0
  256. package/src/providers/operations/provider-rules/inception.ts +49 -0
  257. package/src/providers/operations/provider-rules/index.ts +32 -0
  258. package/src/providers/operations/provider-rules/perch.ts +37 -0
  259. package/src/providers/operations/provider-rules/qoder.ts +70 -0
  260. package/src/providers/operations/provider-rules/trae.ts +81 -0
  261. package/src/providers/provider-contracts.ts +4 -2
  262. package/src/providers/{provider-credential-resolver.ts → provider-credentials.ts} +40 -3
  263. package/src/providers/provider-metadata.ts +40 -10
  264. package/src/providers/provider-probe.ts +15 -1
  265. package/src/providers/provider-registry.ts +42 -33
  266. package/src/providers/quota/quota-limit-windows.ts +16 -5
  267. package/src/providers/quota/quota-support.ts +1 -1
  268. package/src/providers/quota/quota-window-parser.ts +90 -18
  269. package/src/providers/quota/refresh.ts +75 -18
  270. package/src/security/api-key-auth-cache.ts +1 -1
  271. package/src/security/api-key-auth.ts +6 -2
  272. package/src/transport/dispatch/attempt-finalize.ts +14 -13
  273. package/src/transport/dispatch/attempt-loop.ts +71 -17
  274. package/src/transport/dispatch/generation.ts +2 -8
  275. package/src/transport/dispatch/native-shared.ts +25 -0
  276. package/src/transport/dispatch/proxy-request.ts +8 -46
  277. package/src/transport/dispatch/responses-compact.ts +2 -8
  278. package/src/transport/dispatch/streaming/finalize.ts +9 -6
  279. package/src/transport/dispatch/streaming/stream.ts +8 -36
  280. package/src/transport/dispatch/systemone.ts +2 -8
  281. package/src/transport/dispatch/token-count.ts +2 -6
  282. package/src/transport/dispatch/upstream.ts +63 -0
  283. package/src/transport/drain-endpoint.ts +31 -4
  284. package/src/transport/failure-policy.ts +16 -30
  285. package/src/transport/guards/body-policy.ts +7 -1
  286. package/src/transport/guards/error-lifecycle.ts +13 -1
  287. package/src/transport/guards/request-context.ts +22 -14
  288. package/src/transport/request/preparer.ts +81 -71
  289. package/src/transport/routing/bounded-lru.ts +14 -0
  290. package/src/transport/routing/drain-reserve.ts +129 -0
  291. package/src/transport/routing/plan-gate.ts +7 -1
  292. package/src/transport/routing/round-robin.ts +60 -0
  293. package/src/transport/routing/route-catalog.ts +123 -111
  294. package/src/transport/routing/route-model.ts +51 -9
  295. package/src/transport/routing/router.ts +393 -32
  296. package/src/transport/surface/adapters.ts +11 -2
  297. package/src/transport/surface/chat/adapter.ts +2 -2
  298. package/src/transport/surface/chat/encode.ts +47 -4
  299. package/src/transport/surface/chat/parse.ts +2 -12
  300. package/src/transport/surface/completion.ts +44 -33
  301. package/src/transport/surface/content-parts.ts +21 -0
  302. package/src/transport/surface/messages/adapter.ts +7 -1
  303. package/src/transport/surface/messages/encode.ts +11 -3
  304. package/src/transport/surface/messages/parse.ts +2 -1
  305. package/src/transport/surface/messages/stream.ts +33 -17
  306. package/src/transport/surface/responses/encode.ts +22 -5
  307. package/src/transport/surface/responses/parse.ts +3 -11
  308. package/dist/dashboard/assets/BannedPage-BxoG1MrL.js.gz +0 -0
  309. package/dist/dashboard/assets/CliToolDetailPage-ixwgKk53.js.gz +0 -0
  310. package/dist/dashboard/assets/CliToolsPage-BmttNV3U.js.gz +0 -0
  311. package/dist/dashboard/assets/CombosPage-BE3qwwfC.js.gz +0 -0
  312. package/dist/dashboard/assets/ConsoleLogPage-CRZdmMIX.js.gz +0 -0
  313. package/dist/dashboard/assets/CustomizationPage-Dv_FJ6Er.js.gz +0 -0
  314. package/dist/dashboard/assets/HealthEventsModal-CoVrw2sM.js +0 -1
  315. package/dist/dashboard/assets/HealthEventsModal-CoVrw2sM.js.gz +0 -0
  316. package/dist/dashboard/assets/InjectSystemPromptPanel-BOoO8rpz.js.gz +0 -0
  317. package/dist/dashboard/assets/LoginPage-ChJuNNVN.js.gz +0 -0
  318. package/dist/dashboard/assets/ModelPicker-BZAJtPg1.js.gz +0 -0
  319. package/dist/dashboard/assets/OverviewPage-BE40s3u5.js.gz +0 -0
  320. package/dist/dashboard/assets/ProviderDetailPage-sfLoQtPV.js +0 -8
  321. package/dist/dashboard/assets/ProviderDetailPage-sfLoQtPV.js.gz +0 -0
  322. package/dist/dashboard/assets/ProviderIcon-CQ6bPUf_.js.gz +0 -0
  323. package/dist/dashboard/assets/ProvidersPage-DcepmPWo.js +0 -1
  324. package/dist/dashboard/assets/ProvidersPage-DcepmPWo.js.gz +0 -0
  325. package/dist/dashboard/assets/ProxyPage-BJ87Pp0U.js.gz +0 -0
  326. package/dist/dashboard/assets/QuotaPage-gkSyRAKa.js +0 -1
  327. package/dist/dashboard/assets/QuotaPage-gkSyRAKa.js.gz +0 -0
  328. package/dist/dashboard/assets/SettingsPage-C5HPWqEv.js.gz +0 -0
  329. package/dist/dashboard/assets/SetupPage-CFNcATJJ.js.gz +0 -0
  330. package/dist/dashboard/assets/StudioPage-YoD4tY5l.js.gz +0 -0
  331. package/dist/dashboard/assets/UsagePage-DxjaAhb_.js.gz +0 -0
  332. package/dist/dashboard/assets/api-keys-BTlkq_Uk.js.gz +0 -0
  333. package/dist/dashboard/assets/app-BX2nnKqs.css +0 -2
  334. package/dist/dashboard/assets/app-BX2nnKqs.css.gz +0 -0
  335. package/dist/dashboard/assets/assert-CndthXFQ.js +0 -1
  336. package/dist/dashboard/assets/assert-CndthXFQ.js.gz +0 -0
  337. package/dist/dashboard/assets/badge-DdbT_7Am.js.gz +0 -0
  338. package/dist/dashboard/assets/banner-CsGN-nbm.js.gz +0 -0
  339. package/dist/dashboard/assets/card-mgZ_TLLZ.js.gz +0 -0
  340. package/dist/dashboard/assets/cli-tools-A1-v8-03.js.gz +0 -0
  341. package/dist/dashboard/assets/clipboard-button-CJ4Zty89.js.gz +0 -0
  342. package/dist/dashboard/assets/dialog-B5L2jPQ_.js.gz +0 -0
  343. package/dist/dashboard/assets/drawer-DkjUouAj.js.gz +0 -0
  344. package/dist/dashboard/assets/entry-B-LVLV6o.js.gz +0 -0
  345. package/dist/dashboard/assets/entry-CAhdYl3B.js.gz +0 -0
  346. package/dist/dashboard/assets/entry-D2xD0Tmd.js +0 -4
  347. package/dist/dashboard/assets/entry-D2xD0Tmd.js.gz +0 -0
  348. package/dist/dashboard/assets/formatters-CrsdLVsn.js.gz +0 -0
  349. package/dist/dashboard/assets/index-BU64Lqvp.js.gz +0 -0
  350. package/dist/dashboard/assets/inline-B2aDLyyg.js.gz +0 -0
  351. package/dist/dashboard/assets/input-BCVkdR9d.js.gz +0 -0
  352. package/dist/dashboard/assets/layout-DNcAdMK-.js.gz +0 -0
  353. package/dist/dashboard/assets/list-row-DgrIH0hw.js.gz +0 -0
  354. package/dist/dashboard/assets/live-Cae01iu8.js.gz +0 -0
  355. package/dist/dashboard/assets/network-CRib7H_2.js.gz +0 -0
  356. package/dist/dashboard/assets/numbers-BKQE8Xpz.js.gz +0 -0
  357. package/dist/dashboard/assets/pagination-ecPvG_8g.js.gz +0 -0
  358. package/dist/dashboard/assets/quota-DJ1QlAOq.js +0 -1
  359. package/dist/dashboard/assets/quota-DJ1QlAOq.js.gz +0 -0
  360. package/dist/dashboard/assets/routing-Ckm1UXuC.js.gz +0 -0
  361. package/dist/dashboard/assets/select-ChoFWyKz.js.gz +0 -0
  362. package/dist/dashboard/assets/settings-CKjFPlKW.js.gz +0 -0
  363. package/dist/dashboard/assets/stack-DWCwU3fh.js.gz +0 -0
  364. package/dist/dashboard/assets/state-Cg3VL5J_.js.gz +0 -0
  365. package/dist/dashboard/assets/switch-Db70BMix.js.gz +0 -0
  366. package/dist/dashboard/assets/useMutation-BqdE0LuM.js.gz +0 -0
  367. package/src/providers/bundled-discovery-providers.ts +0 -98
  368. package/src/providers/configured-openai-providers.ts +0 -120
  369. package/src/providers/operations/model-discovery-cache.ts +0 -34
  370. package/src/providers/operations/probe-fetch.ts +0 -15
  371. package/src/providers/operations/provider-version-cache.ts +0 -30
  372. package/src/providers/provider-capabilities.ts +0 -33
  373. package/src/providers/provider-credential-envelope.ts +0 -43
  374. /package/dist/dashboard/assets/{app-B7kIWFm5.js → app-DIXLjojg.js} +0 -0
  375. /package/dist/dashboard/assets/{app-B7kIWFm5.js.gz → app-DIXLjojg.js.gz} +0 -0
@@ -0,0 +1,195 @@
1
+ /**
2
+ * Generic streaming prefix-buffer parser for in-band tool-call markup.
3
+ *
4
+ * Three grammars (DSML blocks, OMP `<tool_calls:8hex>` envelopes, Qwen
5
+ * `<tool_call>` JSON blocks) share one stateful loop: append chunks to a
6
+ * pending buffer, scan for the earliest opener, settle visible text up to a
7
+ * safe end that holds back a possible trailing opener fragment, parse a
8
+ * complete block when its closer appears, fail open past a block-size cap, and
9
+ * flush the tail. The grammar-specific parts are supplied as configuration:
10
+ * opener strings, an opener classifier (some grammars must validate beyond the
11
+ * bare prefix, e.g. OMP's hex marker), closer/parse functions, an unclosed
12
+ * block policy (hold vs fail-open pass-through), a flush policy (pass through
13
+ * vs discard an unterminated envelope), and a call-id factory. Each parser
14
+ * instance keeps its own buffer; nothing is shared.
15
+ */
16
+
17
+ /** Result of one push/flush pass: text that settled plus recovered calls. */
18
+ export interface MarkupStreamParseResult<Call extends { readonly id: string }> {
19
+ readonly text: string;
20
+ readonly calls: readonly Call[];
21
+ }
22
+
23
+ /** Raw call shape produced by a grammar's block parser, without its id. */
24
+ export type MarkupRawCall<Call extends { readonly id: string }> = Omit<Call, "id">;
25
+
26
+ /** Builds a complete call (id included) from a parsed raw call. */
27
+ export type MarkupCallFactory<Call extends { readonly id: string }> = (
28
+ call: MarkupRawCall<Call>,
29
+ index: number,
30
+ seq: number,
31
+ ) => Call;
32
+
33
+ /** Classification of one opener candidate occurrence. */
34
+ export type MarkupOpenerClassification<Call extends { readonly id: string }> =
35
+ | {
36
+ /**
37
+ * The candidate may be the start of a real opener (e.g. a partial hex
38
+ * marker): hold everything from the candidate until a later chunk
39
+ * completes or disproves it.
40
+ */
41
+ readonly kind: "hold";
42
+ }
43
+ | {
44
+ /**
45
+ * The candidate is definitely not an opener: emit it as literal text
46
+ * (one character) and rescan after it.
47
+ */
48
+ readonly kind: "literal";
49
+ }
50
+ | {
51
+ readonly kind: "found";
52
+ /** Tag that closes the block opened at this position. */
53
+ readonly closer: string;
54
+ /**
55
+ * Parses one complete block (opener through closer). `undefined` means
56
+ * the block is not valid markup and is passed through as text verbatim.
57
+ */
58
+ readonly parse: (block: string) => readonly MarkupRawCall<Call>[] | undefined;
59
+ /**
60
+ * Call factory for the calls recovered from this block; defaults to the
61
+ * parser-level `callId`. Some grammars derive ids from block-local state
62
+ * (e.g. OMP embeds the envelope marker).
63
+ */
64
+ readonly callId?: MarkupCallFactory<Call>;
65
+ };
66
+
67
+ export interface MarkupStreamParserOptions<Call extends { readonly id: string }> {
68
+ /**
69
+ * Opener frontier. The earliest occurrence wins; trailing fragments that
70
+ * could start one are held back from settled text.
71
+ */
72
+ readonly openers: readonly string[];
73
+ /** Classifies an opener candidate found at a scan position. */
74
+ readonly classifyOpener: (
75
+ pending: string,
76
+ index: number,
77
+ opener: string,
78
+ ) => MarkupOpenerClassification<Call>;
79
+ /**
80
+ * Policy when a block's closer has not arrived yet. Default `"hold"`: keep
81
+ * the whole pending buffer from the opener until a later chunk completes or
82
+ * disproves the block. Returning `{ kind: "passThrough" }` fails open:
83
+ * everything from the opener is emitted as text immediately (the DSML
84
+ * block-size cap).
85
+ */
86
+ readonly onUnclosed?: (
87
+ pending: string,
88
+ openIndex: number,
89
+ ) => "hold" | { readonly kind: "passThrough" };
90
+ /**
91
+ * Tail policy at flush. Returns the text to emit for whatever remains in the
92
+ * buffer (default: everything, i.e. pass through). A grammar that discards
93
+ * unterminated envelopes returns `""` when the tail looks like an opener.
94
+ */
95
+ readonly flush?: (pending: string) => string;
96
+ /** Call factory for recovered calls, unless a block overrides it. */
97
+ readonly callId?: MarkupCallFactory<Call>;
98
+ }
99
+
100
+ export class MarkupStreamParser<Call extends { readonly id: string }> {
101
+ readonly #options: MarkupStreamParserOptions<Call>;
102
+ #pending = "";
103
+ #callSequence = 0;
104
+
105
+ constructor(options: MarkupStreamParserOptions<Call>) {
106
+ this.#options = options;
107
+ }
108
+
109
+ /** Feeds one chunk and returns only settled text and complete calls. */
110
+ push(chunk: string): MarkupStreamParseResult<Call> {
111
+ this.#pending += chunk;
112
+ let cursor = 0;
113
+ let visible = "";
114
+ const calls: Call[] = [];
115
+ for (;;) {
116
+ let openIndex = -1;
117
+ let opener = "";
118
+ for (const candidate of this.#options.openers) {
119
+ const index = this.#pending.indexOf(candidate, cursor);
120
+ if (index >= 0 && (openIndex < 0 || index < openIndex)) {
121
+ openIndex = index;
122
+ opener = candidate;
123
+ }
124
+ }
125
+ if (openIndex < 0) {
126
+ const safeEnd = this.#settledEnd(cursor);
127
+ visible += this.#pending.slice(cursor, safeEnd);
128
+ cursor = safeEnd;
129
+ break;
130
+ }
131
+ visible += this.#pending.slice(cursor, openIndex);
132
+ const classified = this.#options.classifyOpener(this.#pending, openIndex, opener);
133
+ if (classified.kind === "literal") {
134
+ visible += "<";
135
+ cursor = openIndex + 1;
136
+ continue;
137
+ }
138
+ if (classified.kind === "hold") {
139
+ cursor = openIndex;
140
+ break;
141
+ }
142
+ const closeIndex = this.#pending.indexOf(classified.closer, openIndex + opener.length);
143
+ if (closeIndex < 0) {
144
+ const decision = this.#options.onUnclosed?.(this.#pending, openIndex) ?? "hold";
145
+ if (decision === "hold") {
146
+ cursor = openIndex;
147
+ break;
148
+ }
149
+ visible += this.#pending.slice(openIndex);
150
+ cursor = this.#pending.length;
151
+ break;
152
+ }
153
+ const end = closeIndex + classified.closer.length;
154
+ const block = this.#pending.slice(openIndex, end);
155
+ const parsed = classified.parse(block);
156
+ if (parsed === undefined) {
157
+ visible += block;
158
+ } else {
159
+ const callFor = classified.callId ?? this.#options.callId;
160
+ if (callFor === undefined) {
161
+ throw new Error("MarkupStreamParser: no call factory configured");
162
+ }
163
+ parsed.forEach((call, index) => {
164
+ calls.push(callFor(call, index, this.#callSequence++));
165
+ });
166
+ }
167
+ cursor = end;
168
+ }
169
+ this.#pending = this.#pending.slice(cursor);
170
+ return { text: visible, calls };
171
+ }
172
+
173
+ /** Settles trailing text per the configured flush policy. */
174
+ flush(): MarkupStreamParseResult<Call> {
175
+ const text = this.#options.flush?.(this.#pending) ?? this.#pending;
176
+ this.#pending = "";
177
+ return { text, calls: [] };
178
+ }
179
+
180
+ /** Holds back a trailing fragment that could be the start of an opener. */
181
+ #settledEnd(start: number): number {
182
+ const remaining = this.#pending.slice(start);
183
+ const maxOpener = this.#options.openers.reduce(
184
+ (max, open) => Math.max(max, open.length - 1),
185
+ 0,
186
+ );
187
+ for (let length = Math.min(maxOpener, remaining.length); length > 0; length -= 1) {
188
+ const suffix = remaining.slice(-length);
189
+ if (this.#options.openers.some((open) => open.startsWith(suffix))) {
190
+ return this.#pending.length - length;
191
+ }
192
+ }
193
+ return this.#pending.length;
194
+ }
195
+ }
@@ -28,3 +28,22 @@ export function reasoningTextOf(part: ReasoningTextSource): string | undefined {
28
28
  if (typeof summary === "string" && summary.length > 0) return summary;
29
29
  return undefined;
30
30
  }
31
+
32
+ /**
33
+ * The one reader for reasoning text replayed to upstream providers.
34
+ *
35
+ * Request builders echo the previous turn's reasoning back to a provider, and
36
+ * they deliberately prefer `summary` (the digest) ahead of `payload` (the
37
+ * stream) — the opposite of `reasoningTextOf`, which is for display. Returning
38
+ * `undefined` when neither field is a string lets each call site keep its exact
39
+ * fallback ("" for the chat/messages/gemini builders, the raw payload for
40
+ * Qoder's echo), so upstream-visible bytes do not change. Opaque parts are not
41
+ * special-cased here: the replay readers never consulted `opaque` either.
42
+ */
43
+ export function reasoningTextForReplay(part: ReasoningTextSource): string | undefined {
44
+ const summary = part.summary;
45
+ if (typeof summary === "string") return summary;
46
+ const payload = part.payload;
47
+ if (typeof payload === "string") return payload;
48
+ return undefined;
49
+ }
@@ -16,6 +16,7 @@ import { clampThinkingLevelForModel } from "../../protocol/translation/dialect/m
16
16
  import { isClaudeBillingHeaderText, resolveImageSource } from "../codecs";
17
17
  import { resolvePromptCacheKey } from "../../providers/operations/session-resolution";
18
18
  import { reasoningEffortFromIntent } from "../../protocol/reasoning";
19
+ import { reasoningTextForReplay } from "../reasoning-text";
19
20
  import { parseChatResponseToEvents, decodeChatSseStream } from "../response/chat";
20
21
 
21
22
  /** Fallback when no resolved policy is supplied — today's exact behavior. */
@@ -220,6 +221,13 @@ export function canonicalToChatPayload(
220
221
  }
221
222
  }
222
223
  for (const m of request.messages) {
224
+ // Reasoning parts and their presence are computed once per message and
225
+ // shared by every branch below; the presence gate is on the *existence* of
226
+ // a reasoning part, not on non-empty text (see the comment at the first use).
227
+ const reasoningParts = m.content.filter(
228
+ (c): c is Extract<typeof c, { kind: "reasoning" }> => c.kind === "reasoning",
229
+ );
230
+ const hasReasoning = reasoningParts.length > 0;
223
231
  if (m.role === "assistant" && m.content.some((c) => c.kind === "toolCall")) {
224
232
  const toolCalls = m.content
225
233
  .filter((c): c is Extract<typeof c, { kind: "toolCall" }> => c.kind === "toolCall")
@@ -251,16 +259,12 @@ export function canonicalToChatPayload(
251
259
  // thinking mode" (CodeBuddy 400). Chat Completions has no
252
260
  // reasoning content part, so it travels in the provider-native
253
261
  // `reasoning_content` side channel, same as the branches below.
254
- const reasoningContent = m.content
255
- .filter((c): c is Extract<typeof c, { kind: "reasoning" }> => c.kind === "reasoning")
256
- .map((c) => c.summary ?? (typeof c.payload === "string" ? c.payload : ""))
257
- .join("\n");
262
+ const reasoningContent = reasoningParts.map((c) => reasoningTextForReplay(c) ?? "").join("\n");
258
263
  // Gated on the *presence* of a reasoning part, not on non-empty text:
259
264
  // with `display: "omitted"` the provider emits a thinking block whose
260
265
  // text is empty but whose signature marks the turn as a thinking turn,
261
266
  // and the upstream still demands the field back. Omitting it there
262
267
  // reproduces the same 400, so an empty string is the correct value.
263
- const hasReasoning = m.content.some((c) => c.kind === "reasoning");
264
268
  messages.push({
265
269
  role: "assistant",
266
270
  content: effectiveContent,
@@ -295,13 +299,9 @@ export function canonicalToChatPayload(
295
299
  content: p.is_error === true ? `[tool_error] ${resultContent}` : resultContent,
296
300
  });
297
301
  }
298
- const reasoningContent = m.content
299
- .filter((c): c is Extract<typeof c, { kind: "reasoning" }> => c.kind === "reasoning")
300
- .map((c) => c.summary ?? (typeof c.payload === "string" ? c.payload : ""))
301
- .join("\n");
302
+ const reasoningContent = reasoningParts.map((c) => reasoningTextForReplay(c) ?? "").join("\n");
302
303
  // Presence, not length: an `omitted`-display thinking block carries an
303
304
  // empty text with a signature, and the field is still required back.
304
- const hasReasoning = m.content.some((c) => c.kind === "reasoning");
305
305
  const hasRichContent = m.content.some(
306
306
  (c) => c.kind === "image" || c.kind === "file" || c.kind === "audio" || c.kind === "document",
307
307
  );
@@ -15,6 +15,7 @@ import {
15
15
  type RewriteWalk,
16
16
  } from "../codecs";
17
17
  import { geminiThinkingOutputFloor } from "../../protocol/reasoning";
18
+ import { reasoningTextForReplay } from "../reasoning-text";
18
19
  import { base64DecodedByteLength, clampImage } from "../image-clamp";
19
20
 
20
21
  export function geminiModelUrl(base: string, model: string, action: string): string {
@@ -151,8 +152,13 @@ function toGeminiPart(
151
152
  ): Record<string, unknown>[] {
152
153
  if (part.kind === "text") return [{ text: part.text }];
153
154
  if (part.kind === "reasoning") {
154
- const summary = part.summary ?? (typeof part.payload === "string" ? part.payload : "");
155
- return [{ text: summary, thought: true, ...(part.signature ? { thoughtSignature: part.signature } : {}) }];
155
+ // A `thinking` part must carry a non-empty signature, or Gemini-family
156
+ // upstreams reject the whole request with `400 ... thinking.signature:
157
+ // Field required`. Drop unsigned reasoning blocks (missing *or* empty
158
+ // signature) so the conversation replays cleanly.
159
+ if (typeof part.signature !== "string" || part.signature.length === 0) return [];
160
+ const summary = reasoningTextForReplay(part) ?? "";
161
+ return [{ text: summary, thought: true, thoughtSignature: part.signature }];
156
162
  }
157
163
  if (part.kind === "image") {
158
164
  // Resolution goes through the shared `resolveImageSource` primitive rather
@@ -27,6 +27,7 @@ import {
27
27
  splitDataUrl,
28
28
  finiteNumber,
29
29
  } from "../codecs";
30
+ import { reasoningTextForReplay } from "../reasoning-text";
30
31
  import { log } from "../../observability/logger";
31
32
  import { base64DecodedByteLength, clampImage } from "../image-clamp";
32
33
  import { LEVEL_TO_BUDGET } from "../../protocol/translation/thinking";
@@ -374,7 +375,7 @@ function partToClaudeBlock(
374
375
  !part.opaque &&
375
376
  (typeof part.signature !== "string" || part.signature.trim().length === 0)
376
377
  ) {
377
- const text = part.summary ?? (stringValue(part.payload) ? part.payload : "");
378
+ const text = reasoningTextForReplay(part) ?? "";
378
379
  return text.length > 0 ? { type: "text", text: toWellFormedString(text) } : undefined;
379
380
  }
380
381
  if (part.opaque) {
@@ -384,9 +385,7 @@ function partToClaudeBlock(
384
385
  }
385
386
  return {
386
387
  type: "thinking",
387
- thinking: toWellFormedString(
388
- part.summary ?? (stringValue(part.payload) ? part.payload : ""),
389
- ),
388
+ thinking: toWellFormedString(reasoningTextForReplay(part) ?? ""),
390
389
  ...(part.signature === undefined
391
390
  ? {}
392
391
  : { signature: toWellFormedString(part.signature) }),
@@ -9,7 +9,7 @@ import { gatewayErrorFromStreamError } from "../stream-error-frames";
9
9
  import { mergeResponsesUsage } from "./responses";
10
10
  import { usageFromProvider, readReasoningText } from "../../providers/provider-usage";
11
11
  import { createToolEmitLedger } from "../tool-identity";
12
- import { ThinkSplitter, stripThinkingTags } from "../think-split";
12
+ import { ThinkingTagStripper, ThinkSplitter, stripThinkingTags } from "../think-split";
13
13
  import { DsmlStreamParser, parseDsmlToolCalls } from "../dsml";
14
14
  import {
15
15
  OmpToolCallMarkupParser,
@@ -267,10 +267,13 @@ export async function* decodeChatSseStream(
267
267
  // Single emission ledger: one entry per logical call.
268
268
  const ledger = createToolEmitLedger();
269
269
  const think = new ThinkSplitter();
270
- // Separate splitter for the reasoning channel: `reasoning_content` is its own
270
+ // Separate stripper for the reasoning channel: `reasoning_content` is its own
271
271
  // stream, and interleaving it into the answer splitter would let a tag in one
272
- // channel swallow the other's text.
273
- const reasoningThink = new ThinkSplitter();
272
+ // channel swallow the other's text. The reasoning channel is thought in its
273
+ // entirety — untagged prose included — so it strips paired tags statefully
274
+ // while preserving source order instead of splitting into text/reasoning
275
+ // buckets that a `${reasoning}${text}` join would reorder.
276
+ const reasoningThink = new ThinkingTagStripper();
274
277
  const declaredToolNames = declaredToolNamesForRequest(request);
275
278
  const callableToolNames = callableToolNamesForRequest(request);
276
279
  const contentMarkup =
@@ -364,21 +367,13 @@ export async function* decodeChatSseStream(
364
367
  typeof reasoningRaw === "string" ? reasoningMarkup?.push(reasoningRaw) : undefined;
365
368
  // Strip stray `<think>` tags the upstream double-wrapped inside its own
366
369
  // reasoning stream (buddy's hy4-preview-f leaks them per-delta). This
367
- // is the splitter's job, not `stripThinkingTags`'s: a stateless strip
370
+ // is the stripper's job, not `stripThinkingTags`'s: a stateless strip
368
371
  // cannot see a tag split across two deltas, so `"<thi"` was emitted
369
372
  // verbatim and only the following `</think>` removed.
370
- const reasoningSplit =
373
+ const reasoningText =
371
374
  typeof reasoningRaw === "string"
372
375
  ? reasoningThink.push(reasoningMarkupResult?.text ?? reasoningRaw)
373
376
  : undefined;
374
- // Both sides are reasoning. The splitter routes text outside a
375
- // `<think>` span to its `.text` side, but this field is the reasoning
376
- // stream in its entirety: untagged prose here is still thought, not
377
- // answer. Taking only `.reasoning` dropped every un-tagged delta.
378
- const reasoningText =
379
- reasoningSplit === undefined
380
- ? undefined
381
- : `${reasoningSplit.reasoning}${reasoningSplit.text}`;
382
377
  if (typeof reasoningText === "string" && reasoningText.length > 0) {
383
378
  yield {
384
379
  type: "content_delta",
@@ -538,9 +533,7 @@ export async function* decodeChatSseStream(
538
533
  }
539
534
  const reasoningMarkupTail = reasoningMarkup?.flush();
540
535
  if (reasoningMarkupTail?.text) {
541
- const split = reasoningThink.push(reasoningMarkupTail.text);
542
- // Both sides are reasoning: see the per-delta branch above.
543
- const reasoningText = `${split.reasoning}${split.text}`;
536
+ const reasoningText = reasoningThink.push(reasoningMarkupTail.text);
544
537
  if (reasoningText.length > 0) {
545
538
  yield {
546
539
  type: "content_delta",
@@ -549,10 +542,9 @@ export async function* decodeChatSseStream(
549
542
  } as CanonicalEvent;
550
543
  }
551
544
  }
552
- // Release anything the reasoning splitter held for a tag that never
545
+ // Release anything the reasoning stripper held for a tag that never
553
546
  // completed, before `think.flush()` and the terminal.
554
- const reasoningRest = reasoningThink.flush();
555
- const reasoningTail = `${reasoningRest.reasoning}${reasoningRest.text}`;
547
+ const reasoningTail = reasoningThink.flush();
556
548
  if (reasoningTail.length > 0) {
557
549
  yield {
558
550
  type: "content_delta",
@@ -7,6 +7,7 @@
7
7
  * interpreted. Calls are returned only when their names were declared on the
8
8
  * request; an undeclared name is never promoted into executable tool input.
9
9
  */
10
+ import { MarkupStreamParser } from "../markup-stream-parser";
10
11
  export interface OmpTextToolCall {
11
12
  readonly id: string;
12
13
  readonly name: string;
@@ -29,77 +30,49 @@ const TOOL_NAME_PATTERN = /^[A-Za-z0-9_][A-Za-z0-9_.:-]{0,63}$/;
29
30
  * The parser holds a possible envelope across SSE deltas, so a split opening
30
31
  * tag, argument tag, or closing tag is never emitted as visible prose. At
31
32
  * end-of-stream an unterminated envelope is dropped, not executed or leaked.
33
+ *
34
+ * The stateful prefix-buffer loop is the shared {@link MarkupStreamParser}
35
+ * scaffold; this class supplies the OMP grammar as a thin configuration.
32
36
  */
33
37
  export class OmpToolCallMarkupParser {
34
- readonly #allowedNames: ReadonlySet<string>;
35
- #pending = "";
36
- #callSequence = 0;
38
+ readonly #parser: MarkupStreamParser<OmpTextToolCall>;
37
39
 
38
40
  constructor(allowedNames: ReadonlySet<string>) {
39
- this.#allowedNames = allowedNames;
41
+ this.#parser = new MarkupStreamParser<OmpTextToolCall>({
42
+ openers: [OPEN_PREFIX],
43
+ classifyOpener: (pending, openIndex) => {
44
+ const remainder = pending.slice(openIndex);
45
+ const opening = OPEN_PATTERN.exec(remainder);
46
+ if (opening === null || opening[1] === undefined) {
47
+ if (couldBeOpening(remainder)) return { kind: "hold" };
48
+ return { kind: "literal" };
49
+ }
50
+ const marker = opening[1];
51
+ const closeTag = `</tool_calls:${marker}>`;
52
+ return {
53
+ kind: "found",
54
+ closer: closeTag,
55
+ parse: (block) => {
56
+ const parsed = parseEnvelope(block, marker);
57
+ if (parsed === undefined) return undefined;
58
+ if (parsed.some((call) => !allowedNames.has(call.name))) {
59
+ // A syntactically valid call to an undeclared tool is model
60
+ // text, not authority to invoke a tool the client did not offer.
61
+ return undefined;
62
+ }
63
+ return parsed;
64
+ },
65
+ callId: (call, _index, seq) => ({ ...call, id: `call_omp_${marker}_${seq}` }),
66
+ };
67
+ },
68
+ flush: (pending) =>
69
+ couldBeOpening(pending) || OPEN_PATTERN.test(pending) ? "" : pending,
70
+ });
40
71
  }
41
72
 
42
73
  /** Feeds one content or reasoning delta and returns only settled text. */
43
74
  push(chunk: string): OmpTextToolCallResult {
44
- this.#pending += chunk;
45
- let cursor = 0;
46
- let text = "";
47
- const calls: OmpTextToolCall[] = [];
48
-
49
- for (;;) {
50
- const openIndex = this.#pending.indexOf(OPEN_PREFIX, cursor);
51
- if (openIndex < 0) {
52
- const safeEnd = this.#settledEnd(cursor);
53
- text += this.#pending.slice(cursor, safeEnd);
54
- cursor = safeEnd;
55
- break;
56
- }
57
-
58
- text += this.#pending.slice(cursor, openIndex);
59
- const remainder = this.#pending.slice(openIndex);
60
- const opening = OPEN_PATTERN.exec(remainder);
61
- if (opening === null || opening[1] === undefined) {
62
- if (this.#couldBeOpening(remainder)) {
63
- cursor = openIndex;
64
- break;
65
- }
66
- text += "<";
67
- cursor = openIndex + 1;
68
- continue;
69
- }
70
-
71
- const marker = opening[1];
72
- const openTag = opening[0];
73
- const closeTag = `</tool_calls:${marker}>`;
74
- const closeIndex = this.#pending.indexOf(closeTag, openIndex + openTag.length);
75
- if (closeIndex < 0) {
76
- cursor = openIndex;
77
- break;
78
- }
79
-
80
- const envelopeEnd = closeIndex + closeTag.length;
81
- const block = this.#pending.slice(openIndex, envelopeEnd);
82
- const parsed = parseEnvelope(block, marker);
83
- if (parsed === undefined) {
84
- text += block;
85
- } else if (parsed.some((call) => !this.#allowedNames.has(call.name))) {
86
- // A syntactically valid call to an undeclared tool is model text, not
87
- // authority to invoke a tool the client did not offer.
88
- text += block;
89
- } else {
90
- for (const call of parsed) {
91
- calls.push({
92
- id: `call_omp_${marker}_${this.#callSequence++}`,
93
- name: call.name,
94
- arguments: call.arguments,
95
- });
96
- }
97
- }
98
- cursor = envelopeEnd;
99
- }
100
-
101
- this.#pending = this.#pending.slice(cursor);
102
- return { text, calls };
75
+ return this.#parser.push(chunk);
103
76
  }
104
77
 
105
78
  /**
@@ -107,31 +80,15 @@ export class OmpToolCallMarkupParser {
107
80
  * an unfinished envelope is deliberately discarded rather than leaked.
108
81
  */
109
82
  flush(): OmpTextToolCallResult {
110
- const pending = this.#pending;
111
- this.#pending = "";
112
- if (this.#couldBeOpening(pending) || OPEN_PATTERN.test(pending)) {
113
- return { text: "", calls: [] };
114
- }
115
- return { text: pending, calls: [] };
116
- }
117
-
118
- #settledEnd(start: number): number {
119
- const remaining = this.#pending.slice(start);
120
- const maxPrefixLength = Math.min(OPEN_PREFIX.length - 1, remaining.length);
121
- for (let length = maxPrefixLength; length > 0; length -= 1) {
122
- if (OPEN_PREFIX.startsWith(remaining.slice(-length))) {
123
- return this.#pending.length - length;
124
- }
125
- }
126
- return this.#pending.length;
83
+ return this.#parser.flush();
127
84
  }
85
+ }
128
86
 
129
- #couldBeOpening(value: string): boolean {
130
- if (value.length < OPEN_PREFIX.length) return OPEN_PREFIX.startsWith(value);
131
- if (!value.startsWith(OPEN_PREFIX)) return false;
132
- const suffix = value.slice(OPEN_PREFIX.length);
133
- return /^[0-9a-f]{0,8}$/.test(suffix) || /^[0-9a-f]{8}$/.test(suffix) && !value.endsWith(">");
134
- }
87
+ function couldBeOpening(value: string): boolean {
88
+ if (value.length < OPEN_PREFIX.length) return OPEN_PREFIX.startsWith(value);
89
+ if (!value.startsWith(OPEN_PREFIX)) return false;
90
+ const suffix = value.slice(OPEN_PREFIX.length);
91
+ return /^[0-9a-f]{0,8}$/.test(suffix) || /^[0-9a-f]{8}$/.test(suffix) && !value.endsWith(">");
135
92
  }
136
93
 
137
94
  /**
@@ -274,7 +274,16 @@ export function parseResponsesResponseToEvents(
274
274
  canonicalTerminal({
275
275
  sequenceNumber: events.length + 1,
276
276
  responseId: id,
277
- state: status === "cancelled" ? "aborted" : status === "failed" ? "failed" : "complete",
277
+ // `incomplete` maps to `aborted` (not `complete`): the surface encoder
278
+ // turns `aborted` into `response.incomplete`, while `complete` would
279
+ // emit `response.completed` for a response the upstream declared
280
+ // unfinished (max output tokens / content filter).
281
+ state:
282
+ status === "cancelled" || status === "incomplete"
283
+ ? "aborted"
284
+ : status === "failed"
285
+ ? "failed"
286
+ : "complete",
278
287
  stopReason: respJsonStopReason,
279
288
  providerStopReason: respJsonProviderStopReason,
280
289
  usage: usageFromProvider(usage),
@@ -740,8 +749,15 @@ export async function* decodeResponsesSseStream(
740
749
  const truncated = status === undefined;
741
750
  yield canonicalTerminal({
742
751
  sequenceNumber: seq++,
752
+ // `incomplete` maps to `aborted` (not `complete`): the surface encoder
753
+ // turns `aborted` into `response.incomplete`, while `complete` would
754
+ // emit `response.completed` for a stream the upstream declared unfinished.
743
755
  state:
744
- status === "cancelled" ? "aborted" : status === "failed" || truncated ? "failed" : "complete",
756
+ status === "cancelled" || status === "incomplete"
757
+ ? "aborted"
758
+ : status === "failed" || truncated
759
+ ? "failed"
760
+ : "complete",
745
761
  stopReason: responsesStopReason,
746
762
  providerStopReason: responsesProviderStopReason,
747
763
  usage: usageFromProvider(rawUsage),
@@ -1,4 +1,4 @@
1
- import type { CanonicalStopReason } from "./canonical-model";
1
+ import type { CanonicalStopReason, CanonicalTerminalEvent } from "./canonical-model";
2
2
 
3
3
  /**
4
4
  * Canonical stop reason mapped onto each surface's wire vocabulary.
@@ -56,6 +56,28 @@ export function chatFinishReason(reason: CanonicalStopReason | undefined): strin
56
56
  return canonicalStopReasonToWire("chat", reason, "stop");
57
57
  }
58
58
 
59
+ /**
60
+ * Completion's finish_reason, or `null` when the terminal recorded none.
61
+ *
62
+ * Completion inherits chat's wire vocabulary (`length`, `content_filter`,
63
+ * `stop`) but keeps its own convention: a terminal that is not complete
64
+ * (failed/aborted) already surfaced its failure elsewhere, so it yields
65
+ * `null` rather than a stop word, and a complete terminal flattens every
66
+ * reason other than `length`/`content_filter` to `stop`. The sync adapter and
67
+ * the streaming encoder both derive through this function so the two halves
68
+ * of the completion surface cannot drift.
69
+ */
70
+ export function completionFinishReason(
71
+ reason: CanonicalStopReason | undefined,
72
+ state: CanonicalTerminalEvent["state"],
73
+ ): string | null {
74
+ if (state !== "complete") return null;
75
+ if (reason === "length" || reason === "content_filter") {
76
+ return canonicalStopReasonToWire("chat", reason, "stop");
77
+ }
78
+ return "stop";
79
+ }
80
+
59
81
  /** Messages' stop_reason, or `null` when the terminal recorded none. */
60
82
  export function messagesStopReason(reason: CanonicalStopReason | undefined): string | null {
61
83
  return reason === undefined ? null : canonicalStopReasonToWire("messages", reason, "end_turn");