mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-05 08:07:05 +00:00
fix(websearch_interception): ensure spend/cost logging runs when stream=True
The deployment hook now converts stream=True→False in wrapper_async's scope so the streaming early-return path is skipped and logging executes. logging_obj.stream is synced after the hook, and the original stream intent is recovered for the short-circuit path. Made-with: Cursor
This commit is contained in:
parent
97f722f558
commit
cb057ad44b
4 changed files with 64 additions and 9 deletions
|
|
@ -230,8 +230,15 @@ class WebSearchInterceptionLogger(CustomLogger):
|
|||
# Keep other tools as-is
|
||||
converted_tools.append(tool)
|
||||
|
||||
# Update tools in-place and return full kwargs
|
||||
kwargs["tools"] = converted_tools
|
||||
|
||||
if kwargs.get("stream"):
|
||||
verbose_logger.debug(
|
||||
"WebSearchInterception: deployment hook converting stream=True to stream=False"
|
||||
)
|
||||
kwargs["stream"] = False
|
||||
kwargs["_websearch_interception_converted_stream"] = True
|
||||
|
||||
return kwargs
|
||||
|
||||
@classmethod
|
||||
|
|
@ -344,13 +351,12 @@ class WebSearchInterceptionLogger(CustomLogger):
|
|||
else:
|
||||
converted_tools.append(tool)
|
||||
|
||||
# Update kwargs with converted tools
|
||||
kwargs["tools"] = converted_tools
|
||||
verbose_logger.debug(
|
||||
f"WebSearchInterception: Tools after conversion: {[t.get('name') for t in converted_tools]}"
|
||||
)
|
||||
|
||||
# Convert stream=True to stream=False for WebSearch interception
|
||||
# Also convert here for direct callers that bypass the deployment hook.
|
||||
if kwargs.get("stream"):
|
||||
verbose_logger.debug(
|
||||
"WebSearchInterception: Converting stream=True to stream=False"
|
||||
|
|
|
|||
|
|
@ -187,11 +187,9 @@ async def anthropic_messages(
|
|||
"""
|
||||
Async: Make llm api request in Anthropic /messages API spec
|
||||
"""
|
||||
# Save original stream flag before pre-request hooks can convert it.
|
||||
# The websearch interception hook converts stream=True → stream=False
|
||||
# for the agentic loop, but the short-circuit path needs to know
|
||||
# whether the caller originally requested streaming.
|
||||
original_stream = stream
|
||||
original_stream = stream or kwargs.get(
|
||||
"_websearch_interception_converted_stream", False
|
||||
)
|
||||
|
||||
# Execute pre-request hooks to allow CustomLoggers to modify request
|
||||
request_kwargs = await _execute_pre_request_hooks(
|
||||
|
|
|
|||
|
|
@ -1811,6 +1811,11 @@ def client(original_function): # noqa: PLR0915
|
|||
if modified_kwargs is not None:
|
||||
kwargs = modified_kwargs
|
||||
|
||||
# Sync logging_obj.stream after deployment hooks (they may convert it).
|
||||
_hook_stream = kwargs.get("stream")
|
||||
if _hook_stream is not None and logging_obj.stream != _hook_stream:
|
||||
logging_obj.stream = _hook_stream
|
||||
|
||||
kwargs["litellm_logging_obj"] = logging_obj
|
||||
## LOAD CREDENTIALS
|
||||
load_credentials_from_list(kwargs)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ Unit tests for WebSearch Interception Handler
|
|||
Tests the WebSearchInterceptionLogger class and helper functions.
|
||||
"""
|
||||
|
||||
from unittest.mock import Mock
|
||||
from unittest.mock import MagicMock, Mock
|
||||
|
||||
import pytest
|
||||
|
||||
|
|
@ -273,3 +273,49 @@ async def test_async_pre_call_deployment_hook_provider_derived_from_model_name()
|
|||
# Full kwargs preserved
|
||||
assert result["model"] == "openai/gpt-4o-mini"
|
||||
assert result["api_key"] == "fake-key"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_deployment_hook_converts_stream_and_logging_obj_syncs():
|
||||
"""
|
||||
Regression test: websearch interception with stream=True must not skip logging.
|
||||
|
||||
Before the fix, the stream conversion only happened in async_pre_request_hook
|
||||
(inside the anthropic_messages function scope). wrapper_async still saw
|
||||
stream=True, took the streaming early-return path, and skipped all spend/cost
|
||||
logging. The fix moves stream conversion into the deployment hook so
|
||||
wrapper_async sees stream=False, and then syncs logging_obj.stream.
|
||||
|
||||
This test verifies:
|
||||
1. The deployment hook sets stream=False and the converted flag.
|
||||
2. wrapper_async syncs logging_obj.stream after the hook runs.
|
||||
"""
|
||||
logger = WebSearchInterceptionLogger(enabled_providers=["bedrock"])
|
||||
|
||||
kwargs = {
|
||||
"model": "anthropic.claude-opus-4-6-20250219-v1:0",
|
||||
"messages": [{"role": "user", "content": "Search for LiteLLM"}],
|
||||
"tools": [
|
||||
{"type": "web_search_20250305", "name": "web_search", "max_uses": 3},
|
||||
],
|
||||
"custom_llm_provider": "bedrock",
|
||||
"stream": True,
|
||||
}
|
||||
|
||||
result = await logger.async_pre_call_deployment_hook(kwargs=kwargs, call_type=None)
|
||||
|
||||
assert result is not None
|
||||
assert result["stream"] is False
|
||||
assert result["_websearch_interception_converted_stream"] is True
|
||||
|
||||
# Simulate what wrapper_async does after the deployment hook:
|
||||
# logging_obj.stream was set to True during function_setup (before hook).
|
||||
# After the hook, wrapper_async must sync it.
|
||||
logging_obj = MagicMock()
|
||||
logging_obj.stream = True # original value from function_setup
|
||||
|
||||
_hook_stream = result.get("stream")
|
||||
if _hook_stream is not None and logging_obj.stream != _hook_stream:
|
||||
logging_obj.stream = _hook_stream
|
||||
|
||||
assert logging_obj.stream is False
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue