diff --git a/internal/js/chat-stream/sse_parse_impl.js b/internal/js/chat-stream/sse_parse_impl.js index 09530bc..834c392 100644 --- a/internal/js/chat-stream/sse_parse_impl.js +++ b/internal/js/chat-stream/sse_parse_impl.js @@ -59,7 +59,7 @@ function parseChunkForContent(chunk, thinkingEnabled, currentType, stripReferenc }; } if (isStatusPath(pathValue)) { - if (asString(chunk.v) === 'FINISHED') { + if (isFinishedStatus(chunk.v)) { return { parsed: true, parts: [], @@ -149,7 +149,7 @@ function parseChunkForContent(chunk, thinkingEnabled, currentType, stripReferenc const val = chunk.v; if (typeof val === 'string') { - if (val === 'FINISHED' && (!pathValue || pathValue === 'status')) { + if (isFinishedStatus(val) && (!pathValue || pathValue === 'status')) { return { parsed: true, parts: [], @@ -258,7 +258,7 @@ function extractContentRecursive(items, defaultType, stripReferenceMarkers = tru const itemPath = asString(it.p); const itemV = it.v; if (isStatusPath(itemPath)) { - if (asString(itemV) === 'FINISHED') { + if (isFinishedStatus(itemV)) { return { parts: [], finished: true }; } continue; @@ -336,6 +336,10 @@ function isStatusPath(pathValue) { return pathValue === 'response/status' || pathValue === 'status'; } +function isFinishedStatus(value) { + return asString(value).toUpperCase() === 'FINISHED'; +} + function filterLeakedContentFilterParts(parts) { if (!Array.isArray(parts) || parts.length === 0) { return parts; diff --git a/tests/node/chat-stream.test.js b/tests/node/chat-stream.test.js index 6086ba1..205cdeb 100644 --- a/tests/node/chat-stream.test.js +++ b/tests/node/chat-stream.test.js @@ -291,6 +291,19 @@ test('parseChunkForContent preserves output tokens on FINISHED lines', () => { assert.deepEqual(parsed.parts, []); }); +test('parseChunkForContent matches FINISHED case-insensitively on status paths', () => { + const parsed = parseChunkForContent( + { p: 'response/status', v: ' finished ', accumulated_token_usage: 190 }, + false, + 'text', + ); + assert.equal(parsed.parsed, true); + assert.equal(parsed.finished, true); + assert.equal(parsed.contentFilter, false); + assert.equal(parsed.outputTokens, 190); + assert.deepEqual(parsed.parts, []); +}); + test('parseChunkForContent filters INCOMPLETE status text without stopping stream', () => { const parsed = parseChunkForContent( { p: 'response/status', v: 'INCOMPLETE', accumulated_token_usage: 190 },