Menu

#103 OpenAI SSE tool calls without index are merged when each event has a distinct id

open
nobody
None
2026-08-11
2026-08-11
Anonymous
No

Originally created by: yablokolabs

Summary

OpenAI-compatible streaming responses can omit tool_calls[].index while still supplying a stable tool_calls[].id. Atomic currently substitutes each SSE event's array position as the index. When a provider emits one distinct tool call per event, every call is therefore assigned index 0 and the stream consumer concatenates their names and arguments into one invalid call.

This was observed during a Gemini OpenAI-compatible run on Atomic Agent v0.1.72 and remains present by inspection on v0.1.73/current main (9d525ef4a004a9106608bea7a01ecd70ab279c2d).

Current behavior

parse-sse-chunk.ts (github.com) falls back to the tool call's position within the current event:

index: typeof toolCall.index === "number" ? toolCall.index : fallbackIndex

openai-stream-consumer.ts (github.com) then reconstructs calls in a Map<number, MutableToolCall> keyed only by that index.

For two events shaped like this:

data: {"choices":[{"delta":{"tool_calls":[{"id":"call_write","type":"function","function":{"name":"os__fs__write","arguments":"{...}"}}]}}]}

data: {"choices":[{"delta":{"tool_calls":[{"id":"call_read","type":"function","function":{"name":"os__fs__read","arguments":"{...}"}}]}}]}

both calls receive fallback index 0. Observed failures included merged names such as:

os.fs.writeos.fs.writeos.fs.writeos.fs.writeos.fs.write

Expected behavior

When an SSE tool-call delta has no numeric index but has an id, Atomic should assign and retain a request-scoped stable index for that ID. Distinct IDs must remain distinct calls across separate events.

Suggested implementation

Keep an ID-to-index map in the stream consumer:

  1. Preserve an explicit numeric index when supplied.
  2. Bind an accompanying ID to that explicit index.
  3. If index is absent and the ID has been seen, reuse its assigned index.
  4. If index is absent and the ID is new, allocate the next unused index.
  5. Use positional fallback only when both index and ID are absent.

A localhost compatibility adapter using this mapping reconstructed five Gemini calls correctly in the evaluation.

Acceptance criteria

  • A regression test sends multiple SSE events, each containing one distinct ID and no index; the final result contains the same number of distinct tool calls in order.
  • Fragments for the same ID across multiple events are joined into one call.
  • Explicit provider indices continue to work unchanged.
  • Different IDs can never be concatenated solely because both appeared at position zero in separate events.
  • Existing OpenAI stream-consumer tests remain green.

Related

Tickets: #108
Tickets: #119
Tickets: #120

Discussion


Log in to post a comment.