Group deferred tools into namespaces and keep the provider's search items - #63
Merged
Merged
Conversation
…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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 tomainon its own once #62 merges. Both changes editformat_request_payloadandharmonize_response, in disjoint hunks.What it does
The Responses legs serve a hosted tool search. The request carries a
namespacecontainer per family with its members deferred and thetool_searchbuilt-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 Azuregpt-5.6deployment on 2026-09-12 measured each of them.The built-in was added only when
defer_loadingsat 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 backHTTP 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,messageandfunction_call, so the search's own two items were dropped and with them the definitions the search had loaded. They now ridereasoning_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.namespaceand 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
namespacelabel 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 fordefer_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 fromgpt-5.4while the explicit prompt-cache markers beside it need5.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_detailsis the framework's opaque-provider-items channel by function on three adapters and by name on none of them. The name is on the publicHarmonizedResponse, 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-identicaltoolsarray, 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.pypasses unchanged and is not edited.