Call your own functions
A model knows a lot, but it knows nothing about your data. It can’t read your database, check today’s readings, or look through your records. What it can do is ask your program to do those things for it. You describe a function as a tool; the model asks for it when it needs it; your program runs it and sends back the result.
The examples in these guides follow one small program: an assistant for a wildlife research station. On this page, you’ll give it a way to search the station’s records of what its cameras and observers have seen. We’ll start by describing a search function as a tool, and see the model ask for it. Then we’ll run the search and send back the result, and write the loop that keeps going until the model has what it needs. We’ll finish with a common mistake: a tool the model can’t use well because its inputs aren’t described.
LM15 never runs your functions. It carries the model’s request to your program,
and your result back to the model; what runs, when, and after which checks is
up to your code. The exchanges shown are real: we ran them with gpt-5.6-sol
through OpenAI Codex.
The station’s records
Section titled “The station’s records”Four sightings, and a function that searches them by species, place, or date:
SIGHTINGS = [
{"date": "2026-09-18", "place": "oak grove",
"species": "wood mouse", "count": 4},
{"date": "2026-09-19", "place": "oak grove",
"species": "roe deer", "count": 2},
{"date": "2026-09-20", "place": "stream",
"species": "red fox", "count": 1},
{"date": "2026-09-21", "place": "oak grove",
"species": "wild boar", "count": 3},
]
def search_sightings(query):
query = query.lower()
return [s for s in SIGHTINGS
if query in (s["species"], s["place"], s["date"])]This is ordinary code; LM15 isn’t involved yet.
Describe it as a tool
Section titled “Describe it as a tool”The model never sees your function. It sees a description: a name, what the tool does, and a schema for its input.
sightings_tool = FunctionTool(
name="search_sightings",
description=(
"Find the station's recorded sightings by species, place or "
"date."
),
parameters={
"type": "object",
"properties": {
"query": {"type": "string", "description": (
"One species, place or date, such as 'oak grove' or "
"'2026-09-18'."
)},
},
"required": ["query"],
},
)The input has a description of its own, saying what a good search term looks like. We’ll see why that matters at the end of the page.
Offer the tool
Section titled “Offer the tool”Ask the station’s question, with the tool in the request:
request = Request(
model="provider:model",
system=(
"You are the field assistant for a wildlife research "
"station. Answer in two sentences."
),
messages=[Message.user(
"Which animals has the station recorded at the oak grove?"
)],
tools=[sightings_tool],
)
router = LMRouter()
response = router.complete(request)
print(response.finish_reason) # "tool_call"
for call in response.tool_calls:
print(call.name, call.input)search_sightings({"query":"oak grove"})Instead of text, the model answered with a tool call: which tool, and with
what input. Its finish reason is tool_call. Nothing has run yet: the call is
a request to your program, and it’s up to your program to act on it.
Run it and answer
Section titled “Run it and answer”Run each call, and send the results back after the model’s own message. Each result carries the id of the call it answers:
results = {
call.id: json.dumps(search_sightings(**call.input))
for call in response.tool_calls
}
followup = Request(
model=request.model,
system=request.system,
messages=[
*request.messages,
response.message,
Message.tool(results),
],
tools=request.tools,
)
print(router.complete(followup).text)[{"date": "2026-09-18", "place": "oak grove", "species": "wood mouse", "count": 4}, {"date": "2026-09-19", "place": "oak grove", "species": "roe deer", "count": 2}, {"date": "2026-09-21", "place": "oak grove", "species": "wild boar", "count": 3}]The station has recorded wood mice, roe deer, and wild boar at the oak grove. The sightings included four wood mice, two roe deer, and three wild boar.
The model answered from your records. Your program chose to run the search without asking anyone, because it only reads. A tool that writes, sends, or spends is different: check the call before running it, and ask a person when it matters. A model can ask for anything; your program decides.
The loop
Section titled “The loop”A model may ask for a tool more than once (search the grove, then the stream) or for several at once. So keep going until it answers with text, and stop after a few rounds:
import json
from lm15 import FunctionTool, LMRouter, Message, Request
SIGHTINGS = [
{"date": "2026-09-18", "place": "oak grove",
"species": "wood mouse", "count": 4},
{"date": "2026-09-19", "place": "oak grove",
"species": "roe deer", "count": 2},
{"date": "2026-09-20", "place": "stream",
"species": "red fox", "count": 1},
{"date": "2026-09-21", "place": "oak grove",
"species": "wild boar", "count": 3},
]
def search_sightings(query):
query = query.lower()
return [s for s in SIGHTINGS
if query in (s["species"], s["place"], s["date"])]
sightings_tool = FunctionTool(
name="search_sightings",
description=(
"Find the station's recorded sightings by species, place or "
"date."
),
parameters={
"type": "object",
"properties": {
"query": {"type": "string", "description": (
"One species, place or date, such as 'oak grove' or "
"'2026-09-18'."
)},
},
"required": ["query"],
},
)
router = LMRouter()
messages = [Message.user(
"Which animals has the station recorded at the oak grove?"
)]
for turn in range(5):
response = router.complete(Request(
model="provider:model",
system=(
"You are the field assistant for a wildlife research "
"station. Answer in two sentences."
),
messages=messages,
tools=[sightings_tool],
))
messages.append(response.message)
if response.finish_reason != "tool_call":
print(response.text)
break
results = {
call.id: json.dumps(search_sightings(**call.input))
for call in response.tool_calls
}
messages.append(Message.tool(results))
else:
raise RuntimeError("still calling tools after 5 turns")Each round sends the whole conversation: your question, every message the model wrote, every set of results. When the model asks for several tools at once, answer them all in one tool message, as the loop does. The limit of 5 rounds stops a confused model from looping, and spending, forever.
Describe the inputs
Section titled “Describe the inputs”What happens without the input’s description? Here is the same tool with it removed:
sightings_tool = FunctionTool(
name="search_sightings",
description=(
"Find the station's recorded sightings by species, place or "
"date."
),
parameters={
"type": "object",
"properties": {"query": {"type": "string"}},
"required": ["query"],
},
)search_sightings({"query":"place: oak grove"})[]
The station has no recorded animal sightings at the oak grove. The database search returned no entries for that location.
The model searched for {vague[0].calls[0].input.query}. Our search expects
just oak grove, so it found nothing, and the model reported that the station
has no sightings there. This is a subtle but important problem: every step did
what it was asked, so nothing reported an error, but the answer is wrong.
The model knows only what your description tells it. Describe each input the way you would for a new colleague, with an example. And when your function can’t make sense of an input, say so in the result rather than return nothing: the model can then try again.
Try it yourself
Section titled “Try it yourself”Predict before you run each one.
- Ask “What did the station see on 2026-09-20?” What will the model search for, and what will it answer?
- Ask “Has the station ever recorded a badger?” What will your function return, and what should the model say?
- Remove the limit on rounds. What could go wrong?
What to expect
- It searches for the date,
2026-09-20, finds the red fox, and says so. - An empty list. The model should say there is no record of a badger, which is true here; in the vague example above, the same empty answer led to a wrong one.
- A model that keeps asking for tools would keep your program calling it, and paying for each call, with no end.
The same loop works for any function: a database query, a calculation, a sensor reading. Use provider tools covers tools the provider runs for you, such as web search, and the Overview shows where tools sit among the other parts of a request.