Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/simonw/LLM/llms.txt

Use this file to discover all available pages before exploring further.

Tool calling lets language models do more than just generate text — they can request the execution of Python functions, receive the results, and incorporate those results into their response. LLM supports tool usage across both the command-line interface and the Python API, enabling everything from simple calculations to multi-step agentic workflows.
Tools can be dangerous. Applications built on top of LLMs are vulnerable to prompt injection attacks, where malicious third-party content tricks the model into taking harmful actions.Be especially wary of the lethal trifecta — if your tool-enabled LLM has access to private data, exposure to malicious instructions (web pages, emails, GitHub issues), and the ability to exfiltrate information, an attacker could steal your private data by leaving malicious instructions in content your LLM processes.

How Tools Work

A tool is a Python function that the model can request to be executed. LLM manages the full request-execute-respond loop automatically.
1

Model receives tool definitions

The initial prompt includes a list of available tools with their names, descriptions, and parameter schemas.
2

Model requests a tool call

The model returns a structured request to execute one or more tools with specific arguments.
3

LLM executes the tool

LLM runs the specified Python function with the arguments the model provided.
4

Results are fed back

LLM sends the tool output back to the model in a follow-up prompt.
5

Model generates its final response

The model uses the tool output to formulate a complete answer. This loop can repeat multiple times for complex workflows.

Trying Out Tools from the CLI

LLM ships with two built-in tools you can try immediately.

Default Built-in Tools

llm_version

Returns the currently installed version of LLM.

llm_time

Returns the current local time and UTC time, including timezone information.
Try out the version tool:
llm --tool llm_version "What version of LLM is this?" --td
You can also use -T as a shortcut for --tool:
llm -T llm_version "What version of LLM is this?" --td
The --td flag (--tools-debug) prints each tool call and its result as it executes:
Tool call: llm_version({})
  0.26a0

The installed version of the LLM is 0.26a0.
Try both built-in tools together:
llm -T llm_version -T llm_time 'Give me the current time and LLM version' --td
Run llm tools to list all tools available from installed plugins.

Inline Python Functions with --functions

The --functions option lets you define tools directly on the command line as Python code — no plugin required:
llm --functions '
def multiply(x: int, y: int) -> int:
    """Multiply two numbers."""
    return x * y
' 'what is 34234 * 213345' --td
Output:
Tool call: multiply({'x': 34234, 'y': 213345})
  7303652730

34234 multiplied by 213345 is 7,303,652,730.
The --functions option can be passed more than once, and can also point to a .py file:
llm --functions ./my_tools.py 'Use my custom tools'

Interactive Approval with --ta

To approve each tool call interactively before it executes, use --ta (--tools-approve):
llm --functions '
def multiply(x: int, y: int) -> int:
    """Multiply two numbers."""
    return x * y
' 'what is 34234 * 213345' --ta
Output:
Tool call: multiply({'x': 34234, 'y': 213345})
Approve tool call? [y/N]:

Limiting Tool Loops with --cl

By default LLM will run up to 5 consecutive tool-call/response loops before stopping. Use --cl (--chain-limit) to change this:
# Allow up to 10 tool loops
llm -T llm_version 'What version is this?' --cl 10

# No limit — run until the model stops requesting tools
llm -T llm_version 'What version is this?' --cl 0

Plugin Tools with --tool

Tools installed via plugins are available with the --tool / -T flag:
llm install llm-tools-simpleeval
llm --tool simple_eval "4444 * 233423" --td
When you continue a conversation with llm -c, any plugin tools from the previous prompt are automatically re-attached:
llm -T simple_eval "12345 * 12345" --td
# Tool call: simple_eval({'expression': '12345 * 12345'}) → 152399025

llm -c "that * 6" --td
# Tool call: simple_eval({'expression': '152399025 * 6'}) → 914394150

Toolboxes from Plugins

Some plugins bundle multiple related tools into a toolbox — a single --tool argument that loads several tools at once. Toolbox names always start with a capital letter:
llm install llm-tools-datasette
llm -T 'Datasette("https://datasette.io/content")' "Show tables" --td
Toolboxes accept configuration arguments in several formats:
  • Empty: ToolboxName or ToolboxName()
  • JSON object: ToolboxName({"key": "value", "other": 42})
  • Single value: ToolboxName("hello") or ToolboxName([1,2,3])
  • Key-value pairs: ToolboxName(name="test", count=5)
For interactive sessions with a toolbox, use llm chat:
llm chat -T 'Datasette("https://datasette.io/content")' --td

LLM’s Tool Implementation

The Tool Class

Every tool in LLM is an instance of llm.Tool. You can create one from any Python function using Tool.function():
import llm

def multiply(x: int, y: int) -> int:
    """Multiply two numbers together."""
    return x * y

tool = llm.Tool.function(multiply)
# tool.name        → "multiply"
# tool.description → "Multiply two numbers together."
# tool.input_schema → Pydantic model built from the function signature
LLM extracts the tool name from the function name, the description from the docstring, and builds a JSON input schema by inspecting the function signature and type hints.

The Toolbox Class

For more advanced needs — bundling related tools, storing state between calls, or making tools configurable — subclass llm.Toolbox:
import llm

class Memory(llm.Toolbox):
    _memory = None

    def _get_memory(self):
        if self._memory is None:
            self._memory = {}
        return self._memory

    def set(self, key: str, value: str):
        "Set something as a key"
        self._get_memory()[key] = value

    def get(self, key: str):
        "Get something from a key"
        return self._get_memory().get(key) or ""

    def append(self, key: str, value: str):
        "Append something as a key"
        memory = self._get_memory()
        memory[key] = (memory.get(key) or "") + "\n" + value

    def keys(self):
        "Return a list of keys"
        return list(self._get_memory().keys())
Any method that does not start with an underscore is exposed as a tool. The toolbox instance persists state across multiple tool invocations in the same conversation. Use a toolbox from Python:
model = llm.get_model("gpt-4.1-mini")
memory = Memory()

conversation = model.conversation(tools=[memory])
print(conversation.chain("Set name to Simon", after_call=print).text())

print(memory._memory)
# {'name': 'Simon'}

print(conversation.chain("Set name to Penguin", after_call=print).text())
print(conversation.chain("Print current name", after_call=print).text())

Default Built-in Tools (Source)

The two default tools ship with LLM in llm/tools.py:
from datetime import datetime, timezone
from importlib.metadata import version
import time


def llm_version() -> str:
    "Return the installed version of llm"
    return version("llm")


def llm_time() -> dict:
    "Returns the current time, as local time and UTC"
    utc_time = datetime.now(timezone.utc)
    local_time = datetime.now()
    local_tz_name = time.tzname[time.localtime().tm_isdst]
    is_dst = bool(time.localtime().tm_isdst)
    offset_seconds = -time.timezone if not is_dst else -time.altzone
    offset_hours = offset_seconds // 3600
    offset_minutes = (offset_seconds % 3600) // 60
    timezone_offset = (
        f"UTC{'+' if offset_hours >= 0 else ''}{offset_hours:02d}:{offset_minutes:02d}"
    )
    return {
        "utc_time": utc_time.strftime("%Y-%m-%d %H:%M:%S UTC"),
        "utc_time_iso": utc_time.isoformat(),
        "local_timezone": local_tz_name,
        "local_time": local_time.strftime("%Y-%m-%d %H:%M:%S"),
        "timezone_offset": timezone_offset,
        "is_dst": is_dst,
    }
They are registered via the plugin hook in default_plugins/default_tools.py:
import llm
from llm.tools import llm_time, llm_version

@llm.hookimpl
def register_tools(register):
    register(llm_version)
    register(llm_time)

Tips for Implementing Tools

Always include a docstring in your tool functions. The docstring becomes the tool description that the model uses to decide when and how to call the tool.
LLM builds the input schema by inspecting the function signature. Without type hints, all parameters default to str. Be explicit:
def search(query: str, max_results: int = 10) -> list:
    """Search for items matching the query."""
    ...
Tool functions can return a string or any object that can be converted to a string. Returning a dict or list works — LLM will serialize it for the model.To also return attachments (images, files) alongside text output, return an llm.ToolOutput:
import llm

def generate_image(prompt: str) -> llm.ToolOutput:
    """Generate an image based on the prompt."""
    image_content = generate_image_from_prompt(prompt)
    return llm.ToolOutput(
        output="Image generated successfully",
        attachments=[llm.Attachment(
            content=image_content,
            mimetype="image/png"
        )],
    )
If your plugin tool needs API credentials, store them with llm keys set api-name and retrieve them with the llm.get_key() utility. This prevents secrets from being logged to the database as part of tool call arguments.
If your tool needs its own tool_call_id — for example to key external state against a specific invocation — add a parameter named llm_tool_call to your function. It receives the llm.ToolCall object and is hidden from the schema the model sees:
import llm

def lookup(name: str, llm_tool_call: llm.ToolCall) -> str:
    "Look up a name."
    return do_lookup(name, request_id=llm_tool_call.tool_call_id)
This works for both sync and async functions and for llm.Toolbox methods.

Python API

Basic Tool Use

Pass tools to model.prompt() using the tools= keyword argument:
import llm

def upper(text: str) -> str:
    """Convert text to uppercase."""
    return text.upper()

model = llm.get_model("gpt-4.1-mini")
response = model.prompt("Convert panda to upper", tools=[upper])

tool_calls = response.tool_calls()
# [ToolCall(name='upper', arguments={'text': 'panda'}, tool_call_id='...')]
Get the model’s follow-up reply with response.reply(), which automatically executes pending tool calls and feeds the results back:
follow_up = response.reply()
print(follow_up.text())
# The word "panda" converted to uppercase is "PANDA".

Automatic Chaining with model.chain()

For multi-step loops that keep going until the model stops requesting tools, use model.chain():
chain_response = model.chain(
    "Convert panda to upper",
    tools=[upper],
)
print(chain_response.text())
# The word "panda" converted to uppercase is "PANDA".
Stream the response as it generates:
for chunk in model.chain("Convert panda to upper", tools=[upper]):
    print(chunk, end="", flush=True)
Iterate over individual responses in the chain:
chain = model.chain("Convert panda to upper", tools=[upper])
for response in chain.responses():
    print(response.prompt)
    for chunk in response:
        print(chunk, end="", flush=True)

Debug Hooks: before_call and after_call

Use the before_call= parameter to inspect or block tool calls before they execute. Raise llm.CancelToolCall to prevent a specific call:
import llm
from typing import Optional

def upper(text: str) -> str:
    "Convert text to uppercase."
    return text.upper()

def before_call(tool: Optional[llm.Tool], tool_call: llm.ToolCall):
    print(f"About to call {tool.name} with {tool_call.arguments}")
    if tool.name == "upper" and "bad" in repr(tool_call.arguments):
        raise llm.CancelToolCall("Not allowed to call upper on text containing 'bad'")

model = llm.get_model("gpt-4.1-mini")
response = model.chain(
    "Convert panda to upper and badger to upper",
    tools=[upper],
    before_call=before_call,
)
print(response.text())
Use after_call= to log results after each execution:
def after_call(tool: llm.Tool, tool_call: llm.ToolCall, tool_result: llm.ToolResult):
    print(f"{tool.name}({tool_call.arguments}) → {tool_result.output}")

response = model.chain(
    "Convert panda to upper",
    tools=[upper],
    after_call=after_call,
)

CancelToolCall

Raise llm.CancelToolCall inside a before_call hook to stop a specific tool call. The model will be informed that the call was cancelled and can adjust its response:
def before_call(tool, tool_call):
    if tool.name == "delete_files":
        raise llm.CancelToolCall("Deletion not permitted in this session")

PauseChain

Raise llm.PauseChain inside a tool implementation when the tool cannot complete without outside input — for example, when human approval is required:
import llm

def delete_files(path: str) -> str:
    if not approval_already_recorded(path):
        record_approval_request(path)
        raise llm.PauseChain("waiting for approval to delete " + path)
    do_delete(path)
    return "deleted"
Unlike other exceptions (which become "Error: ..." tool results sent back to the model), PauseChain propagates cleanly out of the chain. Before re-raising, LLM populates two attributes:
  • pause.tool_call — the llm.ToolCall whose implementation paused
  • pause.tool_results — results of sibling calls that completed
try:
    chain_response.text()
except llm.PauseChain as pause:
    print("Paused on", pause.tool_call.name, pause.tool_call.tool_call_id)
To resume after a pause (or a crash, or a server restart), re-run the chain with the persisted message history:
chain = model.chain(
    messages=persisted_messages,  # ends in assistant tool calls with no results
    tools=[delete_files],
    system=system_prompt,
)
chain.text()
Calls that already have matching results in the history are skipped — only unresolved calls are re-executed.

Dynamic Toolboxes

Add tools to an existing toolbox instance at runtime with toolbox.add_tool():
def my_function(arg1: str, arg2: int) -> str:
    return f"Received {arg1} and {arg2}"

toolbox.add_tool(my_function)
Pass pass_self=True if the function needs access to the toolbox instance as self:
def my_function(self, arg1: str) -> str:
    return f"Called on {self} with {arg1}"

toolbox.add_tool(my_function, pass_self=True)
Override prepare() (or prepare_async() for async contexts) to run setup logic — such as consulting an MCP server for available tools — before the toolbox is first used:
class MyToolbox(llm.Toolbox):
    def prepare(self):
        tools = fetch_tools_from_server()
        for t in tools:
            self.add_tool(t)

Build docs developers (and LLMs) love