Per-call tool pricing
Tokens aren't the only thing a request can cost you. Two kinds of tool call run up a bill beyond the prompt and completion:
- Vendor server-side tools. Frontier models like xAI Grok, OpenAI GPT, and Kimi can run their own hosted tools inside a single completion — web search, X search, code execution, file search. The vendor executes them and bills you, the operator, per call (xAI charges around $5 per 1,000 web searches, for example), on top of the tokens.
- Your node's own
zs_built-ins.zs_web_search,zs_web_read, andzs_image_searchcost you real compute and bandwidth to run —zs_web_readin particular can spike memory converting a large page.
Left alone, both of these are a hole in your margin: a client can trigger them and you eat the cost. Per-call tool pricing closes that hole. You set a price per tool call; the node reserves for it, caps it, and bills the payer — so a harness that wants Grok's web search keeps working and you get paid for it.
This is a pricing layer on top of the token rates in
Serving models & pricing. Your zs_ built-in tools
themselves are set up in Built-in tools; this page is only
about charging for tool calls.
Two kinds of tool call
The two behave differently, because the difference that matters is who runs the tool:
| Vendor server-side tool | Node zs_ built-in | |
|---|---|---|
| Who executes it | The upstream vendor, inside one completion | Your node |
| Examples | web_search, x_search, code_interpreter, file_search | zs_web_search, zs_web_read, zs_image_search |
| Who bills you | The vendor (per call) | Nobody directly — it's your compute |
| Default when unpriced | Stripped from the request | Free (offered at no charge) |
| Cap it stays under | The vendor's own cap knob + your call_cap | The tool-loop iteration limit |
There is a third group that costs nothing per call and is therefore left alone — see Tools that are never billed.
zs_image_generation / zs_image_edit are not priced here — they already
bill per produced image (see Built-in tools → Image tools).
Default-deny: you're never billed for a tool you didn't price
The moment you enable nothing, the safe thing happens:
- An unpriced vendor server-side tool in a request is stripped before the node forwards it upstream — so the vendor can't run it and can't bill you.
- Your
zs_tools stay free unless you name a price.
This is a behavior change. If today your node forwards a client's
web_search straight through to the
upstream, it will start stripping it after you upgrade — until you either
price the tool (below) or explicitly opt out with vendor.enabled: false (which
passes them through unpriced, at your own cost). If you rely on server-side tools
reaching the upstream, price them.
It does not apply to the never-billed types below (mcp, custom,
local_shell, apply_patch, computer_use) — those keep reaching your upstream
while they stay unpriced. Giving mcp an explicit rate promotes it to the
billable tier, which also subjects it to chat_server_tools on chat.
Tools that are never billed
Not every tool with an exotic type costs you money. These carry no per-call
fee at any vendor — they cost tokens, like any other part of the prompt — so
the node passes them through untouched and never meters them:
| Tool type | What it is | Why it's free |
|---|---|---|
mcp | A remote MCP connector the vendor calls out to | Token cost only (OpenAI, Anthropic) |
custom | OpenAI freeform tools | Your caller executes it |
local_shell | Shell commands run on the caller's machine | Your caller executes it |
apply_patch | Structured file edits the caller applies | Your caller executes it |
computer_use | Lets a computer-use model drive a screen: it emits click / type / scroll actions and reads back screenshots | Your caller executes it — and the model refuses to run unless the tool is declared |
shell with environment.type: "local" | The successor to the deprecated local_shell — the command runs on the caller's machine | Your caller executes it; no container is allocated |
bash, text_editor, str_replace_editor | Anthropic's caller-executed tools (the _YYYYMMDD version suffix is normalized away) | Your caller executes them |
Stripping these would break a caller's harness and save you nothing, which is why
they're exempt from default-deny. Everything else with an unrecognized type is
still treated as billable and stripped unless priced.
The shell row is the one to read carefully: shell is a different tool from
local_shell, and in its container_auto / container_reference modes OpenAI
allocates a managed container and bills you per session — so those stay
default-denied. Only environment.type: "local" is exempt. That distinction
matters because OpenAI has deprecated local_shell in favour of
shell + environment.type: "local", so a Codex harness that has migrated would
otherwise have its shell tool silently stripped.
This is what keeps a Codex or opencode session working through the proxy when it leans on MCP servers or freeform tools — no configuration needed.
If your upstream genuinely does charge per call for a vendor-hosted one —
in practice that means mcp — name it in tools with a rate; an explicit rate
wins over the never-billed default. The blanket default_usd_per_1k_calls
catch-all deliberately does not reach them, so a catch-all meant to cover
Grok's search can't start charging your users for an MCP connector that cost you
nothing.
The caller-executed ones can't be priced at all, and the node rejects the config at startup if you try. Your caller runs those tools, so no vendor ever reports a call for the node to meter — a rate would silently inflate every reserve for the model against a charge that can never land.
A surviving hosted tool still counts against call_cap, even at a rate of
zero — every hosted call re-feeds its results into the context, and that token
growth needs a bound regardless of what the call itself costs. Caller-executed
tools don't count against it.
Pricing tools (zs.tool_pricing)
One block prices both kinds of tool. Rates are USD per 1,000 calls — the unit the vendors quote — so you can copy a number off a price card and add your markup.
zs:
tool_pricing:
tools: # named per-call rates
web_search: { usd_per_1k_calls: 6.00 } # your vendor cost + markup
x_search: { usd_per_1k_calls: 6.00 }
code_interpreter:{ usd_per_1k_calls: 6.00 }
zs_web_search: { usd_per_1k_calls: 4.00 } # also prices YOUR own tools
zs_web_read: { usd_per_1k_calls: 2.00 }
default_usd_per_1k_calls: 6.00 # catch-all for any UNNAMED vendor tool
vendor:
dialect: auto # auto | openai | xai | moonshot
call_cap: 16 # per-request cap on vendor tool calls
tokens_per_call: 3000 # est. context inflation per vendor call
chat_server_tools: strip # strip (default) | allow — see below
Named rates and the vendor catch-all
Every entry in tools is keyed by the tool's canonical name. A key beginning
zs_ prices one of your built-ins; any other key prices a vendor tool. Each entry
must set usd_per_1k_calls — an explicit 0 means free, but a missing rate is
a config error (free has to be deliberate).
default_usd_per_1k_calls is the catch-all: any vendor tool a client
sends that you didn't name is billed at this blanket rate instead of being
stripped. It's what lets a harness use a vendor tool you didn't think to
enumerate and still be billed for it. The catch-all never applies to your
zs_ tools — so setting it to cover Grok's search can't accidentally start
charging users for your own search. An unnamed zs_ tool is simply free.
default_usd_per_1k_calls: 0 — pass everything through, bill nothing
Setting the catch-all to zero is how you say "my upstream doesn't charge me per call." It is not the same as leaving the block out: a zero catch-all makes every vendor server-side tool survive and bill nothing, instead of being stripped.
zs:
tool_pricing:
default_usd_per_1k_calls: 0 # nothing here bills per call — pass it all through
vendor:
call_cap: 64 # a catch-all still caps calls; raise it if that bites
Reach for this when you serve an open-weight or self-hosted upstream (vLLM,
SGLang, llama.cpp, LM Studio) that has no hosted tools to bill for at all, or a
provider that folds tool cost into its token rates. The same works per tool —
web_search: { usd_per_1k_calls: 0 } offers exactly that one for free.
A zero rate removes the per-call fee, not the token cost. Server-side tools still re-feed their results into the context, and you're billed for those input tokens at your normal rate. If your upstream is genuinely free of per-call fees that's fine — but size your token rates knowing the context can grow.
The rules in one line each:
- Never-billed tool (
mcp,custom,local_shell,apply_patch,computer_use) → passed through, never billed, unless you name a rate. - Vendor tool → named rate, else the catch-all, else stripped.
zs_tool → named rate, else free.- No
tool_pricingblock at all → billable vendor tools stripped,zs_tools free (the token-only path is byte-identical to before).
Vendor knobs
Under vendor:
dialect— how the node reads your upstream's tool conventions.autoinfers it fromllm.openai.base_url(api.x.ai→ xai,api.openai.com→ openai, moonshot/kimi → moonshot); an unrecognized host falls back to a generic reader. Set it explicitly if auto-detection guesses wrong.call_cap— the most vendor server-side calls you'll reserve (and pay) for in one request. On the Responses API the node injects this as the vendor's own cap knob (max_tool_calls/max_turns), so the model can't exceed it. Defaults to 16 once you've priced anything (a named rate or a catch-all). With notool_pricingblock at all there is no cap — a never-billed hosted tool likemcppasses through unbounded, which is deliberate: capping it by default would truncate a legitimate remote-MCP session, and the payer pays up tomax_priceeither way. Setvendor.call_capif you want the bound anyway.tokens_per_call— your estimate of how many extra input tokens each vendor call adds. Server-side tools re-feed their results into the context, so a search-heavy request can bill several times its original prompt in input tokens; this term reserves for that.chat_server_tools— what to do with a priced vendor tool on the chat endpoint, where its call count can't be capped upstream (see Chat has no cap knob below):strip(default) — don't offer vendor server-side tools on/v1/chat/completionsat all; they're removed even when priced. You can't be forced to eat uncapped vendor calls. Offer them on the Responses API instead. A tool priced at exactly0is the exception and survives — there's no per-call overage to protect you from. (Forsearch_parameters/web_search_optionsspecifically, it's theweb_searchrate that decides, since those are fields rather than tool entries.)allow— keep priced vendor tools on chat, uncapped, accepting that a request may run more thancall_capcalls and you eat the overage beyond the reserve.
enabled: false— opt out of vendor handling entirely: pass server-side tools through unpriced, at your own cost. For an operator with a separate billing arrangement with the upstream. It doesn't affectzs_pricing.
Per-model overrides
zs.tool_pricing is the fleet-wide default. Override it for one model under
zs.models[<id>].tool_pricing — handy when only some models point at a vendor
that charges for tools:
zs:
models:
grok-4.5:
tool_pricing:
vendor: { dialect: xai, call_cap: 12 }
Per-model entries and knobs are merged over the default, entry by entry.
Examples by vendor
The rates in the comments are the vendor's list price — your cost — at the
time of writing; set usd_per_1k_calls to that plus your margin, and always check
the current price card. These are for OpenAI-compatible upstreams; Gemini and
Anthropic are covered under the caveats below.
- xAI Grok
- OpenAI GPT
- Kimi (Moonshot)
Grok's server-side tools are Responses-API tools, so call_cap is enforced.
Point llm.openai.base_url at https://api.x.ai/v1.
zs:
tool_pricing:
tools:
web_search: { usd_per_1k_calls: 6.00 } # xAI $5/1k + markup
x_search: { usd_per_1k_calls: 6.00 } # xAI $5/1k
code_interpreter: { usd_per_1k_calls: 6.00 } # xAI $5/1k (code execution)
file_search: { usd_per_1k_calls: 3.00 } # xAI $2.50/1k (Collections Search)
attachment_search: { usd_per_1k_calls: 12.00 } # xAI $10/1k (File Attachments)
vendor:
dialect: xai
call_cap: 16
tokens_per_call: 3000
Two naming traps here. file_search is xAI's alias for Collections Search at $2.50/1k — the $10/1k tool is the separate attachment_search, so don't carry an OpenAI-shaped assumption across. And xAI's primary names are code_execution, collections_search and attachment_search; the aliases above are accepted on the REST API but reportedly not on the gRPC SDK. document_search has no published price — leave it unpriced (stripped) rather than guess.
GPT's built-in tools are Responses-API tools (capped by call_cap). Point
base_url at https://api.openai.com/v1. Web search may also bill search-content input
tokens, which ride your normal token rate — size tokens_per_call to cover it.
Note the flat "8k tokens per call" figure applies only to gpt-4o-mini and
gpt-4.1-mini; other models bill content tokens as actually retrieved, and
web_search_preview on non-reasoning models bills none. Measure yours before
committing to a number.
zs:
tool_pricing:
tools:
web_search: { usd_per_1k_calls: 12.00 } # OpenAI $10/1k + markup (but see below)
file_search: { usd_per_1k_calls: 3.00 } # OpenAI $2.50/1k queries
vendor:
dialect: openai
call_cap: 12
tokens_per_call: 9000 # measure yours; see the note below
One tier to watch: web_search_preview on a non-reasoning model is
$25/1k, not $10/1k. It normalizes to the same web_search billing key, so an
operator pricing at 12.00 is underwater on that path — price for the highest tier
you actually serve.
code_interpreter is billed per 20-minute container session (memory-tiered:
$0.03 / $0.12 / $0.48 / $1.92 for 1 / 4 / 16 / 64 GB, minute-billed with a
5-minute minimum), not per call, so it doesn't map cleanly to a per-call rate —
leave it unpriced (stripped) unless you have a reason to offer it. The hosted
shell tool shares that same container pool and price line, so the same applies:
don't price it per call. (shell with environment.type: "local" runs on the
caller's machine, costs you nothing, and is passed through automatically.) On /v1/chat/completions, GPT web search is the
dedicated *-search-preview models via web_search_options (stripped by default
under strict chat) — those search inherently, so price the model instead.
Kimi's published price is $0.005 per successful call = $5/1k (CN platform ¥0.03/call). Note Moonshot currently flags its web-search docs as outdated and advises against relying on the feature near-term — check before you enable it.
Kimi's $web_search runs on chat completions, so it's stripped by default —
set chat_server_tools: allow to offer it (uncapped; size call_cap for a
realistic worst case). Key the rate as web_search — the $ is normalized away.
zs:
tool_pricing:
tools:
web_search: { usd_per_1k_calls: 6.00 } # Kimi $5/1k ($0.005/successful call)
vendor:
dialect: moonshot
call_cap: 8
chat_server_tools: allow
Gemini and Anthropic don't fit yet
- Google Gemini grounding (Google Search) is around $14/1k on the Gemini
3.x family (after a monthly free tier) and $35/1k on 2.5 — but note the
unit differs: 3.x bills per search query the model runs, while 2.5 and
older bill per grounded prompt however many queries fan out. So $14/1k is
not a discount; an agentic prompt averaging 3+ searches costs more on 3.x. It
doesn't fit
tool_pricingcleanly either. Gemini's grounding tool uses a non-OpenAI shape ({"google_search": {}}, with notypefield), so the node doesn't classify it as a priced tool, and on thevertexaiprovider non-function tools are dropped in translation anyway. Fold grounding into the model's token rates, or runlog_raw_usageto see exactly how your Gemini upstream exposes and counts it before trying atool_pricingentry. - Anthropic Claude server-side tools (
web_search,code_execution) run only on the native Messages API, which the node doesn't serve yet — so there's nothing to price.
How it's billed
A tool fee rides on top of the token charge, on the same receipt, and is
capped at the ticket's escrowed max_price — the operator is never paid more
than was reserved. The node counts the calls that actually ran (a vendor's usage
report for server-side tools; the node's own count for zs_ tools) and bills
Σ calls × your rate. Only successful calls are billed. A request that fails
before completing bills $0, even if a tool already ran.
What it does to the reserve
So the fees are actually covered, the node folds a worst-case tool-fee headroom
into max_price for any model that prices tools:
max_tool_iterations × (priciest zs_ rate)
+ call_cap × (priciest vendor rate)
+ the input-token cost of call_cap × tokens_per_call
Two things worth knowing:
-
It's per-model, not per-request. Every reserve against a tool-pricing model locks this headroom up front — even a plain chat turn that uses no tools — and the unused part is refunded at settlement. So keep
call_capandtokens_per_callsized to a realistic worst case, not a paranoid one, or plain requests will lock more of a user's USDC than they need to. -
Chat has no cap knob.
call_capis enforced upstream only on the Responses API (/v1/responses) — that's wheremax_tool_calls/max_turnslive. On/v1/chat/completionsthere's no way to cap the count, so by default the node strips vendor server-side tools from chat requests entirely (vendor.chat_server_tools: strip) rather than run them uncapped. This also closes the door on a client using the chat endpoint to force calls you can't bound — unpriced vendor tools are already stripped on both endpoints, and now priced ones don't run uncapped on chat either. Setvendor.chat_server_tools: allowonly if you want to offer them on chat and accept eating any overage beyond the reserve. The primary, capped surface is the Responses API. A tool priced at exactly0survives strict chat: the rule exists to stop an uncapped per-call overage, and at a rate of zero there isn't one. The two top-level fields follow theweb_searchrate specifically — pricing some other tool at zero won't retain them.Strict chat also removes the top-level chat search fields that aren't tool entries — xAI's legacy
search_parametersand OpenAI'sweb_search_options. One caveat it can't fix: a dedicated search model (OpenAI'sgpt-4o-search-preview, Perplexitysonar, ...) searches on every request as part of the model itself, not as a strippable tool — so removingweb_search_optionsonly stops a client dialing the search up, not the model's baseline search. Price those models to cover their search cost, or don't serve them.
Why did my tool disappear?
When a user reports that a tool they sent didn't run, check the node log for:
tool pricing stripped a vendor server-side tool from a request
The line names the canonical tool and the model, and is emitted once per tool name for the life of the process — so it tells you what to price without flooding a busy node. It logs tool names only, never arguments or request content.
Three ways to make it stop, in rough order of preference:
- Price the tool —
tools: { web_search: { usd_per_1k_calls: 6.00 } }. - Pass it through free if your upstream doesn't charge —
usd_per_1k_calls: 0, ordefault_usd_per_1k_calls: 0for all of them. - Opt out of vendor handling entirely —
vendor.enabled: false(unpriced, at your own cost).
If the tool is on chat rather than the Responses API, also check
chat_server_tools — strict chat strips positively-priced vendor tools there.
Confirm your upstream's counts before going live
The node bills vendor tools from the call counts your upstream reports in its
usage object (or, for OpenAI-style responses, from the tool-call items in the
response). Providers report these differently. Turn on
llm.openai.log_raw_usage: true for a capture run and watch the
raw upstream usage log line to confirm the shape before you rely on it — the
log is counts-only, never prompt or response content.
See Examples by vendor above for the current per-vendor list prices and the tool names to key your rates by.
See also
- Serving models & pricing — the token rates a tool fee sits on top of.
- Built-in tools — enabling and tuning your
zs_tools. - The payment flow — how a reserve, receipt, and settlement fit together.