diff --git a/internal/js/chat-stream/index.js b/internal/js/chat-stream/index.js index ce0587b..a8e6cae 100644 --- a/internal/js/chat-stream/index.js +++ b/internal/js/chat-stream/index.js @@ -59,8 +59,9 @@ async function handler(req, res) { return; } - // Keep all non-stream behavior on Go side to avoid compatibility regressions. - if (!toBool(payload.stream)) { + // Keep all non-stream behavior and non-OpenAI-chat paths on Go side to avoid + // protocol-shape regressions (e.g. Gemini/Claude clients expecting their own formats). + if (!toBool(payload.stream) || !isNodeStreamSupportedPath(req.url || '')) { await proxyToGo(req, res, rawBody); return; } @@ -76,6 +77,23 @@ function isVercelRuntime() { return asString(process.env.VERCEL) !== '' || asString(process.env.NOW_REGION) !== ''; } +function isNodeStreamSupportedPath(rawURL) { + const path = extractPathname(rawURL); + return path === '/v1/chat/completions'; +} + +function extractPathname(rawURL) { + const text = asString(rawURL); + if (!text) { + return ''; + } + const q = text.indexOf('?'); + if (q >= 0) { + return text.slice(0, q); + } + return text; +} + module.exports = handler; module.exports.__test = { @@ -89,4 +107,6 @@ module.exports.__test = { boolDefaultTrue, filterIncrementalToolCallDeltasByAllowed, estimateTokens, + isNodeStreamSupportedPath, + extractPathname, }; diff --git a/tests/node/chat-stream.test.js b/tests/node/chat-stream.test.js index fccec14..07fe0b0 100644 --- a/tests/node/chat-stream.test.js +++ b/tests/node/chat-stream.test.js @@ -18,6 +18,8 @@ const { boolDefaultTrue, filterIncrementalToolCallDeltasByAllowed, shouldSkipPath, + isNodeStreamSupportedPath, + extractPathname, } = handler.__test; test('chat-stream exposes parser test hooks', () => { @@ -225,3 +227,15 @@ test('shouldSkipPath skips dynamic response/fragments/*/status paths only', () = assert.equal(shouldSkipPath('response/fragments/8/status'), true); assert.equal(shouldSkipPath('response/status'), false); }); + +test('node stream path guard only allows /v1/chat/completions', () => { + assert.equal(isNodeStreamSupportedPath('/v1/chat/completions'), true); + assert.equal(isNodeStreamSupportedPath('/v1/chat/completions?x=1'), true); + assert.equal(isNodeStreamSupportedPath('/v1beta/models/gemini-2.5-flash:streamGenerateContent'), false); + assert.equal(isNodeStreamSupportedPath('/anthropic/v1/messages'), false); +}); + +test('extractPathname strips query only', () => { + assert.equal(extractPathname('/v1/chat/completions?stream=true'), '/v1/chat/completions'); + assert.equal(extractPathname('/v1beta/models/gemini-2.5-flash:streamGenerateContent?key=1'), '/v1beta/models/gemini-2.5-flash:streamGenerateContent'); +});