@fudrouter/fsrouter 0.6.72 → 0.6.74

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 (314) hide show
  1. package/dist/lib/headroom/detect.js +97 -7
  2. package/dist/lib/headroom/process.js +136 -4
  3. package/dist/open-sse/config/appConstants.js +23 -79
  4. package/dist/open-sse/config/cliFingerprints.js +1 -1
  5. package/dist/open-sse/config/errorConfig.js +0 -11
  6. package/dist/open-sse/config/grokCli.js +10 -0
  7. package/dist/open-sse/config/kiroConstants.js +99 -46
  8. package/dist/open-sse/config/runtimeConfig.js +2 -0
  9. package/dist/open-sse/config/ttsModels.js +2 -0
  10. package/dist/open-sse/executors/antigravity.js +250 -164
  11. package/dist/open-sse/executors/base.js +16 -11
  12. package/dist/open-sse/executors/codex.js +269 -1108
  13. package/dist/open-sse/executors/commandCode.js +1 -1
  14. package/dist/open-sse/executors/commandcode.js +10 -4
  15. package/dist/open-sse/executors/cursor.js +96 -203
  16. package/dist/open-sse/executors/default.js +146 -244
  17. package/dist/open-sse/executors/github.js +128 -73
  18. package/dist/open-sse/executors/grok-cli.js +527 -94
  19. package/dist/open-sse/executors/grok-web.js +5 -7
  20. package/dist/open-sse/executors/index.js +12 -0
  21. package/dist/open-sse/executors/kiro.js +150 -61
  22. package/dist/open-sse/executors/opencode-go.js +12 -4
  23. package/dist/open-sse/executors/opencode.js +1 -1
  24. package/dist/open-sse/executors/perplexity-web.js +20 -96
  25. package/dist/open-sse/executors/qoder.js +28 -18
  26. package/dist/open-sse/executors/vertex.js +53 -7
  27. package/dist/open-sse/executors/xiaomi-tokenplan.js +6 -5
  28. package/dist/open-sse/handlers/chatCore/nonStreamingHandler.js +97 -85
  29. package/dist/open-sse/handlers/chatCore/requestDetail.js +32 -8
  30. package/dist/open-sse/handlers/chatCore/sseToJsonHandler.js +13 -7
  31. package/dist/open-sse/handlers/chatCore/streamingHandler.js +74 -25
  32. package/dist/open-sse/handlers/chatCore.js +35 -41
  33. package/dist/open-sse/handlers/embeddingProviders/index.js +1 -0
  34. package/dist/open-sse/handlers/embeddingProviders/openai.js +8 -16
  35. package/dist/open-sse/handlers/imageProviders/blackForestLabs.js +2 -21
  36. package/dist/open-sse/handlers/imageProviders/cloudflareAi.js +2 -30
  37. package/dist/open-sse/handlers/imageProviders/codex.js +2 -20
  38. package/dist/open-sse/handlers/imageProviders/comfyui.js +5 -18
  39. package/dist/open-sse/handlers/imageProviders/falAi.js +2 -22
  40. package/dist/open-sse/handlers/imageProviders/gemini.js +2 -16
  41. package/dist/open-sse/handlers/imageProviders/huggingface.js +2 -17
  42. package/dist/open-sse/handlers/imageProviders/index.js +3 -4
  43. package/dist/open-sse/handlers/imageProviders/leonardo.js +1 -1
  44. package/dist/open-sse/handlers/imageProviders/nanobanana.js +4 -23
  45. package/dist/open-sse/handlers/imageProviders/openai.js +15 -43
  46. package/dist/open-sse/handlers/imageProviders/runwayml.js +2 -26
  47. package/dist/open-sse/handlers/imageProviders/sdwebui.js +4 -20
  48. package/dist/open-sse/handlers/imageProviders/stabilityAi.js +2 -26
  49. package/dist/open-sse/handlers/imageProviders/weavy.js +1 -1
  50. package/dist/open-sse/handlers/search/chatSearch.js +60 -14
  51. package/dist/open-sse/handlers/ttsProviders/gemini.js +14 -3
  52. package/dist/open-sse/handlers/ttsProviders/index.js +4 -2
  53. package/dist/open-sse/handlers/ttsProviders/openai.js +4 -1
  54. package/dist/open-sse/handlers/ttsProviders/openrouter.js +7 -4
  55. package/dist/open-sse/handlers/videoCore.js +166 -0
  56. package/dist/open-sse/handlers/videoProviders/weavy.js +1 -1
  57. package/dist/open-sse/mcp-server/catalog.js +1 -1
  58. package/dist/open-sse/mcp-server/server.js +2 -2
  59. package/dist/open-sse/mcp-server/tools/advancedTools.js +2 -2
  60. package/dist/open-sse/mcp-server/tools/agentSkillTools.js +1 -1
  61. package/dist/open-sse/mcp-server/tools/compressionTools.js +4 -4
  62. package/dist/open-sse/mcp-server/tools/gamificationTools.js +10 -10
  63. package/dist/open-sse/mcp-server/tools/githubSkillTools.js +1 -1
  64. package/dist/open-sse/mcp-server/tools/memoryTools.js +3 -3
  65. package/dist/open-sse/mcp-server/tools/notionTools.js +2 -2
  66. package/dist/open-sse/mcp-server/tools/obsidianTools.js +2 -2
  67. package/dist/open-sse/mcp-server/tools/pickFastestModel.js +1 -1
  68. package/dist/open-sse/mcp-server/tools/pluginTools.js +4 -4
  69. package/dist/open-sse/mcp-server/tools/skillTools.js +2 -2
  70. package/dist/open-sse/providers/capabilities.js +24 -4
  71. package/dist/open-sse/providers/registry/alicode-intl.js +1 -1
  72. package/dist/open-sse/providers/registry/anthropic.js +1 -8
  73. package/dist/open-sse/providers/registry/codex.js +0 -9
  74. package/dist/open-sse/providers/registry/cohere.js +0 -2
  75. package/dist/open-sse/providers/registry/gemini.js +0 -2
  76. package/dist/open-sse/providers/registry/github.js +9 -6
  77. package/dist/open-sse/providers/registry/grok-cli.js +28 -18
  78. package/dist/open-sse/providers/registry/index.js +74 -80
  79. package/dist/open-sse/providers/registry/kiro.js +12 -0
  80. package/dist/open-sse/providers/registry/mistral.js +0 -3
  81. package/dist/open-sse/providers/registry/openai.js +0 -3
  82. package/dist/open-sse/providers/registry/perplexity.js +0 -2
  83. package/dist/open-sse/providers/registry/xai.js +5 -3
  84. package/dist/open-sse/rtk/autodetect.js +10 -2
  85. package/dist/open-sse/rtk/caveman.js +2 -93
  86. package/dist/open-sse/rtk/cavemanPrompts.js +34 -2
  87. package/dist/open-sse/rtk/constants.js +7 -2
  88. package/dist/open-sse/rtk/headroom.js +133 -0
  89. package/dist/open-sse/rtk/ponytail.js +1 -1
  90. package/dist/open-sse/rtk/registry.js +2 -0
  91. package/dist/open-sse/services/accountFallback.js +0 -108
  92. package/dist/open-sse/services/combo/quotaExhaustionCutoff.js +2 -2
  93. package/dist/open-sse/services/combo/quotaShareStrategy.js +1 -1
  94. package/dist/open-sse/services/combo/resolveAutoStrategy.js +1 -1
  95. package/dist/open-sse/services/combo/sessionStickiness.js +1 -1
  96. package/dist/open-sse/services/combo/targetSorters.js +2 -2
  97. package/dist/open-sse/services/combo/validateQuality.js +1 -1
  98. package/dist/open-sse/services/comboConfig.js +1 -1
  99. package/dist/open-sse/services/emergencyFallback.js +1 -1
  100. package/dist/open-sse/services/grokCliModels.js +127 -0
  101. package/dist/open-sse/services/model.js +7 -1
  102. package/dist/open-sse/services/provider.js +1 -1
  103. package/dist/open-sse/services/proxyAutoSelector.js +1 -1
  104. package/dist/open-sse/services/tokenRefresh/providers.js +3 -3
  105. package/dist/open-sse/services/usage/codex.js +148 -214
  106. package/dist/open-sse/services/usage/grok-cli.js +61 -7
  107. package/dist/open-sse/services/usage/misc.js +0 -19
  108. package/dist/open-sse/services/usage.js +0 -2
  109. package/dist/open-sse/translator/concerns/paramSupport.js +2 -2
  110. package/dist/open-sse/translator/concerns/thinkingUnified.js +6 -0
  111. package/dist/open-sse/translator/index.js +97 -59
  112. package/dist/open-sse/translator/request/claude-to-kiro.js +67 -40
  113. package/dist/open-sse/translator/request/openai-responses.js +1 -0
  114. package/dist/open-sse/translator/request/openai-to-commandcode.js +0 -4
  115. package/dist/open-sse/translator/request/openai-to-kiro.js +49 -20
  116. package/dist/open-sse/translator/response/commandcode-to-openai.js +0 -4
  117. package/dist/open-sse/utils/bypassHandler.js +18 -3
  118. package/dist/open-sse/utils/claudeCloaking.js +15 -5
  119. package/dist/open-sse/utils/cursorImages.js +1 -1
  120. package/dist/open-sse/utils/error.js +0 -17
  121. package/dist/open-sse/utils/keepaliveThreshold.js +4 -4
  122. package/dist/open-sse/utils/kiroSessionReplay.js +125 -0
  123. package/dist/open-sse/utils/noThinkingAlias.js +1 -1
  124. package/dist/open-sse/utils/proxyFetch.js +0 -1
  125. package/dist/open-sse/utils/reasoningContentInjector.js +4 -7
  126. package/dist/open-sse/utils/responsesStatePolicy.js +1 -1
  127. package/dist/open-sse/utils/responsesStreamHelpers.js +1 -0
  128. package/dist/open-sse/utils/sessionManager.js +49 -22
  129. package/dist/open-sse/utils/stream.js +54 -77
  130. package/dist/open-sse/utils/streamHandler.js +19 -15
  131. package/dist/open-sse/utils/streamHelpers.js +2 -17
  132. package/dist/open-sse/utils/usageTracking.js +103 -12
  133. package/dist/routes/headroom/extras/route.js +58 -0
  134. package/dist/routes/headroom/restart/route.js +33 -0
  135. package/open-sse/config/appConstants.js +23 -79
  136. package/open-sse/config/errorConfig.js +0 -11
  137. package/open-sse/config/grokCli.js +10 -0
  138. package/open-sse/config/kiroConstants.js +99 -46
  139. package/open-sse/config/runtimeConfig.js +2 -0
  140. package/open-sse/config/ttsModels.js +2 -0
  141. package/open-sse/executors/antigravity.js +250 -164
  142. package/open-sse/executors/base.js +16 -11
  143. package/open-sse/executors/codex.js +269 -1108
  144. package/open-sse/executors/commandcode.js +10 -4
  145. package/open-sse/executors/cursor.js +96 -203
  146. package/open-sse/executors/default.js +146 -244
  147. package/open-sse/executors/github.js +128 -73
  148. package/open-sse/executors/grok-cli.js +527 -94
  149. package/open-sse/executors/grok-web.js +5 -7
  150. package/open-sse/executors/index.js +12 -0
  151. package/open-sse/executors/kiro.js +150 -61
  152. package/open-sse/executors/opencode-go.js +12 -4
  153. package/open-sse/executors/opencode.js +1 -1
  154. package/open-sse/executors/perplexity-web.js +20 -96
  155. package/open-sse/executors/qoder.js +28 -18
  156. package/open-sse/executors/vertex.js +53 -7
  157. package/open-sse/executors/xiaomi-tokenplan.js +6 -5
  158. package/open-sse/handlers/chatCore/nonStreamingHandler.js +97 -85
  159. package/open-sse/handlers/chatCore/requestDetail.js +32 -8
  160. package/open-sse/handlers/chatCore/sseToJsonHandler.js +13 -7
  161. package/open-sse/handlers/chatCore/streamingHandler.js +74 -25
  162. package/open-sse/handlers/chatCore.js +35 -41
  163. package/open-sse/handlers/embeddingProviders/index.js +1 -0
  164. package/open-sse/handlers/embeddingProviders/openai.js +8 -16
  165. package/open-sse/handlers/imageProviders/blackForestLabs.js +2 -21
  166. package/open-sse/handlers/imageProviders/cloudflareAi.js +2 -30
  167. package/open-sse/handlers/imageProviders/codex.js +2 -20
  168. package/open-sse/handlers/imageProviders/comfyui.js +5 -18
  169. package/open-sse/handlers/imageProviders/falAi.js +2 -22
  170. package/open-sse/handlers/imageProviders/gemini.js +2 -16
  171. package/open-sse/handlers/imageProviders/huggingface.js +2 -17
  172. package/open-sse/handlers/imageProviders/index.js +3 -4
  173. package/open-sse/handlers/imageProviders/leonardo.js +1 -1
  174. package/open-sse/handlers/imageProviders/nanobanana.js +4 -23
  175. package/open-sse/handlers/imageProviders/openai.js +15 -43
  176. package/open-sse/handlers/imageProviders/runwayml.js +2 -26
  177. package/open-sse/handlers/imageProviders/sdwebui.js +4 -20
  178. package/open-sse/handlers/imageProviders/stabilityAi.js +2 -26
  179. package/open-sse/handlers/imageProviders/weavy.js +1 -1
  180. package/open-sse/handlers/search/chatSearch.js +60 -14
  181. package/open-sse/handlers/ttsProviders/gemini.js +14 -3
  182. package/open-sse/handlers/ttsProviders/index.js +4 -2
  183. package/open-sse/handlers/ttsProviders/openai.js +4 -1
  184. package/open-sse/handlers/ttsProviders/openrouter.js +7 -4
  185. package/open-sse/handlers/videoCore.js +166 -0
  186. package/open-sse/handlers/videoProviders/weavy.js +1 -1
  187. package/open-sse/mcp-server/audit.js +1 -1
  188. package/open-sse/mcp-server/catalog.js +2 -2
  189. package/open-sse/mcp-server/descriptionCompressor.js +1 -1
  190. package/open-sse/mcp-server/mcpCallerIdentity.js +1 -1
  191. package/open-sse/mcp-server/schemas/tools.js +1 -1
  192. package/open-sse/mcp-server/server.js +4 -4
  193. package/open-sse/mcp-server/tools/advancedTools.js +5 -5
  194. package/open-sse/mcp-server/tools/agentSkillTools.js +1 -1
  195. package/open-sse/mcp-server/tools/compressionTools.js +4 -4
  196. package/open-sse/mcp-server/tools/gamificationTools.js +10 -10
  197. package/open-sse/mcp-server/tools/githubSkillTools.js +1 -1
  198. package/open-sse/mcp-server/tools/memoryTools.js +3 -3
  199. package/open-sse/mcp-server/tools/notionTools.js +2 -2
  200. package/open-sse/mcp-server/tools/obsidianTools.js +2 -2
  201. package/open-sse/mcp-server/tools/pickFastestModel.js +3 -3
  202. package/open-sse/mcp-server/tools/pluginTools.js +4 -4
  203. package/open-sse/mcp-server/tools/skillTools.js +2 -2
  204. package/open-sse/providers/capabilities.js +24 -4
  205. package/open-sse/providers/registry/alicode-intl.js +1 -1
  206. package/open-sse/providers/registry/anthropic.js +1 -8
  207. package/open-sse/providers/registry/codex.js +0 -9
  208. package/open-sse/providers/registry/cohere.js +0 -2
  209. package/open-sse/providers/registry/gemini.js +0 -2
  210. package/open-sse/providers/registry/github.js +9 -6
  211. package/open-sse/providers/registry/grok-cli.js +28 -18
  212. package/open-sse/providers/registry/index.js +74 -80
  213. package/open-sse/providers/registry/kiro.js +12 -0
  214. package/open-sse/providers/registry/mistral.js +0 -3
  215. package/open-sse/providers/registry/openai.js +0 -3
  216. package/open-sse/providers/registry/perplexity.js +0 -2
  217. package/open-sse/providers/registry/xai.js +5 -3
  218. package/open-sse/rtk/autodetect.js +10 -2
  219. package/open-sse/rtk/caveman.js +2 -93
  220. package/open-sse/rtk/cavemanPrompts.js +34 -2
  221. package/open-sse/rtk/constants.js +7 -2
  222. package/open-sse/rtk/headroom.js +133 -0
  223. package/open-sse/rtk/ponytail.js +1 -1
  224. package/open-sse/rtk/registry.js +2 -0
  225. package/open-sse/services/accountFallback.js +0 -108
  226. package/open-sse/services/combo/quotaExhaustionCutoff.js +2 -2
  227. package/open-sse/services/combo/quotaShareStrategy.js +1 -1
  228. package/open-sse/services/combo/resolveAutoStrategy.js +1 -1
  229. package/open-sse/services/combo/sessionStickiness.js +1 -1
  230. package/open-sse/services/combo/shadowRouting.js +1 -1
  231. package/open-sse/services/combo/targetSorters.js +4 -4
  232. package/open-sse/services/combo/validateQuality.js +1 -1
  233. package/open-sse/services/evalRouting.js +1 -1
  234. package/open-sse/services/grokCliModels.js +127 -0
  235. package/open-sse/services/model.js +7 -1
  236. package/open-sse/services/modelCapabilities.js +1 -1
  237. package/open-sse/services/modelFamilyFallback.js +1 -1
  238. package/open-sse/services/provider.js +1 -1
  239. package/open-sse/services/providerCooldownTracker.js +1 -1
  240. package/open-sse/services/rateLimitManager.js +1 -1
  241. package/open-sse/services/tokenRefresh/providers.js +3 -3
  242. package/open-sse/services/usage/codex.js +148 -214
  243. package/open-sse/services/usage/grok-cli.js +61 -7
  244. package/open-sse/services/usage/misc.js +0 -19
  245. package/open-sse/services/usage.js +0 -2
  246. package/open-sse/translator/concerns/paramSupport.js +2 -2
  247. package/open-sse/translator/concerns/thinkingUnified.js +6 -0
  248. package/open-sse/translator/index.js +97 -59
  249. package/open-sse/translator/request/claude-to-kiro.js +67 -40
  250. package/open-sse/translator/request/openai-responses.js +1 -0
  251. package/open-sse/translator/request/openai-to-commandcode.js +0 -4
  252. package/open-sse/translator/request/openai-to-kiro.js +49 -20
  253. package/open-sse/translator/response/commandcode-to-openai.js +0 -4
  254. package/open-sse/utils/bypassHandler.js +18 -3
  255. package/open-sse/utils/claudeCloaking.js +15 -5
  256. package/open-sse/utils/error.js +0 -17
  257. package/open-sse/utils/kiroSessionReplay.js +125 -0
  258. package/open-sse/utils/logger.js +1 -1
  259. package/open-sse/utils/proxyFetch.js +0 -1
  260. package/open-sse/utils/reasoningContentInjector.js +4 -7
  261. package/open-sse/utils/responsesStreamHelpers.js +1 -0
  262. package/open-sse/utils/sessionManager.js +49 -22
  263. package/open-sse/utils/stream.js +54 -77
  264. package/open-sse/utils/streamHandler.js +19 -15
  265. package/open-sse/utils/streamHelpers.js +2 -17
  266. package/open-sse/utils/usageTracking.js +103 -12
  267. package/package.json +4 -7
  268. package/public/assets/EndpointPageClient.C2WMTYy0.js +6 -0
  269. package/public/assets/{Loading.Bj-WGl4k.js → Loading.BHPtQ9vj.js} +1 -1
  270. package/public/assets/{NoAuthProxyCard.qAv57b-0.js → NoAuthProxyCard.BDcXaWvM.js} +1 -1
  271. package/public/assets/index.Be3gbrBl.js +89 -0
  272. package/public/assets/index.COMgQZYj.css +1 -0
  273. package/public/assets/{page.CbkENBd5.js → page.-lpNgYAw.js} +1 -1
  274. package/public/assets/{page.CssmrzTT.js → page.11hpNKmq.js} +1 -1
  275. package/public/assets/{page.C72FTTJr.js → page.B-LxsnqU.js} +1 -1
  276. package/public/assets/{page.sOFOrxqq.js → page.B-dzeIUC.js} +1 -1
  277. package/public/assets/{page.CsxWmPoI.js → page.B-nBR75u.js} +1 -1
  278. package/public/assets/{page.Dx_GRRdT.js → page.B3QqvLep.js} +1 -1
  279. package/public/assets/{page.DPyOkPVo.js → page.B8xdkCWA.js} +1 -1
  280. package/public/assets/{page.DUI8kqOY.js → page.BFb7yUd8.js} +1 -1
  281. package/public/assets/page.BbPPdoas.js +1 -0
  282. package/public/assets/page.BdS20a1O.js +1 -0
  283. package/public/assets/{page.DibhF7f-.js → page.BfmI9KYe.js} +1 -1
  284. package/public/assets/{page.DX4Nno6M.js → page.BgGJovOg.js} +1 -1
  285. package/public/assets/{page.BnmTC5jG.js → page.C0gBvR4O.js} +1 -1
  286. package/public/assets/{page.D5gj98Oe.js → page.C6bYizL7.js} +1 -1
  287. package/public/assets/{page.vla5Mzki.js → page.C8sLEMa6.js} +1 -1
  288. package/public/assets/{page.CXd_R9yU.js → page.CCNsZBXp.js} +1 -1
  289. package/public/assets/{page.CQlVGoXg.js → page.CF_YSLwP.js} +1 -1
  290. package/public/assets/{page.BbO1pspY.js → page.CPTtRDxw.js} +1 -1
  291. package/public/assets/{page.D1oKew1j.js → page.CdMmlUEm.js} +1 -1
  292. package/public/assets/{page.DxMDTbSx.js → page.Cged1bOZ.js} +1 -1
  293. package/public/assets/{page.Cj5h09Zo.js → page.Ch647dVk.js} +1 -1
  294. package/public/assets/{page.DVIHRbxB.js → page.CpvXDkQF.js} +1 -1
  295. package/public/assets/{page.i4KDuWcM.js → page.CrBl7f8N.js} +1 -1
  296. package/public/assets/{page.BSLJ4tx5.js → page.CrP7niA2.js} +1 -1
  297. package/public/assets/{page.CnTop8Zt.js → page.CvUFZzXr.js} +1 -1
  298. package/public/assets/{page.O2ddJYoT.js → page.CyUqJPmx.js} +1 -1
  299. package/public/assets/{page.DBhu4Arl.js → page.D8Rp8fFR.js} +1 -1
  300. package/public/assets/{page.CPZav1_c.js → page.Df9qtrdG.js} +1 -1
  301. package/public/assets/{page.Qkkv8roc.js → page.DiL4Gwqe.js} +1 -1
  302. package/public/assets/{page.BdmJ3yY3.js → page.Dl1apZE2.js} +1 -1
  303. package/public/assets/{page.1qjqvrwX.js → page.HV0w1d5C.js} +1 -1
  304. package/public/assets/{page.ChX1NqmC.js → page.N1x4YL8_.js} +1 -1
  305. package/public/assets/{page.Dh5f0XJC.js → page.P2Ny5veR.js} +1 -1
  306. package/public/assets/{page.CSs3-gdt.js → page.fOOCr18J.js} +1 -1
  307. package/public/assets/{page.Ir7KvKcF.js → page.sUIPWIiF.js} +1 -1
  308. package/public/assets/{page.CzlQTyAL.js → page.tE7PpWYW.js} +1 -1
  309. package/public/assets/{page.DzZLqI5Q.js → page.uziaGZsT.js} +1 -1
  310. package/public/index.html +2 -2
  311. package/public/assets/EndpointPageClient.LyNNvZ4z.js +0 -6
  312. package/public/assets/index.BuPfZebu.css +0 -1
  313. package/public/assets/index.CShjRV_B.js +0 -89
  314. package/public/assets/page.DD_czjjP.js +0 -1
