From 87ac3d616d2802664755ab3dc914db5911e61518 Mon Sep 17 00:00:00 2001 From: Ankit Kumar Date: Fri, 2 Oct 2026 23:44:20 +0530 Subject: [PATCH 1/4] Python: fix(ollama): omit tools when tool_choice is none --- .../agent_framework_ollama/_chat_client.py | 12 +++-- .../ollama/tests/test_ollama_chat_client.py | 46 +++++++++++++++++++ 2 files changed, 55 insertions(+), 3 deletions(-) diff --git a/python/packages/ollama/agent_framework_ollama/_chat_client.py b/python/packages/ollama/agent_framework_ollama/_chat_client.py index 7b27a4c660f..01318553c6d 100644 --- a/python/packages/ollama/agent_framework_ollama/_chat_client.py +++ b/python/packages/ollama/agent_framework_ollama/_chat_client.py @@ -32,6 +32,7 @@ Message, ResponseStream, UsageDetails, + validate_tool_mode, ) from agent_framework._settings import load_settings from agent_framework._telemetry import mark_feature_used @@ -97,7 +98,7 @@ class OllamaChatOptions(ChatOptions[ResponseModelT], Generic[ResponseModelT], to (converted to its JSON schema) for structured output. # Options not supported in Ollama: - tool_choice: Ollama only supports auto tool choice. + tool_choice: Ollama only supports auto tool choice, but ``none`` is honored by omitting the tools. allow_multiple_tool_calls: Not configurable. user: Not supported. store: Not supported. @@ -217,7 +218,7 @@ class OllamaChatOptions(ChatOptions[ResponseModelT], Generic[ResponseModelT], to # ChatOptions fields not supported in Ollama tool_choice: None # type: ignore[misc] - """Not supported. Ollama only supports auto tool choice.""" + """Not supported. Ollama only supports auto tool choice, but ``none`` is honored by omitting the tools.""" allow_multiple_tool_calls: None # type: ignore[misc] """Not supported. Not configurable in Ollama.""" @@ -454,7 +455,12 @@ def _prepare_options(self, messages: Sequence[Message], options: Mapping[str, An # tools tools = options.get("tools") - if tools is not None and (prepared_tools := self._prepare_tools_for_ollama(tools)): + tool_mode = validate_tool_mode(options.get("tool_choice")) + if tool_mode and tool_mode.get("mode") == "none": + # Ollama has no tool_choice parameter. + # Omit tools entirely so the model won't attempt tool calls. + run_options.pop("tools", None) + elif tools is not None and (prepared_tools := self._prepare_tools_for_ollama(tools)): run_options["tools"] = prepared_tools return run_options diff --git a/python/packages/ollama/tests/test_ollama_chat_client.py b/python/packages/ollama/tests/test_ollama_chat_client.py index 8c5e21172f2..219ef3530b5 100644 --- a/python/packages/ollama/tests/test_ollama_chat_client.py +++ b/python/packages/ollama/tests/test_ollama_chat_client.py @@ -907,3 +907,49 @@ def test_prepare_options_single_stop_string_becomes_list(ollama_unit_test_env: d request = client._prepare_options(messages, {"stop": "END"}) assert request["options"]["stop"] == ["END"] + + +@pytest.mark.parametrize("tool_choice", ["none", {"mode": "none"}]) +def test_prepare_options_tool_choice_none_omits_tools(ollama_unit_test_env: dict[str, str], tool_choice: Any) -> None: + """Ollama has no tool_choice parameter, so "none" is honored by not offering the tools.""" + client = OllamaChatClient() + messages = [Message(role="user", contents=[Content.from_text(text="hello")])] + + request = client._prepare_options(messages, {"tools": [hello_world], "tool_choice": tool_choice}) + + assert "tools" not in request + assert "tool_choice" not in request + + +@pytest.mark.parametrize("tool_choice", [None, "auto", "required"]) +def test_prepare_options_other_tool_choices_keep_tools(ollama_unit_test_env: dict[str, str], tool_choice: Any) -> None: + """Tool choices other than "none" still send the tools to Ollama.""" + client = OllamaChatClient() + messages = [Message(role="user", contents=[Content.from_text(text="hello")])] + + request = client._prepare_options(messages, {"tools": [hello_world], "tool_choice": tool_choice}) + + assert request["tools"] == [hello_world.to_json_schema_spec()] + assert "tool_choice" not in request + + +@patch.object(AsyncClient, "chat", new_callable=AsyncMock) +async def test_cmc_function_invocation_limit_final_request_omits_tools( + mock_chat: AsyncMock, + ollama_unit_test_env: dict[str, str], + chat_history: list[Message], + mock_chat_completion_tool_call: OllamaChatResponse, + mock_chat_completion_response: OllamaChatResponse, +) -> None: + """After the function invocation limit, the final request must not offer tools to Ollama.""" + mock_chat.side_effect = [mock_chat_completion_tool_call, mock_chat_completion_response] + chat_history.append(Message(contents=["hello world"], role="user")) + + ollama_client = OllamaChatClient() + ollama_client.function_invocation_configuration["max_iterations"] = 1 + result = await ollama_client.get_response(messages=chat_history, options={"tools": [hello_world]}) + + assert mock_chat.call_count == 2 + assert "tools" in mock_chat.call_args_list[0].kwargs + assert "tools" not in mock_chat.call_args_list[1].kwargs + assert result.text == "test" From 652a4f9edf2493b82a4d648dfd3391a76a572cd5 Mon Sep 17 00:00:00 2001 From: Ankit Kumar Date: Tue, 6 Oct 2026 21:27:35 +0530 Subject: [PATCH 2/4] Python: fix(ollama): handle tools in one place and test invalid tool_choice --- .../agent_framework_ollama/_chat_client.py | 10 ++++------ .../ollama/tests/test_ollama_chat_client.py | 16 +++++++++++++++- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/python/packages/ollama/agent_framework_ollama/_chat_client.py b/python/packages/ollama/agent_framework_ollama/_chat_client.py index 01318553c6d..d34a2ff6b23 100644 --- a/python/packages/ollama/agent_framework_ollama/_chat_client.py +++ b/python/packages/ollama/agent_framework_ollama/_chat_client.py @@ -408,7 +408,7 @@ def _prepare_options(self, messages: Sequence[Message], options: Mapping[str, An messages = prepend_instructions_to_messages(list(messages), instructions, role="system") # Keys to exclude from processing - exclude_keys = {"instructions", "tool_choice"} + exclude_keys = {"instructions", "tool_choice", "tools"} # Build run_options and model_options separately run_options: dict[str, Any] = {} @@ -454,13 +454,11 @@ def _prepare_options(self, messages: Sequence[Message], options: Mapping[str, An run_options["model"] = self.model # tools + # Ollama has no tool_choice parameter, so "none" is honored by not sending the tools at all. tools = options.get("tools") tool_mode = validate_tool_mode(options.get("tool_choice")) - if tool_mode and tool_mode.get("mode") == "none": - # Ollama has no tool_choice parameter. - # Omit tools entirely so the model won't attempt tool calls. - run_options.pop("tools", None) - elif tools is not None and (prepared_tools := self._prepare_tools_for_ollama(tools)): + tools_disabled = tool_mode is not None and tool_mode.get("mode") == "none" + if tools is not None and not tools_disabled and (prepared_tools := self._prepare_tools_for_ollama(tools)): run_options["tools"] = prepared_tools return run_options diff --git a/python/packages/ollama/tests/test_ollama_chat_client.py b/python/packages/ollama/tests/test_ollama_chat_client.py index 219ef3530b5..0e374ec85ce 100644 --- a/python/packages/ollama/tests/test_ollama_chat_client.py +++ b/python/packages/ollama/tests/test_ollama_chat_client.py @@ -16,7 +16,12 @@ chat_middleware, tool, ) -from agent_framework.exceptions import ChatClientException, ChatClientInvalidRequestException, SettingNotFoundError +from agent_framework.exceptions import ( + ChatClientException, + ChatClientInvalidRequestException, + ContentError, + SettingNotFoundError, +) from ollama import AsyncClient from ollama._types import ChatResponse as OllamaChatResponse from ollama._types import Message as OllamaMessage @@ -953,3 +958,12 @@ async def test_cmc_function_invocation_limit_final_request_omits_tools( assert "tools" in mock_chat.call_args_list[0].kwargs assert "tools" not in mock_chat.call_args_list[1].kwargs assert result.text == "test" + + +def test_prepare_options_invalid_tool_choice_raises(ollama_unit_test_env: dict[str, str]) -> None: + """An invalid tool_choice is rejected instead of being silently ignored.""" + client = OllamaChatClient() + messages = [Message(role="user", contents=[Content.from_text(text="hello")])] + + with pytest.raises(ContentError): + client._prepare_options(messages, {"tools": [hello_world], "tool_choice": "bogus"}) From d4da8d30709477094e651f79c5bef4ea6c47ff79 Mon Sep 17 00:00:00 2001 From: Ankit Kumar Date: Tue, 6 Oct 2026 22:08:03 +0530 Subject: [PATCH 3/4] Python: fix(ollama): accept tool_choice in OllamaChatOptions --- python/packages/ollama/agent_framework_ollama/_chat_client.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/python/packages/ollama/agent_framework_ollama/_chat_client.py b/python/packages/ollama/agent_framework_ollama/_chat_client.py index d34a2ff6b23..7345b7cb3e3 100644 --- a/python/packages/ollama/agent_framework_ollama/_chat_client.py +++ b/python/packages/ollama/agent_framework_ollama/_chat_client.py @@ -217,9 +217,6 @@ class OllamaChatOptions(ChatOptions[ResponseModelT], Generic[ResponseModelT], to """For thinking models: whether the model should think before responding.""" # ChatOptions fields not supported in Ollama - tool_choice: None # type: ignore[misc] - """Not supported. Ollama only supports auto tool choice, but ``none`` is honored by omitting the tools.""" - allow_multiple_tool_calls: None # type: ignore[misc] """Not supported. Not configurable in Ollama.""" From 6366d5ff60d3bb1205f1384a7d60bf6235524547 Mon Sep 17 00:00:00 2001 From: Ankit Kumar Date: Wed, 7 Oct 2026 15:25:46 +0530 Subject: [PATCH 4/4] Python: fix(ollama): reject tool_choice modes Ollama cannot enforce --- .../agent_framework_ollama/_chat_client.py | 17 ++++++++++--- .../ollama/tests/test_ollama_chat_client.py | 25 ++++++++++++++++--- 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/python/packages/ollama/agent_framework_ollama/_chat_client.py b/python/packages/ollama/agent_framework_ollama/_chat_client.py index 7345b7cb3e3..1f6e59ff814 100644 --- a/python/packages/ollama/agent_framework_ollama/_chat_client.py +++ b/python/packages/ollama/agent_framework_ollama/_chat_client.py @@ -98,7 +98,8 @@ class OllamaChatOptions(ChatOptions[ResponseModelT], Generic[ResponseModelT], to (converted to its JSON schema) for structured output. # Options not supported in Ollama: - tool_choice: Ollama only supports auto tool choice, but ``none`` is honored by omitting the tools. + tool_choice: Only ``auto`` and ``none`` are supported (``none`` omits the tools). + ``required`` and ``allowed_tools`` raise ``ChatClientInvalidRequestException``. allow_multiple_tool_calls: Not configurable. user: Not supported. store: Not supported. @@ -451,9 +452,19 @@ def _prepare_options(self, messages: Sequence[Message], options: Mapping[str, An run_options["model"] = self.model # tools - # Ollama has no tool_choice parameter, so "none" is honored by not sending the tools at all. - tools = options.get("tools") + # Ollama has no tool_choice parameter. Only "auto" (send the tools) and "none" (don't send them) + # can be honored, so anything else is rejected instead of silently behaving like "auto". tool_mode = validate_tool_mode(options.get("tool_choice")) + if tool_mode is not None: + if tool_mode.get("mode") == "required": + raise ChatClientInvalidRequestException( + "Ollama does not support tool_choice mode 'required'. Use 'auto' or 'none'." + ) + if "allowed_tools" in tool_mode: + raise ChatClientInvalidRequestException( + "Ollama does not support tool_choice 'allowed_tools'. Pass only the tools you want to allow." + ) + tools = options.get("tools") tools_disabled = tool_mode is not None and tool_mode.get("mode") == "none" if tools is not None and not tools_disabled and (prepared_tools := self._prepare_tools_for_ollama(tools)): run_options["tools"] = prepared_tools diff --git a/python/packages/ollama/tests/test_ollama_chat_client.py b/python/packages/ollama/tests/test_ollama_chat_client.py index 0e374ec85ce..fefd6aebaa0 100644 --- a/python/packages/ollama/tests/test_ollama_chat_client.py +++ b/python/packages/ollama/tests/test_ollama_chat_client.py @@ -926,9 +926,9 @@ def test_prepare_options_tool_choice_none_omits_tools(ollama_unit_test_env: dict assert "tool_choice" not in request -@pytest.mark.parametrize("tool_choice", [None, "auto", "required"]) -def test_prepare_options_other_tool_choices_keep_tools(ollama_unit_test_env: dict[str, str], tool_choice: Any) -> None: - """Tool choices other than "none" still send the tools to Ollama.""" +@pytest.mark.parametrize("tool_choice", [None, "auto", {"mode": "auto"}]) +def test_prepare_options_auto_tool_choice_keeps_tools(ollama_unit_test_env: dict[str, str], tool_choice: Any) -> None: + """No tool_choice or "auto" sends the tools to Ollama.""" client = OllamaChatClient() messages = [Message(role="user", contents=[Content.from_text(text="hello")])] @@ -967,3 +967,22 @@ def test_prepare_options_invalid_tool_choice_raises(ollama_unit_test_env: dict[s with pytest.raises(ContentError): client._prepare_options(messages, {"tools": [hello_world], "tool_choice": "bogus"}) + + +@pytest.mark.parametrize( + "tool_choice", + [ + "required", + {"mode": "required"}, + {"mode": "required", "required_function_name": "hello_world"}, + {"mode": "auto", "allowed_tools": ["hello_world"]}, + {"mode": "required", "allowed_tools": ["hello_world"]}, + ], +) +def test_prepare_options_unsupported_tool_choice_raises(ollama_unit_test_env: dict[str, str], tool_choice: Any) -> None: + """Ollama can't enforce "required" or "allowed_tools", so they raise instead of acting like "auto".""" + client = OllamaChatClient() + messages = [Message(role="user", contents=[Content.from_text(text="hello")])] + + with pytest.raises(ChatClientInvalidRequestException): + client._prepare_options(messages, {"tools": [hello_world], "tool_choice": tool_choice})