Built-in tools
Your node can run a small set of built-in tools on behalf of the models it serves: web search, web read, and — when you wire an image backend — image generation and editing. They're in-loop tools: when a model calls one mid-turn, your node executes it locally, feeds the result back to the model, and continues the same turn. Every round of a turn settles into one receipt.
Your node advertises which tools it offers on /v1/zs/details, by type only — the
names, descriptions, and parameters are identical fleet-wide, so clients fill them in. A
model only gets tools if it declares tool support and zs.builtin_tools.enabled is
on; a relay-only node runs none.
Built-in tools are free by default. To charge users per search or read — and to charge for a frontier model's own server-side tools — see Per-call tool pricing.
The tools
| Tool | What it does | Runs against |
|---|---|---|
| Web search | Query the web, return numbered results the model cites. | DuckDuckGo HTML — no API key. |
| Web read | Fetch a URL and hand the model clean markdown. | The page itself, fetched by your node — no third party, no API key. |
| Get time | Report the node's clock, optionally in a given IANA timezone. | The node itself — no network. |
| Image generation | Create an image from a prompt, inline in the reply. | Your image_llm (ComfyUI) backend. |
| Image editing | Modify an image — the model's or the conversation's — from a prompt. | Your image_llm (ComfyUI) backend. |
A web image search tool exists in the code but is force-disabled — its DuckDuckGo image backend is broken, so the node won't advertise or run it regardless of config until a working provider is wired up.
Where the requests come from
Web search and web read make outbound requests from your node's network — a client's "search the web" becomes an HTTP request originating from your host. This is the same trust boundary as inference: you already decrypt and read the prompt to answer it. As everywhere else on the node, tool activity is never logged as content — prompts, fetched page bodies, tool arguments, and image bytes stay out of logs, traces, and metrics; only counts, durations, and IDs are recorded. Errors surfaced back to the model are sanitized so upstream URLs don't leak. For the user-facing framing of this boundary, see Privacy & security; for what you bill, see The payment flow.
Web read fetches the page itself. Your node makes the request, converts the HTML to markdown in-process, and hands the result to the model — no intermediary service is told what your users asked to read. (The site itself and your DNS resolver still see the request, of course; what's gone is the middleman.) Two consequences worth knowing:
- The site sees your node's IP, not some intermediary's. A read is your node visiting
a page. Your node identifies itself as
zs-node/web_read, so a site that wants to exclude it can. - Your node will not fetch its own network. Web read refuses any URL that resolves to
a private, loopback, link-local, or carrier-NAT address — checked against the address
actually being dialled, so a public hostname pointing inward (or rebinding mid-request)
is refused too. This is what stops a crafted URL turning a read into a probe of your LAN
or your cloud provider's metadata endpoint. It is on by default and you should leave it
on;
web_read.allow_private_targetsexists for localnet development only.
Web read is also gated on provenance: the model may only read a URL the user supplied or one a prior search returned — never one it invented from a page it just read. That's what stops a hostile page from talking the model into encoding your user's conversation into a URL and fetching it.
Web search still uses a third party. Web read runs entirely on your node, but search
queries go to DuckDuckGo, which necessarily sees the query. If that matters for your
deployment, web_search.enabled: false turns it off and leaves web read working.
Configuration
The web tools and the loop are configured under zs.builtin_tools — see
Configuration → zs.builtin_tools for the full knob
table (per-tool enabled, max_results / max_bytes / max_download, timeout,
safe_search, and the loop guards).
Two ceilings on a read, not one. web_read.max_bytes (64 KiB) caps the markdown the
model receives — that's the context-window guard. web_read.max_download (4 MiB) caps the
raw page your node pulls off the wire before converting it: a page can be far larger than
the markdown it distills to. A page over the download ceiling is refused, not
truncated, because half an HTML document converts to nonsense. Setting max_bytes above
max_download is rejected at startup, since no page could then fill the markdown budget.
max_download is not a memory budget. Converting a page costs roughly 20–70× the
page's own size in peak memory — the readability pass builds and scores a document tree of
the whole page, and the multiple depends on how the page is structured. A 4 MiB page can
mean a few hundred MB while it converts. So if you raise max_download, raise your RAM
expectations by a large multiple — not by the same number of bytes.
What bounds the total is web_read.max_concurrent (default 4): peak memory is
roughly max_download × 20–70 × max_concurrent. That's the knob to reach for when the
node has a memory limit, because it costs latency — reads queue — rather than
capability. Lowering max_download to save memory would instead make large pages
permanently unreadable.
This is the one path that makes the node's memory use spiky rather than flat, so it drives the number if you run the node under a container or cgroup limit — see Sizing the node process.
Content filtering (web_search.safe_search). Web search runs with SafeSearch set to
moderate by default; strict and off are the other choices. This is a content
policy decision — off returns unfiltered results — so treat it like one: set it
deliberately, and note that an unrecognized value is a hard startup error, not a silent
fallback. The same knob shape exists on image_search, but that tool is force-disabled
(above), so it has no effect today.
Two loop guards are worth understanding:
max_iterations— the absolute ceiling on chat→tool→chat rounds in a single turn. It's advertised to clients asmax_tool_iterations.max_stalled_iterations— an internal safety heuristic that stops a model spinning on the same repeated call. It isn't advertised.
Offering tools raises what callers reserve. A tool result is re-fed to the model, so
a tool-carrying request needs more input budget than its prompt alone. Clients size that
from two values you advertise: max_iterations above, and
zs.reserve.tool_headroom_per_iteration. Keep the second
in step with what your tools actually return — a single web_read at the default 64 KiB
max_bytes is roughly 16,000 tokens, well above the 4,000-per-iteration default — or
loops end early at the budget boundary. Tools the client runs itself (a coding agent's
own shell and edit tools) add no headroom, because they never grow the context your node
measures.
Image tools
Image generation and editing inside a text turn are enabled per model, not
globally. A chat model opts in under zs.models[<id>].image_tools.{generation,edit} with
a positive per-image rate, and the images are produced by the same image_llm (ComfyUI)
backend that powers the dedicated /v1/images/* routes. Set up the backend and price
these in Serving models → Image generation and
Serving models → In-loop image tools.
Observability
When built-in tools are enabled, the node exposes tool and loop metrics on its private
listener — zs_builtin_tool_invocations_total, zs_builtin_tool_errors_total,
zs_builtin_tool_duration_seconds, and zs_tool_loop_finished_total{outcome} among
them. See Monitoring & metrics.