Skip to main content

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, and zs_image_search cost you real compute and bandwidth to run — zs_web_read in 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.

info

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 toolNode zs_ built-in
Who executes itThe upstream vendor, inside one completionYour node
Examplesweb_search, x_search, code_interpreter, file_searchzs_web_search, zs_web_read, zs_image_search
Who bills youThe vendor (per call)Nobody directly — it's your compute
Default when unpricedStripped from the requestFree (offered at no charge)
Cap it stays underThe vendor's own cap knob + your call_capThe 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.
warning

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 typeWhat it isWhy it's free
mcpA remote MCP connector the vendor calls out toToken cost only (OpenAI, Anthropic)
customOpenAI freeform toolsYour caller executes it
local_shellShell commands run on the caller's machineYour caller executes it
apply_patchStructured file edits the caller appliesYour caller executes it
computer_useLets a computer-use model drive a screen: it emits click / type / scroll actions and reads back screenshotsYour 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 machineYour caller executes it; no container is allocated
bash, text_editor, str_replace_editorAnthropic'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.

info

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.

info

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.

warning

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_pricing block 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. auto infers it from llm.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 no tool_pricing block at all there is no cap — a never-billed hosted tool like mcp passes through unbounded, which is deliberate: capping it by default would truncate a legitimate remote-MCP session, and the payer pays up to max_price either way. Set vendor.call_cap if 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/completions at 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 exactly 0 is the exception and survives — there's no per-call overage to protect you from. (For search_parameters / web_search_options specifically, it's the web_search rate 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 than call_cap calls 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 affect zs_ 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.

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.

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_pricing cleanly either. Gemini's grounding tool uses a non-OpenAI shape ({"google_search": {}}, with no type field), so the node doesn't classify it as a priced tool, and on the vertexai provider non-function tools are dropped in translation anyway. Fold grounding into the model's token rates, or run log_raw_usage to see exactly how your Gemini upstream exposes and counts it before trying a tool_pricing entry.
  • 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_cap and tokens_per_call sized 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_cap is enforced upstream only on the Responses API (/v1/responses) — that's where max_tool_calls / max_turns live. On /v1/chat/completions there'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. Set vendor.chat_server_tools: allow only 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 exactly 0 survives 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 the web_search rate 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_parameters and OpenAI's web_search_options. One caveat it can't fix: a dedicated search model (OpenAI's gpt-4o-search-preview, Perplexity sonar, ...) searches on every request as part of the model itself, not as a strippable tool — so removing web_search_options only 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:

  1. Price the tool — tools: { web_search: { usd_per_1k_calls: 6.00 } }.
  2. Pass it through free if your upstream doesn't charge — usd_per_1k_calls: 0, or default_usd_per_1k_calls: 0 for all of them.
  3. 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