Skip to content

Commit 9cd8f31

Browse files
committed
fix(litellm): coerce tool_choice to None when tools is empty
LiteLLM rejects tool_choice="required"/"none" when the tools list is falsy. tool_choice and tools were derived from independent config fields with no cross-check, so a caller setting mode=ANY with no function_declarations would reach acompletion() with tools=None and tool_choice="required". Add a guard in _get_completion_inputs to coerce tool_choice to None when tools is falsy, and mirror the same coercion in generate_content_async after the "functions" additional-arg override that already nulls tools. Add two tests covering both paths; update four existing tool_choice tests to include a FunctionDeclaration so they remain valid under the new guard.
1 parent 44c745b commit 9cd8f31

2 files changed

Lines changed: 103 additions & 8 deletions

File tree

src/google/adk/models/lite_llm.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2081,6 +2081,11 @@ async def _get_completion_inputs(
20812081
tool_choice = "none"
20822082
# AUTO → None (provider default)
20832083

2084+
# Coerce tool_choice to None when there are no tools to choose from.
2085+
# LiteLLM rejects tool_choice="required" (or "none") when tools is falsy.
2086+
if not tools:
2087+
tool_choice = None
2088+
20842089
return messages, tools, response_format, generation_params, tool_choice
20852090

20862091

@@ -2354,6 +2359,8 @@ async def generate_content_async(
23542359
if "functions" in self._additional_args:
23552360
# LiteLLM does not support both tools and functions together.
23562361
tools = None
2362+
# Without tools, tool_choice ("required"/"none") would be rejected by LiteLLM.
2363+
tool_choice = None
23572364

23582365
completion_args = {
23592366
"model": effective_model,

tests/unittests/models/test_litellm.py

Lines changed: 96 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5135,17 +5135,24 @@ async def test_get_completion_inputs_tool_choice_none_without_tool_config():
51355135

51365136
@pytest.mark.asyncio
51375137
async def test_get_completion_inputs_tool_choice_required_for_any_mode():
5138-
"""tool_choice must be 'required' when mode=ANY."""
5138+
"""tool_choice must be 'required' when mode=ANY and tools are present."""
51395139
llm_request = LlmRequest(
51405140
contents=[
51415141
types.Content(role="user", parts=[types.Part.from_text(text="Hello")])
51425142
],
51435143
config=types.GenerateContentConfig(
5144+
tools=[
5145+
types.Tool(
5146+
function_declarations=[
5147+
types.FunctionDeclaration(name="my_func", description="A func")
5148+
]
5149+
)
5150+
],
51445151
tool_config=types.ToolConfig(
51455152
function_calling_config=types.FunctionCallingConfig(
51465153
mode=types.FunctionCallingConfigMode.ANY
51475154
)
5148-
)
5155+
),
51495156
),
51505157
)
51515158

@@ -5158,17 +5165,24 @@ async def test_get_completion_inputs_tool_choice_required_for_any_mode():
51585165

51595166
@pytest.mark.asyncio
51605167
async def test_get_completion_inputs_tool_choice_none_for_none_mode():
5161-
"""tool_choice must be 'none' when mode=NONE."""
5168+
"""tool_choice must be 'none' when mode=NONE and tools are present."""
51625169
llm_request = LlmRequest(
51635170
contents=[
51645171
types.Content(role="user", parts=[types.Part.from_text(text="Hello")])
51655172
],
51665173
config=types.GenerateContentConfig(
5174+
tools=[
5175+
types.Tool(
5176+
function_declarations=[
5177+
types.FunctionDeclaration(name="my_func", description="A func")
5178+
]
5179+
)
5180+
],
51675181
tool_config=types.ToolConfig(
51685182
function_calling_config=types.FunctionCallingConfig(
51695183
mode=types.FunctionCallingConfigMode.NONE
51705184
)
5171-
)
5185+
),
51725186
),
51735187
)
51745188

@@ -5206,7 +5220,7 @@ async def test_get_completion_inputs_tool_choice_none_for_auto_mode():
52065220
async def test_generate_content_async_propagates_tool_choice_required(
52075221
mock_acompletion, mock_completion
52085222
):
5209-
"""generate_content_async must pass tool_choice='required' to acompletion."""
5223+
"""generate_content_async must pass tool_choice='required' to acompletion when tools are present."""
52105224
llm_client = MockLLMClient(mock_acompletion, mock_completion)
52115225
lite_llm_instance = LiteLlm(model="openai/gpt-4o", llm_client=llm_client)
52125226

@@ -5217,11 +5231,18 @@ async def test_generate_content_async_propagates_tool_choice_required(
52175231
)
52185232
],
52195233
config=types.GenerateContentConfig(
5234+
tools=[
5235+
types.Tool(
5236+
function_declarations=[
5237+
types.FunctionDeclaration(name="my_func", description="A func")
5238+
]
5239+
)
5240+
],
52205241
tool_config=types.ToolConfig(
52215242
function_calling_config=types.FunctionCallingConfig(
52225243
mode=types.FunctionCallingConfigMode.ANY
52235244
)
5224-
)
5245+
),
52255246
),
52265247
)
52275248

@@ -5237,7 +5258,7 @@ async def test_generate_content_async_propagates_tool_choice_required(
52375258
async def test_generate_content_async_propagates_tool_choice_none_mode(
52385259
mock_acompletion, mock_completion
52395260
):
5240-
"""generate_content_async must pass tool_choice='none' to acompletion for NONE mode."""
5261+
"""generate_content_async must pass tool_choice='none' to acompletion for NONE mode when tools are present."""
52415262
llm_client = MockLLMClient(mock_acompletion, mock_completion)
52425263
lite_llm_instance = LiteLlm(model="openai/gpt-4o", llm_client=llm_client)
52435264

@@ -5248,11 +5269,18 @@ async def test_generate_content_async_propagates_tool_choice_none_mode(
52485269
)
52495270
],
52505271
config=types.GenerateContentConfig(
5272+
tools=[
5273+
types.Tool(
5274+
function_declarations=[
5275+
types.FunctionDeclaration(name="my_func", description="A func")
5276+
]
5277+
)
5278+
],
52515279
tool_config=types.ToolConfig(
52525280
function_calling_config=types.FunctionCallingConfig(
52535281
mode=types.FunctionCallingConfigMode.NONE
52545282
)
5255-
)
5283+
),
52565284
),
52575285
)
52585286

@@ -5313,3 +5341,63 @@ async def test_generate_content_async_omits_tool_choice_without_tool_config(
53135341
mock_acompletion.assert_called_once()
53145342
_, kwargs = mock_acompletion.call_args
53155343
assert "tool_choice" not in kwargs
5344+
5345+
5346+
@pytest.mark.asyncio
5347+
async def test_get_completion_inputs_tool_choice_coerced_to_none_when_no_tools():
5348+
"""tool_choice must be coerced to None when mode=ANY but no function_declarations exist."""
5349+
llm_request = LlmRequest(
5350+
contents=[
5351+
types.Content(role="user", parts=[types.Part.from_text(text="Hello")])
5352+
],
5353+
config=types.GenerateContentConfig(
5354+
tool_config=types.ToolConfig(
5355+
function_calling_config=types.FunctionCallingConfig(
5356+
mode=types.FunctionCallingConfigMode.ANY
5357+
)
5358+
)
5359+
),
5360+
)
5361+
5362+
_, tools, _, _, tool_choice = await _get_completion_inputs(
5363+
llm_request, model="openai/gpt-4o"
5364+
)
5365+
5366+
assert not tools
5367+
assert tool_choice is None
5368+
5369+
5370+
@pytest.mark.asyncio
5371+
async def test_generate_content_async_omits_tool_choice_when_functions_override(
5372+
mock_acompletion, mock_completion
5373+
):
5374+
"""When `functions` is passed as an additional kwarg, tools is nulled and tool_choice must also be dropped."""
5375+
llm_client = MockLLMClient(mock_acompletion, mock_completion)
5376+
lite_llm_instance = LiteLlm(
5377+
model="openai/gpt-4o",
5378+
llm_client=llm_client,
5379+
functions=[{"name": "noop", "parameters": {"type": "object"}}],
5380+
)
5381+
5382+
llm_request = LlmRequest(
5383+
contents=[
5384+
types.Content(
5385+
role="user", parts=[types.Part.from_text(text="Call something")]
5386+
)
5387+
],
5388+
config=types.GenerateContentConfig(
5389+
tool_config=types.ToolConfig(
5390+
function_calling_config=types.FunctionCallingConfig(
5391+
mode=types.FunctionCallingConfigMode.ANY
5392+
)
5393+
)
5394+
),
5395+
)
5396+
5397+
async for _ in lite_llm_instance.generate_content_async(llm_request):
5398+
pass
5399+
5400+
mock_acompletion.assert_called_once()
5401+
_, kwargs = mock_acompletion.call_args
5402+
assert kwargs.get("tools") is None
5403+
assert "tool_choice" not in kwargs

0 commit comments

Comments
 (0)