-
Notifications
You must be signed in to change notification settings - Fork 421
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
1.10.0 (strands-agents
)
Python Version
3.11.11
Operating System
macOS 14.5 (Apple Silicon)
Installation Method
other
Steps to Reproduce
-
Create a project using Strands Agents and install dependencies with
uv add strands-agents 'strands-agents[litellm]' strands-agents-tools
. -
Configure the agent as shown below:
from dotenv import load_dotenv from pydantic import BaseModel from strands import Agent from strands.models.litellm import LiteLLMModel import os load_dotenv() model = LiteLLMModel( client_args={"api_key": os.getenv("NEBIUS_API_KEY")}, model_id="nebius/zai-org/GLM-4.5", ) class PersonInfo(BaseModel): name: str age: int occupation: str agent = Agent( model=model, system_prompt="You are a helpful assistant that extracts structured information about people from text.", ) agent.structured_output(PersonInfo, "John Smith is a 30-year-old software engineer")
-
Run the script via
uv run main.py
.
Expected Behavior
structured_output
should recognize the JSON payload returned in message.content
(even when finish_reason
is "stop"
) and return a populated PersonInfo
instance.
Actual Behavior
The call raises ValueError: No tool_calls found in response
from strands/models/litellm.py
because the LiteLLM adapter only checks for responses with finish_reason == "tool_calls"
.
Additional Context
- Provider: Nebius
zai-org/GLM-4.5
via LiteLLM - The provider returns the structured response in
choice.message.content
withfinish_reason: "stop"
. - Local environment: macOS, Apple Silicon, Strands Agents 1.10.0, Python 3.11.11.
Possible Solution
Add a fallback in LiteLLMModel.structured_output
to attempt parsing the first choice's message.content
as JSON when no tool calls are present. A simple local patch that adds this fallback resolves the issue and produces the expected structured output.
I tried this approach and it worked:
# Fallback: sometimes providers return the structured JSON payload in
# the message content but use a different finish_reason (e.g. "stop").
# Attempt to parse the first choice's message content as JSON and
# return it if successful.
if response.choices:
first_choice = response.choices[0]
content = getattr(getattr(first_choice, "message", None), "content", None)
if content:
try:
tool_call_data = json.loads(content)
yield {"output": output_model(**tool_call_data)}
return
except (json.JSONDecodeError, TypeError, ValueError):
# not JSON or doesn't match expected shape; fall through to error
pass
# If we reach here, no tool_calls or usable JSON content was found
finish_reasons = [getattr(c, "finish_reason", None) for c in response.choices]
raise ValueError(f"No tool_calls found in response (finish_reasons={finish_reasons})")
Related Issues
N/A