@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.
- package/dist/lib/headroom/detect.js +97 -7
- package/dist/lib/headroom/process.js +136 -4
- package/dist/open-sse/config/appConstants.js +23 -79
- package/dist/open-sse/config/cliFingerprints.js +1 -1
- package/dist/open-sse/config/errorConfig.js +0 -11
- package/dist/open-sse/config/grokCli.js +10 -0
- package/dist/open-sse/config/kiroConstants.js +99 -46
- package/dist/open-sse/config/runtimeConfig.js +2 -0
- package/dist/open-sse/config/ttsModels.js +2 -0
- package/dist/open-sse/executors/antigravity.js +250 -164
- package/dist/open-sse/executors/base.js +16 -11
- package/dist/open-sse/executors/codex.js +269 -1108
- package/dist/open-sse/executors/commandCode.js +1 -1
- package/dist/open-sse/executors/commandcode.js +10 -4
- package/dist/open-sse/executors/cursor.js +96 -203
- package/dist/open-sse/executors/default.js +146 -244
- package/dist/open-sse/executors/github.js +128 -73
- package/dist/open-sse/executors/grok-cli.js +527 -94
- package/dist/open-sse/executors/grok-web.js +5 -7
- package/dist/open-sse/executors/index.js +12 -0
- package/dist/open-sse/executors/kiro.js +150 -61
- package/dist/open-sse/executors/opencode-go.js +12 -4
- package/dist/open-sse/executors/opencode.js +1 -1
- package/dist/open-sse/executors/perplexity-web.js +20 -96
- package/dist/open-sse/executors/qoder.js +28 -18
- package/dist/open-sse/executors/vertex.js +53 -7
- package/dist/open-sse/executors/xiaomi-tokenplan.js +6 -5
- package/dist/open-sse/handlers/chatCore/nonStreamingHandler.js +97 -85
- package/dist/open-sse/handlers/chatCore/requestDetail.js +32 -8
- package/dist/open-sse/handlers/chatCore/sseToJsonHandler.js +13 -7
- package/dist/open-sse/handlers/chatCore/streamingHandler.js +74 -25
- package/dist/open-sse/handlers/chatCore.js +35 -41
- package/dist/open-sse/handlers/embeddingProviders/index.js +1 -0
- package/dist/open-sse/handlers/embeddingProviders/openai.js +8 -16
- package/dist/open-sse/handlers/imageProviders/blackForestLabs.js +2 -21
- package/dist/open-sse/handlers/imageProviders/cloudflareAi.js +2 -30
- package/dist/open-sse/handlers/imageProviders/codex.js +2 -20
- package/dist/open-sse/handlers/imageProviders/comfyui.js +5 -18
- package/dist/open-sse/handlers/imageProviders/falAi.js +2 -22
- package/dist/open-sse/handlers/imageProviders/gemini.js +2 -16
- package/dist/open-sse/handlers/imageProviders/huggingface.js +2 -17
- package/dist/open-sse/handlers/imageProviders/index.js +3 -4
- package/dist/open-sse/handlers/imageProviders/leonardo.js +1 -1
- package/dist/open-sse/handlers/imageProviders/nanobanana.js +4 -23
- package/dist/open-sse/handlers/imageProviders/openai.js +15 -43
- package/dist/open-sse/handlers/imageProviders/runwayml.js +2 -26
- package/dist/open-sse/handlers/imageProviders/sdwebui.js +4 -20
- package/dist/open-sse/handlers/imageProviders/stabilityAi.js +2 -26
- package/dist/open-sse/handlers/imageProviders/weavy.js +1 -1
- package/dist/open-sse/handlers/search/chatSearch.js +60 -14
- package/dist/open-sse/handlers/ttsProviders/gemini.js +14 -3
- package/dist/open-sse/handlers/ttsProviders/index.js +4 -2
- package/dist/open-sse/handlers/ttsProviders/openai.js +4 -1
- package/dist/open-sse/handlers/ttsProviders/openrouter.js +7 -4
- package/dist/open-sse/handlers/videoCore.js +166 -0
- package/dist/open-sse/handlers/videoProviders/weavy.js +1 -1
- package/dist/open-sse/mcp-server/catalog.js +1 -1
- package/dist/open-sse/mcp-server/server.js +2 -2
- package/dist/open-sse/mcp-server/tools/advancedTools.js +2 -2
- package/dist/open-sse/mcp-server/tools/agentSkillTools.js +1 -1
- package/dist/open-sse/mcp-server/tools/compressionTools.js +4 -4
- package/dist/open-sse/mcp-server/tools/gamificationTools.js +10 -10
- package/dist/open-sse/mcp-server/tools/githubSkillTools.js +1 -1
- package/dist/open-sse/mcp-server/tools/memoryTools.js +3 -3
- package/dist/open-sse/mcp-server/tools/notionTools.js +2 -2
- package/dist/open-sse/mcp-server/tools/obsidianTools.js +2 -2
- package/dist/open-sse/mcp-server/tools/pickFastestModel.js +1 -1
- package/dist/open-sse/mcp-server/tools/pluginTools.js +4 -4
- package/dist/open-sse/mcp-server/tools/skillTools.js +2 -2
- package/dist/open-sse/providers/capabilities.js +24 -4
- package/dist/open-sse/providers/registry/alicode-intl.js +1 -1
- package/dist/open-sse/providers/registry/anthropic.js +1 -8
- package/dist/open-sse/providers/registry/codex.js +0 -9
- package/dist/open-sse/providers/registry/cohere.js +0 -2
- package/dist/open-sse/providers/registry/gemini.js +0 -2
- package/dist/open-sse/providers/registry/github.js +9 -6
- package/dist/open-sse/providers/registry/grok-cli.js +28 -18
- package/dist/open-sse/providers/registry/index.js +74 -80
- package/dist/open-sse/providers/registry/kiro.js +12 -0
- package/dist/open-sse/providers/registry/mistral.js +0 -3
- package/dist/open-sse/providers/registry/openai.js +0 -3
- package/dist/open-sse/providers/registry/perplexity.js +0 -2
- package/dist/open-sse/providers/registry/xai.js +5 -3
- package/dist/open-sse/rtk/autodetect.js +10 -2
- package/dist/open-sse/rtk/caveman.js +2 -93
- package/dist/open-sse/rtk/cavemanPrompts.js +34 -2
- package/dist/open-sse/rtk/constants.js +7 -2
- package/dist/open-sse/rtk/headroom.js +133 -0
- package/dist/open-sse/rtk/ponytail.js +1 -1
- package/dist/open-sse/rtk/registry.js +2 -0
- package/dist/open-sse/services/accountFallback.js +0 -108
- package/dist/open-sse/services/combo/quotaExhaustionCutoff.js +2 -2
- package/dist/open-sse/services/combo/quotaShareStrategy.js +1 -1
- package/dist/open-sse/services/combo/resolveAutoStrategy.js +1 -1
- package/dist/open-sse/services/combo/sessionStickiness.js +1 -1
- package/dist/open-sse/services/combo/targetSorters.js +2 -2
- package/dist/open-sse/services/combo/validateQuality.js +1 -1
- package/dist/open-sse/services/comboConfig.js +1 -1
- package/dist/open-sse/services/emergencyFallback.js +1 -1
- package/dist/open-sse/services/grokCliModels.js +127 -0
- package/dist/open-sse/services/model.js +7 -1
- package/dist/open-sse/services/provider.js +1 -1
- package/dist/open-sse/services/proxyAutoSelector.js +1 -1
- package/dist/open-sse/services/tokenRefresh/providers.js +3 -3
- package/dist/open-sse/services/usage/codex.js +148 -214
- package/dist/open-sse/services/usage/grok-cli.js +61 -7
- package/dist/open-sse/services/usage/misc.js +0 -19
- package/dist/open-sse/services/usage.js +0 -2
- package/dist/open-sse/translator/concerns/paramSupport.js +2 -2
- package/dist/open-sse/translator/concerns/thinkingUnified.js +6 -0
- package/dist/open-sse/translator/index.js +97 -59
- package/dist/open-sse/translator/request/claude-to-kiro.js +67 -40
- package/dist/open-sse/translator/request/openai-responses.js +1 -0
- package/dist/open-sse/translator/request/openai-to-commandcode.js +0 -4
- package/dist/open-sse/translator/request/openai-to-kiro.js +49 -20
- package/dist/open-sse/translator/response/commandcode-to-openai.js +0 -4
- package/dist/open-sse/utils/bypassHandler.js +18 -3
- package/dist/open-sse/utils/claudeCloaking.js +15 -5
- package/dist/open-sse/utils/cursorImages.js +1 -1
- package/dist/open-sse/utils/error.js +0 -17
- package/dist/open-sse/utils/keepaliveThreshold.js +4 -4
- package/dist/open-sse/utils/kiroSessionReplay.js +125 -0
- package/dist/open-sse/utils/noThinkingAlias.js +1 -1
- package/dist/open-sse/utils/proxyFetch.js +0 -1
- package/dist/open-sse/utils/reasoningContentInjector.js +4 -7
- package/dist/open-sse/utils/responsesStatePolicy.js +1 -1
- package/dist/open-sse/utils/responsesStreamHelpers.js +1 -0
- package/dist/open-sse/utils/sessionManager.js +49 -22
- package/dist/open-sse/utils/stream.js +54 -77
- package/dist/open-sse/utils/streamHandler.js +19 -15
- package/dist/open-sse/utils/streamHelpers.js +2 -17
- package/dist/open-sse/utils/usageTracking.js +103 -12
- package/dist/routes/headroom/extras/route.js +58 -0
- package/dist/routes/headroom/restart/route.js +33 -0
- package/open-sse/config/appConstants.js +23 -79
- package/open-sse/config/errorConfig.js +0 -11
- package/open-sse/config/grokCli.js +10 -0
- package/open-sse/config/kiroConstants.js +99 -46
- package/open-sse/config/runtimeConfig.js +2 -0
- package/open-sse/config/ttsModels.js +2 -0
- package/open-sse/executors/antigravity.js +250 -164
- package/open-sse/executors/base.js +16 -11
- package/open-sse/executors/codex.js +269 -1108
- package/open-sse/executors/commandcode.js +10 -4
- package/open-sse/executors/cursor.js +96 -203
- package/open-sse/executors/default.js +146 -244
- package/open-sse/executors/github.js +128 -73
- package/open-sse/executors/grok-cli.js +527 -94
- package/open-sse/executors/grok-web.js +5 -7
- package/open-sse/executors/index.js +12 -0
- package/open-sse/executors/kiro.js +150 -61
- package/open-sse/executors/opencode-go.js +12 -4
- package/open-sse/executors/opencode.js +1 -1
- package/open-sse/executors/perplexity-web.js +20 -96
- package/open-sse/executors/qoder.js +28 -18
- package/open-sse/executors/vertex.js +53 -7
- package/open-sse/executors/xiaomi-tokenplan.js +6 -5
- package/open-sse/handlers/chatCore/nonStreamingHandler.js +97 -85
- package/open-sse/handlers/chatCore/requestDetail.js +32 -8
- package/open-sse/handlers/chatCore/sseToJsonHandler.js +13 -7
- package/open-sse/handlers/chatCore/streamingHandler.js +74 -25
- package/open-sse/handlers/chatCore.js +35 -41
- package/open-sse/handlers/embeddingProviders/index.js +1 -0
- package/open-sse/handlers/embeddingProviders/openai.js +8 -16
- package/open-sse/handlers/imageProviders/blackForestLabs.js +2 -21
- package/open-sse/handlers/imageProviders/cloudflareAi.js +2 -30
- package/open-sse/handlers/imageProviders/codex.js +2 -20
- package/open-sse/handlers/imageProviders/comfyui.js +5 -18
- package/open-sse/handlers/imageProviders/falAi.js +2 -22
- package/open-sse/handlers/imageProviders/gemini.js +2 -16
- package/open-sse/handlers/imageProviders/huggingface.js +2 -17
- package/open-sse/handlers/imageProviders/index.js +3 -4
- package/open-sse/handlers/imageProviders/leonardo.js +1 -1
- package/open-sse/handlers/imageProviders/nanobanana.js +4 -23
- package/open-sse/handlers/imageProviders/openai.js +15 -43
- package/open-sse/handlers/imageProviders/runwayml.js +2 -26
- package/open-sse/handlers/imageProviders/sdwebui.js +4 -20
- package/open-sse/handlers/imageProviders/stabilityAi.js +2 -26
- package/open-sse/handlers/imageProviders/weavy.js +1 -1
- package/open-sse/handlers/search/chatSearch.js +60 -14
- package/open-sse/handlers/ttsProviders/gemini.js +14 -3
- package/open-sse/handlers/ttsProviders/index.js +4 -2
- package/open-sse/handlers/ttsProviders/openai.js +4 -1
- package/open-sse/handlers/ttsProviders/openrouter.js +7 -4
- package/open-sse/handlers/videoCore.js +166 -0
- package/open-sse/handlers/videoProviders/weavy.js +1 -1
- package/open-sse/mcp-server/audit.js +1 -1
- package/open-sse/mcp-server/catalog.js +2 -2
- package/open-sse/mcp-server/descriptionCompressor.js +1 -1
- package/open-sse/mcp-server/mcpCallerIdentity.js +1 -1
- package/open-sse/mcp-server/schemas/tools.js +1 -1
- package/open-sse/mcp-server/server.js +4 -4
- package/open-sse/mcp-server/tools/advancedTools.js +5 -5
- package/open-sse/mcp-server/tools/agentSkillTools.js +1 -1
- package/open-sse/mcp-server/tools/compressionTools.js +4 -4
- package/open-sse/mcp-server/tools/gamificationTools.js +10 -10
- package/open-sse/mcp-server/tools/githubSkillTools.js +1 -1
- package/open-sse/mcp-server/tools/memoryTools.js +3 -3
- package/open-sse/mcp-server/tools/notionTools.js +2 -2
- package/open-sse/mcp-server/tools/obsidianTools.js +2 -2
- package/open-sse/mcp-server/tools/pickFastestModel.js +3 -3
- package/open-sse/mcp-server/tools/pluginTools.js +4 -4
- package/open-sse/mcp-server/tools/skillTools.js +2 -2
- package/open-sse/providers/capabilities.js +24 -4
- package/open-sse/providers/registry/alicode-intl.js +1 -1
- package/open-sse/providers/registry/anthropic.js +1 -8
- package/open-sse/providers/registry/codex.js +0 -9
- package/open-sse/providers/registry/cohere.js +0 -2
- package/open-sse/providers/registry/gemini.js +0 -2
- package/open-sse/providers/registry/github.js +9 -6
- package/open-sse/providers/registry/grok-cli.js +28 -18
- package/open-sse/providers/registry/index.js +74 -80
- package/open-sse/providers/registry/kiro.js +12 -0
- package/open-sse/providers/registry/mistral.js +0 -3
- package/open-sse/providers/registry/openai.js +0 -3
- package/open-sse/providers/registry/perplexity.js +0 -2
- package/open-sse/providers/registry/xai.js +5 -3
- package/open-sse/rtk/autodetect.js +10 -2
- package/open-sse/rtk/caveman.js +2 -93
- package/open-sse/rtk/cavemanPrompts.js +34 -2
- package/open-sse/rtk/constants.js +7 -2
- package/open-sse/rtk/headroom.js +133 -0
- package/open-sse/rtk/ponytail.js +1 -1
- package/open-sse/rtk/registry.js +2 -0
- package/open-sse/services/accountFallback.js +0 -108
- package/open-sse/services/combo/quotaExhaustionCutoff.js +2 -2
- package/open-sse/services/combo/quotaShareStrategy.js +1 -1
- package/open-sse/services/combo/resolveAutoStrategy.js +1 -1
- package/open-sse/services/combo/sessionStickiness.js +1 -1
- package/open-sse/services/combo/shadowRouting.js +1 -1
- package/open-sse/services/combo/targetSorters.js +4 -4
- package/open-sse/services/combo/validateQuality.js +1 -1
- package/open-sse/services/evalRouting.js +1 -1
- package/open-sse/services/grokCliModels.js +127 -0
- package/open-sse/services/model.js +7 -1
- package/open-sse/services/modelCapabilities.js +1 -1
- package/open-sse/services/modelFamilyFallback.js +1 -1
- package/open-sse/services/provider.js +1 -1
- package/open-sse/services/providerCooldownTracker.js +1 -1
- package/open-sse/services/rateLimitManager.js +1 -1
- package/open-sse/services/tokenRefresh/providers.js +3 -3
- package/open-sse/services/usage/codex.js +148 -214
- package/open-sse/services/usage/grok-cli.js +61 -7
- package/open-sse/services/usage/misc.js +0 -19
- package/open-sse/services/usage.js +0 -2
- package/open-sse/translator/concerns/paramSupport.js +2 -2
- package/open-sse/translator/concerns/thinkingUnified.js +6 -0
- package/open-sse/translator/index.js +97 -59
- package/open-sse/translator/request/claude-to-kiro.js +67 -40
- package/open-sse/translator/request/openai-responses.js +1 -0
- package/open-sse/translator/request/openai-to-commandcode.js +0 -4
- package/open-sse/translator/request/openai-to-kiro.js +49 -20
- package/open-sse/translator/response/commandcode-to-openai.js +0 -4
- package/open-sse/utils/bypassHandler.js +18 -3
- package/open-sse/utils/claudeCloaking.js +15 -5
- package/open-sse/utils/error.js +0 -17
- package/open-sse/utils/kiroSessionReplay.js +125 -0
- package/open-sse/utils/logger.js +1 -1
- package/open-sse/utils/proxyFetch.js +0 -1
- package/open-sse/utils/reasoningContentInjector.js +4 -7
- package/open-sse/utils/responsesStreamHelpers.js +1 -0
- package/open-sse/utils/sessionManager.js +49 -22
- package/open-sse/utils/stream.js +54 -77
- package/open-sse/utils/streamHandler.js +19 -15
- package/open-sse/utils/streamHelpers.js +2 -17
- package/open-sse/utils/usageTracking.js +103 -12
- package/package.json +4 -7
- package/public/assets/EndpointPageClient.C2WMTYy0.js +6 -0
- package/public/assets/{Loading.Bj-WGl4k.js → Loading.BHPtQ9vj.js} +1 -1
- package/public/assets/{NoAuthProxyCard.qAv57b-0.js → NoAuthProxyCard.BDcXaWvM.js} +1 -1
- package/public/assets/index.Be3gbrBl.js +89 -0
- package/public/assets/index.COMgQZYj.css +1 -0
- package/public/assets/{page.CbkENBd5.js → page.-lpNgYAw.js} +1 -1
- package/public/assets/{page.CssmrzTT.js → page.11hpNKmq.js} +1 -1
- package/public/assets/{page.C72FTTJr.js → page.B-LxsnqU.js} +1 -1
- package/public/assets/{page.sOFOrxqq.js → page.B-dzeIUC.js} +1 -1
- package/public/assets/{page.CsxWmPoI.js → page.B-nBR75u.js} +1 -1
- package/public/assets/{page.Dx_GRRdT.js → page.B3QqvLep.js} +1 -1
- package/public/assets/{page.DPyOkPVo.js → page.B8xdkCWA.js} +1 -1
- package/public/assets/{page.DUI8kqOY.js → page.BFb7yUd8.js} +1 -1
- package/public/assets/page.BbPPdoas.js +1 -0
- package/public/assets/page.BdS20a1O.js +1 -0
- package/public/assets/{page.DibhF7f-.js → page.BfmI9KYe.js} +1 -1
- package/public/assets/{page.DX4Nno6M.js → page.BgGJovOg.js} +1 -1
- package/public/assets/{page.BnmTC5jG.js → page.C0gBvR4O.js} +1 -1
- package/public/assets/{page.D5gj98Oe.js → page.C6bYizL7.js} +1 -1
- package/public/assets/{page.vla5Mzki.js → page.C8sLEMa6.js} +1 -1
- package/public/assets/{page.CXd_R9yU.js → page.CCNsZBXp.js} +1 -1
- package/public/assets/{page.CQlVGoXg.js → page.CF_YSLwP.js} +1 -1
- package/public/assets/{page.BbO1pspY.js → page.CPTtRDxw.js} +1 -1
- package/public/assets/{page.D1oKew1j.js → page.CdMmlUEm.js} +1 -1
- package/public/assets/{page.DxMDTbSx.js → page.Cged1bOZ.js} +1 -1
- package/public/assets/{page.Cj5h09Zo.js → page.Ch647dVk.js} +1 -1
- package/public/assets/{page.DVIHRbxB.js → page.CpvXDkQF.js} +1 -1
- package/public/assets/{page.i4KDuWcM.js → page.CrBl7f8N.js} +1 -1
- package/public/assets/{page.BSLJ4tx5.js → page.CrP7niA2.js} +1 -1
- package/public/assets/{page.CnTop8Zt.js → page.CvUFZzXr.js} +1 -1
- package/public/assets/{page.O2ddJYoT.js → page.CyUqJPmx.js} +1 -1
- package/public/assets/{page.DBhu4Arl.js → page.D8Rp8fFR.js} +1 -1
- package/public/assets/{page.CPZav1_c.js → page.Df9qtrdG.js} +1 -1
- package/public/assets/{page.Qkkv8roc.js → page.DiL4Gwqe.js} +1 -1
- package/public/assets/{page.BdmJ3yY3.js → page.Dl1apZE2.js} +1 -1
- package/public/assets/{page.1qjqvrwX.js → page.HV0w1d5C.js} +1 -1
- package/public/assets/{page.ChX1NqmC.js → page.N1x4YL8_.js} +1 -1
- package/public/assets/{page.Dh5f0XJC.js → page.P2Ny5veR.js} +1 -1
- package/public/assets/{page.CSs3-gdt.js → page.fOOCr18J.js} +1 -1
- package/public/assets/{page.Ir7KvKcF.js → page.sUIPWIiF.js} +1 -1
- package/public/assets/{page.CzlQTyAL.js → page.tE7PpWYW.js} +1 -1
- package/public/assets/{page.DzZLqI5Q.js → page.uziaGZsT.js} +1 -1
- package/public/index.html +2 -2
- package/public/assets/EndpointPageClient.LyNNvZ4z.js +0 -6
- package/public/assets/index.BuPfZebu.css +0 -1
- package/public/assets/index.CShjRV_B.js +0 -89
- 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 '
|
|
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
|
|
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\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
|
|
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)
|
|
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 (!
|
|
184
|
-
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
|
197
|
-
//
|
|
198
|
-
|
|
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
|
|
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 '
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
//
|
|
88
|
-
|
|
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 {
|
|
6
|
-
import {
|
|
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
|
|
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
|
|
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 '
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
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
|
-
|