cloudwego / cloudwego/eino-ext
bug(acl/openai): chunkConverter assigns duplicate StreamingMeta index when text is interleaved between parallel tool calls
- Dominant language
- Go
- Stars
- 811
- Forks
- 368
- Avg merge
- 16h 22m
- Merged PRs (30d)
- 13
Description
**Describe the bug**
When a provider streams a **text chunk between parallel tool-call chunks** (observed on Cloudflare Workers AI gateways), `chunkConverter` assigns the text block the **same `StreamingMeta.Index`** as the preceding tool-call block. `schema.ConcatAgenticMessages` then groups blocks of different types under one index and fails:
```
content block type mismatch: expected 'function_tool_call', but got 'assistant_gen_text'
```
**Location**
`libs/acl/openai/agentic_convert.go` → `(*chunkConverter).advanceContent`
```go
func (c *chunkConverter) advanceContent(blockType schema.ContentBlockType, sourceIndex int) {
if !c.started { ... }
if blockType != c.lastContentType || sourceIndex != c.lastContentPartIndex {
c.curIndex++
...
}
}
```
It only tracks content-type transitions and ignores `c.inToolCalls`. Once tool calls have started, an interleaved text chunk still matches the last text block's type, so `curIndex` is not advanced and the text block inherits the tool-call group's index.
Introduced in d833672 (PR #803); still present on `main` as of 2026-08-27.
**Minimal reproduction**
Raw provider SSE (abridged) — note the `{"content":"\n"}` chunk between tool call `index:0` and `index:1`:
```
data: {"choices":[{"delta":{"content":"\n\n"},"index":0}]}
data: {"choices":[{"delta":{"tool_calls":[{"id":"call_a","index":0,"type":"function","function":{"name":"exec","arguments":""}}]},"index":0}]}
data: {"choices":[{"delta":{"tool_calls":[{"id":null,"index":0,"type":"function","function":{"name":null,"arguments":"{\"command\":\"whoami\"}"}}]},"index":0}]}
data: {"choices":[{"delta":{"content":"\n"},"index":0}]}
data: {"choices":[{"delta":{"tool_calls":[{"id":"call_b","index":1,"type":"function","function":{"name":"exec","arguments":""}}]},"index":0}]}
data: {"choices":[{"delta":{"tool_calls":[{"id":null,"index":1,"type":"function","function":{"name":null,"arguments":"{\"command\":\"id\"}"}}]},"index":0}]}
```
Frame indices the converter produces: text(0), toolcall(1), toolcall(1), **text(1)** ← collision, toolcall(2), toolcall(2).
Go repro of the resulting concat failure:
```go
block := func(idx int, b *schema.ContentBlock) *schema.AgenticMessage {
b.StreamingMeta = &schema.StreamingMeta{Index: idx}
return &schema.AgenticMessage{Role: schema.AgenticRoleTypeAssistant, ContentBlocks: []*schema.ContentBlock{b}}
}
frames := []*schema.AgenticMessage{
block(0, schema.NewContentBlock(&schema.AssistantGenText{Text: "\n\n"})),
block(1, schema.NewContentBlock(&schema.FunctionToolCall{CallID: "call_a", Name: "exec"})),
block(1, schema.NewContentBlock(&schema.FunctionToolCall{Arguments: `{"command":"whoami"}`})),
block(1, schema.NewContentBlock(&schema.AssistantGenText{Text: "\n"})), // same index as the tool call
block(2, schema.NewContentBlock(&schema.FunctionToolCall{CallID: "call_b", Name: "exec"})),
block(2, schema.NewContentBlock(&schema.FunctionToolCall{Arguments: `{"command":"id"}`})),
}
_, err := schema.ConcatAgenticMessages(frames)
// err: content block type mismatch: expected 'function_tool_call', but got 'assistant_gen_text'
```
**Suggested fix**
In `advanceContent`, when `c.inToolCalls` is true, always advance to a new index and reset `inToolCalls` (so a following tool call also gets a fresh index):
```go
if c.inToolCalls {
c.curIndex++
c.inToolCalls = false
c.lastContentType = blockType
c.lastContentPartIndex = sourceIndex
return
}
```
**Impact**
Any agentic streaming consumer using parallel tool calls against providers that interleave content deltas between tool-call deltas. Downstream, the concat error is swallowed by eino ADK's `typedConsumeStream`, which returns a nil message — surfacing as a misleading "empty model output" rejection instead of the real parse error.
Contributor guide
Research direction
Start in libs/acl/openai/agentic_convert.go at (*chunkConverter).advanceContent and trace how inToolCalls, lastContentType, and sourceIndex affect StreamingMeta.Index. Use the minimal interleaved SSE or Go reproduction in the issue, then verify that schema.ConcatAgenticMessages no longer reports a content block type mismatch and that text between parallel tool calls receives its own index.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go
- Domain
- ai
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 84/100