Skip to content

Returning ToolReturn with content results in error when LLM calls multiple tools in parallel #2360

@RahulDas-dev

Description

@RahulDas-dev

Initial Checks

Description

Description

I'm encountering an issue when using pydantic-ai>=0.4.7 with OpenAI's gpt-4o-mini. When the agent attempts to invoke multiple tools in a single user prompt (e.g., requesting prices of multiple fruits), the run fails with a 400 error from the OpenAI API:

Error code: 400 - {'error': {'message': "An assistant message with 'tool_calls' must be followed by tool messages responding to each 'tool_call_id'. The following tool_call_ids did not have response messages: ...", 'type': 'invalid_request_error', 'param': 'messages.[4].role', 'code': None}}

This seems to indicate that the agent is not sending tool response messages for all the tool_call_ids generated by the assistant message.

Example Code

import asyncio
import logging
from dataclasses import dataclass
from string import Template
from typing import Union

from dotenv import load_dotenv
from openai import AsyncOpenAI
from pydantic import BaseModel, Field
from pydantic_ai import Agent, RunContext
from pydantic_ai.messages import ToolReturn
from pydantic_ai.models.openai import OpenAIModel
from pydantic_ai.providers.openai import OpenAIProvider

load_dotenv()

logger = logging.getLogger(__name__)


class Failure(BaseModel):
    """Represents structure for action failure"""

    reason: str = Field(description="The Reason why the task failed")


@dataclass(frozen=True, slots=True)
class AgentContext:
    shop_name: str


model_ = OpenAIModel(
    model_name="gpt-4o-mini",
    provider=OpenAIProvider(openai_client=AsyncOpenAI()),
)

agent = Agent[AgentContext, str | Failure](
    name="Fruit Shop Agent",
    model=model_,
    deps_type=AgentContext,
    output_type=Union[str, Failure],  # type: ignore # noqa: PGH003
    retries=3,
)

SYSTEM_MESSAGE = (
    "You are a helpful Food Seller agent.\n"
    "You help users with information about food items, their colors, and other details.\n"
    "You are working at the shop: ${SHOP_NAME}"
)


@agent.system_prompt
async def get_system_prompt(ctx: RunContext[AgentContext]) -> str:
    # The system prompt uses the shop_name from AgentContext
    return Template(SYSTEM_MESSAGE).substitute(SHOP_NAME=ctx.deps.shop_name)


@agent.tool
async def get_fruit_price(ctx: RunContext[AgentContext], food_name: str) -> ToolReturn:
    """Get the price of a fruit per kilogram.

    Args:
        food_name: The name of the fruit.

    Returns:
        returns price of the fruit or an error message if not found.
    """
    food_prices = {
        "apple": 1.2,
        "banana": 0.5,
        "grape": 2.0,
        "pineapple": 3.0,
        "orange": 1.0,
        "kiwi": 1.5,
        "mango": 2.5,
        "strawberry": 2.0,
    }

    price = food_prices.get(food_name.lower(), None)
    if price is None:
        return ToolReturn(
            return_value=None,
            content=f"Price of {food_name} is not known.",
            metadata={"success": False, "error": "UNKNOWN_FRUIT"},
        )
    return ToolReturn(
        return_value=price,
        content=f"The price of {food_name} is {price} per kilogram.",
        metadata={"success": True},
    )


task_str = "what is the price of an apple, banana, grape, pineapple?"


async def run_agent(shop_name: str, task: str) -> None:
    context = AgentContext(shop_name=shop_name)
    results = await agent.run(user_prompt=task, deps=context)
    print(results)


shop_name = "Rahul's Fresh Foods"
asyncio.run(run_agent(shop_name, task_str))

Here is the output

D:\python\open-coder\agentic_coder>uv run test_openai.py
Traceback (most recent call last):
  File "D:\python\open-coder\agentic_coder\.venv\Lib\site-packages\pydantic_ai\models\openai.py", line 332, in _completions_create
    return await self.client.chat.completions.create(
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    ...<25 lines>...
    )
    ^
  File "D:\python\open-coder\agentic_coder\.venv\Lib\site-packages\openai\resources\chat\completions\completions.py", line 2454, in create
    return await self._post(
           ^^^^^^^^^^^^^^^^^
    ...<45 lines>...
    )
    ^
  File "D:\python\open-coder\agentic_coder\.venv\Lib\site-packages\openai\_base_client.py", line 1791, in post
    return await self.request(cast_to, opts, stream=stream, stream_cls=stream_cls)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "D:\python\open-coder\agentic_coder\.venv\Lib\site-packages\openai\_base_client.py", line 1591, in request
    raise self._make_status_error_from_response(err.response) from None
openai.BadRequestError: Error code: 400 - {'error': {'message': "An assistant message with 'tool_calls' must be followed by tool messages responding to each 'tool_call_id'. The following tool_call_ids did not have response messages: call_Eiv5OsuW5aFEDoh9U9GwCbxs, call_czqKque4kEH4li9triArNTBv, call_q3z8CqKk3Yrj3V6ydPW7MWdc", 'type': 'invalid_request_error', 'param': 'messages.[4].role', 'code': None}}

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "D:\python\open-coder\agentic_coder\test_openai.py", line 102, in <module>
    asyncio.run(run_agent(shop_name, task_str))
    ~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\rahul.das\AppData\Local\Programs\Python\Python313\Lib\asyncio\runners.py", line 195, in run
    return runner.run(main)
           ~~~~~~~~~~^^^^^^
  File "C:\Users\rahul.das\AppData\Local\Programs\Python\Python313\Lib\asyncio\runners.py", line 118, in run
    return self._loop.run_until_complete(task)
           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^
  File "C:\Users\rahul.das\AppData\Local\Programs\Python\Python313\Lib\asyncio\base_events.py", line 719, in run_until_complete
    return future.result()
           ~~~~~~~~~~~~~^^
  File "D:\python\open-coder\agentic_coder\test_openai.py", line 97, in run_agent
    results = await agent.run(user_prompt=task, deps=context)
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "D:\python\open-coder\agentic_coder\.venv\Lib\site-packages\pydantic_ai\agent.py", line 562, in run
    async for _ in agent_run:
        pass
  File "D:\python\open-coder\agentic_coder\.venv\Lib\site-packages\pydantic_ai\agent.py", line 2175, in __anext__
    next_node = await self._graph_run.__anext__()
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "D:\python\open-coder\agentic_coder\.venv\Lib\site-packages\pydantic_graph\graph.py", line 809, in __anext__
    return await self.next(self._next_node)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "D:\python\open-coder\agentic_coder\.venv\Lib\site-packages\pydantic_graph\graph.py", line 782, in next
    self._next_node = await node.run(ctx)
                      ^^^^^^^^^^^^^^^^^^^
  File "D:\python\open-coder\agentic_coder\.venv\Lib\site-packages\pydantic_ai\_agent_graph.py", line 299, in run
    return await self._make_request(ctx)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "D:\python\open-coder\agentic_coder\.venv\Lib\site-packages\pydantic_ai\_agent_graph.py", line 359, in _make_request
    model_response = await ctx.deps.model.request(message_history, model_settings, model_request_parameters)
                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "D:\python\open-coder\agentic_coder\.venv\Lib\site-packages\pydantic_ai\models\openai.py", line 244, in request
    response = await self._completions_create(
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        messages, False, cast(OpenAIModelSettings, model_settings or {}), model_request_parameters
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    )
    ^
  File "D:\python\open-coder\agentic_coder\.venv\Lib\site-packages\pydantic_ai\models\openai.py", line 361, in _completions_create
    raise ModelHTTPError(status_code=status_code, model_name=self.model_name, body=e.body) from e
pydantic_ai.exceptions.ModelHTTPError: status_code: 400, model_name: gpt-4o-mini, body: {'message': "An assistant message with 'tool_calls' must be followed by tool messages responding to each 'tool_call_id'. The following tool_call_ids did not have response messages: call_Eiv5OsuW5aFEDoh9U9GwCbxs, call_czqKque4kEH4li9triArNTBv, call_q3z8CqKk3Yrj3V6ydPW7MWdc", 'type': 'invalid_request_error', 'param': 'messages.[4].role', 'code': None}

Python, Pydantic AI & LLM client version

requires-python = ">=3.13"
dependencies = [
    "pydantic>=2.11.7",
    "pydantic-ai>=0.4.7",
    "pydantic-settings>=2.10.1",
]

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions