langchain - ✅(Solved) Fix core: same-index tool_call chunks in one streamed delta can split into blank tool_calls and invalid_tool_calls [2 pull requests]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

The failure mode appears when tool_call_chunks already contains multiple fragments for the same logical tool call before AIMessageChunk.init_tool_calls() parses them.

A real provider payload that triggered this looked like this on the first streamed delta:

{
  "tool_calls": [
    {
      "index": 0,
      "id": "call_...",
      "type": "function",
      "function": {
        "name": "bocha_websearch_tool",
        "arguments": ""
      }
    },
    {
      "index": 0,
      "function": {
        "arguments": "{"
      }
    }
  ]
}

LangChain correctly turns those into two ToolCallChunks, but later stream continuations are merged into the first entry while the dangling second same-index fragment remains in the list. When init_tool_calls() then parses the list, the incomplete fragment becomes a blank tool_call, and the real call falls into invalid_tool_calls.

The underlying problem is that init_tool_calls() parses self.tool_call_chunks as-is, without first normalizing same-index continuation fragments that are already present inside a single AIMessageChunk.

Error Message

"error": None,

Root Cause

Related symptom, but different root cause and code path:

  • #32562
  • #32580

Fix Action

Fix / Workaround

  • This is a bug, not a usage question.
  • I added a clear and descriptive title that summarizes this issue.
  • I used the GitHub search to find similar issues.
  • I am sure that this is a bug in LangChain rather than my code.
  • The bug is not resolved by updating to the latest stable version of LangChain.
  • I posted a self-contained, minimal, reproducible example.

LangChain correctly turns those into two ToolCallChunks, but later stream continuations are merged into the first entry while the dangling second same-index fragment remains in the list. When init_tool_calls() then parses the list, the incomplete fragment becomes a blank tool_call, and the real call falls into invalid_tool_calls.

PR fix notes

PR #36628: fix(core): normalize same-index tool call chunks before parsing

Description (problem / solution / changelog)

Summary

  • normalize tool_call_chunks inside AIMessageChunk.init_tool_calls() before best-effort JSON parsing
  • add a regression test for chat.completions-style streaming where one logical tool call is split into multiple same-index fragments inside a single delta

Why

Some OpenAI-compatible providers emit multiple same-index tool-call fragments in the same streamed delta, for example one fragment with name/id and empty arguments, plus another fragment with just "{".

Without pre-normalizing those fragments, later continuations merge into the first entry while the dangling same-index fragment remains in the list. After parsing, that produces:

  • a blank tool_call
  • a real call in invalid_tool_calls

This is the root cause behind the minimal core repro in #36627. It is distinct from #32562 / #32580, which are about Responses API index advancement.

Testing

  • /opt/anaconda3/envs/langchain/bin/python -m pytest -c /dev/null libs/core/tests/unit_tests/test_messages.py -k "merge_tool_calls or ai_message_chunk_merges_same_index_tool_call_continuations_in_delta"

Closes #36627

Changed files

  • libs/core/langchain_core/messages/ai.py (modified, +27/-0)
  • libs/core/tests/unit_tests/test_messages.py (modified, +29/-0)

PR #36670: fix(core): normalize same-index tool_call_chunks packed in a single streamed delta

Description (problem / solution / changelog)

Summary

Fixes #36627.

Some providers (e.g. Bocha) emit multiple ToolCallChunk fragments for the same logical tool call within a single streamed delta — for example one fragment carries the function name and id, and a second carries the opening { of the arguments JSON.

Before this change AIMessageChunk.init_tool_calls() parsed each fragment independently:

  • The name-carrying fragment accumulated args from later deltas and ended up in invalid_tool_calls (incomplete JSON fragment).
  • The dangling continuation fragment (just {) became a blank tool_call with an empty name.

Root cause

merge_lists() (used by __add__) merges same-index chunks across two different messages. But when two same-index chunks already exist inside a single delta (i.e. in self.tool_call_chunks at construction time), nothing merges them before init_tool_calls() parses them.

Fix

At the start of init_tool_calls(), fold same-index continuation fragments using merge_lists() — the same semantics already used by __add__:

chunks_to_parse: list = []
for chunk in self.tool_call_chunks:
    chunks_to_parse = merge_lists(chunks_to_parse, [chunk]) or []
if len(chunks_to_parse) != len(self.tool_call_chunks):
    self.tool_call_chunks = chunks_to_parse

Writing the result back to self.tool_call_chunks ensures subsequent __add__ calls concatenate new deltas onto the correct merged fragment.

Same-index chunks with distinct non-empty ids are intentionally left separate (they represent genuinely parallel tool calls).

Test plan

  • Reproduction case from the issue passes (tool_calls contains one valid entry, invalid_tool_calls is empty)
  • Normal single-fragment streaming (docstring example) still works
  • Multiple tool calls with different indices still works
  • Invalid args still route to invalid_tool_calls
  • Same-index chunks with different ids are kept separate (not merged)

🤖 Generated with Claude Code

Changed files

  • libs/core/langchain_core/messages/ai.py (modified, +16/-1)

Code Example

from langchain_core.messages import AIMessageChunk
from langchain_core.messages.tool import tool_call_chunk as create_tool_call_chunk

first = AIMessageChunk(
    content="",
    tool_call_chunks=[
        create_tool_call_chunk(name="search", args="", id="id1", index=0),
        create_tool_call_chunk(name=None, args="{", id=None, index=0),
    ],
)

merged = first + AIMessageChunk(
    content="",
    tool_call_chunks=[
        create_tool_call_chunk(
            name=None,
            args='"query": "bar"}',
            id=None,
            index=0,
        )
    ],
)

print("tool_call_chunks=", merged.tool_call_chunks)
print("tool_calls=", merged.tool_calls)
print("invalid_tool_calls=", merged.invalid_tool_calls)

---

tool_call_chunks= [
    {
        "name": "search",
        "args": '"query": "bar"}',
        "id": "id1",
        "index": 0,
        "type": "tool_call_chunk",
    },
    {
        "name": None,
        "args": "{",
        "id": None,
        "index": 0,
        "type": "tool_call_chunk",
    },
]
tool_calls= [{"name": "", "args": {}, "id": None, "type": "tool_call"}]
invalid_tool_calls= [
    {
        "name": "search",
        "args": '"query": "bar"}',
        "id": "id1",
        "error": None,
        "type": "invalid_tool_call",
    }
]

---

tool_call_chunks= [
    {
        "name": "search",
        "args": '{"query": "bar"}',
        "id": "id1",
        "index": 0,
        "type": "tool_call_chunk",
    }
]
tool_calls= [
    {"name": "search", "args": {"query": "bar"}, "id": "id1", "type": "tool_call"}
]
invalid_tool_calls= []

---

{
  "tool_calls": [
    {
      "index": 0,
      "id": "call_...",
      "type": "function",
      "function": {
        "name": "bocha_websearch_tool",
        "arguments": ""
      }
    },
    {
      "index": 0,
      "function": {
        "arguments": "{"
      }
    }
  ]
}
RAW_BUFFERClick to expand / collapse

Checked other resources

  • This is a bug, not a usage question.
  • I added a clear and descriptive title that summarizes this issue.
  • I used the GitHub search to find similar issues.
  • I am sure that this is a bug in LangChain rather than my code.
  • The bug is not resolved by updating to the latest stable version of LangChain.
  • I posted a self-contained, minimal, reproducible example.

Package (Required)

  • langchain
  • langchain-openai
  • langchain-core
  • Other / not sure / general

Related Issues / PRs

Related symptom, but different root cause and code path:

  • #32562
  • #32580

Those issues are about the Responses API _advance(...) index handling. This report is about AIMessageChunk.init_tool_calls() on the chat.completions-style path when a provider emits multiple same-index tool-call fragments inside a single streamed delta.

Reproduction Steps / Example Code (Python)

from langchain_core.messages import AIMessageChunk
from langchain_core.messages.tool import tool_call_chunk as create_tool_call_chunk

first = AIMessageChunk(
    content="",
    tool_call_chunks=[
        create_tool_call_chunk(name="search", args="", id="id1", index=0),
        create_tool_call_chunk(name=None, args="{", id=None, index=0),
    ],
)

merged = first + AIMessageChunk(
    content="",
    tool_call_chunks=[
        create_tool_call_chunk(
            name=None,
            args='"query": "bar"}',
            id=None,
            index=0,
        )
    ],
)

print("tool_call_chunks=", merged.tool_call_chunks)
print("tool_calls=", merged.tool_calls)
print("invalid_tool_calls=", merged.invalid_tool_calls)

Actual output on current stable:

tool_call_chunks= [
    {
        "name": "search",
        "args": '"query": "bar"}',
        "id": "id1",
        "index": 0,
        "type": "tool_call_chunk",
    },
    {
        "name": None,
        "args": "{",
        "id": None,
        "index": 0,
        "type": "tool_call_chunk",
    },
]
tool_calls= [{"name": "", "args": {}, "id": None, "type": "tool_call"}]
invalid_tool_calls= [
    {
        "name": "search",
        "args": '"query": "bar"}',
        "id": "id1",
        "error": None,
        "type": "invalid_tool_call",
    }
]

Expected behavior:

tool_call_chunks= [
    {
        "name": "search",
        "args": '{"query": "bar"}',
        "id": "id1",
        "index": 0,
        "type": "tool_call_chunk",
    }
]
tool_calls= [
    {"name": "search", "args": {"query": "bar"}, "id": "id1", "type": "tool_call"}
]
invalid_tool_calls= []

Description

The failure mode appears when tool_call_chunks already contains multiple fragments for the same logical tool call before AIMessageChunk.init_tool_calls() parses them.

A real provider payload that triggered this looked like this on the first streamed delta:

{
  "tool_calls": [
    {
      "index": 0,
      "id": "call_...",
      "type": "function",
      "function": {
        "name": "bocha_websearch_tool",
        "arguments": ""
      }
    },
    {
      "index": 0,
      "function": {
        "arguments": "{"
      }
    }
  ]
}

LangChain correctly turns those into two ToolCallChunks, but later stream continuations are merged into the first entry while the dangling second same-index fragment remains in the list. When init_tool_calls() then parses the list, the incomplete fragment becomes a blank tool_call, and the real call falls into invalid_tool_calls.

The underlying problem is that init_tool_calls() parses self.tool_call_chunks as-is, without first normalizing same-index continuation fragments that are already present inside a single AIMessageChunk.

Proposed fix

Before best-effort JSON parsing in AIMessageChunk.init_tool_calls(), normalize self.tool_call_chunks by merging same-index continuation fragments using the existing merge_lists() semantics. That preserves the current behavior for truly parallel same-index tool calls with different non-empty ids, while fixing the case where a provider emits multiple fragments for one logical tool call inside the same delta.

System Info

  • OS: macOS
  • Python: 3.12.8
  • langchain: 1.2.15
  • langchain-core: 1.2.26
  • langchain-openai: 1.1.12

extent analysis

TL;DR

The issue can be fixed by normalizing same-index continuation fragments in AIMessageChunk.init_tool_calls() before parsing self.tool_call_chunks.

Guidance

  • The problem arises when tool_call_chunks contains multiple fragments for the same logical tool call before AIMessageChunk.init_tool_calls() parses them.
  • To fix this, self.tool_call_chunks should be normalized by merging same-index continuation fragments using the existing merge_lists() semantics.
  • The init_tool_calls() method should be modified to perform this normalization before parsing self.tool_call_chunks.
  • The expected behavior can be verified by checking the output of tool_call_chunks, tool_calls, and invalid_tool_calls after the fix.

Example

def init_tool_calls(self):
    # Normalize same-index continuation fragments
    self.tool_call_chunks = self._merge_same_index_fragments(self.tool_call_chunks)
    # Perform best-effort JSON parsing
    # ...

Notes

  • The proposed fix assumes that the merge_lists() semantics are already implemented and working correctly.
  • The fix may need to be adapted based on the specific implementation details of AIMessageChunk and init_tool_calls().

Recommendation

Apply workaround: Modify the init_tool_calls() method to normalize self.tool_call_chunks before parsing, as this will fix the issue without requiring an upgrade to a new version of LangChain.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING