From 432a9f45d778a670423e3b814a8861f1bb112204 Mon Sep 17 00:00:00 2001 From: majiayu000 <1835304752@qq.com> Date: Tue, 30 Dec 2025 19:53:50 +0800 Subject: [PATCH 1/8] feat: add Ollama provider support Add LMMEngineOllama class to support local Ollama models. Ollama uses OpenAI-compatible API with default endpoint http://localhost:11434/v1. Closes #44 Signed-off-by: majiayu000 <1835304752@qq.com> --- gui_agents/s3/core/engine.py | 46 ++++++++++++++++++++++++++++++++++++ gui_agents/s3/core/mllm.py | 4 ++++ 2 files changed, 50 insertions(+) diff --git a/gui_agents/s3/core/engine.py b/gui_agents/s3/core/engine.py index 7bf90f14..197e885d 100644 --- a/gui_agents/s3/core/engine.py +++ b/gui_agents/s3/core/engine.py @@ -443,3 +443,49 @@ def generate(self, messages, temperature=0.0, max_new_tokens=None, **kwargs): .choices[0] .message.content ) + + +class LMMEngineOllama(LMMEngine): + def __init__( + self, + base_url=None, + api_key=None, + model=None, + rate_limit=-1, + temperature=None, + **kwargs, + ): + assert model is not None, "model must be provided" + self.model = model + self.api_key = api_key + self.base_url = base_url + self.request_interval = 0 if rate_limit == -1 else 60.0 / rate_limit + self.llm_client = None + self.temperature = temperature + + @backoff.on_exception( + backoff.expo, (APIConnectionError, APIError, RateLimitError), max_time=60 + ) + def generate( + self, + messages, + temperature=0.0, + max_new_tokens=None, + **kwargs, + ): + base_url = self.base_url or os.getenv("OLLAMA_HOST") + if base_url is None: + base_url = "http://localhost:11434/v1" + elif not base_url.endswith("/v1"): + base_url = base_url.rstrip("/") + "/v1" + api_key = self.api_key or "ollama" + if not self.llm_client: + self.llm_client = OpenAI(base_url=base_url, api_key=api_key) + temp = self.temperature if self.temperature is not None else temperature + completion = self.llm_client.chat.completions.create( + model=self.model, + messages=messages, + max_tokens=max_new_tokens if max_new_tokens else 4096, + temperature=temp, + ) + return completion.choices[0].message.content diff --git a/gui_agents/s3/core/mllm.py b/gui_agents/s3/core/mllm.py index fb49e4b0..2540a401 100644 --- a/gui_agents/s3/core/mllm.py +++ b/gui_agents/s3/core/mllm.py @@ -6,6 +6,7 @@ LMMEngineAnthropic, LMMEngineAzureOpenAI, LMMEngineHuggingFace, + LMMEngineOllama, LMMEngineOpenAI, LMMEngineOpenRouter, LMMEngineParasail, @@ -35,6 +36,8 @@ def __init__(self, engine_params=None, system_prompt=None, engine=None): self.engine = LMMEngineOpenRouter(**engine_params) elif engine_type == "parasail": self.engine = LMMEngineParasail(**engine_params) + elif engine_type == "ollama": + self.engine = LMMEngineOllama(**engine_params) else: raise ValueError(f"engine_type '{engine_type}' is not supported") else: @@ -129,6 +132,7 @@ def add_message( LMMEngineGemini, LMMEngineOpenRouter, LMMEngineParasail, + LMMEngineOllama, ), ): # infer role from previous message From c55f3947df4c419d5a712862833016e0c3c7d0cf Mon Sep 17 00:00:00 2001 From: majiayu000 <1835304752@qq.com> Date: Tue, 30 Dec 2025 23:32:11 +0800 Subject: [PATCH 2/8] fix: forward **kwargs to chat.completions.create() for Ollama engine MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This ensures consistency with other engine implementations (OpenAI, Gemini, OpenRouter, etc.) and allows callers to pass additional parameters like stop, top_p, etc. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- gui_agents/s3/core/engine.py | 1 + 1 file changed, 1 insertion(+) diff --git a/gui_agents/s3/core/engine.py b/gui_agents/s3/core/engine.py index 197e885d..4476f567 100644 --- a/gui_agents/s3/core/engine.py +++ b/gui_agents/s3/core/engine.py @@ -487,5 +487,6 @@ def generate( messages=messages, max_tokens=max_new_tokens if max_new_tokens else 4096, temperature=temp, + **kwargs, ) return completion.choices[0].message.content From 02a9da5591c60ffcfd4d49555806784a8fed62d2 Mon Sep 17 00:00:00 2001 From: majiayu000 <1835304752@qq.com> Date: Tue, 13 Jan 2026 22:55:16 +0800 Subject: [PATCH 3/8] refactor: reuse LMMEngineOpenAI for Ollama provider --- gui_agents/s3/core/engine.py | 45 ------------------------------------ gui_agents/s3/core/mllm.py | 11 ++++++--- 2 files changed, 8 insertions(+), 48 deletions(-) diff --git a/gui_agents/s3/core/engine.py b/gui_agents/s3/core/engine.py index 4476f567..21aa019e 100644 --- a/gui_agents/s3/core/engine.py +++ b/gui_agents/s3/core/engine.py @@ -445,48 +445,3 @@ def generate(self, messages, temperature=0.0, max_new_tokens=None, **kwargs): ) -class LMMEngineOllama(LMMEngine): - def __init__( - self, - base_url=None, - api_key=None, - model=None, - rate_limit=-1, - temperature=None, - **kwargs, - ): - assert model is not None, "model must be provided" - self.model = model - self.api_key = api_key - self.base_url = base_url - self.request_interval = 0 if rate_limit == -1 else 60.0 / rate_limit - self.llm_client = None - self.temperature = temperature - - @backoff.on_exception( - backoff.expo, (APIConnectionError, APIError, RateLimitError), max_time=60 - ) - def generate( - self, - messages, - temperature=0.0, - max_new_tokens=None, - **kwargs, - ): - base_url = self.base_url or os.getenv("OLLAMA_HOST") - if base_url is None: - base_url = "http://localhost:11434/v1" - elif not base_url.endswith("/v1"): - base_url = base_url.rstrip("/") + "/v1" - api_key = self.api_key or "ollama" - if not self.llm_client: - self.llm_client = OpenAI(base_url=base_url, api_key=api_key) - temp = self.temperature if self.temperature is not None else temperature - completion = self.llm_client.chat.completions.create( - model=self.model, - messages=messages, - max_tokens=max_new_tokens if max_new_tokens else 4096, - temperature=temp, - **kwargs, - ) - return completion.choices[0].message.content diff --git a/gui_agents/s3/core/mllm.py b/gui_agents/s3/core/mllm.py index 2540a401..7c71d5f0 100644 --- a/gui_agents/s3/core/mllm.py +++ b/gui_agents/s3/core/mllm.py @@ -6,7 +6,7 @@ LMMEngineAnthropic, LMMEngineAzureOpenAI, LMMEngineHuggingFace, - LMMEngineOllama, + LMMEngineOpenAI, LMMEngineOpenRouter, LMMEngineParasail, @@ -37,7 +37,12 @@ def __init__(self, engine_params=None, system_prompt=None, engine=None): elif engine_type == "parasail": self.engine = LMMEngineParasail(**engine_params) elif engine_type == "ollama": - self.engine = LMMEngineOllama(**engine_params) + # Reuse LMMEngineOpenAI for Ollama, defaulting to localhost if not specified + if "base_url" not in engine_params: + engine_params["base_url"] = "http://localhost:11434/v1" + if "api_key" not in engine_params: + engine_params["api_key"] = "ollama" + self.engine = LMMEngineOpenAI(**engine_params) else: raise ValueError(f"engine_type '{engine_type}' is not supported") else: @@ -132,7 +137,7 @@ def add_message( LMMEngineGemini, LMMEngineOpenRouter, LMMEngineParasail, - LMMEngineOllama, + ), ): # infer role from previous message From 234450bb0cbf00d7f1673cc52279f321f9e04db8 Mon Sep 17 00:00:00 2001 From: majiayu000 <1835304752@qq.com> Date: Tue, 13 Jan 2026 23:16:29 +0800 Subject: [PATCH 4/8] fix: restore OLLAMA_HOST env var support --- gui_agents/s3/core/mllm.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/gui_agents/s3/core/mllm.py b/gui_agents/s3/core/mllm.py index 7c71d5f0..d74ebadd 100644 --- a/gui_agents/s3/core/mllm.py +++ b/gui_agents/s3/core/mllm.py @@ -39,7 +39,14 @@ def __init__(self, engine_params=None, system_prompt=None, engine=None): elif engine_type == "ollama": # Reuse LMMEngineOpenAI for Ollama, defaulting to localhost if not specified if "base_url" not in engine_params: - engine_params["base_url"] = "http://localhost:11434/v1" + import os + base_url = os.getenv("OLLAMA_HOST") + if base_url: + if not base_url.endswith("/v1"): + base_url = base_url.rstrip("/") + "/v1" + engine_params["base_url"] = base_url + else: + engine_params["base_url"] = "http://localhost:11434/v1" if "api_key" not in engine_params: engine_params["api_key"] = "ollama" self.engine = LMMEngineOpenAI(**engine_params) From d2271de1704fb042cc42ab1748451e931f7d1056 Mon Sep 17 00:00:00 2001 From: lif <1835304752@qq.com> Date: Thu, 15 Jan 2026 17:16:13 +0800 Subject: [PATCH 5/8] feat: add DeepSeek/Qwen support and improve Ollama config --- gui_agents/s3/core/engine.py | 93 ++++++++++++++++++++++++++++++++++++ gui_agents/s3/core/mllm.py | 16 +++++-- tests/test_providers.py | 68 ++++++++++++++++++++++++++ 3 files changed, 174 insertions(+), 3 deletions(-) create mode 100644 tests/test_providers.py diff --git a/gui_agents/s3/core/engine.py b/gui_agents/s3/core/engine.py index 21aa019e..50829eb7 100644 --- a/gui_agents/s3/core/engine.py +++ b/gui_agents/s3/core/engine.py @@ -445,3 +445,96 @@ def generate(self, messages, temperature=0.0, max_new_tokens=None, **kwargs): ) +class LMMEngineDeepSeek(LMMEngine): + def __init__( + self, base_url=None, api_key=None, model=None, rate_limit=-1, **kwargs + ): + assert model is not None, "DeepSeek model id must be provided" + self.base_url = base_url + self.model = model + self.api_key = api_key + self.request_interval = 0 if rate_limit == -1 else 60.0 / rate_limit + self.llm_client = None + + @backoff.on_exception( + backoff.expo, (APIConnectionError, APIError, RateLimitError), max_time=60 + ) + def generate(self, messages, temperature=0.0, max_new_tokens=None, **kwargs): + api_key = self.api_key or os.getenv("DEEPSEEK_API_KEY") + if api_key is None: + raise ValueError( + "A DeepSeek API key needs to be provided in either the api_key parameter or as an environment variable named DEEPSEEK_API_KEY" + ) + base_url = self.base_url or os.getenv("DEEPSEEK_ENDPOINT_URL") + if base_url is None: + base_url = "https://api.deepseek.com" + + if not self.llm_client: + self.llm_client = OpenAI( + base_url=base_url, + api_key=api_key, + ) + return ( + self.llm_client.chat.completions.create( + model=self.model, + messages=messages, + max_tokens=max_new_tokens if max_new_tokens else 4096, + temperature=temperature, + **kwargs, + ) + .choices[0] + .message.content + ) + + +class LMMEngineQwen(LMMEngine): + def __init__( + self, base_url=None, api_key=None, model=None, rate_limit=-1, **kwargs + ): + assert model is not None, "Qwen model id must be provided" + self.base_url = base_url + self.model = model + self.api_key = api_key + self.request_interval = 0 if rate_limit == -1 else 60.0 / rate_limit + self.llm_client = None + + @backoff.on_exception( + backoff.expo, (APIConnectionError, APIError, RateLimitError), max_time=60 + ) + def generate(self, messages, temperature=0.0, max_new_tokens=None, **kwargs): + api_key = self.api_key or os.getenv("QWEN_API_KEY") # Or DASHSCOPE_API_KEY + if api_key is None: + raise ValueError( + "A Qwen API key needs to be provided in either the api_key parameter or as an environment variable named QWEN_API_KEY" + ) + base_url = self.base_url or os.getenv("QWEN_ENDPOINT_URL") + if base_url is None: + # Alibaba Qwen often uses DashScope, but for compatible APIs let's assume standard or user provided + # If strictly Qwen (via DashScope compatible), valid URL is needed. + # defaulting to DashScope compatible endpoint as placeholder or rely on user. + # For this strict implementation, ensuring we have a URL is better if known, + # but generic "Qwen" usually implies usage via compatible interface (like vLLM serving Qwen or DashScope). + # Let's require it or default to a common one if reasonable. + # Given the other engines, let's enforce user providing it or env var if we don't have a single canonical one (DashScope is common). + # Let's default to DashScope's openai compatible endpoint if none provided? + # https://dashscope.aliyuncs.com/compatible-mode/v1 + base_url = "https://dashscope.aliyuncs.com/compatible-mode/v1" + + if not self.llm_client: + self.llm_client = OpenAI( + base_url=base_url, + api_key=api_key, + ) + return ( + self.llm_client.chat.completions.create( + model=self.model, + messages=messages, + max_tokens=max_new_tokens if max_new_tokens else 4096, + temperature=temperature, + **kwargs, + ) + .choices[0] + .message.content + ) + + diff --git a/gui_agents/s3/core/mllm.py b/gui_agents/s3/core/mllm.py index d74ebadd..90c1c222 100644 --- a/gui_agents/s3/core/mllm.py +++ b/gui_agents/s3/core/mllm.py @@ -12,6 +12,8 @@ LMMEngineParasail, LMMEnginevLLM, LMMEngineGemini, + LMMEngineDeepSeek, + LMMEngineQwen, ) @@ -37,7 +39,7 @@ def __init__(self, engine_params=None, system_prompt=None, engine=None): elif engine_type == "parasail": self.engine = LMMEngineParasail(**engine_params) elif engine_type == "ollama": - # Reuse LMMEngineOpenAI for Ollama, defaulting to localhost if not specified + # Reuse LMMEngineOpenAI for Ollama if "base_url" not in engine_params: import os base_url = os.getenv("OLLAMA_HOST") @@ -46,10 +48,17 @@ def __init__(self, engine_params=None, system_prompt=None, engine=None): base_url = base_url.rstrip("/") + "/v1" engine_params["base_url"] = base_url else: - engine_params["base_url"] = "http://localhost:11434/v1" + # RAISE ERROR instead of default + raise ValueError( + "Ollama endpoint must be provided via 'base_url' parameter or 'OLLAMA_HOST' environment variable." + ) if "api_key" not in engine_params: engine_params["api_key"] = "ollama" self.engine = LMMEngineOpenAI(**engine_params) + elif engine_type == "deepseek": + self.engine = LMMEngineDeepSeek(**engine_params) + elif engine_type == "qwen": + self.engine = LMMEngineQwen(**engine_params) else: raise ValueError(f"engine_type '{engine_type}' is not supported") else: @@ -144,7 +153,8 @@ def add_message( LMMEngineGemini, LMMEngineOpenRouter, LMMEngineParasail, - + LMMEngineDeepSeek, + LMMEngineQwen, ), ): # infer role from previous message diff --git a/tests/test_providers.py b/tests/test_providers.py new file mode 100644 index 00000000..95432663 --- /dev/null +++ b/tests/test_providers.py @@ -0,0 +1,68 @@ + +import os +import unittest +from unittest.mock import patch, MagicMock +from gui_agents.s3.core.mllm import LMMAgent +from gui_agents.s3.core.engine import LMMEngineOpenAI, LMMEngineDeepSeek, LMMEngineQwen + +class TestProviders(unittest.TestCase): + def setUp(self): + # Clear env vars before each test + if "OLLAMA_HOST" in os.environ: + del os.environ["OLLAMA_HOST"] + if "DEEPSEEK_API_KEY" in os.environ: + del os.environ["DEEPSEEK_API_KEY"] + if "QWEN_API_KEY" in os.environ: + del os.environ["QWEN_API_KEY"] + + def test_ollama_missing_config(self): + """Test that Ollama raises ValueError if no endpoint is provided""" + with self.assertRaises(ValueError) as cm: + LMMAgent(engine_params={"engine_type": "ollama", "model": "llama3"}) + self.assertIn("Ollama endpoint must be provided", str(cm.exception)) + + def test_ollama_valid_config_param(self): + """Test Ollama init with base_url param""" + agent = LMMAgent(engine_params={ + "engine_type": "ollama", + "model": "llama3", + "base_url": "http://example.com/v1" + }) + self.assertIsInstance(agent.engine, LMMEngineOpenAI) + self.assertEqual(agent.engine.base_url, "http://example.com/v1") + + def test_ollama_valid_config_env(self): + """Test Ollama init with OLLAMA_HOST env var""" + with patch.dict(os.environ, {"OLLAMA_HOST": "http://env-host:11434"}): + agent = LMMAgent(engine_params={ + "engine_type": "ollama", + "model": "llama3" + }) + self.assertIsInstance(agent.engine, LMMEngineOpenAI) + # Check for /v1 addition + self.assertEqual(agent.engine.base_url, "http://env-host:11434/v1") + + def test_deepseek_init(self): + """Test DeepSeek initialization""" + with patch.dict(os.environ, {"DEEPSEEK_API_KEY": "sk-test"}): + agent = LMMAgent(engine_params={ + "engine_type": "deepseek", + "model": "deepseek-coder" + }) + self.assertIsInstance(agent.engine, LMMEngineDeepSeek) + # Default URL + self.assertEqual(agent.engine.base_url, None) + # (Note: engine.py logic resolves default at generate() time or if client created, + # but init just stores what's passed. Let's verify prompt generation to ensure it doesn't crash on init) + + def test_qwen_init(self): + """Test Qwen initialization""" + with patch.dict(os.environ, {"QWEN_API_KEY": "sk-qwen"}): + agent = LMMAgent(engine_params={ + "engine_type": "qwen", + "model": "qwen-max" + }) + self.assertIsInstance(agent.engine, LMMEngineQwen) + +if __name__ == "__main__": + unittest.main() From c8f306efd7911def2f0231f5fb6fa59e1d62fb93 Mon Sep 17 00:00:00 2001 From: lif <1835304752@qq.com> Date: Mon, 19 Jan 2026 18:06:24 +0800 Subject: [PATCH 6/8] refactor: reuse LMMEngineOpenAI for DeepSeek and Qwen providers --- gui_agents/s3/core/engine.py | 95 ------------------------------------ gui_agents/s3/core/mllm.py | 46 ++++++++++++++--- tests/test_providers.py | 48 +++++++++--------- 3 files changed, 63 insertions(+), 126 deletions(-) diff --git a/gui_agents/s3/core/engine.py b/gui_agents/s3/core/engine.py index 50829eb7..7bf90f14 100644 --- a/gui_agents/s3/core/engine.py +++ b/gui_agents/s3/core/engine.py @@ -443,98 +443,3 @@ def generate(self, messages, temperature=0.0, max_new_tokens=None, **kwargs): .choices[0] .message.content ) - - -class LMMEngineDeepSeek(LMMEngine): - def __init__( - self, base_url=None, api_key=None, model=None, rate_limit=-1, **kwargs - ): - assert model is not None, "DeepSeek model id must be provided" - self.base_url = base_url - self.model = model - self.api_key = api_key - self.request_interval = 0 if rate_limit == -1 else 60.0 / rate_limit - self.llm_client = None - - @backoff.on_exception( - backoff.expo, (APIConnectionError, APIError, RateLimitError), max_time=60 - ) - def generate(self, messages, temperature=0.0, max_new_tokens=None, **kwargs): - api_key = self.api_key or os.getenv("DEEPSEEK_API_KEY") - if api_key is None: - raise ValueError( - "A DeepSeek API key needs to be provided in either the api_key parameter or as an environment variable named DEEPSEEK_API_KEY" - ) - base_url = self.base_url or os.getenv("DEEPSEEK_ENDPOINT_URL") - if base_url is None: - base_url = "https://api.deepseek.com" - - if not self.llm_client: - self.llm_client = OpenAI( - base_url=base_url, - api_key=api_key, - ) - return ( - self.llm_client.chat.completions.create( - model=self.model, - messages=messages, - max_tokens=max_new_tokens if max_new_tokens else 4096, - temperature=temperature, - **kwargs, - ) - .choices[0] - .message.content - ) - - -class LMMEngineQwen(LMMEngine): - def __init__( - self, base_url=None, api_key=None, model=None, rate_limit=-1, **kwargs - ): - assert model is not None, "Qwen model id must be provided" - self.base_url = base_url - self.model = model - self.api_key = api_key - self.request_interval = 0 if rate_limit == -1 else 60.0 / rate_limit - self.llm_client = None - - @backoff.on_exception( - backoff.expo, (APIConnectionError, APIError, RateLimitError), max_time=60 - ) - def generate(self, messages, temperature=0.0, max_new_tokens=None, **kwargs): - api_key = self.api_key or os.getenv("QWEN_API_KEY") # Or DASHSCOPE_API_KEY - if api_key is None: - raise ValueError( - "A Qwen API key needs to be provided in either the api_key parameter or as an environment variable named QWEN_API_KEY" - ) - base_url = self.base_url or os.getenv("QWEN_ENDPOINT_URL") - if base_url is None: - # Alibaba Qwen often uses DashScope, but for compatible APIs let's assume standard or user provided - # If strictly Qwen (via DashScope compatible), valid URL is needed. - # defaulting to DashScope compatible endpoint as placeholder or rely on user. - # For this strict implementation, ensuring we have a URL is better if known, - # but generic "Qwen" usually implies usage via compatible interface (like vLLM serving Qwen or DashScope). - # Let's require it or default to a common one if reasonable. - # Given the other engines, let's enforce user providing it or env var if we don't have a single canonical one (DashScope is common). - # Let's default to DashScope's openai compatible endpoint if none provided? - # https://dashscope.aliyuncs.com/compatible-mode/v1 - base_url = "https://dashscope.aliyuncs.com/compatible-mode/v1" - - if not self.llm_client: - self.llm_client = OpenAI( - base_url=base_url, - api_key=api_key, - ) - return ( - self.llm_client.chat.completions.create( - model=self.model, - messages=messages, - max_tokens=max_new_tokens if max_new_tokens else 4096, - temperature=temperature, - **kwargs, - ) - .choices[0] - .message.content - ) - - diff --git a/gui_agents/s3/core/mllm.py b/gui_agents/s3/core/mllm.py index 90c1c222..c908da4f 100644 --- a/gui_agents/s3/core/mllm.py +++ b/gui_agents/s3/core/mllm.py @@ -6,14 +6,11 @@ LMMEngineAnthropic, LMMEngineAzureOpenAI, LMMEngineHuggingFace, - LMMEngineOpenAI, LMMEngineOpenRouter, LMMEngineParasail, LMMEnginevLLM, LMMEngineGemini, - LMMEngineDeepSeek, - LMMEngineQwen, ) @@ -42,6 +39,7 @@ def __init__(self, engine_params=None, system_prompt=None, engine=None): # Reuse LMMEngineOpenAI for Ollama if "base_url" not in engine_params: import os + base_url = os.getenv("OLLAMA_HOST") if base_url: if not base_url.endswith("/v1"): @@ -56,9 +54,44 @@ def __init__(self, engine_params=None, system_prompt=None, engine=None): engine_params["api_key"] = "ollama" self.engine = LMMEngineOpenAI(**engine_params) elif engine_type == "deepseek": - self.engine = LMMEngineDeepSeek(**engine_params) + if "base_url" not in engine_params: + import os + + base_url = os.getenv("DEEPSEEK_ENDPOINT_URL") + if not base_url: + base_url = "https://api.deepseek.com" + engine_params["base_url"] = base_url + if "api_key" not in engine_params: + import os + + api_key = os.getenv("DEEPSEEK_API_KEY") + if not api_key: + raise ValueError( + "DeepSeek API key must be provided via 'api_key' parameter or 'DEEPSEEK_API_KEY' environment variable." + ) + engine_params["api_key"] = api_key + + self.engine = LMMEngineOpenAI(**engine_params) elif engine_type == "qwen": - self.engine = LMMEngineQwen(**engine_params) + if "base_url" not in engine_params: + import os + + base_url = os.getenv("QWEN_ENDPOINT_URL") + if not base_url: + base_url = ( + "https://dashscope.aliyuncs.com/compatible-mode/v1" + ) + engine_params["base_url"] = base_url + if "api_key" not in engine_params: + import os + + api_key = os.getenv("QWEN_API_KEY") + if not api_key: + raise ValueError( + "Qwen API key must be provided via 'api_key' parameter or 'QWEN_API_KEY' environment variable." + ) + engine_params["api_key"] = api_key + self.engine = LMMEngineOpenAI(**engine_params) else: raise ValueError(f"engine_type '{engine_type}' is not supported") else: @@ -153,8 +186,7 @@ def add_message( LMMEngineGemini, LMMEngineOpenRouter, LMMEngineParasail, - LMMEngineDeepSeek, - LMMEngineQwen, + LMMEngineParasail, ), ): # infer role from previous message diff --git a/tests/test_providers.py b/tests/test_providers.py index 95432663..ff52a2d4 100644 --- a/tests/test_providers.py +++ b/tests/test_providers.py @@ -1,9 +1,9 @@ - import os import unittest from unittest.mock import patch, MagicMock from gui_agents.s3.core.mllm import LMMAgent -from gui_agents.s3.core.engine import LMMEngineOpenAI, LMMEngineDeepSeek, LMMEngineQwen +from gui_agents.s3.core.engine import LMMEngineOpenAI + class TestProviders(unittest.TestCase): def setUp(self): @@ -23,21 +23,20 @@ def test_ollama_missing_config(self): def test_ollama_valid_config_param(self): """Test Ollama init with base_url param""" - agent = LMMAgent(engine_params={ - "engine_type": "ollama", - "model": "llama3", - "base_url": "http://example.com/v1" - }) + agent = LMMAgent( + engine_params={ + "engine_type": "ollama", + "model": "llama3", + "base_url": "http://example.com/v1", + } + ) self.assertIsInstance(agent.engine, LMMEngineOpenAI) self.assertEqual(agent.engine.base_url, "http://example.com/v1") def test_ollama_valid_config_env(self): """Test Ollama init with OLLAMA_HOST env var""" with patch.dict(os.environ, {"OLLAMA_HOST": "http://env-host:11434"}): - agent = LMMAgent(engine_params={ - "engine_type": "ollama", - "model": "llama3" - }) + agent = LMMAgent(engine_params={"engine_type": "ollama", "model": "llama3"}) self.assertIsInstance(agent.engine, LMMEngineOpenAI) # Check for /v1 addition self.assertEqual(agent.engine.base_url, "http://env-host:11434/v1") @@ -45,24 +44,25 @@ def test_ollama_valid_config_env(self): def test_deepseek_init(self): """Test DeepSeek initialization""" with patch.dict(os.environ, {"DEEPSEEK_API_KEY": "sk-test"}): - agent = LMMAgent(engine_params={ - "engine_type": "deepseek", - "model": "deepseek-coder" - }) - self.assertIsInstance(agent.engine, LMMEngineDeepSeek) + agent = LMMAgent( + engine_params={"engine_type": "deepseek", "model": "deepseek-coder"} + ) + self.assertIsInstance(agent.engine, LMMEngineOpenAI) # Default URL - self.assertEqual(agent.engine.base_url, None) - # (Note: engine.py logic resolves default at generate() time or if client created, + self.assertEqual(agent.engine.base_url, "https://api.deepseek.com") + # (Note: engine.py logic resolves default at generate() time or if client created, # but init just stores what's passed. Let's verify prompt generation to ensure it doesn't crash on init) - + def test_qwen_init(self): """Test Qwen initialization""" with patch.dict(os.environ, {"QWEN_API_KEY": "sk-qwen"}): - agent = LMMAgent(engine_params={ - "engine_type": "qwen", - "model": "qwen-max" - }) - self.assertIsInstance(agent.engine, LMMEngineQwen) + agent = LMMAgent(engine_params={"engine_type": "qwen", "model": "qwen-max"}) + self.assertIsInstance(agent.engine, LMMEngineOpenAI) + self.assertEqual( + agent.engine.base_url, + "https://dashscope.aliyuncs.com/compatible-mode/v1", + ) + if __name__ == "__main__": unittest.main() From b6601890d6f4b0eba7b7e1d2837e826d2862948a Mon Sep 17 00:00:00 2001 From: lif <1835304752@qq.com> Date: Mon, 19 Jan 2026 18:16:07 +0800 Subject: [PATCH 7/8] fix: address review comments (robust params, url normalization, cleanup) --- gui_agents/s3/core/mllm.py | 13 +++++++++---- tests/test_providers.py | 2 +- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/gui_agents/s3/core/mllm.py b/gui_agents/s3/core/mllm.py index c908da4f..efea62aa 100644 --- a/gui_agents/s3/core/mllm.py +++ b/gui_agents/s3/core/mllm.py @@ -60,8 +60,11 @@ def __init__(self, engine_params=None, system_prompt=None, engine=None): base_url = os.getenv("DEEPSEEK_ENDPOINT_URL") if not base_url: base_url = "https://api.deepseek.com" + if not base_url.endswith("/v1"): + base_url = base_url.rstrip("/") + "/v1" engine_params["base_url"] = base_url - if "api_key" not in engine_params: + + if not engine_params.get("api_key"): import os api_key = os.getenv("DEEPSEEK_API_KEY") @@ -73,7 +76,7 @@ def __init__(self, engine_params=None, system_prompt=None, engine=None): self.engine = LMMEngineOpenAI(**engine_params) elif engine_type == "qwen": - if "base_url" not in engine_params: + if not engine_params.get("base_url"): import os base_url = os.getenv("QWEN_ENDPOINT_URL") @@ -81,8 +84,11 @@ def __init__(self, engine_params=None, system_prompt=None, engine=None): base_url = ( "https://dashscope.aliyuncs.com/compatible-mode/v1" ) + if not base_url.endswith("/v1"): + base_url = base_url.rstrip("/") + "/v1" engine_params["base_url"] = base_url - if "api_key" not in engine_params: + + if not engine_params.get("api_key"): import os api_key = os.getenv("QWEN_API_KEY") @@ -186,7 +192,6 @@ def add_message( LMMEngineGemini, LMMEngineOpenRouter, LMMEngineParasail, - LMMEngineParasail, ), ): # infer role from previous message diff --git a/tests/test_providers.py b/tests/test_providers.py index ff52a2d4..5eb8399b 100644 --- a/tests/test_providers.py +++ b/tests/test_providers.py @@ -49,7 +49,7 @@ def test_deepseek_init(self): ) self.assertIsInstance(agent.engine, LMMEngineOpenAI) # Default URL - self.assertEqual(agent.engine.base_url, "https://api.deepseek.com") + self.assertEqual(agent.engine.base_url, "https://api.deepseek.com/v1") # (Note: engine.py logic resolves default at generate() time or if client created, # but init just stores what's passed. Let's verify prompt generation to ensure it doesn't crash on init) From 000dd3700646a78db7069a074ebbcc19739b6ab6 Mon Sep 17 00:00:00 2001 From: lif <1835304752@qq.com> Date: Mon, 19 Jan 2026 19:26:51 +0800 Subject: [PATCH 8/8] fix: clean up test env and normalize ollama param checks --- gui_agents/s3/core/mllm.py | 4 ++-- tests/test_providers.py | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/gui_agents/s3/core/mllm.py b/gui_agents/s3/core/mllm.py index efea62aa..58beb93c 100644 --- a/gui_agents/s3/core/mllm.py +++ b/gui_agents/s3/core/mllm.py @@ -37,7 +37,7 @@ def __init__(self, engine_params=None, system_prompt=None, engine=None): self.engine = LMMEngineParasail(**engine_params) elif engine_type == "ollama": # Reuse LMMEngineOpenAI for Ollama - if "base_url" not in engine_params: + if not engine_params.get("base_url"): import os base_url = os.getenv("OLLAMA_HOST") @@ -50,7 +50,7 @@ def __init__(self, engine_params=None, system_prompt=None, engine=None): raise ValueError( "Ollama endpoint must be provided via 'base_url' parameter or 'OLLAMA_HOST' environment variable." ) - if "api_key" not in engine_params: + if not engine_params.get("api_key"): engine_params["api_key"] = "ollama" self.engine = LMMEngineOpenAI(**engine_params) elif engine_type == "deepseek": diff --git a/tests/test_providers.py b/tests/test_providers.py index 5eb8399b..2e1b24d2 100644 --- a/tests/test_providers.py +++ b/tests/test_providers.py @@ -14,6 +14,10 @@ def setUp(self): del os.environ["DEEPSEEK_API_KEY"] if "QWEN_API_KEY" in os.environ: del os.environ["QWEN_API_KEY"] + if "DEEPSEEK_ENDPOINT_URL" in os.environ: + del os.environ["DEEPSEEK_ENDPOINT_URL"] + if "QWEN_ENDPOINT_URL" in os.environ: + del os.environ["QWEN_ENDPOINT_URL"] def test_ollama_missing_config(self): """Test that Ollama raises ValueError if no endpoint is provided"""