From fc2c77fbf192600837df151471152bf02ee49b23 Mon Sep 17 00:00:00 2001 From: jxxghp Date: Tue, 12 May 2026 18:48:31 +0800 Subject: [PATCH] fix(agent): refresh LLM runtime config on each call Read the latest LLM connection settings when building runtime clients so Web updates take effect immediately instead of reusing module-import defaults. Closes #5757 --- app/agent/llm/helper.py | 26 ++++++++----- tests/test_llm_helper_testcall.py | 64 +++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 10 deletions(-) diff --git a/app/agent/llm/helper.py b/app/agent/llm/helper.py index c7f4db55..5bacaccd 100644 --- a/app/agent/llm/helper.py +++ b/app/agent/llm/helper.py @@ -512,9 +512,9 @@ class LLMHelper: provider: str | None = None, model: str | None = None, thinking_level: str | None = None, - api_key: str | None = settings.LLM_API_KEY, - base_url: str | None = settings.LLM_BASE_URL, - base_url_preset: str | None = settings.LLM_BASE_URL_PRESET, + api_key: str | None = None, + base_url: str | None = None, + base_url_preset: str | None = None, ): """ 获取LLM实例 @@ -525,12 +525,18 @@ class LLMHelper: 是否启用思考模式)。支持的级别包括 "off"(关闭)、"auto"(自动)、"minimal"、"low"、"medium"、"high"、"max"/"xhigh"(最大)。 不同模型对思考模式的支持和表现不同,具体映射关系请 参考代码实现。对于不支持思考模式的模型,该参数将被忽略。 - :param api_key: API Key,默认为配置项LLM_API_KEY。对于某些提供商(如 DeepSeek),可能需要同时提供 base_url。 - :param base_url: API Base URL,默认为配置项LLM_BASE_URL。 + :param api_key: API Key。未显式传入时使用当前配置项 LLM_API_KEY。对于某些提供商(如 DeepSeek),可能需要同时提供 base_url。 + :param base_url: API Base URL。未显式传入时使用当前配置项 LLM_BASE_URL。 + :param base_url_preset: Base URL 预设。未显式传入时使用当前配置项 LLM_BASE_URL_PRESET。 :return: LLM实例 """ provider_name = str(provider if provider is not None else settings.LLM_PROVIDER).lower() model_name = model if model is not None else settings.LLM_MODEL + api_key_value = api_key if api_key is not None else settings.LLM_API_KEY + base_url_value = base_url if base_url is not None else settings.LLM_BASE_URL + base_url_preset_value = ( + base_url_preset if base_url_preset is not None else settings.LLM_BASE_URL_PRESET + ) normalized_thinking_level = cls._resolve_thinking_level( thinking_level=thinking_level, ) @@ -542,17 +548,17 @@ class LLMHelper: runtime = await LLMProviderManager().resolve_runtime( provider_id=provider_name, model=model_name, - api_key=api_key, - base_url=base_url, - base_url_preset_id=base_url_preset, + api_key=api_key_value, + base_url=base_url_value, + base_url_preset_id=base_url_preset_value, ) except Exception as err: logger.debug(f"LLM provider 目录不可用,回退到旧运行时逻辑: {err}") runtime = cls._build_legacy_runtime( provider_name=provider_name, model_name=model_name, - api_key=api_key, - base_url=base_url, + api_key=api_key_value, + base_url=base_url_value, ) model_name = runtime.get("model_id") or model_name thinking_kwargs = cls._build_thinking_kwargs( diff --git a/tests/test_llm_helper_testcall.py b/tests/test_llm_helper_testcall.py index d31fb544..541d15c8 100644 --- a/tests/test_llm_helper_testcall.py +++ b/tests/test_llm_helper_testcall.py @@ -244,6 +244,70 @@ class LlmHelperTestCallTest(unittest.TestCase): self.assertEqual(len(calls), 1) self.assertEqual(calls[0].get("reasoning_effort"), "none") + def test_get_llm_reads_latest_settings_when_runtime_args_omitted(self): + resolve_calls = [] + llm_calls = [] + + class _FakeProviderManager: + async def resolve_runtime(self, **kwargs): + resolve_calls.append(kwargs) + return { + "provider_id": kwargs["provider_id"], + "runtime": "openai_compatible", + "model_id": kwargs["model"], + "api_key": kwargs["api_key"], + "base_url": kwargs["base_url"], + "default_headers": None, + "use_responses_api": None, + "model_record": None, + "model_metadata": None, + } + + class _FakeChatOpenAI: + def __init__(self, **kwargs): + llm_calls.append(kwargs) + self.model = kwargs["model"] + self.profile = None + + provider_module = ModuleType("app.agent.llm.provider") + provider_module.LLMProviderManager = _FakeProviderManager + openai_module = ModuleType("langchain_openai") + openai_module.ChatOpenAI = _FakeChatOpenAI + + with patch.object(llm_module.settings, "LLM_PROVIDER", "deepseek"), patch.object( + llm_module.settings, "LLM_MODEL", "deepseek-chat" + ), patch.object(llm_module.settings, "LLM_API_KEY", "updated-key"), patch.object( + llm_module.settings, "LLM_BASE_URL", "https://updated.example.com/v1" + ), patch.object( + llm_module.settings, "LLM_BASE_URL_PRESET", "updated-preset" + ), patch.dict( + sys.modules, + { + "app.agent.llm.provider": provider_module, + "langchain_openai": openai_module, + }, + ): + asyncio.run(llm_module.LLMHelper.get_llm()) + + self.assertEqual(len(resolve_calls), 1) + self.assertEqual( + resolve_calls[0], + { + "provider_id": "deepseek", + "model": "deepseek-chat", + "api_key": "updated-key", + "base_url": "https://updated.example.com/v1", + "base_url_preset_id": "updated-preset", + }, + ) + self.assertEqual(len(llm_calls), 1) + self.assertEqual(llm_calls[0].get("model"), "deepseek-chat") + self.assertEqual(llm_calls[0].get("api_key"), "updated-key") + self.assertEqual( + llm_calls[0].get("base_url"), + "https://updated.example.com/v1", + ) + def test_get_llm_maps_unified_max_to_openai_xhigh(self): calls = []