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
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.
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?")Traceback (most recent call last):
File "main.py", line 10, in <module>
agent.run_sync("Where is my order?")
File "main.py", line 7, in lookup_order
return ORDERS[order_id]
KeyError: 'A-9999'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
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])Sorry: There is no order A-9999. Ask the customer for their order id. ['user-prompt', 'tool-call', 'retry-prompt', 'text']
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.
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?")Traceback (most recent call last):
File "main.py", line 11, in lookup_order
raise ModelRetry(f"There is no order {order_id}.")
pydantic_ai.exceptions.ModelRetry: There is no order A-9999.
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "main.py", line 14, in <module>
agent.run_sync("Where is my order?")
pydantic_ai.exceptions.UnexpectedModelBehavior: Tool 'lookup_order' exceeded max retries count of 1. Consider raising the retry limit, or see the docs on tool retries: https://pydantic.dev/docs/ai/tools-toolsets/tools-advanced/#tool-retriesEach 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 tool | What happens |
|---|---|
return value | The model gets the value. |
raise ModelRetry(message) | The model gets your message and can try again, while that tool has retries left. |
| Any other exception | The 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.
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.Related
- Previous: Dependencies: giving tools your data
- Next: Usage limits: stopping a run that loops
- Reference: Tool retries
- Catch
KeyErrorin the first version and raiseModelRetryfrom it. - Set
retries=2on 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.