continuedev / continuedev/continue
OpenAI parallel tool-call argument fragments can be applied to the wrong call
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 36k
- Forks
- 5.4k
- PR merge metrics
- No merged PRs in 30d
Description
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
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Trace the listed path from core/llm/openaiTypeConverters.ts through gui/src/redux/slices/sessionSlice.ts and gui/src/util/toolCallState.ts. Start by reproducing the four interleaved deltas and inspect how identity is carried through each stage. Done means the final ToolCallState[] keeps A_ONLY with call_A and B_ONLY with call_B, with a regression covering the raw chunk-to-state flow.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- ai, frontend, testing-qa
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 72/100