From e0e1b804a759c09012afdc22bbcbe3152cb23390 Mon Sep 17 00:00:00 2001 From: Will Miao Date: Mon, 6 Jul 2026 10:02:04 +0800 Subject: [PATCH] fix(llm): require api_base for custom provider without preset default --- py/services/llm_service.py | 20 ++++++++++++++------ tests/services/test_llm_service.py | 26 ++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/py/services/llm_service.py b/py/services/llm_service.py index 85947852..b6854302 100644 --- a/py/services/llm_service.py +++ b/py/services/llm_service.py @@ -282,15 +282,17 @@ class LLMService: def is_configured(self) -> bool: """Return ``True`` when the LLM provider is minimally configured. - A provider is considered configured when ``llm_model`` is set and + A provider is considered configured when ``llm_model`` is set, an API key is configured for providers that require one (e.g. - Ollama does not). + Ollama does not), and an API base URL is set for providers that + have no preset default (e.g. ``custom``). """ cfg = self._get_config() has_model = bool(cfg["model"]) has_key = bool(cfg["api_key"]) or not self._provider_requires_key(cfg["provider"]) - return has_model and has_key + has_base = bool(cfg["api_base"]) or bool(_PROVIDER_DEFAULTS.get(cfg["provider"])) + return has_model and has_key and has_base def _resolve_api_base(self, provider: str, api_base: str) -> str: """Resolve the API base URL for the given provider. @@ -314,20 +316,26 @@ class LLMService: def _ensure_configured(self) -> Dict[str, Any]: """Validate configuration and return it, or raise. - A provider is considered configured when ``llm_model`` is set and - (for non-Ollama) an API key is configured. + A provider is considered configured when ``llm_model`` is set, + an API key is configured for providers that require one, and + an API base URL is set for providers without a preset default. """ cfg = self._get_config() has_model = bool(cfg["model"]) needs_key = self._provider_requires_key(cfg["provider"]) has_key = bool(cfg["api_key"]) or not needs_key - if not (has_model and has_key): + has_base = bool(cfg["api_base"]) or bool(_PROVIDER_DEFAULTS.get(cfg["provider"])) + if not (has_model and has_key and has_base): parts = [] if not has_model: parts.append("No LLM model specified") if not has_key and needs_key: parts.append("No LLM API key configured") + if not has_base: + parts.append( + f"No API base URL for provider '{cfg['provider']}'" + ) detail = "; ".join(parts) if parts else "LLM provider is not configured" raise LLMNotConfiguredError( f"{detail}. Configure it in Settings → AI Provider." diff --git a/tests/services/test_llm_service.py b/tests/services/test_llm_service.py index 3ef7584d..5fb1a5e1 100644 --- a/tests/services/test_llm_service.py +++ b/tests/services/test_llm_service.py @@ -136,6 +136,32 @@ class TestLLMServiceConfiguration: with pytest.raises(LLMNotConfiguredError): service._ensure_configured() + def test_not_configured_custom_without_api_base(self): + settings = MockSettings( + llm_enabled=True, llm_provider="custom", + llm_api_key="sk-test", llm_api_base="", llm_model="gpt-4o", + ) + service = LLMService(settings) + assert service.is_configured() is False + + def test_custom_configured_with_api_base(self): + settings = MockSettings( + llm_enabled=True, llm_provider="custom", + llm_api_key="sk-test", + llm_api_base="https://my.api.com/v1", llm_model="gpt-4o", + ) + service = LLMService(settings) + assert service.is_configured() is True + + def test_ensure_configured_raises_custom_without_api_base(self): + settings = MockSettings( + llm_enabled=True, llm_provider="custom", + llm_api_key="sk-test", llm_api_base="", llm_model="gpt-4o", + ) + service = LLMService(settings) + with pytest.raises(LLMNotConfiguredError, match="API base URL"): + service._ensure_configured() + class TestLLMServiceChatCompletion: @pytest.mark.asyncio