Noolog Docs

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

  1. 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.
  2. The model answers with finish_reason: tool_calls naming it.
  3. We reply with a role: "tool" message whose content is the call's arguments, unchanged. Not a result we computed — the arguments themselves.
  4. 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 own arguments, under usage, 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.