Skip to main content

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.

info

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

ToolWhat it doesRuns against
Web searchQuery the web, return numbered results the model cites.DuckDuckGo HTML — no API key.
Web readFetch a URL and hand the model clean markdown.The page itself, fetched by your node — no third party, no API key.
Get timeReport the node's clock, optionally in a given IANA timezone.The node itself — no network.
Image generationCreate an image from a prompt, inline in the reply.Your image_llm (ComfyUI) backend.
Image editingModify an image — the model's or the conversation's — from a prompt.Your image_llm (ComfyUI) backend.
info

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_targets exists 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.

info

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.

warning

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 as max_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.