mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-10 22:41:41 +00:00
fix(router): preserve fusion tool choice semantics
This commit is contained in:
parent
a01c230990
commit
1d21c6c2b8
2 changed files with 83 additions and 10 deletions
|
|
@ -982,8 +982,9 @@ class FusionRouter:
|
|||
kwargs: Final = _outer_kwargs(request_kwargs)
|
||||
kwargs.pop("litellm_metadata", None)
|
||||
kwargs["metadata"] = _fusion_call_metadata(request_kwargs, FUSION_INITIAL_CALL_ORIGIN)
|
||||
client_tools: Final = _client_tools(request_kwargs.get("tools"))
|
||||
kwargs["tools"] = [ # mutable-ok: local provider payload
|
||||
*_client_tools(request_kwargs.get("tools")),
|
||||
*client_tools,
|
||||
_fusion_tool(),
|
||||
] # mutable-ok: local provider payload
|
||||
if self.config.invocation == "required":
|
||||
|
|
@ -993,6 +994,13 @@ class FusionRouter:
|
|||
"name": FUSION_TOOL_NAME
|
||||
}, # mutable-ok: local provider payload
|
||||
} # mutable-ok: local provider payload
|
||||
elif kwargs.get("tool_choice") == "none":
|
||||
# The caller's tool policy applies to executable client tools, not
|
||||
# to Fusion's private reasoning step. Let the outer model deliberate
|
||||
# while keeping client tools unavailable until the final call, where
|
||||
# the original `tool_choice="none"` is preserved.
|
||||
kwargs["tools"] = [_fusion_tool()] # mutable-ok: local provider payload
|
||||
kwargs["tool_choice"] = "auto"
|
||||
elif kwargs.get("tool_choice") is None:
|
||||
kwargs["tool_choice"] = "auto"
|
||||
response: Final = await self._completion(
|
||||
|
|
@ -1198,14 +1206,11 @@ class FusionRouter:
|
|||
final_kwargs: Final = _outer_kwargs(request_kwargs)
|
||||
final_kwargs.pop("litellm_logging_obj", None)
|
||||
final_kwargs.pop("litellm_call_id", None)
|
||||
# `required` has already been satisfied by the private Fusion call. Do
|
||||
# not force the continuation into another tool call (or an impossible
|
||||
# tool call when the caller supplied no client tools).
|
||||
if request_kwargs.get("tool_choice") == "required":
|
||||
if _client_tools(request_kwargs.get("tools")):
|
||||
final_kwargs["tool_choice"] = "auto"
|
||||
else:
|
||||
final_kwargs.pop("tool_choice", None)
|
||||
# A private Fusion call does not satisfy the caller's requirement for
|
||||
# an executable tool call. Preserve `required` when client tools exist;
|
||||
# only remove the impossible no-tools combination.
|
||||
if request_kwargs.get("tool_choice") == "required" and not _client_tools(request_kwargs.get("tools")):
|
||||
final_kwargs.pop("tool_choice", None)
|
||||
final_metadata: Final = _fusion_call_metadata(request_kwargs, FUSION_CONTINUATION_CALL_ORIGIN)
|
||||
final_kwargs.pop("litellm_metadata", None)
|
||||
final_kwargs["metadata"] = final_metadata
|
||||
|
|
|
|||
|
|
@ -168,6 +168,74 @@ async def test_outer_client_tool_call_is_returned_without_running_panel_or_secon
|
|||
assert [call["model"] for call in completion.calls] == ["outer"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_tool_choice_none_allows_private_deliberation_but_never_client_tools() -> None:
|
||||
completion = RecordingCompletion(
|
||||
{
|
||||
"outer": [_fusion_call(), _response("Final answer without a tool call")],
|
||||
"panel-a": [_response("Panel A")],
|
||||
"panel-b": [_response("Panel B")],
|
||||
"analyst": [_response(_analysis())],
|
||||
}
|
||||
)
|
||||
client_tool = {
|
||||
"type": "function",
|
||||
"function": {"name": "send_email", "parameters": {"type": "object"}},
|
||||
}
|
||||
|
||||
response = await _router(completion).acompletion(
|
||||
messages=[{"role": "user", "content": "Research this but do not send anything"}],
|
||||
stream=False,
|
||||
request_kwargs={"tools": [client_tool], "tool_choice": "none"},
|
||||
)
|
||||
|
||||
assert isinstance(response, ModelResponse)
|
||||
assert response.choices[0].message.content == "Final answer without a tool call"
|
||||
assert [call["model"] for call in completion.calls] == [
|
||||
"outer",
|
||||
"panel-a",
|
||||
"panel-b",
|
||||
"analyst",
|
||||
"outer",
|
||||
]
|
||||
initial = completion.calls[0]
|
||||
assert [tool["function"]["name"] for tool in initial["tools"]] == [FUSION_TOOL_NAME]
|
||||
assert initial["tool_choice"] == "auto"
|
||||
final = completion.calls[-1]
|
||||
assert final["tools"] == [client_tool]
|
||||
assert final["tool_choice"] == "none"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_named_client_tool_choice_is_preserved_and_bypasses_private_deliberation() -> None:
|
||||
client_call = {
|
||||
"id": "email-1",
|
||||
"type": "function",
|
||||
"function": {"name": "send_email", "arguments": '{"to":"user@example.com"}'},
|
||||
}
|
||||
completion = RecordingCompletion({"outer": [_response(None, [client_call])]})
|
||||
client_tool = {
|
||||
"type": "function",
|
||||
"function": {"name": "send_email", "parameters": {"type": "object"}},
|
||||
}
|
||||
named_choice = {"type": "function", "function": {"name": "send_email"}}
|
||||
|
||||
response = await _router(completion).acompletion(
|
||||
messages=[{"role": "user", "content": "Send the update"}],
|
||||
stream=False,
|
||||
request_kwargs={"tools": [client_tool], "tool_choice": named_choice},
|
||||
)
|
||||
|
||||
assert isinstance(response, ModelResponse)
|
||||
assert response.choices[0].message.tool_calls[0].function.name == "send_email"
|
||||
assert [call["model"] for call in completion.calls] == ["outer"]
|
||||
assert completion.calls[0]["tool_choice"] == named_choice
|
||||
assert [tool["function"]["name"] for tool in completion.calls[0]["tools"]] == [
|
||||
"send_email",
|
||||
FUSION_TOOL_NAME,
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mixed_fusion_and_client_tool_calls_return_only_executable_client_calls() -> None:
|
||||
client_call = {
|
||||
|
|
@ -321,7 +389,7 @@ async def test_forced_fusion_runs_parallel_panel_then_analyst_then_outer() -> No
|
|||
assert analyst["metadata"]["user_api_key_budget_reservation"] is reservation
|
||||
final = completion.calls[4]
|
||||
assert final["tools"] == [client_tool]
|
||||
assert final["tool_choice"] == "auto"
|
||||
assert final["tool_choice"] == "required"
|
||||
continuation = final["messages"]
|
||||
assert continuation[0] == messages[0]
|
||||
assert continuation[1]["role"] == "developer"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue