mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-19 04:54:37 +00:00
fix(bedrock): forward named-function name in parallel tool_choice config
CodeRabbit follow-up: for a named-function tool_choice with
parallel_tool_calls=False, the config emitted {"type": "tool", ...} but dropped
the function name. _apply_parallel_tool_use_config then removes the native
toolChoice that carried the name, so Bedrock received a nameless "tool" choice
and rejected it.
Mirror the native Anthropic transform (which sets _tool_choice["name"] =
_tool_name): extract _bedrock_parallel_tool_choice() to build the full payload
(type + disable_parallel_tool_use, plus name when type == "tool").
Live before/after against Bedrock claude-sonnet-5 (named tool_choice +
parallel_tool_calls=False):
- before: 400 BadRequest "tool_choice.tool.name: Field required"
- after: 200, single tool_call to the named function
Added a parametrized name assertion + an end-to-end test asserting the request
carries {"type": "tool", "name": ..., "disable_parallel_tool_use": True} and
the native toolChoice is dropped. Non-vacuous (both fail against the nameless
build). ruff/lint clean, C901 strict budget unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
7038d2354a
commit
b1b434e2a9
2 changed files with 84 additions and 9 deletions
|
|
@ -115,6 +115,30 @@ def _bedrock_tool_choice_type(tool_choice: Any) -> str:
|
|||
return "auto"
|
||||
|
||||
|
||||
def _bedrock_parallel_tool_choice(tool_choice: Any, disable_parallel: bool) -> dict:
|
||||
"""Build the Anthropic-on-Bedrock ``tool_choice`` payload for the
|
||||
parallel-tool-use config.
|
||||
|
||||
Derives ``type`` from an explicit tool_choice (see
|
||||
``_bedrock_tool_choice_type``) and carries the ``disable_parallel_tool_use``
|
||||
flag this config exists to set. For a named-function choice (``type ==
|
||||
"tool"``) it also forwards the function ``name`` — mirroring the native
|
||||
Anthropic transform (``_tool_choice["name"] = _tool_name``). Without the
|
||||
name, ``_apply_parallel_tool_use_config`` drops the native ``toolChoice``
|
||||
that carried it, so Bedrock would receive ``type="tool"`` with no tool named
|
||||
and reject the request.
|
||||
"""
|
||||
payload: dict = {
|
||||
"type": _bedrock_tool_choice_type(tool_choice),
|
||||
"disable_parallel_tool_use": disable_parallel,
|
||||
}
|
||||
if payload["type"] == "tool" and isinstance(tool_choice, dict):
|
||||
name = tool_choice.get("function", {}).get("name")
|
||||
if name:
|
||||
payload["name"] = name
|
||||
return payload
|
||||
|
||||
|
||||
def _apply_parallel_tool_use_config(
|
||||
parallel_tool_use_config: dict,
|
||||
additional_request_params: dict,
|
||||
|
|
@ -945,10 +969,7 @@ class AmazonConverseConfig(BaseConfig):
|
|||
# defaults to ``type="auto"`` only when none was given). Without a
|
||||
# ``type`` Bedrock Converse returns 400 "missing field `type`".
|
||||
optional_params["_parallel_tool_use_config"] = {
|
||||
"tool_choice": {
|
||||
"type": _bedrock_tool_choice_type(non_default_params.get("tool_choice")),
|
||||
"disable_parallel_tool_use": not value,
|
||||
}
|
||||
"tool_choice": _bedrock_parallel_tool_choice(non_default_params.get("tool_choice"), not value)
|
||||
}
|
||||
if param == "thinking":
|
||||
optional_params["thinking"] = value
|
||||
|
|
|
|||
|
|
@ -696,17 +696,24 @@ def test_parallel_tool_calls_tool_choice_includes_type(model: str):
|
|||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"tool_choice, expected_type",
|
||||
"tool_choice, expected_type, expected_name",
|
||||
[
|
||||
# An explicit tool_choice must drive the parallel-config type rather than
|
||||
# being silently overridden with "auto" (mirrors the native Anthropic
|
||||
# transform, which sets disable_parallel_tool_use on the existing choice).
|
||||
("required", "any"),
|
||||
("auto", "auto"),
|
||||
({"type": "function", "function": {"name": "get_current_weather"}}, "tool"),
|
||||
("required", "any", None),
|
||||
("auto", "auto", None),
|
||||
# A named-function choice must forward the function name alongside
|
||||
# type="tool" — else _apply_parallel_tool_use_config drops the native
|
||||
# toolChoice carrying it and Bedrock gets a nameless "tool" choice.
|
||||
(
|
||||
{"type": "function", "function": {"name": "get_current_weather"}},
|
||||
"tool",
|
||||
"get_current_weather",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_parallel_tool_calls_preserves_explicit_tool_choice_type(tool_choice, expected_type):
|
||||
def test_parallel_tool_calls_preserves_explicit_tool_choice_type(tool_choice, expected_type, expected_name):
|
||||
old_env = os.environ.get("LITELLM_LOCAL_MODEL_COST_MAP")
|
||||
old_cost = litellm.model_cost
|
||||
os.environ["LITELLM_LOCAL_MODEL_COST_MAP"] = "True"
|
||||
|
|
@ -726,6 +733,53 @@ def test_parallel_tool_calls_preserves_explicit_tool_choice_type(tool_choice, ex
|
|||
tc = optional_params["_parallel_tool_use_config"]["tool_choice"]
|
||||
assert tc["type"] == expected_type, f"expected {expected_type}, got {tc}"
|
||||
assert tc["disable_parallel_tool_use"] is True
|
||||
assert tc.get("name") == expected_name, f"name mismatch: {tc}"
|
||||
finally:
|
||||
litellm.model_cost = old_cost
|
||||
if old_env is None:
|
||||
os.environ.pop("LITELLM_LOCAL_MODEL_COST_MAP", None)
|
||||
else:
|
||||
os.environ["LITELLM_LOCAL_MODEL_COST_MAP"] = old_env
|
||||
|
||||
|
||||
def test_parallel_tool_calls_named_function_survives_to_request():
|
||||
"""End-to-end: a named-function tool_choice with parallel_tool_calls=False
|
||||
must reach Bedrock as a single, complete tool_choice — type="tool" WITH the
|
||||
function name — in additionalModelRequestFields, and the native toolChoice
|
||||
must be dropped so the two don't conflict."""
|
||||
old_env = os.environ.get("LITELLM_LOCAL_MODEL_COST_MAP")
|
||||
old_cost = litellm.model_cost
|
||||
os.environ["LITELLM_LOCAL_MODEL_COST_MAP"] = "True"
|
||||
litellm.model_cost = litellm.get_model_cost_map(url="")
|
||||
try:
|
||||
config = AmazonConverseConfig()
|
||||
optional_params = config.map_openai_params(
|
||||
model="anthropic.claude-sonnet-5",
|
||||
non_default_params={
|
||||
"tool_choice": {
|
||||
"type": "function",
|
||||
"function": {"name": "get_current_weather"},
|
||||
},
|
||||
"parallel_tool_calls": False,
|
||||
"tools": _TOOL_PARAM,
|
||||
},
|
||||
optional_params={},
|
||||
drop_params=False,
|
||||
)
|
||||
data = config._transform_request_helper(
|
||||
model="anthropic.claude-sonnet-5",
|
||||
system_content_blocks=[],
|
||||
optional_params=optional_params,
|
||||
messages=None,
|
||||
)
|
||||
tool_choice = data["additionalModelRequestFields"]["tool_choice"]
|
||||
assert tool_choice == {
|
||||
"type": "tool",
|
||||
"name": "get_current_weather",
|
||||
"disable_parallel_tool_use": True,
|
||||
}
|
||||
# Native toolChoice dropped to avoid the conflicting-field 400.
|
||||
assert "toolChoice" not in data.get("toolConfig", {})
|
||||
finally:
|
||||
litellm.model_cost = old_cost
|
||||
if old_env is None:
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue