cloudwego / cloudwego/eino-ext

bug(acl/openai): chunkConverter assigns duplicate StreamingMeta index when text is interleaved between parallel tool calls

Open Beginner friendly
#975 0 comments 0 reactions 0 assignees View on GitHub
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.