diff --git a/internal/js/chat-stream/sse_parse_impl.js b/internal/js/chat-stream/sse_parse_impl.js index 7c6cfae..a665f8e 100644 --- a/internal/js/chat-stream/sse_parse_impl.js +++ b/internal/js/chat-stream/sse_parse_impl.js @@ -308,6 +308,10 @@ function parseChunkForContent(chunk, thinkingEnabled, currentType, stripReferenc } if (val && typeof val === 'object') { + const directContent = asContentString(val, stripReferenceMarkers); + if (directContent) { + parts.push({ text: directContent, type: partType }); + } const resp = val.response && typeof val.response === 'object' ? val.response : val; if (Array.isArray(resp.fragments)) { for (const frag of resp.fragments) { @@ -593,6 +597,12 @@ function asContentString(v, stripReferenceMarkers = true) { if (Object.prototype.hasOwnProperty.call(v, 'v')) { return asContentString(v.v, stripReferenceMarkers); } + if (Object.prototype.hasOwnProperty.call(v, 'text')) { + return asContentString(v.text, stripReferenceMarkers); + } + if (Object.prototype.hasOwnProperty.call(v, 'value')) { + return asContentString(v.value, stripReferenceMarkers); + } return ''; } if (v == null) { diff --git a/tests/node/chat-stream.test.js b/tests/node/chat-stream.test.js index dbfbe3e..48dc707 100644 --- a/tests/node/chat-stream.test.js +++ b/tests/node/chat-stream.test.js @@ -227,6 +227,20 @@ test('vercel stream exhausts DeepSeek continue before synthetic retry', async () assert.equal(fetchBodies.some((body) => String(body.prompt || '').includes('Previous reply had no visible output')), false); }); + + +test('vercel stream usage completion_tokens does not double-count visible output', async () => { + const sample = 'abcdefghijklmnopqrst'; + const { frames } = await runMockVercelStream([ + `data: ${JSON.stringify({ p: 'response/content', v: sample })}\n\n`, + 'data: [DONE]\n\n', + ]); + const parsed = frames.filter((frame) => frame !== '[DONE]').map((frame) => JSON.parse(frame)); + const terminal = parsed.find((item) => Array.isArray(item.choices) && item.choices[0] && item.choices[0].finish_reason); + assert.ok(terminal); + assert.equal(terminal.usage.completion_tokens, 5); +}); + test('vercel stream reuses prior PoW when refresh fails', async () => { const originalFetch = global.fetch; const fetchURLs = []; @@ -525,6 +539,12 @@ test('parseChunkForContent supports wrapped response.fragments object shape', () assert.equal(parsed.parts.map((p) => p.text).join(''), 'AB'); }); +test('parseChunkForContent reads object-shaped response/content payloads (Go parity)', () => { + const parsed = parseChunkForContent({ p: 'response/content', v: { text: 'vision text' } }, false, 'text', true); + assert.equal(parsed.parsed, true); + assert.deepEqual(parsed.parts, [{ text: 'vision text', type: 'text' }]); +}); + test('parseChunkForContent preserves space-only content tokens', () => { const chunk = { p: 'response/content',