litellm/tests/unit/integrations/websearch_interception
paveln83 9fba44d300
fix(openai): cap and bill web search rounds on Responses-pinned chat completions (#51802)
* fix(openai): cap web search agentic loops with max_agentic_loops on OpenAI chat completions

The OpenAI chat dispatcher ran every custom logger's chat agentic loop
without a depth check, and the web search follow-up never carried the
loop depth, so max_agentic_loops was ignored on openai/ deployments.
The dispatcher now skips loggers the shared chat completion loop in
litellm.acompletion already runs, so those go through its depth, cap,
and repeated-call checks once. Loggers that only implement the
chat-specific hooks still run in the OpenAI dispatcher

* fix(openai): honor max_agentic_loops when chat completions bridge to Responses

A gpt-5.4+ chat completion with function tools on api.openai.com is sent to
/v1/responses by the Responses bridge. The Responses handler then ran its own
agentic loop on that call, which never saw the request's max_agentic_loops or
depth, and the shared chat completion loop in litellm.acompletion ran again on
what it returned, so every chat follow-up restarted the Responses loop at depth
0. With max_agentic_loops=1, web search made 8 Responses calls and 7 searches

The async bridge now marks its Responses call as part of a chat completion
turn, and the Responses handler skips its own loop for such calls. The shared
chat loop owns the whole turn, so its depth, cap, and repeated-call checks run
once

* fix(responses): keep search follow-ups on the Responses API for responses/ pinned models

When an async chat completion crosses the Responses bridge, the shared chat
loop now owns the turn and builds each web search follow-up from the model
recorded on the logging object. responses() overwrote that record with the
stripped model, so a model pinned with the responses/ prefix (for example
openai/responses/<model> on a Responses-only gateway) sent its follow-up to
chat completions, and the turn ended on the unexecuted tool call

responses() now leaves the record alone when the chat loop owns the turn, the
same way completion() skips its own record when the reverse bridge owns it

* fix(agentic-loop): bill every round of a non-streamed Responses-bridged chat loop once

A bridged chat request logs its first round before the chat loop runs, so a
follow-up run as an internal sub-call was never billed. Each follow-up of a
non-streamed bridged parent now writes its own spend row, and a failed
follow-up no longer re-bills rounds that already wrote one

* fix(agentic-loop): keep streamed Responses-bridged web search from billing its final round twice

* fix(openai): bill every bridged search round in one row after guardrails

On the proxy a non-streamed Responses-pinned chat request now parks one
spend row that sums every round the loop served, so the row and the
x-litellm-response-cost header show the whole request and nothing is
written before post-call guardrails pass. A streamed request keeps round
one's row and folds every later round into the replayed final row.

The bridge now skips only Responses hooks that run their own chat loop,
so hooks that only handle the Responses API still run.

* fix(openai): price each bridged search round at its own size

Each round of a bridged chat request is now priced on its own usage and the
combined proxy row adds those prices, so summed usage never crosses a
long-context price tier. A streamed replay keeps that total and its cost
breakdown

* fix(agentic-loop): run the chat hook of bridged callbacks that define both agentic hooks

The bridge skipped the Responses hook of a callback that runs its own chat
loop, and the shared loop only asked the general gate, so a custom callback
with both hooks stopped intercepting bridged chat. Ask its chat gate after
the shared loop. Price tier tests now use test-owned gpt-5.6 prices.

* fix(agentic-loop): route bridged chat to the chat hook only for callbacks that opt in

A custom callback that defined both agentic hooks lost its Responses hook on
chat completions bridged to the Responses API, so a chat hook that declined
left the request unhandled, and one that claimed it changed which hook
answered compared with main.

Callbacks now opt in with wants_chat_completion_hooks_on_bridged_chat. Web
search interception sets it, so its loop still runs once on the chat side.
Every other callback keeps main's routing through its Responses hook.

* fix(websearch_interception): keep main's Responses loop for a bridged web_search_options call

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-11-14 14:36:10 -05:00
..
__init__.py test: move tests/test_litellm integrations and secret_managers into tests/unit (#43194) 2026-09-25 12:57:07 -07:00
conftest.py fix(websearch)!: keep the provider prefix on the agentic follow-up model (#38885) 2026-10-29 04:47:18 -07:00
test_websearch_agentic_loop_cap.py fix(openai): cap and bill web search rounds on Responses-pinned chat completions (#51802) 2026-11-14 14:36:10 -05:00
test_websearch_chat_completion.py fix(tool_choice): accept the Responses API's flat allowed_tools layout on chat completions (#50467) 2026-11-10 13:04:06 -08:00
test_websearch_interception_handler.py refactor(router): move router callers onto the entry points and invert the proxy imports (#48847) 2026-10-29 17:26:33 +00:00
test_websearch_interception_thinking.py test: move tests/test_litellm integrations and secret_managers into tests/unit (#43194) 2026-09-25 12:57:07 -07:00
test_websearch_native_blocks.py fix(exa): return highlights, highlight scores, score and the response envelope from Exa search (#47423) 2026-10-19 15:45:56 -07:00
test_websearch_responses.py fix(tool_choice): accept the Responses API's flat allowed_tools layout on chat completions (#50467) 2026-11-10 13:04:06 -08:00
test_websearch_rich_query_shape.py refactor(router): move router callers onto the entry points and invert the proxy imports (#48847) 2026-10-29 17:26:33 +00:00
test_websearch_short_circuit.py fix(websearch-interception)!: stream native server-tool blocks to clients (#40967) 2026-10-30 00:06:57 -07:00
test_websearch_streaming_wrap.py refactor(anthropic): rename llms/anthropic/pass_through to llms/anthropic/messages (#48510) 2026-10-27 10:08:25 -07:00
test_websearch_thinking_constraint.py fix(agentic)!: surface a failed agentic follow-up call instead of the pre-loop tool call (#49227) 2026-11-04 02:00:33 +00:00