cartethyia 2.7.8 → 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 (519) hide show
  1. package/.env.example +3 -3
  2. package/README.md +29 -59
  3. package/dist/dashboard/assets/BannedPage-Cc0vKpMG.js +1 -0
  4. package/dist/dashboard/assets/BannedPage-Cc0vKpMG.js.gz +0 -0
  5. package/dist/dashboard/assets/{CliToolDetailPage-BRWKPQPf.js → CliToolDetailPage-Bqj2aa1T.js} +1 -1
  6. package/dist/dashboard/assets/CliToolDetailPage-Bqj2aa1T.js.gz +0 -0
  7. package/dist/dashboard/assets/{CliToolsPage-BP-Y5iHN.js → CliToolsPage-CXsaOAO2.js} +1 -1
  8. package/dist/dashboard/assets/CliToolsPage-CXsaOAO2.js.gz +0 -0
  9. package/dist/dashboard/assets/CombosPage-BeLpJ4ma.js +1 -0
  10. package/dist/dashboard/assets/CombosPage-BeLpJ4ma.js.gz +0 -0
  11. package/dist/dashboard/assets/ConsoleLogPage-DnV0uZ3m.js +1 -0
  12. package/dist/dashboard/assets/ConsoleLogPage-DnV0uZ3m.js.gz +0 -0
  13. package/dist/dashboard/assets/CustomizationPage-CBxAjo63.js +1 -0
  14. package/dist/dashboard/assets/CustomizationPage-CBxAjo63.js.gz +0 -0
  15. package/dist/dashboard/assets/HealthEventsModal-B5yEE0k6.js +1 -0
  16. package/dist/dashboard/assets/HealthEventsModal-B5yEE0k6.js.gz +0 -0
  17. package/dist/dashboard/assets/{InjectSystemPromptPanel-Cv-2Ol_K.js → InjectSystemPromptPanel-BWFLECmz.js} +2 -2
  18. package/dist/dashboard/assets/InjectSystemPromptPanel-BWFLECmz.js.gz +0 -0
  19. package/dist/dashboard/assets/{LoginPage-bKfz_b1K.js → LoginPage-CgoPc5B9.js} +1 -1
  20. package/dist/dashboard/assets/LoginPage-CgoPc5B9.js.gz +0 -0
  21. package/dist/dashboard/assets/{ModelPicker-B-WQxx_H.js → ModelPicker-D9bFQern.js} +1 -1
  22. package/dist/dashboard/assets/ModelPicker-D9bFQern.js.gz +0 -0
  23. package/dist/dashboard/assets/OverviewPage-B_p4tL8k.js +1 -0
  24. package/dist/dashboard/assets/OverviewPage-B_p4tL8k.js.gz +0 -0
  25. package/dist/dashboard/assets/ProviderDetailPage-rs5E2rxv.js +8 -0
  26. package/dist/dashboard/assets/ProviderDetailPage-rs5E2rxv.js.gz +0 -0
  27. package/dist/dashboard/assets/ProviderIcon-D_0PiZtg.js +1 -0
  28. package/dist/dashboard/assets/ProviderIcon-D_0PiZtg.js.gz +0 -0
  29. package/dist/dashboard/assets/ProvidersPage-CLI2bJTK.js +1 -0
  30. package/dist/dashboard/assets/ProvidersPage-CLI2bJTK.js.gz +0 -0
  31. package/dist/dashboard/assets/{ProxyPage-emlTuxz6.js → ProxyPage-BwNC_-Cx.js} +1 -1
  32. package/dist/dashboard/assets/ProxyPage-BwNC_-Cx.js.gz +0 -0
  33. package/dist/dashboard/assets/QuotaPage-DA-a1iSN.js +1 -0
  34. package/dist/dashboard/assets/QuotaPage-DA-a1iSN.js.gz +0 -0
  35. package/dist/dashboard/assets/SettingsPage-CctTga2C.js +1 -0
  36. package/dist/dashboard/assets/SettingsPage-CctTga2C.js.gz +0 -0
  37. package/dist/dashboard/assets/{SetupPage-CUZrKs9r.js → SetupPage-febLU4r1.js} +1 -1
  38. package/dist/dashboard/assets/SetupPage-febLU4r1.js.gz +0 -0
  39. package/dist/dashboard/assets/{StudioPage-jWmUdDpy.js → StudioPage-M6fvR5Wz.js} +1 -1
  40. package/dist/dashboard/assets/StudioPage-M6fvR5Wz.js.gz +0 -0
  41. package/dist/dashboard/assets/UsagePage-nyeuRqz9.js +56 -0
  42. package/dist/dashboard/assets/UsagePage-nyeuRqz9.js.gz +0 -0
  43. package/dist/dashboard/assets/api-keys-BFOlvrJC.js +1 -0
  44. package/dist/dashboard/assets/api-keys-BFOlvrJC.js.gz +0 -0
  45. package/dist/dashboard/assets/app-Dx2QgsNj.css +2 -0
  46. package/dist/dashboard/assets/app-Dx2QgsNj.css.gz +0 -0
  47. package/dist/dashboard/assets/{assert-CidQVoNo.js → assert-CqTLw5e7.js} +1 -1
  48. package/dist/dashboard/assets/assert-CqTLw5e7.js.gz +0 -0
  49. package/dist/dashboard/assets/{badge-DyI_mJfi.js → badge-C83EqLB0.js} +1 -1
  50. package/dist/dashboard/assets/badge-C83EqLB0.js.gz +0 -0
  51. package/dist/dashboard/assets/ban-durations-B58FyRxC.js +1 -0
  52. package/dist/dashboard/assets/ban-durations-B58FyRxC.js.gz +0 -0
  53. package/dist/dashboard/assets/{banner-B-yXtCqb.js → banner-D1xn7pIe.js} +1 -1
  54. package/dist/dashboard/assets/banner-D1xn7pIe.js.gz +0 -0
  55. package/dist/dashboard/assets/brand/bansos_page.webp +0 -0
  56. package/dist/dashboard/assets/brand/denia_bansos.webp +0 -0
  57. package/dist/dashboard/assets/brand/favicon.webp +0 -0
  58. package/dist/dashboard/assets/brand/login_register.webp +0 -0
  59. package/dist/dashboard/assets/brand/login_register_initial.webp +0 -0
  60. package/dist/dashboard/assets/brand/og_bansos.webp +0 -0
  61. package/dist/dashboard/assets/brand/og_image.webp +0 -0
  62. package/dist/dashboard/assets/brand/sidebar_ico.webp +0 -0
  63. package/dist/dashboard/assets/{card-_HwAEqFF.js → card-DU3Lq5Lm.js} +1 -1
  64. package/dist/dashboard/assets/card-DU3Lq5Lm.js.gz +0 -0
  65. package/dist/dashboard/assets/cli-tools-Dw1U_drK.js +1 -0
  66. package/dist/dashboard/assets/cli-tools-Dw1U_drK.js.gz +0 -0
  67. package/dist/dashboard/assets/{clipboard-button-ByjCk0o9.js → clipboard-button-CEL5Sg_J.js} +1 -1
  68. package/dist/dashboard/assets/clipboard-button-CEL5Sg_J.js.gz +0 -0
  69. package/dist/dashboard/assets/{dialog-BZhOlfz-.js → dialog-BjFR5UHy.js} +1 -1
  70. package/dist/dashboard/assets/dialog-BjFR5UHy.js.gz +0 -0
  71. package/dist/dashboard/assets/{drawer-BADqIRYP.js → drawer-HvBW0AjI.js} +1 -1
  72. package/dist/dashboard/assets/drawer-HvBW0AjI.js.gz +0 -0
  73. package/dist/dashboard/assets/entry-BSu3gFlt.js +4 -0
  74. package/dist/dashboard/assets/entry-BSu3gFlt.js.gz +0 -0
  75. package/dist/dashboard/assets/{entry-COaC-irN.js → entry-CECVELyy.js} +1 -1
  76. package/dist/dashboard/assets/entry-CECVELyy.js.gz +0 -0
  77. package/dist/dashboard/assets/entry-DYYofQ7W.js +1 -0
  78. package/dist/dashboard/assets/entry-DYYofQ7W.js.gz +0 -0
  79. package/dist/dashboard/assets/{formatters-DcHJyRiu.js → formatters-DLW4i8zK.js} +1 -1
  80. package/dist/dashboard/assets/formatters-DLW4i8zK.js.gz +0 -0
  81. package/dist/dashboard/assets/{index-CgKmFngv.js → index-1-fCqe64.js} +2 -2
  82. package/dist/dashboard/assets/index-1-fCqe64.js.gz +0 -0
  83. package/dist/dashboard/assets/{inline-B2BirKry.js → inline-DGvU2oLK.js} +1 -1
  84. package/dist/dashboard/assets/inline-DGvU2oLK.js.gz +0 -0
  85. package/dist/dashboard/assets/{input-D-qV9dKY.js → input-iNnO1X2K.js} +1 -1
  86. package/dist/dashboard/assets/input-iNnO1X2K.js.gz +0 -0
  87. package/dist/dashboard/assets/{layout-B7tVWhtV.js → layout-0q8UGypb.js} +1 -1
  88. package/dist/dashboard/assets/layout-0q8UGypb.js.gz +0 -0
  89. package/dist/dashboard/assets/{list-row-CtkGE3bF.js → list-row-C0w2bQjJ.js} +1 -1
  90. package/dist/dashboard/assets/list-row-C0w2bQjJ.js.gz +0 -0
  91. package/dist/dashboard/assets/{live-DGizF06-.js → live-M5aJ_xCa.js} +1 -1
  92. package/dist/dashboard/assets/live-M5aJ_xCa.js.gz +0 -0
  93. package/dist/dashboard/assets/{mutation-GU7XdEjz.js → mutation-BQ0ccH_A.js} +1 -1
  94. package/dist/dashboard/assets/mutation-BQ0ccH_A.js.gz +0 -0
  95. package/dist/dashboard/assets/{network-C9UE-bOr.js → network-BKeQGT3K.js} +1 -1
  96. package/dist/dashboard/assets/network-BKeQGT3K.js.gz +0 -0
  97. package/dist/dashboard/assets/{numbers-BnAd22a_.js → numbers-CivAk3da.js} +1 -1
  98. package/dist/dashboard/assets/numbers-CivAk3da.js.gz +0 -0
  99. package/dist/dashboard/assets/{pagination-BAywyMzS.js → pagination-CpzvI5ep.js} +1 -1
  100. package/dist/dashboard/assets/pagination-CpzvI5ep.js.gz +0 -0
  101. package/dist/dashboard/assets/providers/devin.svg +9 -0
  102. package/dist/dashboard/assets/providers/devin.svg.gz +0 -0
  103. package/dist/dashboard/assets/providers/windsurf.svg +10 -0
  104. package/dist/dashboard/assets/providers/windsurf.svg.gz +0 -0
  105. package/dist/dashboard/assets/providers/zro.svg +3 -0
  106. package/dist/dashboard/assets/providers/zro.svg.gz +0 -0
  107. package/dist/dashboard/assets/{query-keys-YSoQOUh6.js → query-keys-IKLh0YFd.js} +1 -1
  108. package/dist/dashboard/assets/query-keys-IKLh0YFd.js.gz +0 -0
  109. package/dist/dashboard/assets/quota-DiPD6UKH.js +1 -0
  110. package/dist/dashboard/assets/quota-DiPD6UKH.js.gz +0 -0
  111. package/dist/dashboard/assets/routing-BFQX2VBc.js +1 -0
  112. package/dist/dashboard/assets/routing-BFQX2VBc.js.gz +0 -0
  113. package/dist/dashboard/assets/{select-BCOcv8WK.js → select-CXbck0jG.js} +2 -2
  114. package/dist/dashboard/assets/select-CXbck0jG.js.gz +0 -0
  115. package/dist/dashboard/assets/settings-C_XvMHh9.js +1 -0
  116. package/dist/dashboard/assets/settings-C_XvMHh9.js.gz +0 -0
  117. package/dist/dashboard/assets/{stack-ByKt3Q1g.js → stack-DggRiCxE.js} +1 -1
  118. package/dist/dashboard/assets/stack-DggRiCxE.js.gz +0 -0
  119. package/dist/dashboard/assets/{state-DXbGTx7N.js → state-ClFzfbYp.js} +1 -1
  120. package/dist/dashboard/assets/state-ClFzfbYp.js.gz +0 -0
  121. package/dist/dashboard/assets/story/25817331.webp +0 -0
  122. package/dist/dashboard/assets/story/Shorekeeper.webp +0 -0
  123. package/dist/dashboard/assets/story/cartethyia-god.webp +0 -0
  124. package/dist/dashboard/assets/story/fleurdelys_plus.webp +0 -0
  125. package/dist/dashboard/assets/story/jinhsi-blossom.webp +0 -0
  126. package/dist/dashboard/assets/story/phrolova-a.webp +0 -0
  127. package/dist/dashboard/assets/story/requestdeniawokkjpg.webp +0 -0
  128. package/dist/dashboard/assets/story/wuthering-waves-hiyuki-aemeath-hiyuki-rover.webp +0 -0
  129. package/dist/dashboard/assets/{switch-ZeTsW623.js → switch-CTIFxKqS.js} +1 -1
  130. package/dist/dashboard/assets/switch-CTIFxKqS.js.gz +0 -0
  131. package/dist/dashboard/assets/{useMutation-CbCP4w1o.js → useMutation-BClG3yWj.js} +1 -1
  132. package/dist/dashboard/assets/useMutation-BClG3yWj.js.gz +0 -0
  133. package/dist/dashboard/console-backgrounds.webp +0 -0
  134. package/dist/dashboard/console-backgrounds_initial.webp +0 -0
  135. package/dist/dashboard/donation/qris-risun-marke.webp +0 -0
  136. package/dist/dashboard/index.html +3 -3
  137. package/dist/dashboard/index.html.gz +0 -0
  138. package/dist/dashboard/precompressed-assets.json +59 -56
  139. package/migrations/full-postgres/0044_add_models_warning_metadata.sql +10 -0
  140. package/migrations/full-postgres/0045_migrate_workbuddy_to_codebuddy.sql +156 -0
  141. package/migrations/full-postgres/0046_add_accounts_quota_plan_fetched_at.sql +7 -0
  142. package/migrations/lite-sqlite3/0044_add_models_warning_metadata.sql +11 -0
  143. package/migrations/lite-sqlite3/0045_migrate_workbuddy_to_codebuddy.sql +156 -0
  144. package/migrations/lite-sqlite3/0046_add_accounts_quota_plan_fetched_at.sql +4 -0
  145. package/package.json +1 -1
  146. package/src/boot/app.ts +10 -13
  147. package/src/boot/dependencies.ts +12 -4
  148. package/src/boot/flush.ts +20 -0
  149. package/src/boot/lifecycle.ts +7 -5
  150. package/src/config/cooldowns.ts +1 -1
  151. package/src/config/routing.ts +26 -0
  152. package/src/config/spec-metadata.ts +72 -0
  153. package/src/config/spec.ts +20 -6
  154. package/src/console/api-keys/contracts.ts +12 -1
  155. package/src/console/api-keys/routes.ts +50 -31
  156. package/src/console/auth/account-reset.ts +7 -9
  157. package/src/console/backup/contracts.ts +4 -0
  158. package/src/console/backup/export-endpoint.ts +7 -5
  159. package/src/console/cli-tools/service.ts +6 -12
  160. package/src/console/console-router-guards.ts +23 -16
  161. package/src/console/console-router.ts +7 -0
  162. package/src/console/dashboard-assets.ts +5 -5
  163. package/src/console/domain-registration.ts +27 -2
  164. package/src/console/inferhub/normalize.ts +24 -28
  165. package/src/console/inferhub/types.ts +3 -0
  166. package/src/console/ip-bans/routes.ts +93 -0
  167. package/src/console/observability/store.ts +0 -53
  168. package/src/console/providers/catalog/catalog-projections.ts +5 -2
  169. package/src/console/providers/catalog/contracts.ts +123 -0
  170. package/src/console/providers/catalog/discover-constants.ts +6 -0
  171. package/src/console/providers/catalog/model-operations.ts +122 -10
  172. package/src/console/providers/catalog/provider-operations.ts +116 -8
  173. package/src/console/providers/catalog/public-model-store.ts +0 -0
  174. package/src/console/providers/catalog/routes.ts +65 -4
  175. package/src/console/providers/catalog/store-helpers.ts +30 -1
  176. package/src/console/providers/catalog/store.ts +249 -4
  177. package/src/console/quota/account-quota-global.ts +97 -19
  178. package/src/console/quota/account-quota-shared.ts +49 -0
  179. package/src/console/quota/account-quota-tenant.ts +6 -18
  180. package/src/console/quota/native-identity.ts +2 -0
  181. package/src/console/quota/quota-view.ts +19 -3
  182. package/src/console/routing/pools/operations.ts +31 -46
  183. package/src/console/settings/contracts.ts +18 -0
  184. package/src/console/settings/store.ts +32 -0
  185. package/src/console/shared/errors.ts +33 -6
  186. package/src/console/system-prompt/contracts.ts +0 -1
  187. package/src/main.ts +3 -3
  188. package/src/network/pool/byte-accounting.ts +3 -2
  189. package/src/network/pool/selector.ts +182 -39
  190. package/src/network/pool-cooldown-store.ts +89 -14
  191. package/src/network/pool-health.ts +49 -61
  192. package/src/network/rate-limit-verdict.ts +1 -1
  193. package/src/network/ssrf.ts +6 -6
  194. package/src/observability/event-log.ts +7 -361
  195. package/src/observability/event-query.ts +18 -4
  196. package/src/observability/performance-metrics.ts +9 -0
  197. package/src/observability/runtime-metrics.ts +14 -9
  198. package/src/observability/telemetry-buffer.ts +15 -25
  199. package/src/observability/telemetry-reconciliation.ts +9 -37
  200. package/src/observability/telemetry-row.ts +125 -0
  201. package/src/persistence/db-mode.ts +4 -1
  202. package/src/persistence/dialect.ts +30 -0
  203. package/src/persistence/migrate.ts +6 -3
  204. package/src/persistence/schema.ts +28 -2
  205. package/src/persistence/ttl-cache.ts +20 -2
  206. package/src/protocol/codecs.ts +1 -1
  207. package/src/protocol/dsml.ts +35 -87
  208. package/src/protocol/gateway-error.ts +1 -0
  209. package/src/protocol/markup-stream-parser.ts +195 -0
  210. package/src/protocol/reasoning-text.ts +19 -0
  211. package/src/protocol/reasoning.ts +1 -1
  212. package/src/protocol/request/chat.ts +14 -14
  213. package/src/protocol/request/gemini.ts +8 -2
  214. package/src/protocol/request/messages.ts +4 -5
  215. package/src/protocol/response/chat.ts +12 -20
  216. package/src/protocol/response/gemini.ts +6 -1
  217. package/src/protocol/response/omp-tool-call-markup.ts +43 -86
  218. package/src/protocol/response/responses.ts +18 -2
  219. package/src/protocol/sse-decoder.ts +25 -8
  220. package/src/protocol/stop-reason.ts +23 -1
  221. package/src/protocol/stream-error-frames.ts +13 -3
  222. package/src/protocol/think-split.ts +112 -0
  223. package/src/protocol/translation/dialect/qwen.ts +26 -58
  224. package/src/protocol/translation/quirks.ts +1 -1
  225. package/src/protocol/translation/tool-repair.ts +2 -2
  226. package/src/protocol/upstream/send-claude-messages.ts +15 -4
  227. package/src/protocol/upstream-parsing.ts +1 -1
  228. package/src/providers/adapters/compatible-adapter.ts +78 -17
  229. package/src/providers/authentication/oauth-client.ts +1 -1
  230. package/src/providers/authentication/oauth-device-flow.ts +98 -8
  231. package/src/providers/authentication/oauth-refresh-service.ts +153 -59
  232. package/src/providers/configured-provider.ts +148 -0
  233. package/src/providers/default-registry.ts +117 -27
  234. package/src/providers/discovery/discovery-types.ts +12 -0
  235. package/src/providers/discovery/probe-phases.ts +74 -33
  236. package/src/providers/discovery/probing-service.ts +293 -165
  237. package/src/providers/integrations/alibabacp/alibabacp-models.ts +1 -2
  238. package/src/providers/integrations/{dashscope-models.ts → alibabacp/dashscope-models.ts} +2 -2
  239. package/src/providers/integrations/antigravity/antigravity-protocol.ts +121 -2
  240. package/src/providers/integrations/antigravity/antigravity.ts +13 -2
  241. package/src/providers/integrations/buddy/buddy-catalog-shared.ts +72 -34
  242. package/src/providers/integrations/buddy/buddy-chat-shared.ts +191 -35
  243. package/src/providers/integrations/buddy/buddy-checkin.ts +17 -39
  244. package/src/providers/integrations/buddy/buddy-conversation-headers.ts +2 -2
  245. package/src/providers/integrations/buddy/buddy-discovery-shared.ts +5 -5
  246. package/src/providers/integrations/buddy/buddy-identity.ts +89 -6
  247. package/src/providers/integrations/buddy/buddy-media.ts +4 -5
  248. package/src/providers/integrations/buddy/buddy-oauth-shared.ts +2 -2
  249. package/src/providers/integrations/buddy/buddy-quota-shared.ts +1 -1
  250. package/src/providers/integrations/buddy/buddy-usage-shared.ts +1 -3
  251. package/src/providers/integrations/buddy/capabilities.ts +6 -43
  252. package/src/providers/integrations/buddy/codebuddy/cn-catalog.ts +2 -1
  253. package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +23 -1
  254. package/src/providers/integrations/buddy/codebuddy/provider.ts +6 -2
  255. package/src/providers/integrations/buddy/codebuddy/shared.ts +38 -27
  256. package/src/providers/integrations/claude/claude-quota.ts +14 -17
  257. package/src/providers/integrations/cline/cline.ts +2 -2
  258. package/src/providers/integrations/commandcode/quota.ts +22 -28
  259. package/src/providers/integrations/cursor/cursor-auth.ts +0 -8
  260. package/src/providers/integrations/cursor/cursor-catalog.ts +2 -0
  261. package/src/providers/integrations/cursor/cursor-exec.ts +745 -0
  262. package/src/providers/integrations/cursor/cursor-http2.ts +2 -0
  263. package/src/providers/integrations/cursor/cursor-request.ts +35 -20
  264. package/src/providers/integrations/cursor/cursor-shared.ts +26 -5
  265. package/src/providers/integrations/cursor/cursor.ts +139 -70
  266. package/src/providers/integrations/cursor/protocol/messages.ts +22 -29
  267. package/src/providers/integrations/cursor/protocol/protobuf-schema.ts +37 -0
  268. package/src/providers/integrations/cursor/protocol/tool-canonical.ts +26 -100
  269. package/src/providers/integrations/devin/devin-auth.ts +353 -0
  270. package/src/providers/integrations/devin/devin-catalog.ts +207 -0
  271. package/src/providers/integrations/devin/devin-http.ts +91 -0
  272. package/src/providers/integrations/devin/devin-quota.ts +214 -0
  273. package/src/providers/integrations/devin/devin-shared.ts +111 -0
  274. package/src/providers/integrations/devin/devin.ts +539 -0
  275. package/src/providers/integrations/devin/protocol/devin-proto.ts +558 -0
  276. package/src/providers/integrations/devin/protocol/framing.ts +124 -0
  277. package/src/providers/integrations/factory/factory-oauth.ts +17 -44
  278. package/src/providers/integrations/factory/factory.ts +2 -2
  279. package/src/providers/integrations/freebuff/assets/official-system-manager.txt +88 -0
  280. package/src/providers/integrations/freebuff/assets/official-system-worker.txt +57 -0
  281. package/src/providers/integrations/freebuff/assets/official-tools.json +1070 -0
  282. package/src/providers/integrations/freebuff/freebuff-catalog.ts +120 -0
  283. package/src/providers/integrations/freebuff/freebuff-constants.ts +1233 -0
  284. package/src/providers/integrations/freebuff/freebuff-discovery.ts +108 -0
  285. package/src/providers/integrations/freebuff/freebuff-oauth.ts +240 -0
  286. package/src/providers/integrations/freebuff/freebuff-quota.ts +260 -0
  287. package/src/providers/integrations/freebuff/freebuff-virtual.ts +89 -0
  288. package/src/providers/integrations/freebuff/freebuff.ts +682 -0
  289. package/src/providers/integrations/freepi/freepi-catalog.ts +60 -0
  290. package/src/providers/integrations/freepi/freepi-discovery.ts +82 -0
  291. package/src/providers/integrations/freepi/freepi-headers.ts +21 -0
  292. package/src/providers/integrations/freepi/freepi-oauth.ts +142 -0
  293. package/src/providers/integrations/freepi/freepi-protocol.ts +242 -0
  294. package/src/providers/integrations/freepi/freepi-quota.ts +126 -0
  295. package/src/providers/integrations/freepi/freepi-realm.ts +35 -0
  296. package/src/providers/integrations/freepi/freepi.ts +292 -0
  297. package/src/providers/integrations/gemini.ts +2 -2
  298. package/src/providers/integrations/grok/grok-oauth.ts +48 -45
  299. package/src/providers/integrations/grok/grok-ratelimit-store.ts +10 -17
  300. package/src/providers/integrations/grok/grok-turn-index.ts +11 -16
  301. package/src/providers/integrations/hermes/hermes-oauth.ts +94 -0
  302. package/src/providers/integrations/{hermes.ts → hermes/hermes.ts} +13 -21
  303. package/src/providers/integrations/hermes/index.ts +2 -0
  304. package/src/providers/integrations/kiro/kiro-catalog.ts +17 -1
  305. package/src/providers/integrations/kiro/kiro-discovery.ts +10 -12
  306. package/src/providers/integrations/kiro/kiro-quota.ts +8 -4
  307. package/src/providers/integrations/kiro/kiro.ts +15 -19
  308. package/src/providers/integrations/ollamacloud/index.ts +2 -0
  309. package/src/providers/integrations/{ollamacloud-models.ts → ollamacloud/models.ts} +2 -2
  310. package/src/providers/integrations/{ollamacloud-quota.ts → ollamacloud/quota.ts} +3 -3
  311. package/src/providers/integrations/opencode/fingerprint.ts +1 -1
  312. package/src/providers/integrations/opencode/opencode-go-quota.ts +2 -8
  313. package/src/providers/integrations/perch/perch-catalog.ts +105 -0
  314. package/src/providers/integrations/perch/perch-credential.ts +25 -0
  315. package/src/providers/integrations/perch/perch-oauth.ts +170 -0
  316. package/src/providers/integrations/perch/perch-protocol.ts +208 -0
  317. package/src/providers/integrations/perch/perch-quota.ts +123 -0
  318. package/src/providers/integrations/perch/perch-realm.ts +82 -0
  319. package/src/providers/integrations/perch/perch-system-prompt.ts +13 -0
  320. package/src/providers/integrations/perch/perch.ts +305 -0
  321. package/src/providers/integrations/perch/saffron-system-prompt.txt +8 -0
  322. package/src/providers/integrations/qoder/chat-proto.ts +328 -328
  323. package/src/providers/integrations/qoder/claim-qoder-credits.ts +19 -60
  324. package/src/providers/integrations/qoder/qoder-adapter.ts +90 -40
  325. package/src/providers/integrations/qoder/qoder-auth.ts +101 -29
  326. package/src/providers/integrations/qoder/qoder-cache.ts +32 -0
  327. package/src/providers/integrations/qoder/qoder-catalog.ts +26 -6
  328. package/src/providers/integrations/qoder/qoder-chat.ts +7 -21
  329. package/src/providers/integrations/qoder/qoder-checkin.ts +6 -6
  330. package/src/providers/integrations/qoder/qoder-grpc.ts +210 -7
  331. package/src/providers/integrations/qoder/qoder-ide.ts +5 -108
  332. package/src/providers/integrations/qoder/qoder-identity-cache.ts +184 -0
  333. package/src/providers/integrations/qoder/qoder-native-identity-store.ts +83 -18
  334. package/src/providers/integrations/qoder/qoder-realm.ts +9 -6
  335. package/src/providers/integrations/qoder/qoder-umid.ts +109 -342
  336. package/src/providers/integrations/qoder/qoder-virtual.ts +33 -1
  337. package/src/providers/integrations/qoder/qoder.ts +3 -7
  338. package/src/providers/integrations/trae/trae-catalog.ts +9 -14
  339. package/src/providers/integrations/trae/trae-headers.ts +36 -11
  340. package/src/providers/integrations/trae/trae-oauth.ts +14 -17
  341. package/src/providers/integrations/trae/trae-protocol.ts +12 -11
  342. package/src/providers/integrations/trae/trae-quota.ts +96 -36
  343. package/src/providers/integrations/trae/trae-realm.ts +74 -15
  344. package/src/providers/integrations/trae/trae.ts +99 -26
  345. package/src/providers/integrations/zcode-start-plan/provider.ts +226 -23
  346. package/src/providers/integrations/zcode-start-plan/quota.ts +19 -2
  347. package/src/providers/integrations/zro/zro.ts +204 -0
  348. package/src/providers/operations/account-error-classification.ts +235 -278
  349. package/src/providers/operations/account-health-service.ts +273 -26
  350. package/src/providers/operations/account-refresh-status.ts +269 -0
  351. package/src/providers/operations/account-tier.ts +66 -0
  352. package/src/providers/operations/client-version-resolver.ts +1 -1
  353. package/src/providers/operations/client-versions.ts +42 -150
  354. package/src/providers/operations/cooldown-rules.ts +56 -0
  355. package/src/providers/operations/{model-catalog-cache.ts → provider-caches.ts} +68 -16
  356. package/src/providers/operations/provider-catalog-seeder.ts +4 -0
  357. package/src/providers/operations/provider-catalog-service.ts +1 -1
  358. package/src/providers/operations/provider-credential-service.ts +102 -32
  359. package/src/providers/operations/provider-rules/github.ts +48 -0
  360. package/src/providers/operations/provider-rules/grok.ts +33 -0
  361. package/src/providers/operations/provider-rules/inception.ts +49 -0
  362. package/src/providers/operations/provider-rules/index.ts +32 -0
  363. package/src/providers/operations/provider-rules/perch.ts +37 -0
  364. package/src/providers/operations/provider-rules/qoder.ts +70 -0
  365. package/src/providers/operations/provider-rules/trae.ts +81 -0
  366. package/src/providers/provider-contracts.ts +33 -2
  367. package/src/providers/{provider-credential-resolver.ts → provider-credentials.ts} +40 -3
  368. package/src/providers/provider-metadata.ts +99 -14
  369. package/src/providers/provider-model-definition.ts +36 -0
  370. package/src/providers/provider-probe.ts +15 -1
  371. package/src/providers/provider-registry.ts +42 -33
  372. package/src/providers/quota/quota-limit-windows.ts +16 -5
  373. package/src/providers/quota/quota-support.ts +1 -1
  374. package/src/providers/quota/quota-window-parser.ts +90 -18
  375. package/src/providers/quota/refresh.ts +96 -18
  376. package/src/security/abuse.ts +71 -0
  377. package/src/security/api-key-auth-cache.ts +1 -1
  378. package/src/security/api-key-auth.ts +6 -2
  379. package/src/transport/dispatch/attempt-finalize.ts +14 -13
  380. package/src/transport/dispatch/attempt-loop.ts +71 -17
  381. package/src/transport/dispatch/generation.ts +2 -8
  382. package/src/transport/dispatch/native-shared.ts +25 -0
  383. package/src/transport/dispatch/proxy-request.ts +9 -46
  384. package/src/transport/dispatch/responses-compact.ts +2 -8
  385. package/src/transport/dispatch/streaming/finalize.ts +9 -6
  386. package/src/transport/dispatch/streaming/stream.ts +9 -36
  387. package/src/transport/dispatch/systemone.ts +2 -8
  388. package/src/transport/dispatch/token-count.ts +2 -6
  389. package/src/transport/dispatch/upstream.ts +67 -1
  390. package/src/transport/drain-endpoint.ts +31 -4
  391. package/src/transport/failure-policy.ts +129 -37
  392. package/src/transport/guards/body-policy.ts +7 -1
  393. package/src/transport/guards/error-lifecycle.ts +13 -1
  394. package/src/transport/guards/request-context.ts +22 -14
  395. package/src/transport/request/preparer.ts +81 -71
  396. package/src/transport/routing/bounded-lru.ts +14 -0
  397. package/src/transport/routing/drain-reserve.ts +129 -0
  398. package/src/transport/routing/plan-gate.ts +7 -1
  399. package/src/transport/routing/round-robin.ts +60 -0
  400. package/src/transport/routing/route-catalog.ts +123 -111
  401. package/src/transport/routing/route-model.ts +51 -9
  402. package/src/transport/routing/router.ts +418 -33
  403. package/src/transport/surface/adapters.ts +11 -2
  404. package/src/transport/surface/chat/adapter.ts +2 -2
  405. package/src/transport/surface/chat/encode.ts +47 -4
  406. package/src/transport/surface/chat/parse.ts +2 -12
  407. package/src/transport/surface/completion.ts +44 -33
  408. package/src/transport/surface/content-parts.ts +21 -0
  409. package/src/transport/surface/messages/adapter.ts +9 -2
  410. package/src/transport/surface/messages/encode.ts +11 -3
  411. package/src/transport/surface/messages/parse.ts +2 -1
  412. package/src/transport/surface/messages/stream.ts +33 -17
  413. package/src/transport/surface/responses/encode.ts +34 -6
  414. package/src/transport/surface/responses/parse.ts +3 -11
  415. package/src/workers/daily-checkin.ts +1 -1
  416. package/src/workers/quota-refresh-sweep.ts +1 -1
  417. package/src/workers/scheduler.ts +22 -4
  418. package/dist/dashboard/assets/BannedPage-DeIbIiBU.js +0 -1
  419. package/dist/dashboard/assets/BannedPage-DeIbIiBU.js.gz +0 -0
  420. package/dist/dashboard/assets/CliToolDetailPage-BRWKPQPf.js.gz +0 -0
  421. package/dist/dashboard/assets/CliToolsPage-BP-Y5iHN.js.gz +0 -0
  422. package/dist/dashboard/assets/CombosPage-B7JHozVa.js +0 -1
  423. package/dist/dashboard/assets/CombosPage-B7JHozVa.js.gz +0 -0
  424. package/dist/dashboard/assets/ConsoleLogPage-GX7iGGcu.js +0 -1
  425. package/dist/dashboard/assets/ConsoleLogPage-GX7iGGcu.js.gz +0 -0
  426. package/dist/dashboard/assets/CustomizationPage-HVonbGEa.js +0 -1
  427. package/dist/dashboard/assets/CustomizationPage-HVonbGEa.js.gz +0 -0
  428. package/dist/dashboard/assets/HealthEventsModal-xaut4OJ9.js +0 -1
  429. package/dist/dashboard/assets/HealthEventsModal-xaut4OJ9.js.gz +0 -0
  430. package/dist/dashboard/assets/InjectSystemPromptPanel-Cv-2Ol_K.js.gz +0 -0
  431. package/dist/dashboard/assets/LoginPage-bKfz_b1K.js.gz +0 -0
  432. package/dist/dashboard/assets/ModelPicker-B-WQxx_H.js.gz +0 -0
  433. package/dist/dashboard/assets/OverviewPage-Z9pJZFo8.js +0 -1
  434. package/dist/dashboard/assets/OverviewPage-Z9pJZFo8.js.gz +0 -0
  435. package/dist/dashboard/assets/ProviderDetailPage-CMeDBxR1.js +0 -7
  436. package/dist/dashboard/assets/ProviderDetailPage-CMeDBxR1.js.gz +0 -0
  437. package/dist/dashboard/assets/ProviderIcon-jjqXJDA3.js +0 -1
  438. package/dist/dashboard/assets/ProviderIcon-jjqXJDA3.js.gz +0 -0
  439. package/dist/dashboard/assets/ProvidersPage-BXPdOpG4.js +0 -1
  440. package/dist/dashboard/assets/ProvidersPage-BXPdOpG4.js.gz +0 -0
  441. package/dist/dashboard/assets/ProxyPage-emlTuxz6.js.gz +0 -0
  442. package/dist/dashboard/assets/QuotaPage-CMJKNYWb.js +0 -1
  443. package/dist/dashboard/assets/QuotaPage-CMJKNYWb.js.gz +0 -0
  444. package/dist/dashboard/assets/SettingsPage-BSy7EwGo.js +0 -1
  445. package/dist/dashboard/assets/SettingsPage-BSy7EwGo.js.gz +0 -0
  446. package/dist/dashboard/assets/SetupPage-CUZrKs9r.js.gz +0 -0
  447. package/dist/dashboard/assets/StudioPage-jWmUdDpy.js.gz +0 -0
  448. package/dist/dashboard/assets/UsagePage-5F9tGVGG.js +0 -56
  449. package/dist/dashboard/assets/UsagePage-5F9tGVGG.js.gz +0 -0
  450. package/dist/dashboard/assets/api-keys-KvX83mRe.js +0 -1
  451. package/dist/dashboard/assets/api-keys-KvX83mRe.js.gz +0 -0
  452. package/dist/dashboard/assets/app-BEsU8hKs.css +0 -2
  453. package/dist/dashboard/assets/app-BEsU8hKs.css.gz +0 -0
  454. package/dist/dashboard/assets/assert-CidQVoNo.js.gz +0 -0
  455. package/dist/dashboard/assets/badge-DyI_mJfi.js.gz +0 -0
  456. package/dist/dashboard/assets/banner-B-yXtCqb.js.gz +0 -0
  457. package/dist/dashboard/assets/brand/bansos_page.png +0 -0
  458. package/dist/dashboard/assets/brand/denia_bansos.jpg +0 -0
  459. package/dist/dashboard/assets/brand/favicon.jpg +0 -0
  460. package/dist/dashboard/assets/brand/login_register.jpg +0 -0
  461. package/dist/dashboard/assets/brand/login_register_initial.jpg +0 -0
  462. package/dist/dashboard/assets/brand/og_bansos.jpg +0 -0
  463. package/dist/dashboard/assets/brand/og_image.jpg +0 -0
  464. package/dist/dashboard/assets/brand/sidebar_ico.jpg +0 -0
  465. package/dist/dashboard/assets/card-_HwAEqFF.js.gz +0 -0
  466. package/dist/dashboard/assets/cli-tools-Bwv5-zGZ.js +0 -1
  467. package/dist/dashboard/assets/cli-tools-Bwv5-zGZ.js.gz +0 -0
  468. package/dist/dashboard/assets/clipboard-button-ByjCk0o9.js.gz +0 -0
  469. package/dist/dashboard/assets/dialog-BZhOlfz-.js.gz +0 -0
  470. package/dist/dashboard/assets/drawer-BADqIRYP.js.gz +0 -0
  471. package/dist/dashboard/assets/entry-COaC-irN.js.gz +0 -0
  472. package/dist/dashboard/assets/entry-Ca7LSCet.js +0 -4
  473. package/dist/dashboard/assets/entry-Ca7LSCet.js.gz +0 -0
  474. package/dist/dashboard/assets/entry-PDd27uCd.js +0 -1
  475. package/dist/dashboard/assets/entry-PDd27uCd.js.gz +0 -0
  476. package/dist/dashboard/assets/formatters-DcHJyRiu.js.gz +0 -0
  477. package/dist/dashboard/assets/index-CgKmFngv.js.gz +0 -0
  478. package/dist/dashboard/assets/inline-B2BirKry.js.gz +0 -0
  479. package/dist/dashboard/assets/input-D-qV9dKY.js.gz +0 -0
  480. package/dist/dashboard/assets/layout-B7tVWhtV.js.gz +0 -0
  481. package/dist/dashboard/assets/list-row-CtkGE3bF.js.gz +0 -0
  482. package/dist/dashboard/assets/live-DGizF06-.js.gz +0 -0
  483. package/dist/dashboard/assets/minimize-2-BTmMTK4o.js +0 -1
  484. package/dist/dashboard/assets/minimize-2-BTmMTK4o.js.gz +0 -0
  485. package/dist/dashboard/assets/mutation-GU7XdEjz.js.gz +0 -0
  486. package/dist/dashboard/assets/network-C9UE-bOr.js.gz +0 -0
  487. package/dist/dashboard/assets/numbers-BnAd22a_.js.gz +0 -0
  488. package/dist/dashboard/assets/pagination-BAywyMzS.js.gz +0 -0
  489. package/dist/dashboard/assets/query-keys-YSoQOUh6.js.gz +0 -0
  490. package/dist/dashboard/assets/quota-DJ1QlAOq.js +0 -1
  491. package/dist/dashboard/assets/quota-DJ1QlAOq.js.gz +0 -0
  492. package/dist/dashboard/assets/routing-fnCsxgNO.js +0 -1
  493. package/dist/dashboard/assets/routing-fnCsxgNO.js.gz +0 -0
  494. package/dist/dashboard/assets/select-BCOcv8WK.js.gz +0 -0
  495. package/dist/dashboard/assets/settings-BxslsErn.js +0 -1
  496. package/dist/dashboard/assets/settings-BxslsErn.js.gz +0 -0
  497. package/dist/dashboard/assets/stack-ByKt3Q1g.js.gz +0 -0
  498. package/dist/dashboard/assets/state-DXbGTx7N.js.gz +0 -0
  499. package/dist/dashboard/assets/switch-ZeTsW623.js.gz +0 -0
  500. package/dist/dashboard/assets/useMutation-CbCP4w1o.js.gz +0 -0
  501. package/dist/dashboard/console-backgrounds.jpg +0 -0
  502. package/dist/dashboard/console-backgrounds_initial.jpg +0 -0
  503. package/dist/dashboard/donation/qris-risun-marke.png +0 -0
  504. package/src/providers/bundled-discovery-providers.ts +0 -98
  505. package/src/providers/configured-openai-providers.ts +0 -120
  506. package/src/providers/integrations/buddy/turing-device-token.ts +0 -262
  507. package/src/providers/integrations/buddy/workbuddy/cn-oauth.ts +0 -30
  508. package/src/providers/integrations/buddy/workbuddy/cn-provider.ts +0 -58
  509. package/src/providers/integrations/buddy/workbuddy/oauth.ts +0 -31
  510. package/src/providers/integrations/buddy/workbuddy/provider.ts +0 -209
  511. package/src/providers/integrations/buddy/workbuddy/quota.ts +0 -161
  512. package/src/providers/integrations/buddy/workbuddy/shared.ts +0 -285
  513. package/src/providers/operations/model-discovery-cache.ts +0 -34
  514. package/src/providers/operations/probe-fetch.ts +0 -15
  515. package/src/providers/operations/provider-version-cache.ts +0 -30
  516. package/src/providers/provider-capabilities.ts +0 -33
  517. package/src/providers/provider-credential-envelope.ts +0 -43
  518. /package/dist/dashboard/assets/{app-CTxoqyYB.js → app-DIXLjojg.js} +0 -0
  519. /package/dist/dashboard/assets/{app-CTxoqyYB.js.gz → app-DIXLjojg.js.gz} +0 -0
@@ -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
+ }
@@ -34,7 +34,7 @@ export function applyOpenAIReasoning(payload: Record<string, unknown>, request:
34
34
  payload["reasoning_effort"] = effort;
35
35
  }
36
36
 
37
- /** Applies the DeepSeek chat contract shared by CodeBuddy and WorkBuddy. */
37
+ /** Applies the DeepSeek chat contract shared by CodeBuddy. */
38
38
  export function applyDeepSeekReasoning(payload: Record<string, unknown>): void {
39
39
  const model = typeof payload["model"] === "string" ? payload["model"].trim().toLowerCase() : "";
40
40
  if (!model.startsWith("deepseek")) return;
@@ -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")
@@ -248,19 +256,15 @@ export function canonicalToChatPayload(
248
256
  // to be replayed, and the tool-call turn is exactly where a thinking
249
257
  // model emits it — dropping it here made the next request fail with
250
258
  // "the reasoning content from the previous turn must be passed back in
251
- // thinking mode" (WorkBuddy/CodeBuddy 400). Chat Completions has no
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
  );
@@ -365,7 +365,7 @@ export function canonicalToChatPayload(
365
365
  // nothing left to say: emitting it as an empty-content `user` message
366
366
  // right after the tool results breaks strict upstreams (a bare `user`
367
367
  // turn between a result and the next assistant turn reads as a broken
368
- // tool sequence — WorkBuddy/CodeBuddy `11148`). Fold the leftover
368
+ // tool sequence — CodeBuddy `11148`). Fold the leftover
369
369
  // reasoning into the last tool message's side channel when one was
370
370
  // just emitted, and skip the turn otherwise; a turn that still
371
371
  // carries text or rich parts emits below as usual.
@@ -382,7 +382,7 @@ export function canonicalToChatPayload(
382
382
  // or a non-empty serialised payload. Pushing `{content:"",
383
383
  // reasoning_content:""}` is the opposite of load-bearing — the
384
384
  // upstream reads it as "thinking mode with the reasoning stripped"
385
- // and rejects the whole request (`WorkBuddy/CodeBuddy 11151 "history
385
+ // and rejects the whole request (`CodeBuddy 11151 "history
386
386
  // not found"`). The canonical form that reaches this branch with no
387
387
  // trace is an input `reasoning` item whose `summary` was absent and
388
388
  // whose `payload` was `null` — a signature-less thinking block
@@ -570,7 +570,7 @@ export function canonicalToChatPayload(
570
570
  // Resolved unconditionally, not only when the caller sent a value: the bound
571
571
  // table now carries a `defaultMaxTokens` for this wire, and an omitted limit
572
572
  // must reach the upstream as a real number. Upstreams that require the field
573
- // reject a large prompt whose `max_tokens` is absent outright (WorkBuddy
573
+ // reject a large prompt whose `max_tokens` is absent outright (CodeBuddy
574
574
  // `400 model_param_invalid`, reproduced through the gateway).
575
575
  const bounded = resolveWireMaxTokens("chat", { max_tokens: requestedMaxTokens }, {
576
576
  ...(options.maxOutputTokens === undefined ? {} : { maxOutputTokens: options.maxOutputTokens }),
@@ -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";
@@ -39,7 +40,7 @@ import { claudeResponseToEvents, parseClaudeSseStream } from "../response/messag
39
40
  * Gateway default when the caller sent no max_tokens (Anthropic requires
40
41
  * the field). Explicit and warned at the call site — never silent.
41
42
  */
42
- const ANTHROPIC_DEFAULT_MAX_TOKENS = WIRE_TOKEN_BOUNDS["messages"]?.defaultMaxTokens ?? 4_096;
43
+ export const ANTHROPIC_DEFAULT_MAX_TOKENS = WIRE_TOKEN_BOUNDS["messages"]?.defaultMaxTokens ?? 4_096;
43
44
 
44
45
  /** Anthropic Messages' documented base64-image media-type allowlist. */
45
46
  const ANTHROPIC_IMAGE_MEDIA_TYPES = new Set([
@@ -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",
@@ -38,15 +38,20 @@ export function responseParts(parts: readonly Record<string, unknown>[]): {
38
38
  readonly text: string;
39
39
  readonly calls: readonly { readonly id: string; readonly name: string; readonly args: Record<string, unknown> }[];
40
40
  readonly thought: string;
41
+ readonly thoughtSignature: string;
41
42
  } {
42
43
  const text: string[] = [];
43
44
  const thought: string[] = [];
45
+ let thoughtSignature = "";
44
46
  const calls: Array<{ id: string; name: string; args: Record<string, unknown> }> = [];
45
47
  let callIndex = 0;
46
48
  for (const part of parts) {
47
49
  if (typeof part["text"] === "string") {
48
50
  if (part["thought"] === true) {
49
51
  thought.push(part["text"] as string);
52
+ if (thoughtSignature.length === 0 && typeof part["thoughtSignature"] === "string") {
53
+ thoughtSignature = part["thoughtSignature"] as string;
54
+ }
50
55
  } else text.push(part["text"] as string);
51
56
  }
52
57
  if (isRecord(part["functionCall"]) && typeof (part["functionCall"] as Record<string, unknown>)["name"] === "string") {
@@ -58,7 +63,7 @@ export function responseParts(parts: readonly Record<string, unknown>[]): {
58
63
  });
59
64
  }
60
65
  }
61
- return { text: text.join(""), calls, thought: thought.join("") };
66
+ return { text: text.join(""), calls, thought: thought.join(""), thoughtSignature };
62
67
  }
63
68
 
64
69
  export function mapGeminiUsage(body: Record<string, unknown>): Record<string, unknown> {
@@ -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),
@@ -93,8 +93,19 @@ function isCompleteJsonValue(data: string): boolean {
93
93
  // invited a retry of the identical request against a broken upstream. The
94
94
  // `upstream` origin keeps the public envelope from prefixing it
95
95
  // a gateway-branded message, which blamed the gateway for the provider's bytes.
96
- function oversized(what: string): GatewayError {
97
- return new GatewayError("platform_unavailable", 502, `SSE ${what} exceeds size bound (request {requestId}). The upstream sent more than we can hold — retry 🌙`, {}, "upstream");
96
+ // The message quotes the offending line (bounded) so the client and operator
97
+ // see the actual bytes the upstream sent rather than a generic sentence.
98
+ const MAX_SSE_SNIPPET = 160;
99
+ function snippet(line: string): string {
100
+ const collapsed = line.replace(/[\r\n]+/g, " ").trim();
101
+ if (collapsed.length === 0) return "<empty line>";
102
+ return collapsed.length > MAX_SSE_SNIPPET
103
+ ? `${collapsed.slice(0, MAX_SSE_SNIPPET)}…`
104
+ : collapsed;
105
+ }
106
+ function oversized(what: string, line?: string): GatewayError {
107
+ const quote = line === undefined ? "" : `: ${snippet(line)}`;
108
+ return new GatewayError("platform_unavailable", 502, `SSE ${what} exceeds size bound${quote} (request {requestId}). The upstream sent more than we can hold — retry 🌙`, {}, "upstream");
98
109
  }
99
110
 
100
111
  export async function* decodeSseEvents(
@@ -161,7 +172,7 @@ export async function* decodeSseEvents(
161
172
  const rest = last.subarray(newlineAt + 1);
162
173
  if (rest.byteLength > 0) pending.unshift(rest);
163
174
  const line = decoder.decode(lineBytes).replace(/\r$/, "");
164
- if (line.length > maxLineBytes) throw oversized("line");
175
+ if (line.length > maxLineBytes) throw oversized("line", line);
165
176
  return line;
166
177
  }
167
178
  function* dispatchEvent(): Iterable<SseEvent> {
@@ -188,7 +199,7 @@ export async function* decodeSseEvents(
188
199
  const piece = line.slice(5).trimStart();
189
200
  dataLines.push(piece);
190
201
  eventBytes += piece.length;
191
- if (eventBytes > maxEventBytes) throw oversized("event");
202
+ if (eventBytes > maxEventBytes) throw oversized("event", line);
192
203
  return;
193
204
  }
194
205
  if (line.startsWith("id:") || line.startsWith("retry:")) return;
@@ -220,10 +231,10 @@ export async function* decodeSseEvents(
220
231
  if (dataLines.length > 0) {
221
232
  dataLines.push(line);
222
233
  eventBytes += line.length;
223
- if (eventBytes > maxEventBytes) throw oversized("event");
234
+ if (eventBytes > maxEventBytes) throw oversized("event", line);
224
235
  return;
225
236
  }
226
- throw new GatewayError("platform_unavailable", 502, "Malformed SSE line (request {requestId}). The upstream sent something we can't read — retry 🌙", {}, "upstream");
237
+ throw new GatewayError("platform_unavailable", 502, `Malformed SSE line: ${snippet(line)} (request {requestId}). The upstream sent something we can't read — retry 🌙`, {}, "upstream");
227
238
  }
228
239
 
229
240
  try {
@@ -233,7 +244,10 @@ export async function* decodeSseEvents(
233
244
  if (chunk.value && chunk.value.byteLength > 0) {
234
245
  pending.push(chunk.value);
235
246
  pendingBytes += chunk.value.byteLength;
236
- if (!hasPendingNewline() && pendingBytes > maxLineBytes) throw oversized("line");
247
+ if (!hasPendingNewline() && pendingBytes > maxLineBytes) {
248
+ const partial = decoder.decode(new Uint8Array(pendingBytes)).slice(0, MAX_SSE_SNIPPET);
249
+ throw oversized("line", partial);
250
+ }
237
251
  }
238
252
  let line = takeLine();
239
253
  while (line !== undefined) {
@@ -243,7 +257,10 @@ export async function* decodeSseEvents(
243
257
  if (chunk.done) break;
244
258
  }
245
259
  if (pendingBytes > 0) {
246
- if (pendingBytes > maxLineBytes) throw oversized("line");
260
+ if (pendingBytes > maxLineBytes) {
261
+ const partial = decoder.decode(new Uint8Array(pendingBytes)).slice(0, MAX_SSE_SNIPPET);
262
+ throw oversized("line", partial);
263
+ }
247
264
  const total = new Uint8Array(pendingBytes);
248
265
  let offset = 0;
249
266
  for (const chunk of pending.splice(0, pending.length)) {
@@ -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");
@@ -17,7 +17,11 @@
17
17
  * which is honest: the upstream failed and we do not know more.
18
18
  */
19
19
  import { GatewayError } from "./gateway-error";
20
- import { classifyUpstreamError, extractUpstreamMessage, upstreamErrorIdentifier } from "../transport/failure-policy";
20
+ import {
21
+ classifyUpstreamError,
22
+ upstreamErrorIdentifier,
23
+ extractUpstreamMessageText,
24
+ } from "../transport/failure-policy";
21
25
  import { isRecord } from "./codecs";
22
26
 
23
27
 
@@ -54,19 +58,25 @@ export function gatewayErrorFromStreamError(
54
58
  }
55
59
  if (!isRecord(error)) return undefined;
56
60
 
57
- const message = extractUpstreamMessage(error);
58
61
  const identifier = upstreamErrorIdentifier(error);
59
62
  const explicitStatus = numericStatus(error["status"], error["statusCode"], error["status_code"]);
60
63
  const classification = classifyUpstreamError(explicitStatus, identifier);
61
64
  const raw = JSON.stringify(error, null, 2).slice(0, 16_384);
65
+ // Only genuine prose (`message`/`msg`/`error` fields) counts as upstream
66
+ // passthrough text. A `code`/identifier is a classification key, not prose —
67
+ // it still serves as the message fallback when no text exists, but it must
68
+ // not set `upstreamMessageAsIs`.
69
+ const prose = extractUpstreamMessageText(error);
70
+ const message = prose.slice(0, 500) || identifier || label;
62
71
  return new GatewayError(
63
72
  classification.code,
64
73
  classification.status,
65
- message || label,
74
+ message,
66
75
  {
67
76
  ...(explicitStatus === undefined ? {} : { providerStatus: explicitStatus }),
68
77
  ...(identifier ? { providerCode: identifier } : {}),
69
78
  raw,
79
+ ...(prose ? { upstreamMessageAsIs: true } : {}),
70
80
  ...(classification.credentialEvidence ? { credentialEvidence: true } : {}),
71
81
  ...(classification.rateLimitScope
72
82
  ? { rateLimitScope: classification.rateLimitScope }