Summary
OpenAI streamed tool-call deltas carry a stable index, while id is optional on the SDK's ChatCompletionChunk.Choice.Delta.ToolCall type.
Continue's fromChatCompletionChunk() currently drops tool_call.index, and applyToolCallDelta() falls back to updating the most recently tracked tool call whenever an incoming continuation fragment has no id.
With two parallel tool calls whose argument fragments interleave, this can apply one call's arguments to another call.
Current code path
core/llm/openaiTypeConverters.ts
→ fromChatCompletionChunk()
gui/src/redux/slices/sessionSlice.ts
→ applyToolCallDelta()
gui/src/util/toolCallState.ts
→ addToolCallDeltaToState()
This code is shared by Continue's OpenAI/OpenAI-compatible provider stream-conversion and accumulation path, so the affected surface may include any provider reusing that path.
Minimal reproduction
Four streamed tool-call deltas:
1. index=0, id=call_A, name=tool_a, arguments=""
2. index=1, id=call_B, name=tool_b, arguments=""
3. index=0, id absent, arguments='{"target":"A_ONLY"}'
4. index=1, id absent, arguments='{"target":"B_ONLY"}'
Actual result
call_A.function.arguments === ""
call_B.function.arguments === '{"target":"A_ONLY"}'
call_A's arguments are associated with call_B, while call_B's own argument fragment is lost.
Expected result
call_A.function.arguments === '{"target":"A_ONLY"}'
call_B.function.arguments === '{"target":"B_ONLY"}'
Why this sequence is supported by the SDK model
In Continue's OpenAI SDK dependency, ChatCompletionChunk.Choice.Delta.ToolCall defines:
index: number
id?: string
So index is the stable correlation information available even when a streamed fragment does not contain an id.
Suggested fix direction
Preserve tool_call.index through ToolCallDelta (or an equivalent stable correlation field) and correlate id-less continuation fragments using that identity rather than falling back to the last array position.
The existing id-based correlation path can remain the first choice, and the existing positional fallback can remain for providers that supply neither identity field.
Regression test
A local integration-style regression exercising:
raw ChatCompletionChunk
→ fromChatCompletionChunk()
→ session/streamUpdate
→ applyToolCallDelta()
→ addToolCallDeltaToState()
→ final ToolCallState[]
reproduced the cross-attribution deterministically.
Environment
Continue main: 5522c6f44ca0ac3528b37244818fbfa39b5af470
Summary
OpenAI streamed tool-call deltas carry a stable
index, whileidis optional on the SDK'sChatCompletionChunk.Choice.Delta.ToolCalltype.Continue's
fromChatCompletionChunk()currently dropstool_call.index, andapplyToolCallDelta()falls back to updating the most recently tracked tool call whenever an incoming continuation fragment has noid.With two parallel tool calls whose argument fragments interleave, this can apply one call's arguments to another call.
Current code path
This code is shared by Continue's OpenAI/OpenAI-compatible provider stream-conversion and accumulation path, so the affected surface may include any provider reusing that path.
Minimal reproduction
Four streamed tool-call deltas:
Actual result
call_A's arguments are associated withcall_B, whilecall_B's own argument fragment is lost.Expected result
Why this sequence is supported by the SDK model
In Continue's OpenAI SDK dependency,
ChatCompletionChunk.Choice.Delta.ToolCalldefines:So
indexis the stable correlation information available even when a streamed fragment does not contain anid.Suggested fix direction
Preserve
tool_call.indexthroughToolCallDelta(or an equivalent stable correlation field) and correlate id-less continuation fragments using that identity rather than falling back to the last array position.The existing id-based correlation path can remain the first choice, and the existing positional fallback can remain for providers that supply neither identity field.
Regression test
A local integration-style regression exercising:
reproduced the cross-attribution deterministically.
Environment
Continue
main:5522c6f44ca0ac3528b37244818fbfa39b5af470