@@ -1,19 +1,74 @@
1
1
  import { FORMATS } from "../../translator/formats.js";
2
2
  import { needsTranslation } from "../../translator/index.js";
3
+ import { fromOpenAIFinish } from "../../translator/concerns/finishReason.js";
3
4
  import { ollamaBodyToOpenAI } from "../../translator/response/ollama-to-openai.js";
4
5
  import { addBufferToUsage, filterUsageForFormat } from "../../utils/usageTracking.js";
5
6
  import { createErrorResult } from "../../utils/error.js";
6
7
  import { HTTP_STATUS } from "../../config/runtimeConfig.js";
7
8
  import { parseSSEToOpenAIResponse } from "./sseToJsonHandler.js";
8
- import { buildRequestDetail, extractRequestConfig, extractUsageFromResponse, saveUsageStats } from "./requestDetail.js";
9
- import { appendRequestLog, saveRequestDetail } from '../../../dist/lib/usageDb.js';
9
+ import { buildRequestDetail, extractRequestConfig, extractUsageFromResponse, saveUsageStats, formatDoneLine } from "./requestDetail.js";
10
+ import { appendRequestLog, saveRequestDetail } from '../../dist/lib/usageDb.js';
10
11
  import { decloakToolNames } from "../../utils/claudeCloaking.js";
