mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-11 08:38:24 +00:00
fix(mcp): check hosted MCP tools once per request and keep base tool_choice handling
Each route now runs one request-level step before its param mapping. When the drop leaves no tools, the request sends no tools, tool_choice or parallel_tool_calls. Converse and GigaChat go back to their base tool_choice and empty-tools handling.
This commit is contained in:
parent
9a3604f56d
commit
cb9e031571
12 changed files with 140 additions and 132 deletions
|
|
@ -1,4 +1,5 @@
|
|||
from collections.abc import Callable, Mapping, Sequence
|
||||
from copy import copy
|
||||
from dataclasses import dataclass
|
||||
from functools import reduce
|
||||
from typing import Annotated, Final, TypeAlias, TypeVar
|
||||
|
|
@ -203,3 +204,24 @@ def tools_without_hosted_mcp(tools: Sequence[_ToolT], model: str, route: str, dr
|
|||
)
|
||||
verbose_logger.warning(HOSTED_MCP_DROP_WARNING, named_servers, model, route)
|
||||
return [tool for tool in tools if _hosted_mcp_server_label(tool) is None]
|
||||
|
||||
|
||||
_TOOL_SELECTION_PARAMS: Final = frozenset({"tools", "tool_choice", "parallel_tool_calls"})
|
||||
_ParamsT: Final = TypeVar("_ParamsT", bound=dict[str, object])
|
||||
|
||||
|
||||
def tool_params_without_hosted_mcp(params: _ParamsT, model: str, route: str, drop_params: bool) -> _ParamsT:
|
||||
tools: Final = params.get("tools")
|
||||
if not isinstance(tools, list) or not tools:
|
||||
return params
|
||||
sent_tools: Final = _TOOLS.validate_python(tools)
|
||||
kept_tools: Final = tools_without_hosted_mcp(tools=sent_tools, model=model, route=route, drop_params=drop_params)
|
||||
if len(kept_tools) == len(sent_tools):
|
||||
return params
|
||||
kept_params: Final = copy(params)
|
||||
if kept_tools:
|
||||
kept_params["tools"] = list(kept_tools)
|
||||
return kept_params
|
||||
for key in _TOOL_SELECTION_PARAMS:
|
||||
_ = kept_params.pop(key, None)
|
||||
return kept_params
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ from litellm.litellm_core_utils.prompt_templates.mid_conversation_system import
|
|||
place_mid_conversation_system,
|
||||
split_leading_system_run,
|
||||
)
|
||||
from litellm.litellm_core_utils.tool_fields import tools_without_hosted_mcp
|
||||
from litellm.litellm_core_utils.tool_fields import tool_params_without_hosted_mcp
|
||||
from litellm.llms.base_llm.base_utils import (
|
||||
ResponseFormatSchema,
|
||||
as_json_object,
|
||||
|
|
@ -1239,15 +1239,6 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
|
|||
self._resolved_provider
|
||||
)
|
||||
|
||||
def _tools_with_hosted_mcp_checked(
|
||||
self, tools: Sequence[object], model: str, drop_params: bool
|
||||
) -> Sequence[object]:
|
||||
if self.runs_hosted_mcp():
|
||||
return tools
|
||||
return tools_without_hosted_mcp(
|
||||
tools=tools, model=model, route=self._resolved_provider, drop_params=drop_params
|
||||
)
|
||||
|
||||
def _tools_param_with_openai_tool_search_mapped(
|
||||
self, non_default_params: Mapping[str, object], model: str, drop_params: bool
|
||||
) -> Mapping[str, object]:
|
||||
|
|
@ -1836,7 +1827,14 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
|
|||
model: str,
|
||||
drop_params: bool,
|
||||
) -> dict:
|
||||
is_thinking_enabled: Final = self.is_thinking_enabled(non_default_params=non_default_params)
|
||||
request_params: Final = (
|
||||
non_default_params
|
||||
if self.runs_hosted_mcp()
|
||||
else tool_params_without_hosted_mcp(
|
||||
non_default_params, model=model, route=self._resolved_provider, drop_params=drop_params
|
||||
)
|
||||
)
|
||||
is_thinking_enabled: Final = self.is_thinking_enabled(non_default_params=request_params)
|
||||
|
||||
# NB: ``map_openai_params`` deliberately does NOT sanitize tool names
|
||||
# here. Names are the *original* OpenAI names at this stage, and must
|
||||
|
|
@ -1849,21 +1847,12 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
|
|||
# via ``data = {**optional_params}`` and would 400 with
|
||||
# ``Extra inputs are not permitted``).
|
||||
|
||||
for param, value in non_default_params.items():
|
||||
for param, value in request_params.items():
|
||||
if param == "max_tokens" or param == "max_completion_tokens":
|
||||
optional_params["max_tokens"] = value if isinstance(value, int) else max(1, int(round(value)))
|
||||
elif param == "tools":
|
||||
checked_tools = self._tools_with_hosted_mcp_checked(
|
||||
tools=_TOOLS_ADAPTER.validate_python(value), model=model, drop_params=drop_params
|
||||
)
|
||||
if value and not checked_tools:
|
||||
continue
|
||||
anthropic_tools, mcp_servers = self._map_tools(
|
||||
list(
|
||||
self._tools_with_openai_tool_search_mapped(
|
||||
tools=checked_tools, model=model, drop_params=drop_params
|
||||
)
|
||||
)
|
||||
list(self._tools_with_openai_tool_search_mapped(tools=value, model=model, drop_params=drop_params))
|
||||
)
|
||||
optional_params = self._add_tools_to_optional_params(
|
||||
optional_params=optional_params, tools=anthropic_tools
|
||||
|
|
@ -1872,8 +1861,8 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
|
|||
optional_params["mcp_servers"] = mcp_servers
|
||||
elif param == "tool_choice" or param == "parallel_tool_calls":
|
||||
_tool_choice: AnthropicMessagesToolChoice | None = self._map_tool_choice(
|
||||
tool_choice=non_default_params.get("tool_choice"),
|
||||
parallel_tool_use=non_default_params.get("parallel_tool_calls"),
|
||||
tool_choice=request_params.get("tool_choice"),
|
||||
parallel_tool_use=request_params.get("parallel_tool_calls"),
|
||||
)
|
||||
|
||||
if _tool_choice is not None:
|
||||
|
|
@ -1899,7 +1888,7 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
|
|||
if self.uses_native_structured_output_for_schema(
|
||||
model,
|
||||
self._extract_json_schema_from_response_format(value),
|
||||
REQUEST_PARAMS.validate_python(non_default_params),
|
||||
REQUEST_PARAMS.validate_python(request_params),
|
||||
):
|
||||
_output_format = self.map_response_format_to_anthropic_output_format(value)
|
||||
if _output_format is not None:
|
||||
|
|
@ -1935,7 +1924,7 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
|
|||
# `thinking={type: enabled, budget_tokens}` interface a
|
||||
# pre-4.6 model actually supports instead of forwarding a
|
||||
# shape the model will reject.
|
||||
max_tokens = non_default_params.get("max_completion_tokens") or non_default_params.get("max_tokens")
|
||||
max_tokens = request_params.get("max_completion_tokens") or request_params.get("max_tokens")
|
||||
legacy_thinking = AnthropicConfig._map_reasoning_effort(
|
||||
reasoning_effort="medium",
|
||||
model=model,
|
||||
|
|
@ -2028,7 +2017,7 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
|
|||
|
||||
## handle thinking tokens
|
||||
self.update_optional_params_with_thinking_tokens(
|
||||
non_default_params=non_default_params, optional_params=optional_params
|
||||
non_default_params=request_params, optional_params=optional_params
|
||||
)
|
||||
|
||||
return optional_params
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ from litellm.litellm_core_utils.prompt_templates.mid_conversation_system import
|
|||
message_field,
|
||||
parts_of,
|
||||
)
|
||||
from litellm.litellm_core_utils.tool_fields import tools_without_hosted_mcp
|
||||
from litellm.litellm_core_utils.tool_fields import tool_params_without_hosted_mcp
|
||||
from litellm.llms.anthropic.chat.transformation import (
|
||||
DROP_UNSUPPORTED_ADAPTIVE_THINKING_WARNING,
|
||||
DROP_UNSUPPORTED_OUTPUT_CONFIG_WARNING,
|
||||
|
|
@ -1056,20 +1056,23 @@ class AmazonConverseConfig(BaseConfig):
|
|||
drop_params: bool,
|
||||
model_info: Mapping[str, object] | None = None,
|
||||
) -> dict:
|
||||
is_thinking_enabled: Final = self.is_thinking_enabled(non_default_params)
|
||||
request_params: Final = tool_params_without_hosted_mcp(
|
||||
non_default_params, model=model, route="bedrock", drop_params=drop_params
|
||||
)
|
||||
is_thinking_enabled: Final = self.is_thinking_enabled(request_params)
|
||||
base_model: Final = BedrockModelInfo.get_base_model(model)
|
||||
drop_thinking_param: Final = self._is_deepseek_model(model=model, base_model=base_model)
|
||||
drop_reasoning_effort_param: Final = self._model_rejects_reasoning_effort_param(
|
||||
model=model, base_model=base_model
|
||||
)
|
||||
|
||||
for param, value in non_default_params.items():
|
||||
for param, value in request_params.items():
|
||||
if param == "response_format" and isinstance(value, dict):
|
||||
optional_params = self._translate_response_format_param(
|
||||
value=value,
|
||||
model=model,
|
||||
optional_params=optional_params,
|
||||
non_default_params=non_default_params,
|
||||
non_default_params=request_params,
|
||||
is_thinking_enabled=is_thinking_enabled,
|
||||
model_info=model_info,
|
||||
)
|
||||
|
|
@ -1114,12 +1117,7 @@ class AmazonConverseConfig(BaseConfig):
|
|||
list[OpenAIChatCompletionToolParam],
|
||||
list(
|
||||
tools_without_unsupported_tool_search(
|
||||
tools=tools_without_hosted_mcp(
|
||||
tools=_TOOLS_AS_RECEIVED.validate_python(value),
|
||||
model=model,
|
||||
route="bedrock",
|
||||
drop_params=drop_params,
|
||||
),
|
||||
tools=_TOOLS_AS_RECEIVED.validate_python(value),
|
||||
model=model,
|
||||
route="bedrock",
|
||||
drop_params=drop_params,
|
||||
|
|
@ -1128,7 +1126,7 @@ class AmazonConverseConfig(BaseConfig):
|
|||
),
|
||||
),
|
||||
model=model,
|
||||
non_default_params=non_default_params,
|
||||
non_default_params=request_params,
|
||||
optional_params=optional_params,
|
||||
)
|
||||
if param == "tool_choice":
|
||||
|
|
@ -1154,7 +1152,7 @@ class AmazonConverseConfig(BaseConfig):
|
|||
and value.get("type") == "adaptive"
|
||||
and not AnthropicConfig.is_adaptive_thinking_model(model, "bedrock")
|
||||
):
|
||||
max_tokens = non_default_params.get("max_completion_tokens") or non_default_params.get("max_tokens")
|
||||
max_tokens = request_params.get("max_completion_tokens") or request_params.get("max_tokens")
|
||||
legacy_thinking = AnthropicConfig.map_reasoning_effort(
|
||||
reasoning_effort="medium",
|
||||
model=model,
|
||||
|
|
@ -1231,7 +1229,7 @@ class AmazonConverseConfig(BaseConfig):
|
|||
# Nova 2 handles token budgeting differently through reasoningConfig
|
||||
if "gpt-oss" not in model and not self._is_nova_2_model(model):
|
||||
self.update_optional_params_with_thinking_tokens(
|
||||
non_default_params=non_default_params, optional_params=optional_params
|
||||
non_default_params=request_params, optional_params=optional_params
|
||||
)
|
||||
|
||||
final_is_thinking_enabled: Final = self.is_thinking_enabled(optional_params)
|
||||
|
|
@ -2062,8 +2060,8 @@ class AmazonConverseConfig(BaseConfig):
|
|||
break
|
||||
|
||||
bedrock_tool_config: ToolConfigBlock | None = None
|
||||
tool_choice_values: Final[ToolChoiceValuesBlock | None] = inference_params.pop("tool_choice", None)
|
||||
if len(bedrock_tools) > 0:
|
||||
tool_choice_values: Final[ToolChoiceValuesBlock] = inference_params.pop("tool_choice", None)
|
||||
bedrock_tool_config = ToolConfigBlock(
|
||||
tools=bedrock_tools,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ from litellm.litellm_core_utils.prompt_templates.common_utils import (
|
|||
strip_name_from_message,
|
||||
strip_prefill_prefix_field,
|
||||
)
|
||||
from litellm.litellm_core_utils.tool_fields import tools_without_hosted_mcp
|
||||
from litellm.litellm_core_utils.tool_fields import tool_params_without_hosted_mcp
|
||||
from litellm.litellm_core_utils.url_utils import join_api_base
|
||||
from litellm.llms.base_llm.base_model_iterator import BaseModelResponseIterator
|
||||
from litellm.llms.base_llm.base_utils import (
|
||||
|
|
@ -343,7 +343,7 @@ class DatabricksConfig(DatabricksBase, OpenAILikeChatConfig, AnthropicConfig):
|
|||
use_ai_gateway: Final = self.uses_ai_gateway(model)
|
||||
return join_api_base(self._get_api_base(api_base, use_ai_gateway=use_ai_gateway), "chat/completions")
|
||||
|
||||
def get_supported_openai_params(self, model: str | None = None) -> list:
|
||||
def get_supported_openai_params(self, model: str | None = None) -> list[str]:
|
||||
return [
|
||||
"stream",
|
||||
"stop",
|
||||
|
|
@ -398,19 +398,14 @@ class DatabricksConfig(DatabricksBase, OpenAILikeChatConfig, AnthropicConfig):
|
|||
anthropic_tools, _ = self._map_tools(
|
||||
tools=list(
|
||||
tools_without_unsupported_tool_search(
|
||||
tools=tools_without_hosted_mcp(
|
||||
tools=_TOOLS_ADAPTER.validate_python(tools),
|
||||
model=model,
|
||||
route="databricks",
|
||||
drop_params=drop_params,
|
||||
),
|
||||
tools=_TOOLS_ADAPTER.validate_python(tools),
|
||||
model=model,
|
||||
route="databricks",
|
||||
drop_params=drop_params,
|
||||
remedy="",
|
||||
)
|
||||
)
|
||||
)
|
||||
) # unclear how mcp tool calling on databricks works
|
||||
databricks_tools: Final = [
|
||||
cast(DatabricksTool, self.convert_anthropic_tool_to_databricks_tool(tool)) for tool in anthropic_tools
|
||||
]
|
||||
|
|
@ -454,30 +449,31 @@ class DatabricksConfig(DatabricksBase, OpenAILikeChatConfig, AnthropicConfig):
|
|||
drop_params: bool,
|
||||
replace_max_completion_tokens_with_max_tokens: bool = True,
|
||||
) -> dict:
|
||||
is_thinking_enabled: Final = self.is_thinking_enabled(non_default_params)
|
||||
mapped_params: Final = super().map_openai_params(non_default_params, optional_params, model, drop_params)
|
||||
request_params: Final = (
|
||||
tool_params_without_hosted_mcp(non_default_params, model=model, route="databricks", drop_params=drop_params)
|
||||
if "claude" in model
|
||||
else non_default_params
|
||||
)
|
||||
is_thinking_enabled: Final = self.is_thinking_enabled(request_params)
|
||||
mapped_params: Final = super().map_openai_params(request_params, optional_params, model, drop_params)
|
||||
if "claude" in model:
|
||||
AnthropicConfig.translate_legacy_thinking_for_adaptive_model(
|
||||
model=model, optional_params=mapped_params, custom_llm_provider="databricks", drop_params=drop_params
|
||||
)
|
||||
if "tools" in mapped_params:
|
||||
dbrx_tools: Final = self._map_openai_to_dbrx_tool(
|
||||
mapped_params["tools"] = self._map_openai_to_dbrx_tool(
|
||||
model=model, tools=mapped_params["tools"], drop_params=drop_params
|
||||
)
|
||||
if mapped_params["tools"] and not dbrx_tools:
|
||||
del mapped_params["tools"]
|
||||
else:
|
||||
mapped_params["tools"] = dbrx_tools
|
||||
if "max_completion_tokens" in non_default_params and replace_max_completion_tokens_with_max_tokens:
|
||||
mapped_params["max_tokens"] = non_default_params[
|
||||
if "max_completion_tokens" in request_params and replace_max_completion_tokens_with_max_tokens:
|
||||
mapped_params["max_tokens"] = request_params[
|
||||
"max_completion_tokens"
|
||||
] # most openai-compatible providers support 'max_tokens' not 'max_completion_tokens'
|
||||
mapped_params.pop("max_completion_tokens", None)
|
||||
|
||||
if "response_format" in non_default_params and "claude" in model:
|
||||
if "response_format" in request_params and "claude" in model:
|
||||
_tool: Final = self.map_response_format_to_databricks_tool(
|
||||
model,
|
||||
non_default_params["response_format"],
|
||||
request_params["response_format"],
|
||||
mapped_params,
|
||||
is_thinking_enabled,
|
||||
)
|
||||
|
|
@ -495,8 +491,8 @@ class DatabricksConfig(DatabricksBase, OpenAILikeChatConfig, AnthropicConfig):
|
|||
"response_format", None
|
||||
) # unsupported for claude models - if json_schema -> convert to tool call
|
||||
|
||||
if "reasoning_effort" in non_default_params and self._uses_anthropic_thinking_param(model):
|
||||
reasoning_effort_value: Final = non_default_params.get("reasoning_effort")
|
||||
if "reasoning_effort" in request_params and self._uses_anthropic_thinking_param(model):
|
||||
reasoning_effort_value: Final = request_params.get("reasoning_effort")
|
||||
mapped_thinking: Final = AnthropicConfig._map_reasoning_effort(
|
||||
reasoning_effort=reasoning_effort_value,
|
||||
model=model,
|
||||
|
|
@ -522,7 +518,7 @@ class DatabricksConfig(DatabricksBase, OpenAILikeChatConfig, AnthropicConfig):
|
|||
optional_params.pop("reasoning_effort", None)
|
||||
## handle thinking tokens
|
||||
self.update_optional_params_with_thinking_tokens(
|
||||
non_default_params=non_default_params, optional_params=mapped_params
|
||||
non_default_params=request_params, optional_params=mapped_params
|
||||
)
|
||||
|
||||
return mapped_params
|
||||
|
|
|
|||
|
|
@ -5,14 +5,12 @@ Translates from OpenAI's `/v1/chat/completions` to DeepSeek's `/v1/chat/completi
|
|||
from collections.abc import Coroutine, Mapping, Sequence
|
||||
from typing import Final, Literal, cast, overload
|
||||
|
||||
from pydantic import TypeAdapter
|
||||
|
||||
import litellm
|
||||
from litellm.litellm_core_utils.prompt_templates.common_utils import (
|
||||
convert_content_list_to_str,
|
||||
extract_search_results_text,
|
||||
)
|
||||
from litellm.litellm_core_utils.tool_fields import tools_without_hosted_mcp
|
||||
from litellm.litellm_core_utils.tool_fields import tool_params_without_hosted_mcp
|
||||
from litellm.litellm_core_utils.url_utils import ensure_api_route
|
||||
from litellm.secret_managers.main import get_secret_str
|
||||
from litellm.types.llms.openai import AllMessageValues
|
||||
|
|
@ -20,23 +18,19 @@ from litellm.utils import supports_reasoning, supports_vision
|
|||
|
||||
from ...openai.chat.gpt_transformation import OpenAIGPTConfig
|
||||
|
||||
_TOOLS_AS_SENT: Final = TypeAdapter(list[object])
|
||||
_TOOL_SELECTION_PARAMS: Final = frozenset({"tools", "tool_choice", "parallel_tool_calls"})
|
||||
SUPPORTED_REASONING_EFFORTS: Final = ("minimal", "low", "medium", "high", "xhigh", "max", "ultra")
|
||||
|
||||
|
||||
class DeepSeekChatConfig(OpenAIGPTConfig):
|
||||
def get_supported_openai_params(self, model: str) -> list:
|
||||
def get_supported_openai_params(self, model: str) -> list[str]:
|
||||
"""
|
||||
DeepSeek reasoner models support thinking parameter.
|
||||
"""
|
||||
params: Final = super().get_supported_openai_params(model)
|
||||
params.extend(["thinking", "reasoning_effort"])
|
||||
return params
|
||||
return [*super().get_supported_openai_params(model), "thinking", "reasoning_effort"]
|
||||
|
||||
def map_openai_params(
|
||||
self,
|
||||
non_default_params: dict,
|
||||
non_default_params: dict[str, object],
|
||||
optional_params: dict,
|
||||
model: str,
|
||||
drop_params: bool,
|
||||
|
|
@ -52,11 +46,12 @@ class DeepSeekChatConfig(OpenAIGPTConfig):
|
|||
Reference: https://api-docs.deepseek.com/guides/thinking_mode
|
||||
"""
|
||||
# Let parent handle standard params first
|
||||
optional_params = {
|
||||
**self._without_hosted_mcp_tools(
|
||||
super().map_openai_params(non_default_params, optional_params, model, drop_params), model, drop_params
|
||||
)
|
||||
}
|
||||
optional_params = super().map_openai_params(
|
||||
tool_params_without_hosted_mcp(non_default_params, model=model, route="deepseek", drop_params=drop_params),
|
||||
optional_params,
|
||||
model,
|
||||
drop_params,
|
||||
)
|
||||
|
||||
# Pop thinking/reasoning_effort from optional_params first (parent may have added them)
|
||||
# Then re-add only if valid for DeepSeek
|
||||
|
|
@ -243,20 +238,6 @@ class DeepSeekChatConfig(OpenAIGPTConfig):
|
|||
and thinking.get("type") == "enabled"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _without_hosted_mcp_tools(
|
||||
optional_params: Mapping[str, object], model: str, drop_params: bool
|
||||
) -> Mapping[str, object]:
|
||||
tools: Final = optional_params.get("tools")
|
||||
if not isinstance(tools, list) or not tools:
|
||||
return optional_params
|
||||
kept_tools: Final = tools_without_hosted_mcp(
|
||||
tools=_TOOLS_AS_SENT.validate_python(tools), model=model, route="deepseek", drop_params=drop_params
|
||||
)
|
||||
if kept_tools:
|
||||
return {**optional_params, "tools": list(kept_tools)}
|
||||
return {k: v for k, v in optional_params.items() if k not in _TOOL_SELECTION_PARAMS}
|
||||
|
||||
@staticmethod
|
||||
def _drop_unsupported_tools(optional_params: dict) -> dict:
|
||||
"""
|
||||
|
|
@ -275,6 +256,7 @@ class DeepSeekChatConfig(OpenAIGPTConfig):
|
|||
tools: Final = optional_params.get("tools")
|
||||
if not isinstance(tools, list) or not tools:
|
||||
return optional_params
|
||||
sent_tools: Final[list[object]] = tools
|
||||
|
||||
def _is_function_tool(tool: object) -> bool:
|
||||
return isinstance(tool, dict) and tool.get("type") == "function"
|
||||
|
|
@ -299,14 +281,14 @@ class DeepSeekChatConfig(OpenAIGPTConfig):
|
|||
name: Final = function.get("name")
|
||||
return isinstance(name, str) and name in function_tool_names
|
||||
|
||||
function_tools: Final = [tool for tool in tools if _is_function_tool(tool)]
|
||||
if len(function_tools) == len(tools):
|
||||
function_tools: Final = [tool for tool in sent_tools if _is_function_tool(tool)]
|
||||
if len(function_tools) == len(sent_tools):
|
||||
return optional_params
|
||||
|
||||
dropped_types: Final = sorted(
|
||||
{
|
||||
str(tool.get("type")) if isinstance(tool, dict) else type(tool).__name__
|
||||
for tool in tools
|
||||
for tool in sent_tools
|
||||
if not _is_function_tool(tool)
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import httpx
|
|||
from pydantic import TypeAdapter
|
||||
|
||||
from litellm._logging import verbose_logger
|
||||
from litellm.litellm_core_utils.tool_fields import tools_without_hosted_mcp
|
||||
from litellm.litellm_core_utils.tool_fields import tool_params_without_hosted_mcp
|
||||
from litellm.litellm_core_utils.url_utils import join_api_base
|
||||
from litellm.llms.base_llm.chat.transformation import BaseConfig, BaseLLMException
|
||||
from litellm.llms.gigachat.utils import convert_usage, get_api_base
|
||||
|
|
@ -39,7 +39,6 @@ else:
|
|||
|
||||
_EMPTY_FUNCTION: Final[Mapping[str, object]] = MappingProxyType({})
|
||||
_JSON_VALUE: Final = TypeAdapter(object)
|
||||
_TOOLS_AS_SENT: Final = TypeAdapter(list[object])
|
||||
|
||||
|
||||
def is_valid_json(value: str) -> bool:
|
||||
|
|
@ -162,7 +161,10 @@ class GigaChatConfig(BaseConfig):
|
|||
drop_params: bool,
|
||||
) -> dict: # mutable-ok: base class contract returns dict
|
||||
"""Map OpenAI parameters to GigaChat parameters."""
|
||||
for param, value in non_default_params.items():
|
||||
request_params: Final = tool_params_without_hosted_mcp(
|
||||
dict(non_default_params), model=model, route="gigachat", drop_params=drop_params
|
||||
)
|
||||
for param, value in request_params.items():
|
||||
if param == "stream":
|
||||
optional_params["stream"] = value
|
||||
elif param == "temperature":
|
||||
|
|
@ -181,14 +183,7 @@ class GigaChatConfig(BaseConfig):
|
|||
elif param == "tools":
|
||||
# Convert tools to functions format
|
||||
if isinstance(value, Sequence):
|
||||
gigachat_tools = tools_without_hosted_mcp(
|
||||
tools=_TOOLS_AS_SENT.validate_python(value),
|
||||
model=model,
|
||||
route="gigachat",
|
||||
drop_params=drop_params,
|
||||
)
|
||||
if gigachat_tools:
|
||||
optional_params["functions"] = self._convert_tools_to_functions(gigachat_tools)
|
||||
optional_params["functions"] = self._convert_tools_to_functions(value)
|
||||
elif param == "tool_choice":
|
||||
# Map OpenAI tool_choice to GigaChat function_call
|
||||
if isinstance(value, (str, Mapping)):
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ from litellm.litellm_core_utils.json_fragment_accumulator import JSONFragmentAcc
|
|||
from litellm.litellm_core_utils.prompt_templates.factory import (
|
||||
encode_tool_call_id_with_signature,
|
||||
)
|
||||
from litellm.litellm_core_utils.tool_fields import tools_without_hosted_mcp
|
||||
from litellm.litellm_core_utils.tool_fields import tool_params_without_hosted_mcp
|
||||
from litellm.llms.base_llm.chat.transformation import BaseConfig, BaseLLMException
|
||||
from litellm.llms.custom_httpx.http_handler import (
|
||||
AsyncHTTPHandler,
|
||||
|
|
@ -252,7 +252,6 @@ _GEMINI_SPEECH_CONFIG_KEY_MAP: Final[Mapping[str, str]] = MappingProxyType(
|
|||
|
||||
|
||||
_SPEECH_CONFIG_ITEMS: Final = TypeAdapter(list[object])
|
||||
_TOOLS_AS_SENT: Final = TypeAdapter(list[object])
|
||||
|
||||
|
||||
def _normalize_gemini_speech_config_item(value: Mapping[str, object]) -> Mapping[str, object]:
|
||||
|
|
@ -1027,9 +1026,7 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig):
|
|||
|
||||
return googleSearch, googleSearchRetrieval, enterpriseWebSearch, urlContext
|
||||
|
||||
def _map_function(
|
||||
self, value: Sequence[object], optional_params: dict, send_integer_enums: bool = False
|
||||
) -> list[Tools]:
|
||||
def _map_function(self, value: list[dict], optional_params: dict, send_integer_enums: bool = False) -> list[Tools]:
|
||||
"""
|
||||
Map OpenAI-style tools/functions to Vertex AI format.
|
||||
|
||||
|
|
@ -1621,9 +1618,12 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig):
|
|||
drop_params: bool,
|
||||
allow_integer_enums: bool = True,
|
||||
) -> dict:
|
||||
self._apply_include_server_side_tool_invocations(non_default_params, optional_params)
|
||||
request_params: Final = tool_params_without_hosted_mcp(
|
||||
non_default_params, model=model, route=self._route, drop_params=drop_params
|
||||
)
|
||||
self._apply_include_server_side_tool_invocations(request_params, optional_params)
|
||||
supports_sampling_params: Final = self.supports_sampling_params(model)
|
||||
for param, value in non_default_params.items():
|
||||
for param, value in request_params.items():
|
||||
if param in ("temperature", "top_p", "top_k") and not supports_sampling_params:
|
||||
continue
|
||||
if param == "temperature":
|
||||
|
|
@ -1673,19 +1673,9 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig):
|
|||
elif param == "top_logprobs":
|
||||
optional_params["logprobs"] = value
|
||||
elif (param == "tools" or param == "functions") and isinstance(value, list) and value:
|
||||
gemini_tools = list(
|
||||
tools_without_hosted_mcp(
|
||||
tools=_TOOLS_AS_SENT.validate_python(value),
|
||||
model=model,
|
||||
route=self._route,
|
||||
drop_params=drop_params,
|
||||
)
|
||||
)
|
||||
if not gemini_tools:
|
||||
continue
|
||||
# Pass optional_params so _map_function can add toolConfig if needed
|
||||
mapped_tools = with_integer_enums_that_fit(
|
||||
partial(self._map_function, gemini_tools, optional_params),
|
||||
partial(self._map_function, value, optional_params),
|
||||
allow_integer_enums and self._is_gemini_3_or_newer(model),
|
||||
)
|
||||
optional_params = self._add_tools_to_optional_params(optional_params, mapped_tools)
|
||||
|
|
@ -1697,7 +1687,7 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig):
|
|||
if _tool_choice_value is not None:
|
||||
optional_params["tool_choice"] = _tool_choice_value
|
||||
elif param == "parallel_tool_calls":
|
||||
tools_list = non_default_params.get("tools", non_default_params.get("functions"))
|
||||
tools_list = request_params.get("tools", request_params.get("functions"))
|
||||
num_tools = len(tools_list) if isinstance(tools_list, list) else 0
|
||||
# Gemini does not support parallel_tool_calls=False with multiple
|
||||
# tools. Drop the param instead of failing — Responses API clients
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import pytest
|
|||
import litellm
|
||||
from litellm import Router
|
||||
from litellm.litellm_core_utils.litellm_logging import Logging
|
||||
from litellm.litellm_core_utils.tool_fields import tools_without_hosted_mcp
|
||||
from litellm.litellm_core_utils.tool_fields import tool_params_without_hosted_mcp, tools_without_hosted_mcp
|
||||
from litellm.utils import async_pre_call_deployment_hook
|
||||
|
||||
_GOOD_TOOL: Final = {"type": "function", "function": {"name": "get_weather", "parameters": {"type": "object"}}}
|
||||
|
|
@ -201,3 +201,36 @@ def test_a_hosted_mcp_error_names_every_server_and_marks_an_unlabeled_one(monkey
|
|||
drop_params=False,
|
||||
)
|
||||
assert "the hosted MCP tool `deepwiki`, `unlabeled` " in str(raised.value)
|
||||
|
||||
|
||||
_HOSTED_MCP_TOOL: Final = {"type": "mcp", "server_label": "deepwiki", "server_url": "https://mcp.deepwiki.com/mcp"}
|
||||
|
||||
|
||||
def test_dropping_a_hosted_mcp_tool_keeps_the_other_tool_params_and_leaves_the_caller_dict_alone() -> None:
|
||||
tool_choice: Final = {"type": "function", "function": {"name": "get_weather"}}
|
||||
sent: Final = {
|
||||
"tools": [_HOSTED_MCP_TOOL, _GOOD_TOOL],
|
||||
"tool_choice": tool_choice,
|
||||
"parallel_tool_calls": False,
|
||||
"temperature": 0.2,
|
||||
}
|
||||
params: Final = tool_params_without_hosted_mcp(sent, model="deepseek-v4-flash", route="deepseek", drop_params=True)
|
||||
|
||||
assert params == {
|
||||
"tools": [_GOOD_TOOL],
|
||||
"tool_choice": tool_choice,
|
||||
"parallel_tool_calls": False,
|
||||
"temperature": 0.2,
|
||||
}
|
||||
assert sent["tools"] == [_HOSTED_MCP_TOOL, _GOOD_TOOL]
|
||||
|
||||
|
||||
def test_dropping_every_hosted_mcp_tool_clears_the_tool_selection_params() -> None:
|
||||
params: Final = tool_params_without_hosted_mcp(
|
||||
{"tools": [_HOSTED_MCP_TOOL], "tool_choice": "required", "parallel_tool_calls": True, "temperature": 0.2},
|
||||
model="deepseek-v4-flash",
|
||||
route="deepseek",
|
||||
drop_params=True,
|
||||
)
|
||||
|
||||
assert params == {"temperature": 0.2}
|
||||
|
|
|
|||
|
|
@ -9878,10 +9878,11 @@ def test_claude_routes_without_the_mcp_connector_send_no_tools_when_drop_params_
|
|||
):
|
||||
monkeypatch.setattr(litellm, "drop_params", False)
|
||||
mapped: Final = config_class().map_openai_params(
|
||||
non_default_params={"tools": [_HOSTED_MCP_TOOL]},
|
||||
non_default_params={"tools": [_HOSTED_MCP_TOOL], "tool_choice": "required"},
|
||||
optional_params={},
|
||||
model="claude-haiku-5-5",
|
||||
drop_params=True,
|
||||
)
|
||||
assert "tools" not in mapped
|
||||
assert "mcp_servers" not in mapped
|
||||
assert "tool_choice" not in mapped
|
||||
|
|
|
|||
|
|
@ -2784,9 +2784,10 @@ def test_databricks_claude_drops_a_hosted_mcp_tool_under_drop_params_and_keeps_t
|
|||
def test_databricks_claude_sends_no_tools_when_drop_params_drops_every_hosted_mcp_tool(monkeypatch):
|
||||
monkeypatch.setattr(litellm, "drop_params", False)
|
||||
mapped: Final = DatabricksConfig().map_openai_params(
|
||||
non_default_params={"tools": [_HOSTED_MCP_TOOL]},
|
||||
non_default_params={"tools": [_HOSTED_MCP_TOOL], "tool_choice": "required"},
|
||||
optional_params={},
|
||||
model="databricks-claude-opus-5-5",
|
||||
drop_params=True,
|
||||
)
|
||||
assert "tools" not in mapped
|
||||
assert "tool_choice" not in mapped
|
||||
|
|
|
|||
|
|
@ -623,10 +623,10 @@ def test_gigachat_drops_a_hosted_mcp_tool_under_drop_params_and_keeps_the_functi
|
|||
def test_gigachat_sends_no_functions_when_drop_params_drops_every_hosted_mcp_tool(monkeypatch):
|
||||
monkeypatch.setattr(litellm, "drop_params", False)
|
||||
mapped: Final = GigaChatConfig().map_openai_params(
|
||||
non_default_params={"tools": [_HOSTED_MCP_TOOL]},
|
||||
non_default_params={"tools": [_HOSTED_MCP_TOOL], "tool_choice": "auto"},
|
||||
optional_params={},
|
||||
model="GigaChat-2-Max",
|
||||
drop_params=True,
|
||||
)
|
||||
assert "functions" not in mapped
|
||||
assert "tools" not in mapped
|
||||
assert "function_call" not in mapped
|
||||
|
|
|
|||
|
|
@ -10712,9 +10712,10 @@ def test_gemini_drops_a_hosted_mcp_tool_under_drop_params_and_keeps_the_function
|
|||
def test_gemini_sends_no_tools_when_drop_params_drops_every_hosted_mcp_tool(monkeypatch, config_class, route):
|
||||
monkeypatch.setattr(litellm, "drop_params", False)
|
||||
mapped: Final = config_class().map_openai_params(
|
||||
non_default_params={"tools": [_HOSTED_MCP_TOOL]},
|
||||
non_default_params={"tools": [_HOSTED_MCP_TOOL], "tool_choice": "required"},
|
||||
optional_params={},
|
||||
model="gemini-3.8-flash",
|
||||
drop_params=True,
|
||||
)
|
||||
assert "tools" not in mapped
|
||||
assert "tool_choice" not in mapped
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue