diff --git a/app/api/endpoints/llm.py b/app/api/endpoints/llm.py index ec0c6db3..1fb618c1 100644 --- a/app/api/endpoints/llm.py +++ b/app/api/endpoints/llm.py @@ -48,7 +48,7 @@ class LlmProviderAuthStartRequest(BaseModel): method: str -def _sanitize_llm_test_error(message: str, api_key: Optional[str] = None) -> str: +def _sanitize_llm_error(message: str, api_key: Optional[str] = None) -> str: """ 清理错误信息中的敏感字段,避免回显密钥。 """ @@ -70,11 +70,14 @@ def _sanitize_llm_test_error(message: str, api_key: Optional[str] = None) -> str ) normalized_message = sanitized.lower().replace("_", "").replace(" ", "") - if "str" in normalized_message and "modeldump" in normalized_message: + if "str" in normalized_message and ( + "modeldump" in normalized_message + or "setprivateattributes" in normalized_message + ): return ( - "服务返回内容不是兼容的模型响应," - "请检查基础地址是否填写为 API Base URL,不要填写网页地址或完整的 " - "chat/completions 路径" + "服务返回内容不是兼容的模型响应,请检查基础地址是否填写为 " + "API Base URL,如果服务要求 /v1 等版本路径,请包含在基础地址中," + "不要填写网页地址或完整的 chat/completions 路径" ) return sanitized @@ -113,7 +116,10 @@ async def get_llm_models( }, ) except Exception as err: - return schemas.Response(success=False, message=str(err)) + return schemas.Response( + success=False, + message=_sanitize_llm_error(str(err), api_key), + ) @router.get("/providers", summary="获取LLM提供商目录", response_model=schemas.Response) @@ -312,5 +318,5 @@ async def llm_test( except Exception as err: return schemas.Response( success=False, - message=_sanitize_llm_test_error(str(err), payload.api_key), + message=_sanitize_llm_error(str(err), payload.api_key), ) diff --git a/tests/test_llm_endpoint_error_messages.py b/tests/test_llm_endpoint_error_messages.py index 04098847..708dc596 100644 --- a/tests/test_llm_endpoint_error_messages.py +++ b/tests/test_llm_endpoint_error_messages.py @@ -24,3 +24,54 @@ def test_llm_test_maps_internal_model_dump_error_to_base_url_hint(): assert "基础地址" in resp.message assert "API Base URL" in resp.message assert "model_dump" not in resp.message + + +def test_llm_test_maps_internal_private_attribute_error_to_base_url_hint(): + """LLM 测试遇到 SDK 内部属性错误时应提示检查基础地址。""" + with patch.object(llm_endpoint.settings, "AI_AGENT_ENABLE", True), patch.object( + llm_endpoint.settings, "LLM_PROVIDER", "openai" + ), patch.object(llm_endpoint.settings, "LLM_MODEL", "gpt-4o-mini"), patch.object( + llm_endpoint.settings, "LLM_API_KEY", "sk-test" + ), patch.object( + llm_endpoint.LLMHelper, + "test_current_settings", + AsyncMock( + side_effect=AttributeError( + "'str' object has no attribute '_set_private_attributes'" + ) + ), + create=True, + ): + resp = asyncio.run(llm_endpoint.llm_test(_="token")) + + assert not resp.success + assert "基础地址" in resp.message + assert "API Base URL" in resp.message + assert "_set_private_attributes" not in resp.message + + +def test_llm_models_maps_internal_private_attribute_error_to_base_url_hint(): + """LLM 模型列表遇到 SDK 内部属性错误时应提示检查基础地址。""" + with patch.object( + llm_endpoint.LLMHelper, + "get_models", + AsyncMock( + side_effect=AttributeError( + "'str' object has no attribute '_set_private_attributes'" + ) + ), + create=True, + ): + resp = asyncio.run( + llm_endpoint.get_llm_models( + provider="openai", + api_key="sk-test", + base_url="https://example.com", + _="token", + ) + ) + + assert not resp.success + assert "基础地址" in resp.message + assert "API Base URL" in resp.message + assert "_set_private_attributes" not in resp.message