Skip to content

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

Description

@canblmz1

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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions