mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-15 09:12:18 +00:00
Merge 50b379d85d into 570f41daad
This commit is contained in:
commit
199a2cf0c3
5 changed files with 97 additions and 22 deletions
|
|
@ -15,6 +15,7 @@ from litellm.llms.anthropic.messages.utils import (
|
|||
is_reasoning_auto_summary_enabled,
|
||||
json_copy_or_original,
|
||||
prompt_cache_key_from_user_id,
|
||||
validated_anthropic_tool_choice,
|
||||
)
|
||||
from litellm.responses.utils import auto_reasoning_summary
|
||||
|
||||
|
|
@ -803,20 +804,20 @@ class LiteLLMAnthropicMessagesAdapter:
|
|||
def translate_anthropic_tool_choice_to_openai(
|
||||
self, tool_choice: AnthropicMessagesToolChoice
|
||||
) -> ChatCompletionToolChoiceValues:
|
||||
if tool_choice["type"] == "any":
|
||||
return "required"
|
||||
elif tool_choice["type"] == "auto":
|
||||
return "auto"
|
||||
elif tool_choice["type"] == "tool":
|
||||
# Truncate tool name if it exceeds OpenAI's 64-char limit
|
||||
original_name: Final = tool_choice.get("name", "")
|
||||
truncated_name: Final = truncate_tool_name(original_name)
|
||||
tc_function_param: Final = ChatCompletionToolChoiceFunctionParam(name=truncated_name)
|
||||
return ChatCompletionToolChoiceObjectParam(type="function", function=tc_function_param)
|
||||
elif tool_choice["type"] == "none":
|
||||
return "none"
|
||||
else:
|
||||
raise ValueError(f"Incompatible tool choice param submitted - {tool_choice}")
|
||||
validated: Final = validated_anthropic_tool_choice(tool_choice)
|
||||
match validated["type"]:
|
||||
case "any":
|
||||
return "required"
|
||||
case "auto":
|
||||
return "auto"
|
||||
case "tool":
|
||||
# Truncate tool name if it exceeds OpenAI's 64-char limit
|
||||
original_name: Final = validated.get("name", "")
|
||||
truncated_name: Final = truncate_tool_name(original_name)
|
||||
tc_function_param: Final = ChatCompletionToolChoiceFunctionParam(name=truncated_name)
|
||||
return ChatCompletionToolChoiceObjectParam(type="function", function=tc_function_param)
|
||||
case "none":
|
||||
return "none"
|
||||
|
||||
def translate_anthropic_tools_to_openai(
|
||||
self, tools: list[AllAnthropicToolsValues], model: str | None = None
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ from litellm.llms.anthropic.messages.utils import (
|
|||
is_reasoning_auto_summary_enabled,
|
||||
json_copy_or_original,
|
||||
prompt_cache_key_from_user_id,
|
||||
validated_anthropic_tool_choice,
|
||||
)
|
||||
from litellm.types.llms.anthropic import (
|
||||
AllAnthropicPassThroughMessageValues,
|
||||
|
|
@ -488,14 +489,16 @@ class LiteLLMAnthropicToResponsesAPIAdapter:
|
|||
tool_choice: AnthropicMessagesToolChoice,
|
||||
) -> str | dict[str, object]:
|
||||
"""Convert Anthropic tool_choice to Responses API tool_choice."""
|
||||
tc_type: Final = tool_choice.get("type")
|
||||
if tc_type == "any":
|
||||
return "required"
|
||||
elif tc_type == "tool":
|
||||
return {"type": "function", "name": tool_choice.get("name", "")}
|
||||
elif tc_type == "none":
|
||||
return "none"
|
||||
return "auto"
|
||||
validated: Final = validated_anthropic_tool_choice(tool_choice)
|
||||
match validated["type"]:
|
||||
case "any":
|
||||
return "required"
|
||||
case "tool":
|
||||
return {"type": "function", "name": validated.get("name", "")}
|
||||
case "none":
|
||||
return "none"
|
||||
case "auto":
|
||||
return "auto"
|
||||
|
||||
@staticmethod
|
||||
def translate_context_management_to_responses_api(
|
||||
|
|
|
|||
|
|
@ -5,11 +5,13 @@ from types import MappingProxyType
|
|||
from typing import TYPE_CHECKING, Final, TypeVar
|
||||
|
||||
from pydantic import ConfigDict, TypeAdapter, ValidationError
|
||||
from pydantic_core import ErrorDetails
|
||||
from typing_extensions import ReadOnly
|
||||
|
||||
import litellm
|
||||
from litellm.constants import X_LITELLM_DISABLE_CALLBACKS
|
||||
from litellm.litellm_core_utils.internal_call_metadata import sanitize_user_api_key_auth
|
||||
from litellm.types.llms.anthropic import AnthropicMessagesToolChoice
|
||||
from litellm.types.llms.base import LiteLLMBaseModel
|
||||
from litellm.types.utils import TRUSTED_CALLBACK_VARS_FIELD, ModelInfo, StandardCallbackDynamicParams
|
||||
|
||||
|
|
@ -27,6 +29,33 @@ _EFFORT_DEGRADATION_CHAIN: Final[Mapping[str, tuple[str, ...]]] = MappingProxyTy
|
|||
)
|
||||
_THINKING_OFF: Final = "none"
|
||||
|
||||
_ANTHROPIC_TOOL_CHOICE: Final = TypeAdapter(AnthropicMessagesToolChoice)
|
||||
|
||||
|
||||
def _tool_choice_error(error: ErrorDetails) -> str:
|
||||
field: Final = ".".join(map(str, error["loc"]))
|
||||
return f"tool_choice.{field}: {error['msg']}" if field else f"tool_choice: {error['msg']}"
|
||||
|
||||
|
||||
def validated_anthropic_tool_choice(tool_choice: object) -> AnthropicMessagesToolChoice:
|
||||
"""The client's ``tool_choice`` as the Messages API defines it, or a 400 naming what is wrong with it.
|
||||
|
||||
Both ``/v1/messages`` adapters took the incoming dict on trust, so a missing or unknown ``type``
|
||||
either raised a bare ``KeyError``/``ValueError`` the Router then retried as a 500, or silently
|
||||
became ``auto`` and dropped the client's choice. The provider answers the same request with a
|
||||
400, so every route through the adapters answers with one too.
|
||||
"""
|
||||
try:
|
||||
return _ANTHROPIC_TOOL_CHOICE.validate_python(tool_choice)
|
||||
except ValidationError as invalid:
|
||||
detail: Final = "; ".join(_tool_choice_error(error) for error in invalid.errors())
|
||||
raise litellm.exceptions.BadRequestError(
|
||||
message=detail,
|
||||
model="",
|
||||
llm_provider="anthropic",
|
||||
) from invalid
|
||||
|
||||
|
||||
_JsonValue: Final = TypeVar("_JsonValue")
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -4108,6 +4108,28 @@ def test_translate_anthropic_tool_choice_none():
|
|||
assert result == "none"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"tool_choice",
|
||||
[
|
||||
{"name": "get_weather"},
|
||||
{"type": "web_search"},
|
||||
"auto",
|
||||
],
|
||||
)
|
||||
def test_translate_anthropic_tool_choice_invalid_is_a_400(tool_choice):
|
||||
"""
|
||||
Regression test for issue #46179.
|
||||
|
||||
A tool_choice the Messages API itself rejects has to come back as a 400 naming the field it
|
||||
came in on. Before, a missing type raised a bare KeyError and an unknown one a bare ValueError,
|
||||
neither carrying a status, so the Router retried them and a gateway in front reported a 500.
|
||||
"""
|
||||
adapter = LiteLLMAnthropicMessagesAdapter()
|
||||
|
||||
with pytest.raises(litellm.BadRequestError, match="tool_choice"):
|
||||
adapter.translate_anthropic_tool_choice_to_openai(tool_choice)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# PolyfillResult integration tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import pytest
|
|||
from openai.types.responses import ResponseInputImageParam
|
||||
from pydantic import TypeAdapter
|
||||
|
||||
import litellm
|
||||
from litellm.constants import (
|
||||
DEFAULT_REASONING_EFFORT_HIGH_THINKING_BUDGET,
|
||||
DEFAULT_REASONING_EFFORT_LOW_THINKING_BUDGET,
|
||||
|
|
@ -948,6 +949,25 @@ class TestTranslateToolChoiceToResponsesAPI:
|
|||
result = _ADAPTER.translate_tool_choice_to_responses_api({"type": "tool", "name": "get_weather"})
|
||||
assert result == {"type": "function", "name": "get_weather"}
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"tool_choice",
|
||||
[
|
||||
{"name": "get_weather"},
|
||||
{"type": "web_search"},
|
||||
"auto",
|
||||
],
|
||||
)
|
||||
def test_invalid_tool_choice_is_a_400(self, tool_choice):
|
||||
"""
|
||||
Regression test for issue #46179.
|
||||
|
||||
A tool_choice the Messages API itself rejects used to fall through to "auto" here, so the
|
||||
client's choice was dropped and the request went upstream as a different one. It has to
|
||||
come back as the same 400 the provider answers with.
|
||||
"""
|
||||
with pytest.raises(litellm.BadRequestError, match="tool_choice"):
|
||||
_ADAPTER.translate_tool_choice_to_responses_api(tool_choice)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# translate_thinking_to_reasoning
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue