Provider-executed tools
Some backends offer tools they execute themselves — search being the common one. The model asks for the tool, and the backend performs it; the caller never implements anything. That is a different contract from the tools we register, and mixing the two breaks the loop.
Declaring one
- name: "Example"
provider_executed_tools: ["$some_provider_tool"]
Names are whatever that backend expects, verbatim. The list is empty for every agent whose backend offers none, which is most of them.
The name also decides the shape sent on the wire, because backends disagree about it:
| Name | Sent as | Used by |
|---|---|---|
$web_search |
{"type": "builtin_function", "function": {"name": "$web_search"}} |
a backend that wraps its own tools in a function envelope |
web_search |
{"type": "web_search"} |
a router that takes the tool as a type on its own |
Sending the wrong shape is rejected before the model sees it, so a name that does not match its backend fails the whole call rather than degrading.
What happens on a call
- The tool is declared alongside ours in
tools, but with"type": "builtin_function"rather than"function"— that is what tells the backend it owns execution. - The model answers with
finish_reason: tool_callsnaming it. - We reply with a
role: "tool"message whose content is the call'sarguments, unchanged. Not a result we computed — the arguments themselves. - The backend runs the tool and continues the completion.
The ReAct loop recognises these names and echoes rather than dispatching. A provider tool that reached local dispatch would fail as an unknown tool, since we have no implementation for it and are not supposed to.
What it costs
Two charges the per-token estimate does not model:
- A fee per call, on backends that charge one.
- The result is billed as prompt tokens on the FOLLOWING request. A single
search observed in testing returned 8,245 tokens of results, which landed in
the next call's
prompt_tokens. The count is reported inside the tool call's ownarguments, underusage, so it can be read before deciding whether to continue.
In a deliberation this multiplies: each agent that searches adds its results to its own next prompt, every round.
Availability is per endpoint, not per model
The same model reached through a different route generally cannot run these — the tool belongs to the backend, not the weights. An agent that needs one must point at the endpoint that offers it, which may constrain where that agent's traffic goes.
When a backend refuses a mixed tool array
Some backends accept their search tool only when it is the only tool in the request. Vertex answers a mixed array with:
Multiple tools are supported only when they are all search tools.
A deliberating agent always sends function tools — the propose tool, the scratchpad, any user tools — so declaring a provider search tool beside them makes every task fail with a 400, not just the search.
Set delegated_search instead of provider_executed_tools for such a seat:
delegated_search: "web_search" # the backend's own name for the tool
The agent is then given an ordinary function tool named nsed_delegated_search.
The name is namespaced on purpose: a backend whose own search tool shares a
spelling gets translated onto it by the gateway, and a room may name its
user_tools anything at all. Calling it issues a second completion that declares the provider's search tool alone —
the shape those backends do accept — and returns what came back. Costs one
extra round trip per search.
Setting both is refused, at config load and again when the seat starts:
agent "COREPUNK23" sets both `delegated_search` ("web_search") and
`provider_executed_tools` (["web_search"]); they are alternatives — ...
The pair is what produces Duplicate function declaration found from the
backend: provider_executed_tools are appended to the same tools array the
function tools go in, so the name is declared twice. It is refused rather than
deduplicated because the two entries mean different things — one runs the
provider's search, the other our nested call — and dropping the wrong one
leaves an agent that starts, offers the name, and calls something else behind
it. Clear provider_executed_tools to keep the delegated form.
Which of the two a model needs is measured against the endpoint, not inferred from the vendor: of the models on one EU router, some accept the mix, one accepts search only when alone, and others reject the search tool in any shape and should carry neither field.
How it shows up in the logs
A seat misconfigured this way produces nothing — every task fails identically,
which reads like a dead model rather than a config error. The refusal is now
recognised and logged with the seat, the model, and the declared
provider_executed_tools:
backend refuses our tools declared beside a provider-executed one — every tool
call from this seat will fail. Use the nested search-only call instead of
declaring the tool alongside.
Parse failures name the model too, for the same reason: a seat that never produces a usable proposal is a property of the model behind it, and a log line that omits which one leaves the roster to be bisected by hand.