From 8ac43713877349958ffe06f27baa3d14c512c497 Mon Sep 17 00:00:00 2001 From: Yijia-Xiao Date: Thu, 17 Sep 2026 01:10:48 +0000 Subject: [PATCH] fix(llm): suppress object-form tool_choice for Ollama (#1062) - Ollama takes the local-compatible client, like the generic endpoint --- tests/test_ollama_base_url.py | 25 ++++++++++++++++++++++ tests/test_provider_registry.py | 3 ++- tradingagents/llm_clients/openai_client.py | 3 ++- 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/tests/test_ollama_base_url.py b/tests/test_ollama_base_url.py index c54162da6..3c949e63c 100644 --- a/tests/test_ollama_base_url.py +++ b/tests/test_ollama_base_url.py @@ -197,3 +197,28 @@ def test_ollama_offers_custom_model_id(): assert "custom" in values, f"Ollama {mode!r} missing 'custom' option: {entries}" # Custom option is last so it doesn't push the curated defaults off-screen assert values[-1] == "custom", f"'custom' should be last entry: {values}" + + +@pytest.mark.unit +def test_structured_output_suppresses_object_tool_choice(monkeypatch): + """Ollama rejects the object-form tool_choice like other local servers + (#1062), and a local model ID has no capability entry saying otherwise, so + it takes the same client as the generic local endpoint.""" + from langchain_openai import ChatOpenAI + from pydantic import BaseModel + + from tradingagents.llm_clients import create_llm_client + + class Schema(BaseModel): + x: int + + captured = {} + monkeypatch.setattr( + ChatOpenAI, + "with_structured_output", + lambda self, schema, method=None, **kw: captured.update({"method": method, **kw}) or "BOUND", + ) + + create_llm_client(provider="ollama", model="qwen3:30b").get_llm().with_structured_output(Schema) + + assert captured["tool_choice"] is None diff --git a/tests/test_provider_registry.py b/tests/test_provider_registry.py index 596108a77..36bd4fcbb 100644 --- a/tests/test_provider_registry.py +++ b/tests/test_provider_registry.py @@ -7,6 +7,7 @@ import pytest from tradingagents.llm_clients.openai_client import ( OPENAI_COMPATIBLE_PROVIDERS, DeepSeekChatOpenAI, + LocalCompatibleChatOpenAI, MinimaxChatOpenAI, NormalizedChatOpenAI, is_openai_compatible, @@ -39,7 +40,7 @@ def test_registry_membership(): ("kimi", "https://api.moonshot.ai/v1", NormalizedChatOpenAI, False), ("groq", "https://api.groq.com/openai/v1", NormalizedChatOpenAI, False), ("nvidia", "https://integrate.api.nvidia.com/v1", NormalizedChatOpenAI, False), - ("ollama", "http://localhost:11434/v1", NormalizedChatOpenAI, False), + ("ollama", "http://localhost:11434/v1", LocalCompatibleChatOpenAI, False), ]) def test_registry_spec(provider, base_url, chat_class, responses): spec = OPENAI_COMPATIBLE_PROVIDERS[provider] diff --git a/tradingagents/llm_clients/openai_client.py b/tradingagents/llm_clients/openai_client.py index 2caa728c1..b1d7b65d6 100644 --- a/tradingagents/llm_clients/openai_client.py +++ b/tradingagents/llm_clients/openai_client.py @@ -225,7 +225,8 @@ OPENAI_COMPATIBLE_PROVIDERS: dict[str, ProviderSpec] = { "groq": ProviderSpec(base_url="https://api.groq.com/openai/v1"), "nvidia": ProviderSpec(base_url="https://integrate.api.nvidia.com/v1"), "ollama": ProviderSpec(base_url="http://localhost:11434/v1", base_url_env="OLLAMA_BASE_URL", - key_optional=True, placeholder_key="ollama"), + key_optional=True, placeholder_key="ollama", + chat_class=LocalCompatibleChatOpenAI), # Generic endpoint: user supplies base_url; key optional (keyless local). "openai_compatible": ProviderSpec( require_base_url=True, key_optional=True, chat_class=LocalCompatibleChatOpenAI