Pydantic AIPydantic AI 2.51 · Python 3.10+
Dashboard
0%
1
Curious builder0 XP earned · 300 to level 2
0 daysFinish a lesson to begin
Badge collection0 of 6 unlocked
29 small wins to finish your pathNext lesson →

Tool errors: ModelRetry and crashes

ModelRetry is an exception you raise inside a tool to send the model a message and let it try again. Any other exception ends the whole run and reaches your code.

Last updated: 28 Sep, 2026 · Pydantic AI 2.51

A tool can fail in two ways that need different handling: a problem the model can recover from, like a wrong order id, and a bug in your code that should stop everything. The model function below always asks for order A-9999, which does not exist.

The model that asks for a missing order

python
from pydantic_ai import Agent, ModelResponse, ModelRetry, TextPart, ToolCallPart
from pydantic_ai.models.function import FunctionModel

ORDERS = {"A-1001": "shipped"}


def asks(messages, info):
    last = messages[-1].parts[-1]
    if last.part_kind == "user-prompt":
        return ModelResponse(parts=[ToolCallPart("lookup_order", {"order_id": "A-9999"})])
    if last.part_kind == "retry-prompt":
        return ModelResponse(parts=[TextPart(f"Sorry: {last.content}")])
    return ModelResponse(parts=[TextPart(last.content)])

An ordinary exception ends the run

The first tool reads the dictionary with no guard, so the missing key raises KeyError.

Example
agent = Agent(FunctionModel(asks))


@agent.tool_plain
def lookup_order(order_id: str) -> str:
    """Look up an order."""
    return ORDERS[order_id]


agent.run_sync("Where is my order?")

Pydantic AI let the KeyError through: the run stopped and the model was never told. That is right for a bug. For something the model can fix, tell it instead.

ModelRetry sends the model a message

Example
agent = Agent(FunctionModel(asks))


@agent.tool_plain
def lookup_order(order_id: str) -> str:
    """Look up an order."""
    if order_id not in ORDERS:
        raise ModelRetry(f"There is no order {order_id}. Ask the customer for their order id.")
    return ORDERS[order_id]


result = agent.run_sync("Where is my order?")
print(result.output)
print([part.part_kind for message in result.all_messages() for part in message.parts])

ModelRetry became a retry-prompt part, and its text is your message. A real model would now ask the customer for the id, as the message says, or call the tool again with a different one. The parts list shows the whole path: prompt, tool call, retry prompt, then the text answer.

When the retries run out

A tool that raises ModelRetry every time, with a model that never changes its call, cannot make progress.

Example
def stubborn(messages, info):
    return ModelResponse(parts=[ToolCallPart("lookup_order", {"order_id": "A-9999"})])


agent = Agent(FunctionModel(stubborn))


@agent.tool_plain
def lookup_order(order_id: str) -> str:
    """Look up an order."""
    raise ModelRetry(f"There is no order {order_id}.")


agent.run_sync("Where is my order?")

Each tool has its own small retry budget, one in this version of Pydantic AI. A model that keeps sending the same bad id runs out and ends the run with UnexpectedModelBehavior. Set the budget yourself with @agent.tool_plain(retries=3) for one tool, or Agent(retries=...) for the default.

What each return does

In the toolWhat happens
return valueThe model gets the value.
raise ModelRetry(message)The model gets your message and can try again, while that tool has retries left.
Any other exceptionThe run stops and the exception reaches your code.

When you reach for ModelRetry

  • The model passed an id, name or code that does not exist: tell it what was wrong.
  • A value failed a business rule the model could fix, like a date in the past.
  • An outside service asked you to retry: pass that back so the model waits and tries again.
Watch out. Do not raise ModelRetry for a real bug, like a missing config value or a broken connection. The model cannot fix it, so it burns the retry budget and then ends the run with a confusing UnexpectedModelBehavior instead of the actual error.
Try it yourself
  • Catch KeyError in the first version and raise ModelRetry from it.
  • Set retries=2 on the stubborn tool and count the requests in the error.
  • Return the string "not found" instead of raising. What does the model get?

Slow is fine. Stopping is the only problem.