fix(vercel): align JS stream parser with Go object-shaped content

This commit is contained in:
CJACK.
2026-04-29 23:56:16 +08:00
parent 273c18ba0f
commit 192cdf8562
2 changed files with 30 additions and 0 deletions

View File

@@ -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) {

View File

@@ -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',