Description
When using Agno Agent with tools in streaming mode, particularly with Claude models via LiteLLM, tool function names are being overwritten with empty strings, causing LLM API validation errors.
In streaming responses, the parse_tool_calls method incorrectly handles tool metadata that spans multiple stream chunks. When tool information is fragmented across chunks (name in early chunk, arguments in later chunks), the method overwrites the valid tool name with an empty string from a later chunk.
Steps to Reproduce
from agno.agent import Agent
from agno.models.litellm import LiteLLM
from agno.tools import tool
@tool(name="add", description="Add two numbers")
def add_func(a: int, b: int) -> str:
return f"{a} + {b} = {a+b}"
llm = LiteLLM(id="litellm_proxy/claude-sonnet-4-5", api_key="your-key")
agent = Agent(model=llm, tools=[add_func])
# Run in streaming mode
for event in agent.run("What is 5 + 3?", stream=True):
pass
Agent Configuration (if applicable)
No response
Expected Behavior
The agent should successfully invoke the add tool and return the correct result.
Actual Behavior
ERROR: litellm.BadRequestError: Litellm_proxyException - 2 validation errors detected:
Value at 'messages.2.member.content.1.member.toolUse.name' failed to satisfy constraint:
Member must have length greater than or equal to 1
Screenshots or Logs (if applicable)
No response
Environment
- **Agno version:** 2.5.5 (confirmed with bug)
- **LiteLLM version:** Latest
- **Python version:** 3.12
- **Models affected:** Claude (all versions through LiteLLM)
Possible Solutions (optional)
Root Cause Analysis
Stream Chunks Structure
LiteLLM splits Claude's tool call response into multiple chunks:
Chunk 1: {index: 1, id: "tooluse_...", type: "function", function: {name: "add", arguments: ""}}
Chunk 2: {index: 1, id: None, type: "function", function: {name: "", arguments: "{\"a\": 5"}}
Chunk 3: {index: 1, id: None, type: "function", function: {name: "", arguments: ", \"b\": 3}"}}
Bug in parse_tool_calls
In agno/models/litellm.py, the parse_tool_calls method (around line 420-450):
# Current problematic code:
if function_data.get("name") is not None:
name = function_data.get("name", "")
tool_calls_by_index[index]["function"]["name"] = name
The Problem:
- When
function_data.get("name") returns an empty string "", the condition "" is not None evaluates to True
- This causes the previously captured valid name (e.g.,
"add") to be overwritten with the empty string
- The final result has
name: "" instead of name: "add"
Execution Flow:
- Chunk 1:
name="add" → condition True → sets name="add" ✓
- Chunk 2:
name="" → condition True (because "" is not None) → overwrites to name="" ✗
- Chunk 3:
name="" → condition True → keeps name="" ✗
Solution
Change the condition to check for truthiness instead of None:
# Fixed code:
if function_data.get("name"): # Only update if name is non-empty
name = function_data.get("name", "")
tool_calls_by_index[index]["function"]["name"] = name
This way:
- Chunk 1:
name="add" → truthy → sets name="add" ✓
- Chunk 2:
name="" → falsy → skips update, preserves "add" ✓
- Chunk 3:
name="" → falsy → keeps "add" ✓
Additional Context
Affected Models
- Primary: Claude (via LiteLLM's Claude provider)
- All providers using Claude: AWS Bedrock, Anthropic API, etc.
- Not affected: Gemini, other models (they handle tool metadata differently)
Impact Level
- Severity: High (breaks all tool calling functionality with Claude in streaming mode)
- Scope: Affects anyone using Agno Agent with Claude models and tools in streaming mode
- Workaround: Available (override
parse_tool_calls in subclass)
Proposed Fix
File: agno/models/litellm.py
Method: parse_tool_calls (static method)
Line: Change if function_data.get("name") is not None: to if function_data.get("name"):
Testing
Add test case for streaming tool calls with fragmented metadata:
def test_parse_tool_calls_with_empty_strings():
"""Test that empty strings don't overwrite valid tool names in streaming chunks"""
# Simulates chunks received from Claude API via LiteLLM
chunks = [
{"index": 0, "id": "id_1", "function": {"name": "add", "arguments": ""}},
{"index": 0, "id": None, "function": {"name": "", "arguments": "{\"a\""}},
{"index": 0, "id": None, "function": {"name": "", "arguments": ": 5}"}},
]
result = LiteLLM.parse_tool_calls(chunks)
# Current bug: name becomes ""
# Expected: name should remain "add"
assert result[0]["function"]["name"] == "add", f"Got empty string, expected 'add'"
assert result[0]["function"]["arguments"] == '{"a": 5}'
Reproducibility
- Status: Consistently reproducible in Agno 2.5.5
- Impact: 100% failure rate when calling Claude tools in streaming mode with Agent
- Non-streaming mode: Not affected (tool calls work correctly)
Version History
This bug appears to be introduced in Agno 2.5.x based on the streaming response handling implementation. Earlier versions may not have this issue if they used non-streaming or had different tool call parsing logic.
References
Description
When using Agno Agent with tools in streaming mode, particularly with Claude models via LiteLLM, tool function names are being overwritten with empty strings, causing LLM API validation errors.
In streaming responses, the
parse_tool_callsmethod incorrectly handles tool metadata that spans multiple stream chunks. When tool information is fragmented across chunks (name in early chunk, arguments in later chunks), the method overwrites the valid tool name with an empty string from a later chunk.Steps to Reproduce
Agent Configuration (if applicable)
No response
Expected Behavior
The agent should successfully invoke the
addtool and return the correct result.Actual Behavior
Screenshots or Logs (if applicable)
No response
Environment
Possible Solutions (optional)
Root Cause Analysis
Stream Chunks Structure
LiteLLM splits Claude's tool call response into multiple chunks:
Bug in parse_tool_calls
In
agno/models/litellm.py, theparse_tool_callsmethod (around line 420-450):The Problem:
function_data.get("name")returns an empty string"", the condition"" is not Noneevaluates toTrue"add") to be overwritten with the empty stringname: ""instead ofname: "add"Execution Flow:
name="add"→ conditionTrue→ setsname="add"✓name=""→ conditionTrue(because"" is not None) → overwrites toname=""✗name=""→ conditionTrue→ keepsname=""✗Solution
Change the condition to check for truthiness instead of
None:This way:
name="add"→ truthy → setsname="add"✓name=""→ falsy → skips update, preserves"add"✓name=""→ falsy → keeps"add"✓Additional Context
Affected Models
Impact Level
parse_tool_callsin subclass)Proposed Fix
File:
agno/models/litellm.pyMethod:
parse_tool_calls(static method)Line: Change
if function_data.get("name") is not None:toif function_data.get("name"):Testing
Add test case for streaming tool calls with fragmented metadata:
Reproducibility
Version History
This bug appears to be introduced in Agno 2.5.x based on the streaming response handling implementation. Earlier versions may not have this issue if they used non-streaming or had different tool call parsing logic.
References
agno/models/litellm.py,parse_tool_callsmethod