mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-05 02:41:56 +00:00
fix(websearch): deduplicate overlapping keys between kwargs and optional_params in follow-up requests
When the same parameter key (e.g. temperature, top_p) appeared in both kwargs and optional_params, the follow-up LLM call in the agentic loop would fail with TypeError: got multiple values for keyword argument because both dicts were unpacked via ** at the call site. Remove overlapping keys from kwargs_for_followup in both the Anthropic and Chat Completion request patch builders so that optional_params takes precedence.
This commit is contained in:
parent
0992bf2271
commit
00975028e1
2 changed files with 154 additions and 0 deletions
|
|
@ -846,6 +846,14 @@ class WebSearchInterceptionLogger(CustomLogger):
|
|||
}
|
||||
kwargs_for_followup = self._prepare_followup_kwargs(kwargs)
|
||||
|
||||
# Remove keys from kwargs that are already in optional_params
|
||||
# to avoid "got multiple values for keyword argument" errors
|
||||
kwargs_for_followup = {
|
||||
k: v
|
||||
for k, v in kwargs_for_followup.items()
|
||||
if k not in optional_params_without_max_tokens
|
||||
}
|
||||
|
||||
if logging_obj is not None:
|
||||
agentic_params = logging_obj.model_call_details.get(
|
||||
"agentic_loop_params", {}
|
||||
|
|
@ -1105,6 +1113,14 @@ class WebSearchInterceptionLogger(CustomLogger):
|
|||
if tools_param is not None:
|
||||
optional_params_clean["tools"] = tools_param
|
||||
|
||||
# Remove keys from kwargs that are already in optional_params
|
||||
# to avoid "got multiple values for keyword argument" errors
|
||||
kwargs_for_followup = {
|
||||
k: v
|
||||
for k, v in kwargs_for_followup.items()
|
||||
if k not in optional_params_clean
|
||||
}
|
||||
|
||||
return AgenticLoopRequestPatch(
|
||||
model=full_model_name,
|
||||
messages=follow_up_messages,
|
||||
|
|
|
|||
|
|
@ -124,6 +124,144 @@ async def test_async_build_agentic_loop_plan_returns_request_patch():
|
|||
assert plan.request_patch.kwargs["temperature"] == 0.2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_anthropic_path_dedup_overlapping_keys():
|
||||
"""Regression test: overlapping keys between kwargs and optional_params must be deduped.
|
||||
|
||||
When the same key (e.g. temperature, top_p) appears in both kwargs and
|
||||
anthropic_messages_optional_request_params, the follow-up request would fail with
|
||||
``TypeError: got multiple values for keyword argument`` because both dicts are
|
||||
unpacked via ``**`` at the call site. The fix removes overlapping keys from
|
||||
kwargs_for_followup so that optional_params takes precedence.
|
||||
"""
|
||||
logger = WebSearchInterceptionLogger(enabled_providers=["bedrock"])
|
||||
logger._execute_search = AsyncMock( # type: ignore
|
||||
return_value="Title: LiteLLM\nURL: docs\nSnippet: test"
|
||||
)
|
||||
|
||||
tools_dict = {
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "toolu_456",
|
||||
"type": "tool_use",
|
||||
"name": "litellm_web_search",
|
||||
"input": {"query": "test query"},
|
||||
}
|
||||
],
|
||||
"response_format": "anthropic",
|
||||
}
|
||||
logging_obj = MagicMock()
|
||||
logging_obj.model_call_details = {
|
||||
"agentic_loop_params": {"model": "bedrock/invoke/claude-3-5-sonnet"}
|
||||
}
|
||||
|
||||
# temperature and top_p appear in BOTH kwargs and optional_params
|
||||
kwargs = {
|
||||
"temperature": 0.2,
|
||||
"top_p": 0.9,
|
||||
"custom_param": "keep_me",
|
||||
"litellm_logging_obj": object(),
|
||||
"_websearch_interception_converted_stream": True,
|
||||
}
|
||||
optional_params = {
|
||||
"max_tokens": 2048,
|
||||
"temperature": 0.5,
|
||||
"top_p": 0.8,
|
||||
"tools": [{"name": "litellm_web_search"}],
|
||||
}
|
||||
|
||||
plan = await logger.async_build_agentic_loop_plan(
|
||||
tools=tools_dict,
|
||||
model="claude-3-5-sonnet",
|
||||
messages=[{"role": "user", "content": "search test"}],
|
||||
response=None,
|
||||
anthropic_messages_provider_config=None,
|
||||
anthropic_messages_optional_request_params=optional_params,
|
||||
logging_obj=logging_obj,
|
||||
stream=False,
|
||||
kwargs=kwargs,
|
||||
)
|
||||
|
||||
assert plan.request_patch is not None
|
||||
# Overlapping keys must be removed from kwargs (optional_params takes precedence)
|
||||
assert "temperature" not in plan.request_patch.kwargs
|
||||
assert "top_p" not in plan.request_patch.kwargs
|
||||
# Non-overlapping key must be preserved
|
||||
assert plan.request_patch.kwargs["custom_param"] == "keep_me"
|
||||
# Internal keys must still be filtered
|
||||
assert "litellm_logging_obj" not in plan.request_patch.kwargs
|
||||
assert "_websearch_interception_converted_stream" not in plan.request_patch.kwargs
|
||||
# optional_params values must be preserved
|
||||
assert plan.request_patch.optional_params["temperature"] == 0.5
|
||||
assert plan.request_patch.optional_params["top_p"] == 0.8
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_chat_completion_path_dedup_overlapping_keys():
|
||||
"""Regression test: overlapping keys between kwargs and optional_params in chat completion path.
|
||||
|
||||
Same issue as the Anthropic path, but for the OpenAI-style chat completion flow
|
||||
where ``optional_params_clean`` and ``kwargs_for_followup`` are both unpacked via
|
||||
``**`` in ``_execute_chat_completion_agentic_loop``.
|
||||
"""
|
||||
logger = WebSearchInterceptionLogger(enabled_providers=["openai"])
|
||||
logger._execute_search = AsyncMock( # type: ignore
|
||||
return_value="Title: LiteLLM\nURL: docs\nSnippet: test"
|
||||
)
|
||||
|
||||
tools_dict = {
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "call_789",
|
||||
"type": "tool_use",
|
||||
"name": "litellm_web_search",
|
||||
"input": {"query": "test query"},
|
||||
}
|
||||
],
|
||||
"response_format": "openai",
|
||||
}
|
||||
|
||||
# temperature and stop appear in BOTH kwargs and optional_params
|
||||
kwargs = {
|
||||
"temperature": 0.3,
|
||||
"stop": ["\n"],
|
||||
"api_key": "fake-key",
|
||||
"custom_llm_provider": "openai",
|
||||
"litellm_logging_obj": object(),
|
||||
"_websearch_interception_converted_stream": True,
|
||||
}
|
||||
optional_params = {
|
||||
"temperature": 0.7,
|
||||
"stop": ["END"],
|
||||
"tools": [{"type": "function", "function": {"name": "litellm_web_search"}}],
|
||||
}
|
||||
|
||||
plan = await logger.async_build_chat_completion_agentic_loop_plan(
|
||||
tools=tools_dict,
|
||||
model="gpt-4o",
|
||||
messages=[{"role": "user", "content": "search test"}],
|
||||
response=None,
|
||||
optional_params=optional_params,
|
||||
logging_obj=None,
|
||||
stream=False,
|
||||
kwargs=kwargs,
|
||||
)
|
||||
|
||||
assert plan.request_patch is not None
|
||||
# Overlapping keys must be removed from kwargs (optional_params takes precedence)
|
||||
assert "temperature" not in plan.request_patch.kwargs
|
||||
assert "stop" not in plan.request_patch.kwargs
|
||||
# Non-overlapping keys must be preserved
|
||||
assert plan.request_patch.kwargs["api_key"] == "fake-key"
|
||||
# Internal keys must still be filtered
|
||||
assert "litellm_logging_obj" not in plan.request_patch.kwargs
|
||||
assert "custom_llm_provider" not in plan.request_patch.kwargs
|
||||
assert "_websearch_interception_converted_stream" not in plan.request_patch.kwargs
|
||||
# optional_params values must be preserved
|
||||
assert plan.request_patch.optional_params["temperature"] == 0.7
|
||||
assert plan.request_patch.optional_params["stop"] == ["END"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_internal_flags_filtered_from_followup_kwargs():
|
||||
"""Test that internal _websearch_interception flags are filtered from follow-up request kwargs.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue