fix(ollama): leave JSON mode off when drop_params drops every hosted MCP tool

Ollama generate set format json for any request with tools, even after the
hosted MCP check dropped all of them, so a plain answer came back as JSON or
as a call to a tool the model never had. The check now runs before the JSON
mode switch, which stays on only while a tool is left for the prompt.
This commit is contained in:
mateo-berri 2026-10-29 15:40:44 -07:00
parent aa4e8c552e
commit 5bb6fff01e
2 changed files with 27 additions and 8 deletions

View file

@ -97,7 +97,11 @@ from litellm.litellm_core_utils.model_param_helper import peek_reasoning_summary
from litellm.litellm_core_utils.offline_mode import is_offline_mode
from litellm.litellm_core_utils.sensitive_data_masker import redact_credentials_in_payload
from litellm.litellm_core_utils.tokenizer import strip_special_tokens
from litellm.litellm_core_utils.tool_fields import MalformedField, resolve_deployment_tool_fields
from litellm.litellm_core_utils.tool_fields import (
MalformedField,
resolve_deployment_tool_fields,
tools_without_hosted_mcp,
)
from litellm.rust_bridge import tokenizer as tokenizer_dispatch
from litellm.rust_bridge.catalog import decision
from litellm.rust_bridge.configuration import Decision
@ -5168,7 +5172,12 @@ def remove_sensitive_keys_from_dict(d: dict) -> dict:
return d
def pre_process_optional_params(passed_params: dict, non_default_params: dict, custom_llm_provider: str) -> dict:
_TOOL_LIST: Final = TypeAdapter(list[object])
def pre_process_optional_params(
passed_params: dict, non_default_params: dict, custom_llm_provider: str, model: str, drop_params: bool | None
) -> dict:
"""For .completion(), preprocess optional params"""
optional_params: dict = {}
@ -5225,14 +5234,20 @@ def pre_process_optional_params(passed_params: dict, non_default_params: dict, c
and custom_llm_provider not in litellm.openai_compatible_providers
):
if custom_llm_provider == "ollama":
if "tools" in non_default_params:
sent_tools: Final = _TOOL_LIST.validate_python(non_default_params.pop("tools"))
non_default_params.pop("tool_choice", None) # causes ollama requests to hang
prompt_tools: Final = tools_without_hosted_mcp(
sent_tools, model=model, route=custom_llm_provider, drop_params=bool(drop_params)
)
if sent_tools and not prompt_tools:
return optional_params
optional_params["functions_unsupported_model"] = prompt_tools
elif "functions" in non_default_params:
optional_params["functions_unsupported_model"] = non_default_params.pop("functions")
# ollama actually supports json output
optional_params["format"] = "json"
litellm.add_function_to_prompt = True # so that main.py adds the function call to the prompt
if "tools" in non_default_params:
optional_params["functions_unsupported_model"] = non_default_params.pop("tools")
non_default_params.pop("tool_choice", None) # causes ollama requests to hang
elif "functions" in non_default_params:
optional_params["functions_unsupported_model"] = non_default_params.pop("functions")
elif litellm.add_function_to_prompt: # if user opts to add it to prompt instead
optional_params["functions_unsupported_model"] = non_default_params.pop(
"tools", non_default_params.pop("functions", None)
@ -5367,6 +5382,8 @@ def get_optional_params(
passed_params=passed_params,
non_default_params=non_default_params,
custom_llm_provider=custom_llm_provider,
model=model,
drop_params=drop_params,
)
def _check_valid_arg(supported_params: list[str]):

View file

@ -1053,7 +1053,9 @@ def test_ollama_drop_params_keeps_a_hosted_mcp_tool_out_of_the_prompt(
drop_params=True,
)
prompt: Final = json.loads(route.calls.last.request.content)["prompt"]
body: Final = json.loads(route.calls.last.request.content)
prompt: Final = body["prompt"]
assert (body.get("format") == "json") is prompt_names_weather
assert "deepwiki" not in prompt
assert ("get_weather" in prompt) is prompt_names_weather
assert prompt.startswith("### User:\nweather in Paris?")