-
Notifications
You must be signed in to change notification settings - Fork 496
Description
Checks
- I have updated to the lastest minor and patch version of Strands
- I have checked the documentation and this is not expected behavior
- I have searched ./issues and there are no duplicates of my issue
Strands Version
v1.5.0
Python Version
3.13.5
Operating System
macOs 15.5
Installation Method
pip
Steps to Reproduce
- Use the same example: https://strandsagents.com/latest/documentation/docs/user-guide/concepts/model-providers/litellm/
Structured Outputsection - Run the example using LiteLLM (Model I used is "claude-3-7-sonnet-20250219")
- Error I keep seeing
No tool_calls found in response
it seems that litellm finish_reason is "tool_use" NOT "tool_calls"
Expected Behavior
Structured output should work with litellm
The original structured_output method in LiteLLMModel only handled a single response format (finish_reason == "tool_calls"), which was insufficient for handling various response formats from different models and LiteLLM's specific response structures.
Actual Behavior
- Use the same example: https://strandsagents.com/latest/documentation/docs/user-guide/concepts/model-providers/litellm/
Structured Outputsection - Error I keep seeing
No tool_calls found in response
{"timestamp": "2025-08-26T04:59:54.592638", "message": "❌ Error: No tool_calls found in response", "level": "ERROR", "logger": "", "module": "conversations", "function": "", "line": 490, "exception": "Traceback (most recent call last):\n File "REDACTED", line 476, in REDACTED\n result: AgentResponse = await asyncio.wait_for(\n ^^^^^^^^^^^^^^^^^^^^^^^\n ...<2 lines>...\n )\n ^\n File "/opt/homebrew/Cellar/[email protected]/3.13.5/Frameworks/Python.framework/Versions/3.13/lib/python3.13/asyncio/tasks.py", line 507, in wait_for\n return await fut\n ^^^^^^^^^\n File "REDACTED/python3.13/site-packages/strands/agent/agent.py", line 461, in structured_output_async\n async for event in events:\n if "callback" in event:\n self.callback_handler(**cast(dict, event["callback"]))\n File "REDACTED/lib/python3.13/site-packages/strands/models/litellm.py", line 225, in structured_output\n raise ValueError("No tool_calls found in response")\nValueError: No tool_calls found in response"}
it seems that litellm finish_reason is "tool_use" NOT "tool_calls"
Additional Context
The original implementation was too restrictive and didn't account for:
Different response formats (tool_use vs tool_calls)
LiteLLM's specific stop_reason handling
Content arrays with structured data
Fallback parsing for structured content in regular responses
Possible Solution
File: venv/lib/python3.13/site-packages/strands/models/litellm.py
Method: structured_output
Change: Expanded from single format to multiple format support
Step 1: Enhanced Response Format Handling
File: venv/lib/python3.13/site-packages/strands/models/litellm.py
Method: structured_output
Change: Expanded from single format to multiple format support
# Before: Only handled tool_calls
if choice.finish_reason == "tool_calls":
# Simple JSON parsing
# After: Multiple format support
# 1. Tool calls format (original)
if choice.finish_reason == "tool_calls" or choice.finish_reason == "tool_use":
# Enhanced parsing
# 2. Content array with tool_use
if hasattr(choice.message, 'content') and isinstance(choice.message.content, list):
for content_item in choice.message.content:
if hasattr(content_item, 'type') and content_item.type == 'tool_use':
# Parse tool_use from content array
# 3. LiteLLM specific stop_reason
if hasattr(choice, 'stop_reason') and choice.stop_reason == "tool_use":
# Handle LiteLLM's stop_reason format
# 4. Fallback parsing
if choice.finish_reason == "stop" and hasattr(choice.message, 'content'):
# Try parsing regular content as structured data
Step 2: Content Array Support
Change: Added support for LiteLLM's content array format
# New: Handle content arrays with tool_use
if hasattr(choice.message, 'content') and isinstance(choice.message.content, list):
for content_item in choice.message.content:
if hasattr(content_item, 'type') and content_item.type == 'tool_use':
if hasattr(content_item, 'input') and content_item.input:
yield {"output": output_model(**content_item.input)}
Step 3: Fallback Structured Data Parsing
Change: Added fallback for when structured data is in regular content
# New: Try parsing regular content as JSON if it looks structured
if choice.finish_reason == "stop" and hasattr(choice.message, 'content'):
try:
content_data = json.loads(choice.message.content)
if isinstance(content_data, dict) and all(isinstance(k, str) for k in content_data.keys()):
yield {"output": output_model(**content_data)}
except (json.JSONDecodeError, TypeError, ValueError):
pass # Continue to next choice
Related Issues
No response