Skip to content

Group deferred tools into namespaces and keep the provider's search items - #63

Merged
rezaho merged 2 commits into
mainfrom
tool-search-namespaces
Sep 13, 2026
Merged

rezaho merged 2 commits into
mainfrom
tool-search-namespaces

Conversation

@rezaho

@rezaho rezaho commented Sep 12, 2026

Copy link
Copy Markdown
Owner

Depends on #62. This branch is written on top of prompt-cache-diagnostics, and the base is set to that branch so the diff shows only this change; it retargets to main on its own once #62 merges. Both changes edit format_request_payload and harmonize_response, in disjoint hunks.

What it does

The Responses legs serve a hosted tool search. The request carries a namespace container per family with its members deferred and the tool_search built-in beside them; the model searches, the provider injects the loaded definitions at the end of the context, and the model calls in the same reply. Three things stood between this adapter and that shape. A live probe of an Azure gpt-5.6 deployment on 2026-09-12 measured each of them.

The built-in was added only when defer_loading sat at a tool's top level. The flag never sits on a container — that is the vendor's own shape — so a namespace request went out without the built-in and came back HTTP 400, invalid_request_error, param tools.defer_loading: "Invalid Value: 'tools.defer_loading'. Deferred tools require tools.tool_search." A refusal, not a degradation. Deferral is now read wherever it sits.

The parser knew reasoning, message and function_call, so the search's own two items were dropped and with them the definitions the search had loaded. They now ride reasoning_details, the opaque provider-items channel the Anthropic and Google legs already use for the blocks their endpoints demand back verbatim, and the request builder replays them ahead of the row's message and calls — including for a reply that searches and then answers in text, which carries no tool call to hang them on. Without the replay the model searches the whole family again at its next need; with it the loaded definition rides the conversation body into the cached prefix and is read at a tenth of the price from the next call on. Measured: the replay cost 216 prompt tokens for a two-member namespace, against a second search of the whole family.

The function call came back stamped with the container it was loaded from, and that was dropped too. It now rides ToolCall.namespace and goes back on the replayed item, because the item that goes back is the provider's own. Absent when the provider named none, so a tool call on every other leg serializes exactly as it did.

Shape notes

The grouping travels as a per-tool namespace label rather than a container the caller builds. The three legs that must not see it then strip one key off a flat dict — the move they already make for defer_loading — instead of unwrapping a tool type they have no shape for. OpenRouter strips and warns, Google warns, and the Anthropic builders drop it by construction while keeping their existing deferred-loading mapping unchanged.

supports_hosted_tool_search(provider, model) is its own function with its own floor. Tool search is served from gpt-5.4 while the explicit prompt-cache markers beside it need 5.6, so one gate for both would either withhold the search from two generations that serve it or send cache fields to a model that answers them with a 400. It shares the name-reading regex with the cache gate and nothing else, and it carries the same Azure caveat, which bites harder here: a deployment label is operator-chosen, and a wrong one is a 400 on every call rather than a price.

Recorded debt

reasoning_details is the framework's opaque-provider-items channel by function on three adapters and by name on none of them. The name is on the public HarmonizedResponse, on the tracing events and attributes, on the agent memory, and in every row already written by a running deployment, so renaming it needs a dual-read window and buys no behaviour. The field's declared meaning is widened where it is declared; the rename is left recorded rather than paid here.

Verification

tests/models/test_hosted_tool_search.py, 39 tests, no network: the grouping and its order, the container's name and description, members keeping the flag while the container carries none, the caller's dicts unmutated, the built-in on inner and top-level deferral and never duplicated, the three legs stripping or warning, the parser keeping both items verbatim and reading the call's namespace, the replay order and the other legs' blocks left alone, the search-then-answer row, a two-request round trip whose second input is the first's items plus the reply's four with a byte-identical tools array, the predicate's own floor against the cache gate's, and byte identity everywhere nothing is labelled and nothing deferred.

tests/models/test_deferred_tool_loading.py passes unchanged and is not edited.

rezaho and others added 2 commits September 12, 2026 17:17
…tems

The Responses legs serve a hosted tool search: the request carries a namespace
container per family with its members deferred and the tool_search built-in
beside them, the model searches, and the provider injects the loaded definitions
at the end of the context. Three things stood between this adapter and that
shape, and a live probe of an Azure gpt-5.6 deployment measured each of them.

The built-in was added only when defer_loading sat at a tool's top level. The
flag never sits on a container, so a namespace request went out without the
built-in and came back HTTP 400, "Invalid Value: 'tools.defer_loading'. Deferred
tools require tools.tool_search." — a refusal, not a degradation. Deferral is now
read wherever it sits.

The parser knew reasoning, message and function_call, so the search's own two
items were dropped and with them the definitions the search had loaded. They ride
the opaque provider-items channel the Anthropic and Google legs already use for
the blocks their endpoints demand back verbatim, and the request builder replays
them ahead of the row's message and calls — including for a reply that searches
and then answers in text, which carries no tool call to hang them on. Without the
replay the model searches the whole family again at its next need; with it the
loaded definition rides the conversation body into the cached prefix.

The function call came back stamped with the container it was loaded from and
that stamp was dropped too. It rides the tool call and goes back on the replayed
item, because the item that goes back is the provider's own. Absent when the
provider named none, so a call on every other leg serializes exactly as it did.

The grouping travels as a per-tool label rather than a container the caller
builds: the three legs that must not see it strip one key off a flat dict, the
move they already make for defer_loading, instead of unwrapping a tool type they
have no shape for. OpenRouter strips and warns, Google warns, and the Anthropic
builders drop it by construction while keeping their own deferred-loading
mapping.

The hosted-search predicate is its own function with its own floor. Tool search
is served from gpt-5.4 while the explicit prompt-cache markers beside it need
5.6, so one gate for both would either withhold the search from two generations
that serve it or send cache fields to a model that answers them with a 400.

Depends on #62: written on top of it, since both change the request builder and
the response harmonizer.

With nothing labelled and nothing deferred every payload on every leg is
byte-identical to before.
The hosted-tool-search predicate and the explicit prompt-cache gate parsed the
same model name with the same four lines each. They share one reader now, so the
floor is the only thing that separates them — which is the only thing that
should.
@rezaho
rezaho changed the base branch from prompt-cache-diagnostics to main September 13, 2026 21:01
@rezaho
rezaho merged commit fd48c83 into main Sep 13, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant