diff --git a/internal/js/chat-stream/toolcall_policy.js b/internal/js/chat-stream/toolcall_policy.js index 4523b89..ff4611e 100644 --- a/internal/js/chat-stream/toolcall_policy.js +++ b/internal/js/chat-stream/toolcall_policy.js @@ -60,6 +60,9 @@ function formatIncrementalToolCallDeltas(deltas, idStore) { if (typeof d.arguments === 'string' && d.arguments !== '') { fn.arguments = d.arguments; } + if (Object.keys(fn).length === 0) { + continue; + } if (Object.keys(fn).length > 0) { item.function = fn; } diff --git a/internal/js/helpers/stream-tool-sieve/parse.js b/internal/js/helpers/stream-tool-sieve/parse.js index 22d11d1..eeb2e34 100644 --- a/internal/js/helpers/stream-tool-sieve/parse.js +++ b/internal/js/helpers/stream-tool-sieve/parse.js @@ -17,15 +17,18 @@ function extractToolNames(tools) { return []; } const out = []; + const seen = new Set(); for (const t of tools) { if (!t || typeof t !== 'object') { continue; } const fn = t.function && typeof t.function === 'object' ? t.function : t; const name = toStringSafe(fn.name); - // Keep parity with Go injectToolPrompt: object tools without name still - // enter tool mode via fallback name "unknown". - out.push(name || 'unknown'); + if (!name || seen.has(name)) { + continue; + } + seen.add(name); + out.push(name); } return out; } diff --git a/tests/node/chat-stream.test.js b/tests/node/chat-stream.test.js index 48be6ff..6f3317a 100644 --- a/tests/node/chat-stream.test.js +++ b/tests/node/chat-stream.test.js @@ -98,6 +98,12 @@ test('incremental and final tool formatting share stable id via idStore', () => assert.equal(incremental[0].id, finalCalls[0].id); }); +test('formatIncrementalToolCallDeltas drops empty deltas (Go parity)', () => { + const idStore = new Map(); + const formatted = formatIncrementalToolCallDeltas([{ index: 0 }], idStore); + assert.deepEqual(formatted, []); +}); + test('parseChunkForContent keeps split response/content fragments inside response array', () => { const chunk = { p: 'response', diff --git a/tests/node/stream-tool-sieve.test.js b/tests/node/stream-tool-sieve.test.js index d4b5481..498155f 100644 --- a/tests/node/stream-tool-sieve.test.js +++ b/tests/node/stream-tool-sieve.test.js @@ -31,13 +31,14 @@ function collectText(events) { .join(''); } -test('extractToolNames keeps tool mode enabled with unknown fallback', () => { +test('extractToolNames keeps only declared tool names (Go parity)', () => { const names = extractToolNames([ { function: { description: 'no name tool' } }, { function: { name: ' read_file ' } }, + { function: { name: 'read_file' } }, {}, ]); - assert.deepEqual(names, ['unknown', 'read_file', 'unknown']); + assert.deepEqual(names, ['read_file']); }); test('parseToolCalls keeps non-object argument strings as _raw (Go parity)', () => {