continuedev / continuedev/continue

OpenAI parallel tool-call argument fragments can be applied to the wrong call

Open
#13,223 0 comments 0 reactions 0 assignees View on GitHub

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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.