11
12
 
13
+ function parseToolArguments(value) {
14
+ if (!value) return {};
15
+ if (typeof value === "object") return value;
16
+ try {
17
+ return JSON.parse(value);
18
+ } catch {
19
+ return {};
20
+ }
21
+ }
22
+
23
+ function openAICompletionToClaudeMessage(responseBody) {
24
+ if (!responseBody?.choices?.[0]) return responseBody;
25
+ const choice = responseBody.choices[0];
26
+ const message = choice.message || {};
27
+ const content = [];
28
+
29
+ const reasoning = message.reasoning_content || message.provider_specific_fields?.reasoning_content || "";
30
+ if (reasoning) {
31
+ content.push({ type: "thinking", thinking: reasoning });
32
+ }
33
+ if (typeof message.content === "string" && message.content.length > 0) {
34
+ content.push({ type: "text", text: message.content });
35
+ }
36
+ for (const toolCall of message.tool_calls || []) {
37
+ const fn = toolCall.function || {};
38
+ content.push({
39
+ type: "tool_use",
40
+ id: toolCall.id || `toolu_${Date.now()}_${content.length}`,
41
+ name: fn.name || toolCall.name || "",
42
+ input: parseToolArguments(fn.arguments || toolCall.arguments),
43
+ });
44
+ }
45
+ if (content.length === 0) content.push({ type: "text", text: "" });
46
+
47
+ const usage = responseBody.usage || {};
48
+ return {
49
+ id: String(responseBody.id || `msg_${Date.now()}`).replace(/^chatcmpl-/, ""),
50
+ type: "message",
51
+ role: "assistant",
52
+ model: responseBody.model || "unknown",
53
+ content,
54
+ stop_reason: fromOpenAIFinish(choice.finish_reason, FORMATS.CLAUDE),
55
+ stop_sequence: null,
56
+ usage: {
57
+ input_tokens: usage.prompt_tokens || usage.input_tokens || 0,
58
+ output_tokens: usage.completion_tokens || usage.output_tokens || 0,
59
+ },
60
+ };
61
+ }
62
+
12
63
  /**
13
64
  * Translate non-streaming response body from provider format → OpenAI format.
14
65
  */
15
66
  export function translateNonStreamingResponse(responseBody, targetFormat, sourceFormat) {
16
- if (targetFormat === sourceFormat || targetFormat === FORMATS.OPENAI) return responseBody;
67
+ if (targetFormat === sourceFormat) return responseBody;
68
+ if (targetFormat === FORMATS.OPENAI && sourceFormat === FORMATS.CLAUDE) {
69
+ return openAICompletionToClaudeMessage(responseBody);
70
+ }
71
+ if (targetFormat === FORMATS.OPENAI) return responseBody;
17
72
 
18
73
  // Gemini / Antigravity
19
74
  if (targetFormat === FORMATS.GEMINI || targetFormat === FORMATS.ANTIGRAVITY || targetFormat === FORMATS.GEMINI_CLI || targetFormat === FORMATS.VERTEX) {
@@ -37,6 +92,12 @@ export function translateNonStreamingResponse(responseBody, targetFormat, source
37
92
  function: { name: part.functionCall.name, arguments: JSON.stringify(part.functionCall.args || {}) }
38
93
  });
39
94
  }
95
+ // Handle inline image data (from image generation models)
96
+ const inlineData = part.inlineData || part.inline_data;
97
+ if (inlineData?.data) {
98
+ const mimeType = inlineData.mimeType || inlineData.mime_type || "image/png";
99
+ textContent += `\n![image](data:${mimeType};base64,${inlineData.data})\n`;
100
+ }
40
101
  }
41
102
  }
42
103
 
@@ -76,7 +137,12 @@ export function translateNonStreamingResponse(responseBody, targetFormat, source
76
137
  // missing/null (e.g. M3 with max_tokens:1 spends the budget on thinking
77
138
  // and returns `content: null`). Returning the raw body would leave the
78
139
  // OpenAI client without a `choices` array and surface as a UI test error.
79
- if (responseBody.content && !Array.isArray(responseBody.content)) return responseBody;
140
+ // Early return if the response is already in OpenAI format (has choices array)
141
+ // or if it has content as a non-array value (likely a different non-Claude format).
142
+ // Some providers (e.g. xiaomi-tokenplan) return OpenAI-format responses even when
143
+ // the request was translated to Claude format — the targetFormat is Claude but the
144
+ // actual response is OpenAI-native and needs no further translation.
145
+ if (responseBody.choices || (responseBody.content && !Array.isArray(responseBody.content))) return responseBody;
80
146
 
81
147
  let textContent = "", thinkingContent = "";
82
148
  const toolCalls = [];
@@ -132,7 +198,7 @@ export function translateNonStreamingResponse(responseBody, targetFormat, source
132
198
  /**
133
199
  * Handle non-streaming response from provider.
134
200
  */
135
- export async function handleNonStreamingResponse({ providerResponse, provider, model, sourceFormat, targetFormat, body, stream, translatedBody, finalBody, requestStartTime, connectionId, apiKey, clientRawRequest, onRequestSuccess, reqLogger, toolNameMap, trackDone, appendLog }) {
201
+ export async function handleNonStreamingResponse({ providerResponse, provider, model, sourceFormat, targetFormat, body, stream, translatedBody, finalBody, requestStartTime, connectionId, apiKey, clientRawRequest, onRequestSuccess, reqLogger, toolNameMap, trackDone, appendLog, pxpipe, reqTag, log }) {
136
202
  trackDone();
137
203
  const contentType = providerResponse.headers.get("content-type") || "";
138
204
  let responseBody;
@@ -156,18 +222,26 @@ export async function handleNonStreamingResponse({ providerResponse, provider, m
156
222
  }
157
223
 
158
224
  reqLogger.logProviderResponse(providerResponse.status, providerResponse.statusText, providerResponse.headers, responseBody);
159
- if (onRequestSuccess) await onRequestSuccess();
225
+ if (onRequestSuccess) {
226
+ Promise.resolve()
227
+ .then(onRequestSuccess)
228
+ .catch(err => {
229
+ console.error("[ChatCore] onRequestSuccess failed:", err?.message || err);
230
+ });
231
+ }
160
232
 
161
233
  // Decloak tool_use names once on raw Claude body, before any translation (INPUT side)
162
234
  responseBody = decloakToolNames(responseBody, toolNameMap);
163
235
 
164
236
  const usage = extractUsageFromResponse(responseBody);
165
237
  appendLog({ tokens: usage, status: "200 OK" });
166
- saveUsageStats({ provider, model, tokens: usage, connectionId, apiKey, endpoint: clientRawRequest?.endpoint });
238
+ saveUsageStats({ provider, model, tokens: usage, connectionId, apiKey, endpoint: clientRawRequest?.endpoint, silent: true });
239
+ if (log?.line) log.line(reqTag, "📊", formatDoneLine({ usage, latency: { total: Date.now() - requestStartTime } }));
167
240
 
168
241
  const translatedResponse = needsTranslation(targetFormat, sourceFormat)
169
242
  ? translateNonStreamingResponse(responseBody, targetFormat, sourceFormat)
170
243
  : responseBody;
244
+ const isClaudeMessageResponse = sourceFormat === FORMATS.CLAUDE && translatedResponse?.type === "message";
171
245
 
172
246
  // Fix finish_reason for tool_calls: some providers return non-standard values (e.g. "other")
173
247
  if (translatedResponse?.choices?.[0]) {
@@ -180,95 +254,32 @@ export async function handleNonStreamingResponse({ providerResponse, provider, m
180
254
  }
181
255
 
182
256
  // Ensure OpenAI-required fields
183
- if (!translatedResponse.object) translatedResponse.object = "chat.completion";
184
- if (!translatedResponse.created) translatedResponse.created = Math.floor(Date.now() / 1000);
257
+ if (!isClaudeMessageResponse) {
258
+ if (!translatedResponse.object) translatedResponse.object = "chat.completion";
259
+ if (!translatedResponse.created) translatedResponse.created = Math.floor(Date.now() / 1000);
260
+ }
185
261
 
186
262
  // Strip Azure-specific fields
187
- delete translatedResponse.prompt_filter_results;
188
- if (translatedResponse?.choices) {
189
- for (const choice of translatedResponse.choices) delete choice.content_filter_results;
263
+ if (!isClaudeMessageResponse) {
264
+ delete translatedResponse.prompt_filter_results;
265
+ if (translatedResponse?.choices) {
266
+ for (const choice of translatedResponse.choices) delete choice.content_filter_results;
267
+ }
190
268
  }
191
269
 
192
270
  if (translatedResponse?.usage) {
193
271
  translatedResponse.usage = filterUsageForFormat(addBufferToUsage(translatedResponse.usage), sourceFormat);
194
272
  }
195
273
 
196
- // Strip reasoning_content some clients (e.g. Firecrawl AI SDK) have JSON parsers that
197
- // break on this non-standard field, even though OpenAI allows it in extensions.
198
- if (translatedResponse?.choices && sourceFormat !== FORMATS.CLAUDE) {
274
+ // Strip reasoning_content only when content is non-empty.
275
+ // When content is empty (e.g. thinking models that used all tokens for reasoning),
276
+ // reasoning_content is the only useful output and must be preserved.
277
+ if (!isClaudeMessageResponse && translatedResponse?.choices) {
199
278
  for (const choice of translatedResponse.choices) {
200
- if (choice?.message) delete choice.message.reasoning_content;
201
- }
202
- }
203
-
204
- // If client sent Anthropic Messages format (/v1/messages), convert response back to
205
- // Anthropic format. 9router always works in OpenAI format internally; this step
206
- // is the reverse-translation for Claude Code / Anthropic SDK clients.
207
- if (sourceFormat === FORMATS.CLAUDE) {
208
- const choice = translatedResponse?.choices?.[0];
209
- const msg = choice?.message || {};
210
- const contentBlocks = [];
211
- if (msg.reasoning_content) contentBlocks.push({ type: "thinking", thinking: msg.reasoning_content });
212
- if (msg.content) contentBlocks.push({ type: "text", text: msg.content });
213
- if (Array.isArray(msg.tool_calls)) {
214
- for (const tc of msg.tool_calls) {
215
- let input = {};
216
- try { input = JSON.parse(tc.function?.arguments || "{}"); } catch {}
217
- contentBlocks.push({ type: "tool_use", id: tc.id, name: tc.function?.name, input });
279
+ if (choice?.message?.reasoning_content && choice.message.content) {
280
+ delete choice.message.reasoning_content;
218
281
  }
219
282
  }
220
- // Claude Code requires at least one text or tool_use block — cannot be only thinking
221
- const hasTextOrTool = contentBlocks.some(b => b.type === "text" || b.type === "tool_use");
222
- if (!hasTextOrTool) contentBlocks.push({ type: "text", text: "" });
223
-
224
- let stopReason = "end_turn";
225
- const fr = choice?.finish_reason;
226
- if (fr === "tool_calls") stopReason = "tool_use";
227
- else if (fr === "length") stopReason = "max_tokens";
228
-
229
- const anthropicResponse = {
230
- id: translatedResponse.id || `msg_${Date.now()}`,
231
- type: "message",
232
- role: "assistant",
233
- content: contentBlocks.length ? contentBlocks : [{ type: "text", text: "" }],
234
- model: translatedResponse.model || model,
235
- stop_reason: stopReason,
236
- stop_sequence: null,
237
- };
238
- if (translatedResponse.usage) {
239
- anthropicResponse.usage = {
240
- input_tokens: translatedResponse.usage.prompt_tokens || 0,
241
- output_tokens: translatedResponse.usage.completion_tokens || 0,
242
- };
243
- } else {
244
- anthropicResponse.usage = { input_tokens: 0, output_tokens: 0 };
245
- }
246
-
247
- reqLogger.logConvertedResponse(anthropicResponse);
248
- const totalLatency = Date.now() - requestStartTime;
249
- saveRequestDetail(buildRequestDetail({
250
- provider, model, connectionId,
251
- latency: { ttft: totalLatency, total: totalLatency },
252
- tokens: usage || { prompt_tokens: 0, completion_tokens: 0 },
253
- request: extractRequestConfig(body, stream),
254
- providerRequest: finalBody || translatedBody || null,
255
- providerResponse: responseBody || null,
256
- response: {
257
- content: msg.content || null,
258
- thinking: msg.reasoning_content || null,
259
- finish_reason: choice?.finish_reason || "unknown"
260
- },
261
- status: "success"
262
- }, { endpoint: clientRawRequest?.endpoint || null })).catch(err => {
263
- console.error("[RequestDetail] Failed to save:", err.message);
264
- });
265
-
266
- return {
267
- success: true,
268
- response: new Response(JSON.stringify(anthropicResponse), {
269
- headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" }
270
- })
271
- };
272
283
  }
273
284
 
274
285
  reqLogger.logConvertedResponse(translatedResponse);
@@ -286,6 +297,7 @@ export async function handleNonStreamingResponse({ providerResponse, provider, m
286
297
  thinking: translatedResponse?.choices?.[0]?.message?.reasoning_content || translatedResponse?.reasoning_content || null,
287
298
  finish_reason: translatedResponse?.choices?.[0]?.finish_reason || "unknown"
288
299
  },
300
+ pxpipe,
289
301
  status: "success"
290
302
  }, { endpoint: clientRawRequest?.endpoint || null })).catch(err => {
291
303
  console.error("[RequestDetail] Failed to save:", err.message);
@@ -1,5 +1,6 @@
1
- import { saveRequestUsage, appendRequestLog, saveRequestDetail } from '../../../dist/lib/usageDb.js';
1
+ import { saveRequestUsage, appendRequestLog, saveRequestDetail } from '../../dist/lib/usageDb.js';
2
2
  import { COLORS } from "../../utils/stream.js";
3
+ import { canonicalizeUsage } from "../../utils/usageTracking.js";
3
4
 
4
5
  const OPTIONAL_PARAMS = [
5
6
  "temperature", "top_p", "top_k",
@@ -48,7 +49,8 @@ export function extractUsageFromResponse(responseBody) {
48
49
  return {
49
50
  prompt_tokens: responseBody.usageMetadata.promptTokenCount || 0,
50
51
  completion_tokens: responseBody.usageMetadata.candidatesTokenCount || 0,
51
- reasoning_tokens: responseBody.usageMetadata.thoughtsTokenCount
52
+ cached_tokens: responseBody.usageMetadata.cachedContentTokenCount || 0,
53
+ reasoning_tokens: responseBody.usageMetadata.thoughtsTokenCount || 0
52
54
  };
53
55
  }
54
56
 
@@ -67,12 +69,31 @@ export function buildRequestDetail(base, overrides = {}) {
67
69
  providerRequest: base.providerRequest || null,
68
70
  providerResponse: base.providerResponse || null,
69
71
  response: base.response || {},
72
+ pxpipe: base.pxpipe || undefined,
70
73
  status: base.status || "success",
71
74
  ...overrides
72
75
  };
73
76
  }
74
77
 
75
- export function saveUsageStats({ provider, model, tokens, connectionId, apiKey, endpoint, label = "USAGE" }) {
78
+ // Build the "done" summary: duration, ttft, in/out tokens with cache breakdown
79
+ export function formatDoneLine({ usage, latency }) {
80
+ const u = usage || {};
81
+ const inTok = u.prompt_tokens ?? u.input_tokens ?? 0;
82
+ const outTok = u.completion_tokens ?? u.output_tokens ?? 0;
83
+ const cacheRead = u.cache_read_input_tokens ?? u.cached_tokens ?? u.prompt_tokens_details?.cached_tokens ?? 0;
84
+ const cacheCreate = u.cache_creation_input_tokens ?? 0;
85
+ let inStr = `IN ${inTok}`;
86
+ if (cacheRead || cacheCreate) {
87
+ const parts = [];
88
+ if (cacheRead) parts.push(`↻${cacheRead}`);
89
+ if (cacheCreate) parts.push(`+${cacheCreate}`);
90
+ inStr += ` (CACHE ${parts.join(" ")})`;
91
+ }
92
+ const ttftStr = latency?.ttft ? ` · TTFT ${latency.ttft}ms` : "";
93
+ return `DONE ${latency?.total ?? 0}ms${ttftStr} · ${inStr} · OUT ${outTok}`;
94
+ }
95
+
96
+ export function saveUsageStats({ provider, model, tokens, connectionId, apiKey, endpoint, label = "USAGE", silent = false }) {
76
97
  if (!tokens || typeof tokens !== "object") return;
77
98
 
78
99
  const inTokens = tokens.input_tokens ?? tokens.prompt_tokens ?? 0;
@@ -80,12 +101,15 @@ export function saveUsageStats({ provider, model, tokens, connectionId, apiKey,
80
101
 
81
102
  if (inTokens === 0 && outTokens === 0) return;
82
103
 
83
- const time = new Date().toLocaleTimeString("en-US", { hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit" });
84
- const accountSuffix = connectionId ? ` | account=${connectionId.slice(0, 8)}...` : "";
85
- console.log(`${COLORS.green}[${time}] 📊 [${label}] ${provider.toUpperCase()} | in=${inTokens} | out=${outTokens}${accountSuffix}${COLORS.reset}`);
104
+ if (!silent) {
105
+ const time = new Date().toLocaleTimeString("en-US", { hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit" });
106
+ const accountSuffix = connectionId ? ` | account=${connectionId.slice(0, 8)}...` : "";
107
+ console.log(`${COLORS.green}[${time}] 📊 [${label}] ${provider.toUpperCase()} | in=${inTokens} | out=${outTokens}${accountSuffix}${COLORS.reset}`);
108
+ }
86
109
 
87
- // Normalize to OpenAI token shape for storage
88
- const normalized = {
110
+ // Canonicalize to one storage convention (prompt_tokens cache-inclusive) so
111
+ // cached/cache-creation tokens survive to cost calc + stats. See canonicalizeUsage.
112
+ const normalized = canonicalizeUsage(tokens) || {
89
113
  prompt_tokens: tokens.prompt_tokens ?? tokens.input_tokens ?? 0,
90
114
  completion_tokens: tokens.completion_tokens ?? tokens.output_tokens ?? 0
91
115
  };
@@ -2,8 +2,12 @@ import { convertResponsesStreamToJson } from "../../transformer/streamToJsonConv
2
2
  import { createErrorResult } from "../../utils/error.js";
3
3
  import { HTTP_STATUS } from "../../config/runtimeConfig.js";
4
4
  import { FORMATS } from "../../translator/formats.js";
5
- import { buildRequestDetail, extractRequestConfig, saveUsageStats } from "./requestDetail.js";
6
- import { saveRequestDetail, appendRequestLog } from '../../../dist/lib/usageDb.js';
5
+ import { PROVIDERS } from "../../config/providers.js";
6
+ import { buildRequestDetail, extractRequestConfig, saveUsageStats, formatDoneLine } from "./requestDetail.js";
7
+
8
+ // Responses-API providers (e.g. codex) may emit SSE without content-type + use Responses output shape
9
+ const isResponsesProvider = (p) => PROVIDERS[p]?.format === FORMATS.OPENAI_RESPONSES;
10
+ import { saveRequestDetail, appendRequestLog } from '../../dist/lib/usageDb.js';
7
11
 
8
12
  function textFromResponsesMessageItem(item) {
9
13
  if (!item?.content || !Array.isArray(item.content)) return "";
@@ -98,9 +102,9 @@ export function parseSSEToOpenAIResponse(rawSSE, fallbackModel) {
98
102
  * Handle case: provider forced streaming but client wants JSON.
99
103
  * Supports both Codex/Responses API SSE and standard Chat Completions SSE.
100
104
  */
101
- export async function handleForcedSSEToJson({ providerResponse, sourceFormat, provider, model, body, stream, translatedBody, finalBody, requestStartTime, connectionId, apiKey, clientRawRequest, onRequestSuccess, trackDone, appendLog }) {
105
+ export async function handleForcedSSEToJson({ providerResponse, sourceFormat, provider, model, body, stream, translatedBody, finalBody, requestStartTime, connectionId, apiKey, clientRawRequest, onRequestSuccess, trackDone, appendLog, reqTag, log }) {
102
106
  const contentType = providerResponse.headers.get("content-type") || "";
103
- const isSSE = contentType.includes("text/event-stream") || (contentType === "" && provider === "codex");
107
+ const isSSE = contentType.includes("text/event-stream") || (contentType === "" && isResponsesProvider(provider));
104
108
  if (!isSSE) return null; // not handled here
105
109
 
106
110
  trackDone();
@@ -112,7 +116,7 @@ export async function handleForcedSSEToJson({ providerResponse, sourceFormat, pr
112
116
  };
113
117
 
114
118
  // Codex/Responses API SSE path
115
- const isCodexResponsesApi = provider === "codex" || sourceFormat === FORMATS.OPENAI_RESPONSES;
119
+ const isCodexResponsesApi = isResponsesProvider(provider) || sourceFormat === FORMATS.OPENAI_RESPONSES;
116
120
  if (isCodexResponsesApi) {
117
121
  try {
118
122
  const jsonResponse = await convertResponsesStreamToJson(providerResponse.body);
@@ -120,7 +124,8 @@ export async function handleForcedSSEToJson({ providerResponse, sourceFormat, pr
120
124
 
121
125
  const usage = jsonResponse.usage || {};
122
126
  appendLog({ tokens: usage, status: "200 OK" });
123
- saveUsageStats({ provider, model, tokens: usage, connectionId, apiKey, endpoint: clientRawRequest?.endpoint });
127
+ saveUsageStats({ provider, model, tokens: usage, connectionId, apiKey, endpoint: clientRawRequest?.endpoint, silent: true });
128
+ if (log?.line) log.line(reqTag, "📊", formatDoneLine({ usage, latency: { total: Date.now() - requestStartTime } }));
124
129
 
125
130
  const { msgItem, textContent } = pickAssistantMessageForChatCompletion(jsonResponse.output);
126
131
  const totalLatency = Date.now() - requestStartTime;
@@ -196,7 +201,8 @@ export async function handleForcedSSEToJson({ providerResponse, sourceFormat, pr
196
201
 
197
202
  const usage = parsed.usage || {};
198
203
  appendLog({ tokens: usage, status: "200 OK" });
199
- saveUsageStats({ provider, model, tokens: usage, connectionId, apiKey, endpoint: clientRawRequest?.endpoint });
204
+ saveUsageStats({ provider, model, tokens: usage, connectionId, apiKey, endpoint: clientRawRequest?.endpoint, silent: true });
205
+ if (log?.line) log.line(reqTag, "📊", formatDoneLine({ usage, latency: { total: Date.now() - requestStartTime } }));
200
206
 
201
207
  const totalLatency = Date.now() - requestStartTime;
202
208
  saveRequestDetail(buildRequestDetail({
@@ -2,15 +2,21 @@ import { FORMATS } from "../../translator/formats.js";
2
2
  import { needsTranslation } from "../../translator/index.js";
3
3
  import { createSSETransformStreamWithLogger, createPassthroughStreamWithLogger } from "../../utils/stream.js";
4
4
  import { pipeWithDisconnect } from "../../utils/streamHandler.js";
5
+ import { PROVIDERS } from "../../config/providers.js";
6
+ import { STREAM_STALL_TIMEOUT_MS } from "../../config/runtimeConfig.js";
5
7
  import { buildAbortedResponsesTerminalBytes } from "../../utils/responsesStreamHelpers.js";
6
- import { buildRequestDetail, extractRequestConfig, saveUsageStats } from "./requestDetail.js";
7
- import { saveRequestDetail } from '../../../dist/lib/usageDb.js';
8
-
9
- const SSE_HEADERS = {
10
- "Content-Type": "text/event-stream",
11
- "Cache-Control": "no-cache",
12
- "Connection": "keep-alive",
13
- "Access-Control-Allow-Origin": "*"
8
+ import { buildRequestDetail, extractRequestConfig, saveUsageStats, formatDoneLine } from "./requestDetail.js";
9
+ import { saveRequestDetail } from '../../dist/lib/usageDb.js';
10
+ import { SSE_HEADERS_CORS as SSE_HEADERS } from "../../utils/sseConstants.js";
11
+
12
+ // Codex returns Responses API SSE → which client format to translate INTO, by request sourceFormat.
13
+ // Gemini-family all map to ANTIGRAVITY decoder; unknown sources fall back to OPENAI.
14
+ const CODEX_SOURCE_TO_TARGET = {
15
+ [FORMATS.OPENAI_RESPONSES]: FORMATS.OPENAI_RESPONSES,
16
+ [FORMATS.CLAUDE]: FORMATS.CLAUDE,
17
+ [FORMATS.ANTIGRAVITY]: FORMATS.ANTIGRAVITY,
18
+ [FORMATS.GEMINI]: FORMATS.ANTIGRAVITY,
19
+ [FORMATS.GEMINI_CLI]: FORMATS.ANTIGRAVITY,
14
20
  };
15
21
 
16
22
  /**
@@ -18,15 +24,12 @@ const SSE_HEADERS = {
18
24
  */
19
25
  function buildTransformStream({ provider, sourceFormat, targetFormat, userAgent, reqLogger, toolNameMap, model, connectionId, body, onStreamComplete, apiKey }) {
20
26
  const isDroidCLI = userAgent?.toLowerCase().includes("droid") || userAgent?.toLowerCase().includes("codex-cli");
21
- const needsCodexTranslation = provider === "codex" && targetFormat === FORMATS.OPENAI_RESPONSES && !isDroidCLI;
27
+ // Responses-API providers (e.g. codex) emit Responses SSE translate into client format
28
+ const isResponsesProvider = PROVIDERS[provider]?.format === FORMATS.OPENAI_RESPONSES;
29
+ const needsCodexTranslation = isResponsesProvider && targetFormat === FORMATS.OPENAI_RESPONSES && !isDroidCLI;
22
30
 
23
31
  if (needsCodexTranslation) {
24
- // Codex returns Responses API SSE → translate to client format
25
- let codexTarget;
26
- if (sourceFormat === FORMATS.OPENAI_RESPONSES) codexTarget = FORMATS.OPENAI_RESPONSES;
27
- else if (sourceFormat === FORMATS.CLAUDE) codexTarget = FORMATS.CLAUDE;
28
- else if (sourceFormat === FORMATS.ANTIGRAVITY || sourceFormat === FORMATS.GEMINI || sourceFormat === FORMATS.GEMINI_CLI) codexTarget = FORMATS.ANTIGRAVITY;
29
- else codexTarget = FORMATS.OPENAI;
32
+ const codexTarget = CODEX_SOURCE_TO_TARGET[sourceFormat] || FORMATS.OPENAI;
30
33
  return createSSETransformStreamWithLogger(FORMATS.OPENAI_RESPONSES, codexTarget, provider, reqLogger, toolNameMap, model, connectionId, body, onStreamComplete, apiKey);
31
34
  }
32
35
 
@@ -40,19 +43,63 @@ function buildTransformStream({ provider, sourceFormat, targetFormat, userAgent,
40
43
  /**
41
44
  * Handle streaming response — pipe provider SSE through transform stream to client.
42
45
  */
43
- export function handleStreamingResponse({ providerResponse, provider, model, sourceFormat, targetFormat, userAgent, body, stream, translatedBody, finalBody, requestStartTime, connectionId, apiKey, clientRawRequest, onRequestSuccess, reqLogger, toolNameMap, streamController, onStreamComplete }) {
44
- if (onRequestSuccess) onRequestSuccess();
46
+ export async function handleStreamingResponse({ providerResponse, provider, model, sourceFormat, targetFormat, userAgent, body, stream, translatedBody, finalBody, requestStartTime, connectionId, apiKey, clientRawRequest, onRequestSuccess, reqLogger, toolNameMap, streamController, onStreamComplete, streamDetailId, pxpipe, reqTag, log }) {
47
+ if (onRequestSuccess) {
48
+ Promise.resolve()
49
+ .then(onRequestSuccess)
50
+ .catch(err => {
51
+ console.error("[ChatCore] onRequestSuccess failed:", err?.message || err);
52
+ });
53
+ }
54
+
55
+ // When upstream returns HTML/text instead of SSE (e.g. Cloudflare 5xx error
56
+ // page), piping it through the SSE transform stream causes Next.js
57
+ // "failed to pipe response" and crashes the chat router. Read the body,
58
+ // pull a short human-readable message from the <title>, sanitize it, and
59
+ // return a clean JSON error instead. The message is stripped of HTML tags
60
+ // and clamped so untrusted upstream text never reaches the client verbatim
61
+ // (the UI may render error.message as HTML).
62
+ const upstreamContentType = (providerResponse.headers.get('content-type') || '').toLowerCase();
63
+ if (upstreamContentType && !upstreamContentType.includes('text/event-stream') && !upstreamContentType.includes('application/json')) {
64
+ const bodyText = await providerResponse.text().catch(() => '');
65
+ const titleMatch = bodyText.match(/<title>([^<]+)<\/title>/i);
66
+ const sanitizedTitle = (titleMatch?.[1] || '').replace(/<[^>]*>/g, '').replace(/[\r\n]+/g, ' ').trim().slice(0, 160);
67
+ const shortMsg = sanitizedTitle
68
+ || (bodyText.length < 200 ? bodyText.replace(/<[^>]*>/g, '').trim().slice(0, 160) : `Upstream returned non-SSE response (${upstreamContentType})`);
69
+ const status = providerResponse.status || 502;
70
+ if (log?.errorLine) log.errorLine(reqTag, "✗", `BLOCKED ${status} · ${provider}/${model} · non-SSE (${upstreamContentType})\n ${shortMsg}`);
71
+ else console.warn(`[STREAM] ${provider} | ${model} | blocked pipe: ${shortMsg} [${status}]`);
72
+ streamController?.handleError?.(new Error(`upstream non-SSE: ${status}`));
73
+ return {
74
+ success: false,
75
+ response: new Response(JSON.stringify({ error: { message: `[${status}]: ${shortMsg}` } }), {
76
+ status,
77
+ headers: { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' },
78
+ }),
79
+ };
80
+ }
45
81
 
46
82
  const transformStream = buildTransformStream({ provider, sourceFormat, targetFormat, userAgent, reqLogger, toolNameMap, model, connectionId, body, onStreamComplete, apiKey });
47
83
 
48
84
  // Responses passthrough: synthesize response.failed + [DONE] if the stream aborts/stalls before a terminal event
49
85
  const isResponsesPassthrough = sourceFormat === FORMATS.OPENAI_RESPONSES && targetFormat === FORMATS.OPENAI_RESPONSES;
50
86
  const onAbortTerminal = isResponsesPassthrough ? buildAbortedResponsesTerminalBytes : null;
51
- const transformedBody = pipeWithDisconnect(providerResponse, transformStream, streamController, onAbortTerminal);
52
-
53
- // NOTE: we intentionally do NOT save a placeholder "streaming in progress" record here.
54
- // The final record is saved by onStreamComplete (built by buildOnStreamComplete) when the
55
- // stream finishes. This avoids creating two DB rows per request (the phantom ttft=0 duplicate).
87
+ const stallTimeoutMs = PROVIDERS[provider]?.stallTimeoutMs || STREAM_STALL_TIMEOUT_MS;
88
+ const transformedBody = pipeWithDisconnect(providerResponse, transformStream, streamController, onAbortTerminal, stallTimeoutMs);
89
+
90
+ saveRequestDetail(buildRequestDetail({
91
+ provider, model, connectionId,
92
+ latency: { ttft: 0, total: Date.now() - requestStartTime },
93
+ tokens: { prompt_tokens: 0, completion_tokens: 0 },
94
+ request: extractRequestConfig(body, stream),
95
+ providerRequest: finalBody || translatedBody || null,
96
+ providerResponse: "[Streaming - raw response not captured]",
97
+ response: { content: "[Streaming in progress...]", thinking: null, type: "streaming" },
98
+ pxpipe,
99
+ status: "success"
100
+ }, { id: streamDetailId })).catch(err => {
101
+ console.error("[RequestDetail] Failed to save streaming request:", err.message);
102
+ });
56
103
 
57
104
  return {
58
105
  success: true,
@@ -63,7 +110,7 @@ export function handleStreamingResponse({ providerResponse, provider, model, sou
63
110
  /**
64
111
  * Build onStreamComplete callback for streaming usage tracking.
65
112
  */
66
- export function buildOnStreamComplete({ provider, model, connectionId, apiKey, requestStartTime, body, stream, finalBody, translatedBody, clientRawRequest }) {
113
+ export function buildOnStreamComplete({ provider, model, connectionId, apiKey, requestStartTime, body, stream, finalBody, translatedBody, clientRawRequest, pxpipe, reqTag, log }) {
67
114
  const streamDetailId = `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
68
115
 
69
116
  const onStreamComplete = (contentObj, usage, ttftAt) => {
@@ -82,14 +129,16 @@ export function buildOnStreamComplete({ provider, model, connectionId, apiKey, r
82
129
  providerRequest: finalBody || translatedBody || null,
83
130
  providerResponse: safeContent,
84
131
  response: { content: safeContent, thinking: safeThinking, type: "streaming" },
132
+ pxpipe,
85
133
  status: "success"
86
134
  }, { id: streamDetailId })).catch(err => {
87
135
  console.error("[RequestDetail] Failed to update streaming content:", err.message);
88
136
  });
89
137
 
90
- saveUsageStats({ provider, model, tokens: usage, connectionId, apiKey, endpoint: clientRawRequest?.endpoint, label: "STREAM USAGE" });
138
+ // Persist stream usage to DB (no console line; the "📊 done" line below is authoritative)
139
+ saveUsageStats({ provider, model, tokens: usage, connectionId, apiKey, endpoint: clientRawRequest?.endpoint, label: "STREAM USAGE", silent: true });
140
+ if (log?.line) log.line(reqTag, "📊", formatDoneLine({ usage, latency }));
91
141
  };
92
142
 
93
143
  return { onStreamComplete, streamDetailId };
94
144
  }
95
-