mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-07-09 17:19:02 +00:00
fix(core): preserve no-argument tool calls that stream an empty arguments string (#6250)
* fix(core): preserve no-argument tool calls that stream an empty arguments string
For tools that take no parameters, some OpenAI-compatible providers
stream `arguments: ""` (or omit the field entirely) and never send an
argument fragment. The streaming parser dropped such calls wholesale
(`meta?.name && buffer.trim()`), while the non-streaming path keeps
them with `args: {}` — so a turn containing only that call looked
empty and geminiChat raised "Model stream ended with empty response
text", triggering pointless retries.
Align the streaming parser with the non-streaming path: emit the call
with empty args when the buffer is empty at stream end. Rewrite the
unit test that encoded the drop, and add regression coverage at parser
and converter chunk level.
* fix(core): use name metadata as slot-occupancy signal for no-argument tool calls
Follow-up to review feedback: after empty buffers became a legal end
state for no-argument tool calls, three parser methods still used
buffer.trim() to decide whether an index slot was occupied. A provider
reusing indices could then silently overwrite a completed no-argument
call (addChunk collision guard, findNextAvailableIndex) or append stray
continuation chunks to it (findMostRecentIncompleteIndex).
Switch the occupancy signal in all three places to the name metadata,
keeping the JSON-completeness check for non-empty buffers. Add
regression tests for both corruption paths and update the stale
getCompletedToolCalls JSDoc.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(core): collapse non-object argument parses at emit and lock canonical empty-opener shape
Review follow-up on the no-ID continuation routing at addChunk. Mid-stream,
an empty buffer with name metadata is formally undecidable between "completed
no-argument call" and "canonical opener awaiting its first argument fragment"
(every OpenAI-compatible provider opens with arguments:"" and streams
fragments ID-less at the same index). Routing must favor the canonical shape,
so the guard stays; a new test pins that shape, which the suite previously
did not cover.
The corruption concern from review is instead bounded at emit time: a buffer
polluted by a stray fragment can parse or repair to a non-object value, which
now collapses to {} so a polluted no-argument call still emits empty args.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(core): add debug logging for empty-buffer emission and non-object argument collapse
Review follow-up: a stray fragment that happens to parse as a valid JSON
object is indistinguishable from real arguments at emit time, so log both
the non-object collapse and empty-buffer emissions to aid diagnosis.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(core): extend replay guard to opener-shaped replays of no-argument tool calls
Review follow-up: after empty buffers became a legal completed state, a
replayed opener (duplicate ID, #5107 lineage) could overwrite a completed
no-argument call's name metadata, since the replay guard only engaged on
non-empty buffers.
Swallowing every known-ID chunk at that state would drop ID-bearing
argument fragments for providers whose opener streams empty arguments, so
the guard uses the protocol shape as discriminator: a chunk carrying a
name but no argument content is an opener replay and is ignored; a chunk
with argument content is a continuation and appends. Regression tests
cover both directions.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(core): cover null/array argument collapse and multi-slot relocation scan
Review follow-up: pin the null and array branches of the emit-time
non-object collapse, and exercise findNextAvailableIndex scanning past
multiple occupied no-argument slots during collision relocation.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: FiaShi <FiaShi@fiashideMacBook-Air.local>
Co-authored-by: tomsen-ai <230283659+tomsen-ai@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
0f98842ff2
commit
ac123bbc59
3 changed files with 299 additions and 49 deletions
|
|
@ -293,6 +293,64 @@ describe('OpenAIContentConverter', () => {
|
|||
expect(fnB?.args).toEqual({ file_path: '/b/y.ts' });
|
||||
expect(fnB?.id).toBe('call_B');
|
||||
});
|
||||
|
||||
it('emits no-argument tool calls that stream an empty arguments string', () => {
|
||||
// Providers may finish a no-argument tool call with `arguments: ""`
|
||||
// and no follow-up fragment (e.g. llama.cpp-style servers). The call
|
||||
// must reach the caller with empty args instead of being dropped,
|
||||
// which would make the whole turn look empty and trigger retries.
|
||||
const stream = withStreamParser(new StreamingToolCallParser());
|
||||
|
||||
const opener = {
|
||||
object: 'chat.completion.chunk',
|
||||
id: 'noargs-open',
|
||||
created: 1,
|
||||
model: 'test',
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
delta: {
|
||||
tool_calls: [
|
||||
{
|
||||
index: 0,
|
||||
id: 'call_noargs',
|
||||
type: 'function' as const,
|
||||
function: { name: 'list_sessions', arguments: '' },
|
||||
},
|
||||
],
|
||||
},
|
||||
finish_reason: null,
|
||||
logprobs: null,
|
||||
},
|
||||
],
|
||||
} as unknown as OpenAI.Chat.ChatCompletionChunk;
|
||||
|
||||
const finisher = {
|
||||
object: 'chat.completion.chunk',
|
||||
id: 'noargs-finish',
|
||||
created: 2,
|
||||
model: 'test',
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
delta: {},
|
||||
finish_reason: 'tool_calls',
|
||||
logprobs: null,
|
||||
},
|
||||
],
|
||||
} as unknown as OpenAI.Chat.ChatCompletionChunk;
|
||||
|
||||
converter.convertOpenAIChunkToGemini(opener, stream);
|
||||
const result = converter.convertOpenAIChunkToGemini(finisher, stream);
|
||||
|
||||
const fn = result.candidates?.[0]?.content?.parts?.find(
|
||||
(p: Part) => p.functionCall,
|
||||
)?.functionCall;
|
||||
|
||||
expect(fn?.name).toBe('list_sessions');
|
||||
expect(fn?.args).toEqual({});
|
||||
expect(fn?.id).toBe('call_noargs');
|
||||
});
|
||||
});
|
||||
|
||||
describe('convertGeminiRequestToOpenAI', () => {
|
||||
|
|
|
|||
|
|
@ -391,11 +391,124 @@ describe('StreamingToolCallParser', () => {
|
|||
expect(completed).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should not return tool calls with empty buffer', () => {
|
||||
parser.addChunk(0, '', 'call_1', 'function1'); // Empty buffer
|
||||
it('should return no-argument tool calls with empty args when buffer is empty', () => {
|
||||
// For tools without parameters, some providers stream
|
||||
// `arguments: ""` (or omit the field) and never send an argument
|
||||
// fragment. The call must survive with empty args, matching the
|
||||
// non-streaming path.
|
||||
parser.addChunk(0, '', 'call_1', 'function1');
|
||||
|
||||
const completed = parser.getCompletedToolCalls();
|
||||
expect(completed).toHaveLength(0);
|
||||
expect(completed).toEqual([
|
||||
{ id: 'call_1', name: 'function1', args: {}, index: 0 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return empty args for whitespace-only argument buffers', () => {
|
||||
parser.addChunk(0, ' ', 'call_1', 'function1');
|
||||
|
||||
const completed = parser.getCompletedToolCalls();
|
||||
expect(completed).toHaveLength(1);
|
||||
expect(completed[0].args).toEqual({});
|
||||
});
|
||||
|
||||
it('should not overwrite a completed no-argument tool call when a new call reuses its index', () => {
|
||||
// First tool call: no arguments, provider never sends a fragment
|
||||
parser.addChunk(0, '', 'call_1', 'no_arg_function');
|
||||
|
||||
// Second tool call arrives at the same index with a different ID
|
||||
parser.addChunk(0, '{"param": "value"}', 'call_2', 'function2');
|
||||
|
||||
// Both calls must survive: the second is relocated to a new index
|
||||
const completed = parser.getCompletedToolCalls();
|
||||
expect(completed).toEqual([
|
||||
{ id: 'call_1', name: 'no_arg_function', args: {}, index: 0 },
|
||||
{
|
||||
id: 'call_2',
|
||||
name: 'function2',
|
||||
args: { param: 'value' },
|
||||
index: 1,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should route ID-less argument fragments to a call whose opener streamed empty arguments', () => {
|
||||
// Canonical OpenAI-compatible streaming shape: the opener carries
|
||||
// id + name + `arguments: ""`, then argument fragments follow at the
|
||||
// same index without an ID. Mid-stream, an empty buffer with name
|
||||
// metadata must therefore stay continuable at its own index — it is
|
||||
// indistinguishable from a completed no-argument call until stream end.
|
||||
parser.addChunk(0, '', 'call_1', 'function1');
|
||||
parser.addChunk(0, '{"x":');
|
||||
parser.addChunk(0, '1}');
|
||||
|
||||
const completed = parser.getCompletedToolCalls();
|
||||
expect(completed).toEqual([
|
||||
{ id: 'call_1', name: 'function1', args: { x: 1 }, index: 0 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should emit empty args for a no-argument call polluted by a stray fragment at its index', () => {
|
||||
// If a misbehaving provider reuses a completed no-argument call's
|
||||
// index for another call's ID-less fragment, the fragment cannot be
|
||||
// re-routed (see canonical-shape test above). The damage must stay
|
||||
// bounded: the polluted buffer repairs to a non-object value, which
|
||||
// collapses to {} at emit time.
|
||||
parser.addChunk(0, '{"key":', 'call_1', 'function1');
|
||||
parser.addChunk(1, '', 'call_2', 'no_arg_function');
|
||||
parser.addChunk(1, '"value"}');
|
||||
|
||||
const completed = parser.getCompletedToolCalls();
|
||||
const noArg = completed.find((c) => c.id === 'call_2');
|
||||
expect(noArg?.args).toEqual({});
|
||||
});
|
||||
|
||||
it('should collapse null argument buffers to empty args', () => {
|
||||
parser.addChunk(0, 'null', 'call_1', 'function1');
|
||||
|
||||
const completed = parser.getCompletedToolCalls();
|
||||
expect(completed[0].args).toEqual({});
|
||||
});
|
||||
|
||||
it('should collapse array argument buffers to empty args', () => {
|
||||
parser.addChunk(0, '[1,2,3]', 'call_1', 'function1');
|
||||
|
||||
const completed = parser.getCompletedToolCalls();
|
||||
expect(completed[0].args).toEqual({});
|
||||
});
|
||||
|
||||
it('should scan past occupied no-argument slots when relocating a colliding call', () => {
|
||||
parser.addChunk(0, '', 'call_a', 'no_arg_a');
|
||||
parser.addChunk(1, '', 'call_b', 'no_arg_b');
|
||||
// Collision at index 0 must relocate past both occupied no-arg slots
|
||||
parser.addChunk(0, '{"x": 1}', 'call_c', 'fn_c');
|
||||
|
||||
const completed = parser.getCompletedToolCalls();
|
||||
expect(completed).toEqual([
|
||||
{ id: 'call_a', name: 'no_arg_a', args: {}, index: 0 },
|
||||
{ id: 'call_b', name: 'no_arg_b', args: {}, index: 1 },
|
||||
{ id: 'call_c', name: 'fn_c', args: { x: 1 }, index: 2 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should not route continuation chunks to a completed no-argument tool call', () => {
|
||||
// Incomplete tool call accumulating arguments at index 0
|
||||
parser.addChunk(0, '{"key":', 'call_1', 'function1');
|
||||
// Completed no-argument tool call at the higher index 1
|
||||
parser.addChunk(1, '', 'call_2', 'no_arg_function');
|
||||
// Completed tool call at index 2
|
||||
parser.addChunk(2, '{"x": 1}', 'call_3', 'function3');
|
||||
|
||||
// Continuation chunk without an ID arriving at a completed index must
|
||||
// be routed to the incomplete call_1, not to the no-argument call_2
|
||||
parser.addChunk(2, '"value"}');
|
||||
|
||||
const completed = parser.getCompletedToolCalls();
|
||||
expect(completed).toEqual([
|
||||
{ id: 'call_1', name: 'function1', args: { key: 'value' }, index: 0 },
|
||||
{ id: 'call_2', name: 'no_arg_function', args: {}, index: 1 },
|
||||
{ id: 'call_3', name: 'function3', args: { x: 1 }, index: 2 },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -568,6 +681,31 @@ describe('StreamingToolCallParser', () => {
|
|||
expect(parser.getBuffer(0)).toBe('{"param1": "value1"}');
|
||||
});
|
||||
|
||||
it('should ignore replayed openers for a completed no-argument tool call', () => {
|
||||
parser.addChunk(0, '', 'call_1', 'list_sessions');
|
||||
// Provider replays the same ID's opener with a different name; the
|
||||
// surviving call must not be mutated
|
||||
parser.addChunk(0, '', 'call_1', 'different_function');
|
||||
|
||||
const completed = parser.getCompletedToolCalls();
|
||||
expect(completed).toHaveLength(1);
|
||||
expect(completed[0].name).toBe('list_sessions');
|
||||
expect(completed[0].args).toEqual({});
|
||||
});
|
||||
|
||||
it('should append ID-bearing argument fragments after an empty opener', () => {
|
||||
// Some providers repeat the tool call ID on argument fragments. A
|
||||
// known-ID chunk carrying argument content is a continuation, not a
|
||||
// replay, and must not be swallowed by the replay guard.
|
||||
parser.addChunk(0, '', 'call_1', 'function1');
|
||||
const result = parser.addChunk(0, '{"x":1}', 'call_1');
|
||||
|
||||
expect(result.complete).toBe(true);
|
||||
expect(parser.getCompletedToolCalls()).toEqual([
|
||||
{ id: 'call_1', name: 'function1', args: { x: 1 }, index: 0 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should ignore metadata-only replay chunks after a tool call ID completes', () => {
|
||||
parser.addChunk(0, '{"file_path": "a.ts"}', 'call_1', 'read_file');
|
||||
|
||||
|
|
|
|||
|
|
@ -88,20 +88,28 @@ export class StreamingToolCallParser {
|
|||
const existingDepth = this.depths.get(index)!;
|
||||
const existingMeta = this.toolCallMeta.get(index);
|
||||
|
||||
// Check if we have a complete tool call at this index
|
||||
// Check if we have a complete tool call at this index. Occupancy
|
||||
// is signaled by the name metadata, not the buffer: an empty
|
||||
// buffer with a name is a complete no-argument call.
|
||||
if (
|
||||
existingBuffer.trim() &&
|
||||
existingMeta?.name &&
|
||||
existingDepth === 0 &&
|
||||
existingMeta?.id &&
|
||||
existingMeta.id !== id
|
||||
) {
|
||||
try {
|
||||
JSON.parse(existingBuffer);
|
||||
let existingComplete = true;
|
||||
if (existingBuffer.trim()) {
|
||||
try {
|
||||
JSON.parse(existingBuffer);
|
||||
} catch {
|
||||
// Existing buffer is not complete JSON, we can reuse this index
|
||||
existingComplete = false;
|
||||
}
|
||||
}
|
||||
if (existingComplete) {
|
||||
// We have a complete tool call with a different ID at this index
|
||||
// Find a new index for this tool call
|
||||
actualIndex = this.findNextAvailableIndex();
|
||||
} catch {
|
||||
// Existing buffer is not complete JSON, we can reuse this index
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -146,15 +154,31 @@ export class StreamingToolCallParser {
|
|||
|
||||
const currentBuffer = this.buffers.get(actualIndex)!;
|
||||
const currentDepth = this.depths.get(actualIndex)!;
|
||||
if (isKnownId && currentBuffer.trim() && currentDepth === 0) {
|
||||
try {
|
||||
JSON.parse(currentBuffer);
|
||||
if (isKnownId && currentDepth === 0) {
|
||||
if (currentBuffer.trim()) {
|
||||
try {
|
||||
JSON.parse(currentBuffer);
|
||||
debugLogger.debug(
|
||||
`Ignoring replay chunk for completed toolCall id=${id}`,
|
||||
);
|
||||
return { complete: false };
|
||||
} catch {
|
||||
// Not complete yet; append the incoming chunk below.
|
||||
}
|
||||
} else if (
|
||||
this.toolCallMeta.get(actualIndex)?.name &&
|
||||
name &&
|
||||
!chunk.trim()
|
||||
) {
|
||||
// The call at this index may be a completed no-argument call. An
|
||||
// incoming chunk that carries a name but no argument content is an
|
||||
// opener-shaped replay (duplicate ID) and must not mutate the
|
||||
// surviving call; a chunk with argument content is a continuation
|
||||
// for a call whose opener streamed empty arguments and must append.
|
||||
debugLogger.debug(
|
||||
`Ignoring replay chunk for completed toolCall id=${id}`,
|
||||
`Ignoring replayed opener for no-argument toolCall id=${id}`,
|
||||
);
|
||||
return { complete: false };
|
||||
} catch {
|
||||
// Not complete yet; append the incoming chunk below.
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -244,8 +268,10 @@ export class StreamingToolCallParser {
|
|||
* 2. Auto-close unclosed strings and retry
|
||||
* 3. Fallback to safeJsonParse for malformed data
|
||||
*
|
||||
* Only returns tool calls with both name metadata and non-empty buffers.
|
||||
* Should be called when streaming is complete (finish_reason is present).
|
||||
* Only returns tool calls with name metadata. An empty buffer yields
|
||||
* empty arguments ({}), matching the non-streaming path for no-argument
|
||||
* tools. Should be called when streaming is complete (finish_reason is
|
||||
* present).
|
||||
*
|
||||
* @returns Array of completed tool calls with their metadata and parsed arguments
|
||||
*/
|
||||
|
|
@ -265,7 +291,7 @@ export class StreamingToolCallParser {
|
|||
|
||||
for (const [index, buffer] of this.buffers.entries()) {
|
||||
const meta = this.toolCallMeta.get(index);
|
||||
if (meta?.name && buffer.trim()) {
|
||||
if (meta?.name) {
|
||||
if (meta.id) {
|
||||
if (emittedIds.has(meta.id)) {
|
||||
continue;
|
||||
|
|
@ -275,22 +301,45 @@ export class StreamingToolCallParser {
|
|||
|
||||
let args: Record<string, unknown> = {};
|
||||
|
||||
// Try to parse the final buffer
|
||||
try {
|
||||
args = JSON.parse(buffer);
|
||||
} catch {
|
||||
// Try with repair (auto-close strings)
|
||||
const inString = this.inStrings.get(index);
|
||||
if (inString) {
|
||||
try {
|
||||
args = JSON.parse(buffer + '"');
|
||||
} catch {
|
||||
// If all parsing fails, use safeJsonParse as fallback
|
||||
// An empty buffer is a legal end state: for no-argument tools some
|
||||
// providers stream `arguments: ""` (or omit the field entirely) and
|
||||
// never send an argument fragment. Keep args as {} to match the
|
||||
// non-streaming path in converter.ts.
|
||||
if (buffer.trim()) {
|
||||
// Try to parse the final buffer
|
||||
try {
|
||||
args = JSON.parse(buffer);
|
||||
} catch {
|
||||
// Try with repair (auto-close strings)
|
||||
const inString = this.inStrings.get(index);
|
||||
if (inString) {
|
||||
try {
|
||||
args = JSON.parse(buffer + '"');
|
||||
} catch {
|
||||
// If all parsing fails, use safeJsonParse as fallback
|
||||
args = safeJsonParse(buffer, {});
|
||||
}
|
||||
} else {
|
||||
args = safeJsonParse(buffer, {});
|
||||
}
|
||||
} else {
|
||||
args = safeJsonParse(buffer, {});
|
||||
}
|
||||
// Tool arguments are always JSON objects; a corrupted buffer can
|
||||
// parse or repair to a non-object value (e.g. a bare string), so
|
||||
// collapse anything else to {}.
|
||||
if (
|
||||
typeof args !== 'object' ||
|
||||
args === null ||
|
||||
Array.isArray(args)
|
||||
) {
|
||||
debugLogger.debug(
|
||||
`Collapsing non-object arguments for tool call ${meta.name} (id=${meta.id}) at index ${index}; buffer likely polluted by a misrouted fragment`,
|
||||
);
|
||||
args = {};
|
||||
}
|
||||
} else {
|
||||
debugLogger.debug(
|
||||
`Emitting no-argument tool call ${meta.name} (id=${meta.id}) at index ${index} with empty buffer`,
|
||||
);
|
||||
}
|
||||
|
||||
completed.push({
|
||||
|
|
@ -309,8 +358,9 @@ export class StreamingToolCallParser {
|
|||
* Finds the next available index for a new tool call
|
||||
*
|
||||
* Scans indices starting from nextAvailableIndex to find one that's safe to use.
|
||||
* Reuses indices with empty buffers or incomplete parsing states.
|
||||
* Skips indices with complete, parseable tool call data to prevent overwriting.
|
||||
* Reuses indices never claimed by a named tool call or with incomplete
|
||||
* parsing states. Skips indices with complete tool call data — including
|
||||
* no-argument calls with empty buffers — to prevent overwriting.
|
||||
*
|
||||
* @returns The next available index safe for storing a new tool call
|
||||
*/
|
||||
|
|
@ -321,22 +371,23 @@ export class StreamingToolCallParser {
|
|||
const depth = this.depths.get(this.nextAvailableIndex)!;
|
||||
const meta = this.toolCallMeta.get(this.nextAvailableIndex);
|
||||
|
||||
// If buffer is empty or incomplete (depth > 0), this index is available
|
||||
if (!buffer.trim() || depth > 0 || !meta?.id) {
|
||||
// If the slot was never claimed by a named call or is incomplete
|
||||
// (depth > 0), this index is available. An empty buffer alone does
|
||||
// not mean available: with name metadata it is a complete
|
||||
// no-argument call.
|
||||
if (!meta?.name || depth > 0 || !meta?.id) {
|
||||
return this.nextAvailableIndex;
|
||||
}
|
||||
|
||||
// Try to parse the buffer to see if it's complete
|
||||
try {
|
||||
JSON.parse(buffer);
|
||||
// If parsing succeeds and depth is 0, this index has a complete tool call
|
||||
if (depth === 0) {
|
||||
this.nextAvailableIndex++;
|
||||
continue;
|
||||
// A non-empty buffer must parse as complete JSON for the slot to be
|
||||
// occupied; an empty buffer is already complete (no-argument call)
|
||||
if (buffer.trim()) {
|
||||
try {
|
||||
JSON.parse(buffer);
|
||||
} catch {
|
||||
// If parsing fails, this index is available for reuse
|
||||
return this.nextAvailableIndex;
|
||||
}
|
||||
} catch {
|
||||
// If parsing fails, this index is available for reuse
|
||||
return this.nextAvailableIndex;
|
||||
}
|
||||
|
||||
this.nextAvailableIndex++;
|
||||
|
|
@ -348,8 +399,9 @@ export class StreamingToolCallParser {
|
|||
* Finds the most recent incomplete tool call index
|
||||
*
|
||||
* Used when continuation chunks arrive without IDs. Scans existing tool calls
|
||||
* to find the highest index with incomplete parsing state (depth > 0, empty buffer,
|
||||
* or unparseable JSON). Falls back to creating a new index if none found.
|
||||
* to find the highest index with incomplete parsing state (depth > 0, empty
|
||||
* buffer with no name metadata yet, or unparseable JSON). Falls back to
|
||||
* creating a new index if none found.
|
||||
*
|
||||
* @returns The index of the most recent incomplete tool call, or a new available index
|
||||
*/
|
||||
|
|
@ -360,8 +412,10 @@ export class StreamingToolCallParser {
|
|||
const depth = this.depths.get(index)!;
|
||||
const meta = this.toolCallMeta.get(index);
|
||||
|
||||
// Check if this tool call is incomplete
|
||||
if (meta?.id && (depth > 0 || !buffer.trim())) {
|
||||
// Check if this tool call is incomplete. An empty buffer counts only
|
||||
// while no name has arrived: with name metadata it is a complete
|
||||
// no-argument call, and stray chunks must not be appended to it
|
||||
if (meta?.id && (depth > 0 || (!buffer.trim() && !meta?.name))) {
|
||||
maxIndex = Math.max(maxIndex, index);
|
||||
} else if (buffer.trim()) {
|
||||
// Check if buffer is parseable (complete)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue