This commit is contained in:
Purvee Singh 2026-09-02 12:36:25 -07:00 committed by GitHub
commit 58cd344bbd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 101 additions and 3 deletions

View file

@ -15,6 +15,7 @@ from typing import TYPE_CHECKING, Any, Final, Protocol, TypedDict
import httpx
from typing_extensions import ReadOnly
from litellm.exceptions import UnsupportedParamsError
from litellm.types.llms.openai import AllMessageValues, ChatCompletionToolCallChunk
from litellm.types.utils import (
ChatCompletionMessageToolCall,
@ -308,21 +309,43 @@ class SnowflakeConfig(SnowflakeBaseConfig, OpenAIGPTConfig):
return body
def _transform_tool_choice_to_anthropic(self, tool_choice: Any) -> dict[str, Any]:
def _transform_tool_choice_to_anthropic(self, tool_choice: Any) -> dict[str, Any] | None:
"""
Convert tool_choice from OpenAI format to Anthropic format.
OpenAI string values: "auto", "required", "none"
OpenAI dict: {"type": "function", "function": {"name": "..."}}
Anthropic: {"type": "auto"}, {"type": "any"}, {"type": "tool", "name": "..."}
Returns None when the value is unsupported and `litellm.drop_params`
is set, in which case the caller drops the parameter.
Unrecognized strings raise rather than defaulting to "auto". Falling
back inverts the caller's intent — "required" means the model must
call a tool, "auto" means it may and the only symptom is the model
occasionally not calling one, which reads as a model quality problem
rather than a silently dropped constraint.
"""
import litellm
if isinstance(tool_choice, str):
mapping: Final = {
"auto": {"type": "auto"},
"required": {"type": "any"},
"none": {"type": "none"},
}
return mapping.get(tool_choice, {"type": "auto"})
if tool_choice not in mapping:
if litellm.drop_params is True:
return None
raise UnsupportedParamsError(
message=(
f"Snowflake doesn't support tool_choice={tool_choice}. "
"Supported tool_choice values=['auto', 'required', 'none', json object]. "
"To drop it from the call, set `litellm.drop_params = True`."
),
status_code=400,
)
return mapping[tool_choice]
elif isinstance(tool_choice, dict):
if tool_choice.get("type") == "function":
func: Final = tool_choice.get("function", {})
@ -345,7 +368,11 @@ class SnowflakeConfig(SnowflakeBaseConfig, OpenAIGPTConfig):
optional_params["tools"] = self._transform_tools_to_anthropic(optional_params["tools"])
if "tool_choice" in optional_params:
optional_params["tool_choice"] = self._transform_tool_choice_to_anthropic(optional_params["tool_choice"])
transformed_tool_choice: Final = self._transform_tool_choice_to_anthropic(optional_params["tool_choice"])
if transformed_tool_choice is None: # unsupported + litellm.drop_params
optional_params.pop("tool_choice")
else:
optional_params["tool_choice"] = transformed_tool_choice
max_completion_tokens: Final = optional_params.pop("max_completion_tokens", None)
if max_completion_tokens and "max_tokens" not in optional_params:

View file

@ -17,6 +17,7 @@ import pytest
import litellm
from litellm import completion, acompletion
from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler, HTTPHandler
from litellm.exceptions import UnsupportedParamsError
from litellm.llms.snowflake.chat.transformation import SnowflakeConfig
from litellm.types.utils import ModelResponse
@ -118,6 +119,76 @@ class TestSnowflakeToolTransformation:
f"got {transformed_request['tool_choice']}"
)
def test_claude_tool_choice_maps_supported_string_values(self):
"""
Claude models route through the Anthropic request path, where OpenAI
string tool_choice values are translated to Anthropic's object form.
"""
config = SnowflakeConfig()
expected = {
"auto": {"type": "auto"},
"required": {"type": "any"},
"none": {"type": "none"},
}
for value, want in expected.items():
transformed_request = config.transform_request(
model="snowflake/claude-3-5-sonnet",
messages=[{"role": "user", "content": "Test"}],
optional_params={"tool_choice": value},
litellm_params={},
headers={},
)
assert transformed_request["tool_choice"] == want, (
f"tool_choice='{value}' should map to {want}, got {transformed_request['tool_choice']}"
)
@pytest.mark.parametrize(
"tool_choice",
["any", "Required", "REQUIRED", "required ", "requried", "tool"],
)
def test_claude_unsupported_tool_choice_string_raises(self, tool_choice):
"""
Unrecognized strings must raise, not silently fall back to "auto".
"required" means the model must call a tool and "auto" means it may,
so defaulting inverts the caller's intent with no error surfaced. The
lookup is exact-match, so casing and whitespace variants land here too.
"""
config = SnowflakeConfig()
with pytest.raises(UnsupportedParamsError) as exc_info:
config.transform_request(
model="snowflake/claude-3-5-sonnet",
messages=[{"role": "user", "content": "Test"}],
optional_params={"tool_choice": tool_choice},
litellm_params={},
headers={},
)
assert "tool_choice" in str(exc_info.value)
def test_claude_unsupported_tool_choice_dropped_when_drop_params_set(self):
"""With litellm.drop_params, an unsupported value is dropped, not raised."""
config = SnowflakeConfig()
original = litellm.drop_params
litellm.drop_params = True
try:
transformed_request = config.transform_request(
model="snowflake/claude-3-5-sonnet",
messages=[{"role": "user", "content": "Test"}],
optional_params={"tool_choice": "any"},
litellm_params={},
headers={},
)
finally:
litellm.drop_params = original
assert "tool_choice" not in transformed_request
def test_transform_response_with_tool_calls(self):
"""
Test that standard OpenAI tool_calls response format is parsed correctly.