fix: read tool_call_chunks for Anthropic streaming tool args
Anthropic/LangChain streams tool arguments as JSON string deltas in chunk.tool_call_chunks (not chunk.tool_calls which has empty args dict). Collect and accumulate both sources, then merge by tool_call_id. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
fed7a2eab7
commit
23dba2fa7b
1 changed files with 45 additions and 9 deletions
|
|
@ -342,7 +342,8 @@ class LLMFactory:
|
|||
|
||||
for _round in range(max_rounds):
|
||||
full_content = ""
|
||||
tool_calls_in_response = []
|
||||
tool_calls_in_response = [] # from chunk.tool_calls (id + name, often empty args)
|
||||
tool_call_chunks_raw = [] # from chunk.tool_call_chunks (actual args as string deltas)
|
||||
|
||||
# Stream the LLM response
|
||||
async for chunk in llm_with_tools.astream(lc_messages):
|
||||
|
|
@ -368,14 +369,38 @@ class LLMFactory:
|
|||
if tc:
|
||||
tool_calls_in_response.extend(tc)
|
||||
|
||||
# If no tool calls, we're done
|
||||
if not tool_calls_in_response:
|
||||
# Collect tool_call_chunks — Anthropic streams args as JSON string deltas here
|
||||
if hasattr(chunk, 'tool_call_chunks') and chunk.tool_call_chunks:
|
||||
tool_call_chunks_raw.extend(chunk.tool_call_chunks)
|
||||
|
||||
# If no tool calls at all, we're done
|
||||
if not tool_calls_in_response and not tool_call_chunks_raw:
|
||||
return
|
||||
|
||||
# Accumulate tool calls across streaming chunks.
|
||||
# Anthropic: first chunk has name + empty args dict; later chunks have args but no name.
|
||||
# OpenAI: args arrive as incremental JSON string fragments across chunks.
|
||||
# Strategy: accumulate name, merge dict args, concatenate string args per id.
|
||||
# Build per-call accumulator keyed by index (tool_call_chunks use index, not id)
|
||||
# Then enrich with id/name from tool_calls_in_response.
|
||||
#
|
||||
# tool_call_chunks structure (Anthropic/LangChain):
|
||||
# first chunk: {"id": "toolu_01", "name": "code_interpreter", "args": "", "index": 0}
|
||||
# later chunks: {"id": None, "name": None, "args": '{"code":"import...', "index": 0}
|
||||
#
|
||||
# tool_calls structure (all providers, but args often empty for Anthropic):
|
||||
# {"id": "toolu_01", "name": "code_interpreter", "args": {}}
|
||||
|
||||
by_index: dict = {} # index -> {id, name, args_str}
|
||||
for tcc in tool_call_chunks_raw:
|
||||
idx = tcc.get("index", 0)
|
||||
if idx not in by_index:
|
||||
by_index[idx] = {"id": "", "name": "", "args_str": ""}
|
||||
entry = by_index[idx]
|
||||
if tcc.get("id") and not entry["id"]:
|
||||
entry["id"] = tcc["id"]
|
||||
if tcc.get("name") and not entry["name"]:
|
||||
entry["name"] = tcc["name"]
|
||||
if tcc.get("args"):
|
||||
entry["args_str"] += tcc["args"]
|
||||
|
||||
# Also accumulate from tool_calls (handles OpenAI-style and non-chunk providers)
|
||||
acc: dict = {} # tc_id -> {id, name, args_dict, args_str}
|
||||
for tc in tool_calls_in_response:
|
||||
tc_id = tc.get("id") or None
|
||||
|
|
@ -385,7 +410,6 @@ class LLMFactory:
|
|||
tc_args = tc.get("args")
|
||||
if tc_args is None:
|
||||
tc_args = tc.get("function", {}).get("arguments", "")
|
||||
|
||||
if tc_id not in acc:
|
||||
acc[tc_id] = {"id": tc_id, "name": "", "args_dict": {}, "args_str": ""}
|
||||
entry = acc[tc_id]
|
||||
|
|
@ -396,7 +420,19 @@ class LLMFactory:
|
|||
elif isinstance(tc_args, str) and tc_args:
|
||||
entry["args_str"] += tc_args
|
||||
|
||||
# Fall back to raw list if no ids were found (shouldn't happen)
|
||||
# Merge by_index (Anthropic chunks) into acc
|
||||
for idx_entry in by_index.values():
|
||||
tc_id = idx_entry["id"]
|
||||
if not tc_id:
|
||||
continue
|
||||
if tc_id not in acc:
|
||||
acc[tc_id] = {"id": tc_id, "name": "", "args_dict": {}, "args_str": ""}
|
||||
if idx_entry["name"] and not acc[tc_id]["name"]:
|
||||
acc[tc_id]["name"] = idx_entry["name"]
|
||||
if idx_entry["args_str"]:
|
||||
acc[tc_id]["args_str"] += idx_entry["args_str"]
|
||||
|
||||
# Fall back to raw list if no ids were found
|
||||
if not acc:
|
||||
acc = {i: {"id": None, "name": tc.get("name", ""), "args_dict": tc.get("args", {}), "args_str": ""}
|
||||
for i, tc in enumerate(tool_calls_in_response)}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue