Llama 3.1's Built-In Tools: Brave Search, Wolfram and Code Interpreter
9 min read · updated August 11, 2026
Llama 3.1 was post-trained to call three tools by name: brave_search, wolfram_alpha and a Python code interpreter. There is no parameter that turns them on. They are enabled by two lines of text in the system prompt, and Meta ships no service behind any of them.
A convention, not an API
Every provider-hosted tool API — a tools array, a tool_choice, a structured tool-call object in the response — is a layer somebody built over a model that only ever reads and writes text. With an open-weights model you can see the layer removed. Meta’s Llama 3.1 prompt format documentation defines the built-in tools purely as text: particular words in the system header, and a particular special token the model emits when it wants to call one.
That means three things follow immediately. The names are not configurable — the model was tuned on brave_search, so calling your search backend web_search and hoping for the same reliability gives up the tuning. The behaviour degrades gracefully rather than erroring, because there is nothing to error. And an OpenAI-compatible server sitting in front of the weights may or may not expose any of it, depending on whether its chat template includes the relevant lines.
The two lines that enable them
Both live in the system message, at the top of the raw prompt. The first switches on the code interpreter; the second lists the search and computation tools you want available:
<|begin_of_text|><|start_header_id|>system<|end_header_id|> Environment: ipython Tools: brave_search, wolfram_alpha Cutting Knowledge Date: December 2023 Today Date: 11 August 2026 You are a helpful assistant. Use the search tool for anything that happened after your knowledge cutoff.<|eot_id|>
Environment: ipython is the line that matters most and the one most often missing. It tells the model that a Python execution environment exists, and it is what licenses the model to emit the Python tag described below. Without it the model has no reason to believe code will be run, and it will tend to print code in a markdown fence for a human instead.
The Tools: line is a plain comma-separated list, and it takes only the built-in names. Your own functions do not go here — they go in the user turn as JSON, which is a different convention covered in Llama 3’s function calling format. Mixing the two in one prompt is legal and is how a real agent is usually built.
The Cutting Knowledge Date and Today Date lines are part of the documented header too. They are not magic — they are just facts the model reads — but they are what makes the model reach for search instead of guessing about recent events, which is the entire point of giving it search.
What the model emits
When the model decides to call a built-in tool it emits the special token <|python_tag|> and then a call written as Python-ish source, not as JSON:
<|python_tag|>brave_search.call(query="llama 4 release date")<|eom_id|>
Wolfram Alpha uses the same shape with its own argument name:
<|python_tag|>wolfram_alpha.call(query="integrate x^2 sin(x) dx")<|eom_id|>
And the code interpreter is the degenerate case — after the Python tag, the model simply writes the code it wants executed, with no .call() wrapper at all:
<|python_tag|>import pandas as pd
df = pd.read_csv("sales.csv")
print(df.groupby("region")["revenue"].sum())<|eom_id|>Two practical notes on that shape. The model can emit a short line of prose before the Python tag — reasoning about why it is searching — so a parser that assumes the tag is the first thing in the turn will drop text the user should see. And if the model wants two searches, it will generally take two turns rather than emitting two calls in one message, because the convention has no separator for a second call. Handle the one-call-per-message case and let the loop iterate.
Parsing this is string work. You are looking for the Python tag, then either a name.call(...) form for the two named tools or raw source for the interpreter. That is more fragile than parsing JSON, which is precisely why the later JSON function-calling convention exists for custom tools.
eom_id versus eot_id
The token that ends a tool call is <|eom_id|> — end-of-message — and not <|eot_id|>, end-of-turn. The distinction is the whole control flow of an agent loop:
<|eot_id|>— the turn is finished and it is the user’s move. Stop and hand back to the human.<|eom_id|>— the message is finished but the turn is not. The model is waiting for something: run the tool, append the result, and continue generating in the same assistant turn.
A serving stack that only treats <|eot_id|> as a stop token will run straight past <|eom_id|> and start hallucinating the tool’s output — a specific and very recognisable failure where the model invents plausible search results. Both tokens belong in the stop set. This is the same class of problem as eot_id being confused with end-of-text, and it is worth checking in whatever template your server loaded.
Whether to use them at all
The built-in convention and the JSON custom-function convention both work on the same checkpoint, and you have to choose. The honest case for each:
- Use the built-in names when what you want really is web search, symbolic computation or code execution, and you are serving raw Llama. The model was post-trained on these exact names, so it is more reliable at deciding when to reach for them than it is with an arbitrary function you describe in the prompt. That reliability is the entire benefit, and it does not transfer to a renamed tool.
- Use JSON function calling for anything else, and for anything that must survive a change of model. The Python-ish call syntax has no schema, no type information and no validation — you are parsing an expression with a regular expression and hoping the argument names are what you expect. A JSON object against a declared schema fails loudly instead.
- Use whatever your serving layer exposes if you are behind an OpenAI-compatible endpoint. A server that implements the
toolsparameter is already rendering one of these two conventions into the prompt for you, and fighting it by hand-writing the other into a system message produces two tool protocols in one prompt.
The portability point deserves weight. Every line in the prompt above is Llama-specific. Moving the same application to a different open-weights family means a different template, different special tokens and a different tool convention, with no error to tell you the old one stopped working — the model simply answers as though no tools exist.
You are the backend
Nothing in the checkpoint reaches the internet. brave_search is a name the model was trained to say; if you want results you sign up with Brave, call their API, and feed the output back in. Wolfram Alpha likewise. The code interpreter is a sandbox you have to build, and it is the one with real consequences — you are about to execute model-generated Python, so it belongs in a container with no network and no credentials, not in your application process.
The result then goes back to the model in the ipython role, which is the third piece of the convention and has its own page: multi-turn tool results in the chat template.