From 83acc8978e8a7b81c22661cfb8103d681613b455 Mon Sep 17 00:00:00 2001 From: vansangpfiev Date: Mon, 30 Sep 2024 12:34:39 +0700 Subject: [PATCH 01/12] feat: e2e test for APIs --- engine/controllers/engines.h | 2 +- engine/e2e-test/main.py | 8 +++++++ engine/e2e-test/test_api_engine_get.py | 22 ++++++++++++++++++ engine/e2e-test/test_api_engine_install.py | 22 ++++++++++++++++++ engine/e2e-test/test_api_engine_uninstall.py | 22 ++++++++++++++++++ .../test_api_model_pull_direct_url.py | 23 +++++++++++++++++++ engine/e2e-test/test_runner.py | 2 +- 7 files changed, 99 insertions(+), 2 deletions(-) create mode 100644 engine/e2e-test/test_api_engine_get.py create mode 100644 engine/e2e-test/test_api_engine_install.py create mode 100644 engine/e2e-test/test_api_engine_uninstall.py create mode 100644 engine/e2e-test/test_api_model_pull_direct_url.py diff --git a/engine/controllers/engines.h b/engine/controllers/engines.h index f1d76ff82..34cdf0934 100644 --- a/engine/controllers/engines.h +++ b/engine/controllers/engines.h @@ -12,7 +12,7 @@ class Engines : public drogon::HttpController { Engines() : engine_service_{EngineService()} {}; METHOD_LIST_BEGIN - METHOD_ADD(Engines::InstallEngine, "/{1}/install", Post); + METHOD_ADD(Engines::InstallEngine, "/install/{1}", Post); METHOD_ADD(Engines::UninstallEngine, "/{1}", Delete); METHOD_ADD(Engines::ListEngine, "", Get); METHOD_ADD(Engines::GetEngine, "/{1}", Get); diff --git a/engine/e2e-test/main.py b/engine/e2e-test/main.py index 37725a2fa..50f64ec8c 100644 --- a/engine/e2e-test/main.py +++ b/engine/e2e-test/main.py @@ -1,6 +1,14 @@ import pytest import sys +### e2e tests are expensive, have to keep engines tests in order from test_api_engine_list import TestApiEngineList +from test_api_engine_install import TestApiEngineInstall +from test_api_engine_get import TestApiEngineGet +from test_api_engine_uninstall import TestApiEngineUninstall +### +### models +from test_api_model_pull_direct_url import TestApiModelPullDirectUrl +### from test_cli_engine_get import TestCliEngineGet from test_cli_engine_install import TestCliEngineInstall from test_cli_engine_list import TestCliEngineList diff --git a/engine/e2e-test/test_api_engine_get.py b/engine/e2e-test/test_api_engine_get.py new file mode 100644 index 000000000..6627c7926 --- /dev/null +++ b/engine/e2e-test/test_api_engine_get.py @@ -0,0 +1,22 @@ +import pytest +import requests +from test_runner import start_server, stop_server + + +class TestApiEngineGet: + + @pytest.fixture(autouse=True) + def setup_and_teardown(self): + # Setup + success = start_server() + if not success: + raise Exception("Failed to start server") + + yield + + # Teardown + stop_server() + + def test_engines_get_llamacpp_should_be_successful(self): + response = requests.get("http://localhost:3928/engines/cortex.llamacpp") + assert response.status_code == 200 diff --git a/engine/e2e-test/test_api_engine_install.py b/engine/e2e-test/test_api_engine_install.py new file mode 100644 index 000000000..fbc7c6639 --- /dev/null +++ b/engine/e2e-test/test_api_engine_install.py @@ -0,0 +1,22 @@ +import pytest +import requests +from test_runner import start_server, stop_server + + +class TestApiEngineInstall: + + @pytest.fixture(autouse=True) + def setup_and_teardown(self): + # Setup + success = start_server() + if not success: + raise Exception("Failed to start server") + + yield + + # Teardown + stop_server() + + def test_engines_install_llamacpp_should_be_successful(self): + response = requests.post("http://localhost:3928/engines/install/cortex.llamacpp") + assert response.status_code == 200 diff --git a/engine/e2e-test/test_api_engine_uninstall.py b/engine/e2e-test/test_api_engine_uninstall.py new file mode 100644 index 000000000..be6223df6 --- /dev/null +++ b/engine/e2e-test/test_api_engine_uninstall.py @@ -0,0 +1,22 @@ +import pytest +import requests +from test_runner import start_server, stop_server + + +class TestApiEngineUninstall: + + @pytest.fixture(autouse=True) + def setup_and_teardown(self): + # Setup + success = start_server() + if not success: + raise Exception("Failed to start server") + + yield + + # Teardown + stop_server() + + def test_engines_uninstall_llamacpp_should_be_successful(self): + response = requests.delete("http://localhost:3928/engines/cortex.llamacpp") + assert response.status_code == 200 diff --git a/engine/e2e-test/test_api_model_pull_direct_url.py b/engine/e2e-test/test_api_model_pull_direct_url.py new file mode 100644 index 000000000..177be42ab --- /dev/null +++ b/engine/e2e-test/test_api_model_pull_direct_url.py @@ -0,0 +1,23 @@ +import pytest +import requests +from test_runner import start_server, stop_server + + +class TestApiModelPullDirectUrl: + @pytest.fixture(autouse=True) + def setup_and_teardown(self): + # Setup + success = start_server() + if not success: + raise Exception("Failed to start server") + + yield + + # Teardown + stop_server() + + def test_model_pull_with_direct_url_should_be_success(self): + myobj = {'modelId': 'https://huggingface.co/TheBloke/TinyLlama-1.1B-Chat-v0.3-GGUF/blob/main/tinyllama-1.1b-chat-v0.3.Q2_K.gguf'} + response = requests.post("http://localhost:3928/models/pull", json = myobj) + assert response.status_code == 200 + diff --git a/engine/e2e-test/test_runner.py b/engine/e2e-test/test_runner.py index 7716c55a0..f25f145eb 100644 --- a/engine/e2e-test/test_runner.py +++ b/engine/e2e-test/test_runner.py @@ -7,7 +7,7 @@ from typing import List # You might want to change the path of the executable based on your build directory -executable_windows_path = "build\\Debug\\cortex.exe" +executable_windows_path = "build\\Debug\\cortex-nightly.exe" executable_unix_path = "build/cortex" # Timeout From fee8723be8b706202fdb7c3b71da9757af556de8 Mon Sep 17 00:00:00 2001 From: vansangpfiev Date: Mon, 30 Sep 2024 13:07:41 +0700 Subject: [PATCH 02/12] feat: more models --- engine/controllers/models.cc | 15 ++++------- engine/controllers/models.h | 5 ++-- engine/e2e-test/main.py | 33 +++++++++++++----------- engine/e2e-test/test_api_model_delete.py | 28 ++++++++++++++++++++ engine/e2e-test/test_api_model_get.py | 28 ++++++++++++++++++++ engine/e2e-test/test_api_model_list.py | 22 ++++++++++++++++ 6 files changed, 104 insertions(+), 27 deletions(-) create mode 100644 engine/e2e-test/test_api_model_delete.py create mode 100644 engine/e2e-test/test_api_model_get.py create mode 100644 engine/e2e-test/test_api_model_list.py diff --git a/engine/controllers/models.cc b/engine/controllers/models.cc index 6ac0c1664..a578ecb61 100644 --- a/engine/controllers/models.cc +++ b/engine/controllers/models.cc @@ -103,13 +103,9 @@ void Models::ListModel( } } -void Models::GetModel( - const HttpRequestPtr& req, - std::function&& callback) const { - if (!http_util::HasFieldInReq(req, callback, "modelId")) { - return; - } - auto model_handle = (*(req->getJsonObject())).get("modelId", "").asString(); +void Models::GetModel(const HttpRequestPtr& req, + std::function&& callback, + const std::string& model_handle) const { LOG_DEBUG << "GetModel, Model handle: " << model_handle; Json::Value ret; ret["object"] = "list"; @@ -226,9 +222,8 @@ void Models::ImportModel( std::filesystem::path("imported") / std::filesystem::path(modelHandle + ".yml")) .string(); - cortex::db::ModelEntry model_entry{ - modelHandle, "local", "imported", - model_yaml_path, modelHandle}; + cortex::db::ModelEntry model_entry{modelHandle, "local", "imported", + model_yaml_path, modelHandle}; try { std::filesystem::create_directories( std::filesystem::path(model_yaml_path).parent_path()); diff --git a/engine/controllers/models.h b/engine/controllers/models.h index 9c67ff7dc..cb72304dd 100644 --- a/engine/controllers/models.h +++ b/engine/controllers/models.h @@ -13,7 +13,7 @@ class Models : public drogon::HttpController { METHOD_LIST_BEGIN METHOD_ADD(Models::PullModel, "/pull", Post); METHOD_ADD(Models::ListModel, "", Get); - METHOD_ADD(Models::GetModel, "/get", Post); + METHOD_ADD(Models::GetModel, "/{1}", Get); METHOD_ADD(Models::UpdateModel, "/update/", Post); METHOD_ADD(Models::ImportModel, "/import", Post); METHOD_ADD(Models::DeleteModel, "/{1}", Delete); @@ -25,7 +25,8 @@ class Models : public drogon::HttpController { void ListModel(const HttpRequestPtr& req, std::function&& callback) const; void GetModel(const HttpRequestPtr& req, - std::function&& callback) const; + std::function&& callback, + const std::string& model_handle) const; void UpdateModel( const HttpRequestPtr& req, std::function&& callback) const; diff --git a/engine/e2e-test/main.py b/engine/e2e-test/main.py index 50f64ec8c..f4b1607ef 100644 --- a/engine/e2e-test/main.py +++ b/engine/e2e-test/main.py @@ -1,24 +1,27 @@ import pytest import sys ### e2e tests are expensive, have to keep engines tests in order -from test_api_engine_list import TestApiEngineList -from test_api_engine_install import TestApiEngineInstall -from test_api_engine_get import TestApiEngineGet -from test_api_engine_uninstall import TestApiEngineUninstall +# from test_api_engine_list import TestApiEngineList +# from test_api_engine_install import TestApiEngineInstall +# from test_api_engine_get import TestApiEngineGet +# from test_api_engine_uninstall import TestApiEngineUninstall ### ### models -from test_api_model_pull_direct_url import TestApiModelPullDirectUrl +# from test_api_model_pull_direct_url import TestApiModelPullDirectUrl +# from test_api_model_delete import TestApiModelDelete +# from test_api_model_list import TestApiModelList +from test_api_model_get import TestApiModelGet ### -from test_cli_engine_get import TestCliEngineGet -from test_cli_engine_install import TestCliEngineInstall -from test_cli_engine_list import TestCliEngineList -from test_cli_engine_uninstall import TestCliEngineUninstall -from test_cli_model_delete import TestCliModelDelete -from test_cli_model_pull_direct_url import TestCliModelPullDirectUrl -from test_cli_server_start import TestCliServerStart -from test_cortex_update import TestCortexUpdate -from test_create_log_folder import TestCreateLogFolder -from test_cli_model_import import TestCliModelImport +# from test_cli_engine_get import TestCliEngineGet +# from test_cli_engine_install import TestCliEngineInstall +# from test_cli_engine_list import TestCliEngineList +# from test_cli_engine_uninstall import TestCliEngineUninstall +# from test_cli_model_delete import TestCliModelDelete +# from test_cli_model_pull_direct_url import TestCliModelPullDirectUrl +# from test_cli_server_start import TestCliServerStart +# from test_cortex_update import TestCortexUpdate +# from test_create_log_folder import TestCreateLogFolder +# from test_cli_model_import import TestCliModelImport if __name__ == "__main__": sys.exit(pytest.main([__file__, "-v"])) diff --git a/engine/e2e-test/test_api_model_delete.py b/engine/e2e-test/test_api_model_delete.py new file mode 100644 index 000000000..544cc9117 --- /dev/null +++ b/engine/e2e-test/test_api_model_delete.py @@ -0,0 +1,28 @@ +import pytest +import requests +from test_runner import popen, run +from test_runner import start_server, stop_server + + +class TestApiModelDelete: + + @pytest.fixture(autouse=True) + def setup_and_teardown(self): + # Setup + success = start_server() + if not success: + raise Exception("Failed to start server") + + # TODO: using pull with branch for easy testing tinyllama:gguf for example + popen(["pull", "tinyllama"], "1\n") + + yield + + # Teardown + # Clean up + run("Delete model", ["models", "delete", "tinyllama"]) + stop_server() + + def test_models_delete_should_be_successful(self): + response = requests.delete("http://localhost:3928/models/tinyllama:gguf") + assert response.status_code == 200 diff --git a/engine/e2e-test/test_api_model_get.py b/engine/e2e-test/test_api_model_get.py new file mode 100644 index 000000000..f60c949ef --- /dev/null +++ b/engine/e2e-test/test_api_model_get.py @@ -0,0 +1,28 @@ +import pytest +import requests +from test_runner import popen, run +from test_runner import start_server, stop_server + + +class TestApiModelGet: + + @pytest.fixture(autouse=True) + def setup_and_teardown(self): + # Setup + success = start_server() + if not success: + raise Exception("Failed to start server") + + # TODO: using pull with branch for easy testing tinyllama:gguf for example + popen(["pull", "tinyllama"], "1\n") + + yield + + # Teardown + # Clean up + run("Delete model", ["models", "delete", "tinyllama"]) + stop_server() + + def test_models_get_should_be_successful(self): + response = requests.get("http://localhost:3928/models/tinyllama:gguf") + assert response.status_code == 200 diff --git a/engine/e2e-test/test_api_model_list.py b/engine/e2e-test/test_api_model_list.py new file mode 100644 index 000000000..dc3889906 --- /dev/null +++ b/engine/e2e-test/test_api_model_list.py @@ -0,0 +1,22 @@ +import pytest +import requests +from test_runner import start_server, stop_server + + +class TestApiModelList: + + @pytest.fixture(autouse=True) + def setup_and_teardown(self): + # Setup + success = start_server() + if not success: + raise Exception("Failed to start server") + + yield + + # Teardown + stop_server() + + def test_models_list_should_be_successful(self): + response = requests.get("http://localhost:3928/models") + assert response.status_code == 200 From f5fbbb53024343b9a1f4403a702f8aa8b880c013 Mon Sep 17 00:00:00 2001 From: vansangpfiev Date: Mon, 30 Sep 2024 16:29:33 +0700 Subject: [PATCH 03/12] feat: models update --- engine/controllers/models.cc | 12 ++++------ engine/controllers/models.h | 8 +++---- engine/e2e-test/main.py | 3 ++- engine/e2e-test/test_api_model_get.py | 2 +- engine/e2e-test/test_api_model_update.py | 29 ++++++++++++++++++++++++ 5 files changed, 40 insertions(+), 14 deletions(-) create mode 100644 engine/e2e-test/test_api_model_update.py diff --git a/engine/controllers/models.cc b/engine/controllers/models.cc index a578ecb61..8c4014830 100644 --- a/engine/controllers/models.cc +++ b/engine/controllers/models.cc @@ -162,13 +162,9 @@ void Models::DeleteModel(const HttpRequestPtr& req, } } -void Models::UpdateModel( - const HttpRequestPtr& req, - std::function&& callback) const { - if (!http_util::HasFieldInReq(req, callback, "modelId")) { - return; - } - auto model_id = (*(req->getJsonObject())).get("modelId", "").asString(); +void Models::UpdateModel(const HttpRequestPtr& req, + std::function&& callback, + const std::string& model_id) const { auto json_body = *(req->getJsonObject()); try { cortex::db::Models model_list_utils; @@ -188,7 +184,7 @@ void Models::UpdateModel( ret["message"] = message; auto resp = cortex_utils::CreateCortexHttpJsonResponse(ret); - resp->setStatusCode(k400BadRequest); + resp->setStatusCode(k200OK); callback(resp); } catch (const std::exception& e) { diff --git a/engine/controllers/models.h b/engine/controllers/models.h index cb72304dd..7f286e8f7 100644 --- a/engine/controllers/models.h +++ b/engine/controllers/models.h @@ -14,7 +14,7 @@ class Models : public drogon::HttpController { METHOD_ADD(Models::PullModel, "/pull", Post); METHOD_ADD(Models::ListModel, "", Get); METHOD_ADD(Models::GetModel, "/{1}", Get); - METHOD_ADD(Models::UpdateModel, "/update/", Post); + METHOD_ADD(Models::UpdateModel, "/{1}", Post); METHOD_ADD(Models::ImportModel, "/import", Post); METHOD_ADD(Models::DeleteModel, "/{1}", Delete); METHOD_ADD(Models::SetModelAlias, "/alias", Post); @@ -27,9 +27,9 @@ class Models : public drogon::HttpController { void GetModel(const HttpRequestPtr& req, std::function&& callback, const std::string& model_handle) const; - void UpdateModel( - const HttpRequestPtr& req, - std::function&& callback) const; + void UpdateModel(const HttpRequestPtr& req, + std::function&& callback, + const std::string& model_id) const; void ImportModel( const HttpRequestPtr& req, std::function&& callback) const; diff --git a/engine/e2e-test/main.py b/engine/e2e-test/main.py index f4b1607ef..c1b2f2884 100644 --- a/engine/e2e-test/main.py +++ b/engine/e2e-test/main.py @@ -10,7 +10,8 @@ # from test_api_model_pull_direct_url import TestApiModelPullDirectUrl # from test_api_model_delete import TestApiModelDelete # from test_api_model_list import TestApiModelList -from test_api_model_get import TestApiModelGet +# from test_api_model_get import TestApiModelGet +from test_api_model_update import TestApiModelUpdate ### # from test_cli_engine_get import TestCliEngineGet # from test_cli_engine_install import TestCliEngineInstall diff --git a/engine/e2e-test/test_api_model_get.py b/engine/e2e-test/test_api_model_get.py index f60c949ef..9b50636ed 100644 --- a/engine/e2e-test/test_api_model_get.py +++ b/engine/e2e-test/test_api_model_get.py @@ -20,7 +20,7 @@ def setup_and_teardown(self): # Teardown # Clean up - run("Delete model", ["models", "delete", "tinyllama"]) + run("Delete model", ["models", "delete", "tinyllama:gguf"]) stop_server() def test_models_get_should_be_successful(self): diff --git a/engine/e2e-test/test_api_model_update.py b/engine/e2e-test/test_api_model_update.py new file mode 100644 index 000000000..727d56782 --- /dev/null +++ b/engine/e2e-test/test_api_model_update.py @@ -0,0 +1,29 @@ +import pytest +import requests +from test_runner import popen, run +from test_runner import start_server, stop_server + + +class TestApiModelUpdate: + + @pytest.fixture(autouse=True) + def setup_and_teardown(self): + # Setup + success = start_server() + if not success: + raise Exception("Failed to start server") + + # TODO: using pull with branch for easy testing tinyllama:gguf for example + popen(["pull", "tinyllama"], "1\n") + + yield + + # Teardown + # Clean up + run("Delete model", ["models", "delete", "tinyllama:gguf"]) + stop_server() + + def test_models_update_should_be_successful(self): + body_json = {'modelId': 'tinyllama:gguf'} + response = requests.post("http://localhost:3928/models/tinyllama:gguf", json = body_json) + assert response.status_code == 200 From 67de5a39d618f8051ece3d59f97fe7a5dc0c4f59 Mon Sep 17 00:00:00 2001 From: vansangpfiev Date: Mon, 30 Sep 2024 16:51:29 +0700 Subject: [PATCH 04/12] feat: models alias --- engine/e2e-test/main.py | 3 ++- engine/e2e-test/test_api_model_alias.py | 30 +++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 engine/e2e-test/test_api_model_alias.py diff --git a/engine/e2e-test/main.py b/engine/e2e-test/main.py index c1b2f2884..4c7f531c3 100644 --- a/engine/e2e-test/main.py +++ b/engine/e2e-test/main.py @@ -11,7 +11,8 @@ # from test_api_model_delete import TestApiModelDelete # from test_api_model_list import TestApiModelList # from test_api_model_get import TestApiModelGet -from test_api_model_update import TestApiModelUpdate +# from test_api_model_update import TestApiModelUpdate +from test_api_model_alias import TestApiModelAlias ### # from test_cli_engine_get import TestCliEngineGet # from test_cli_engine_install import TestCliEngineInstall diff --git a/engine/e2e-test/test_api_model_alias.py b/engine/e2e-test/test_api_model_alias.py new file mode 100644 index 000000000..12c26d7de --- /dev/null +++ b/engine/e2e-test/test_api_model_alias.py @@ -0,0 +1,30 @@ +import pytest +import requests +from test_runner import popen, run +from test_runner import start_server, stop_server + + +class TestApiModelAlias: + + @pytest.fixture(autouse=True) + def setup_and_teardown(self): + # Setup + success = start_server() + if not success: + raise Exception("Failed to start server") + + # TODO: using pull with branch for easy testing tinyllama:gguf for example + popen(["pull", "tinyllama"], "1\n") + + yield + + # Teardown + # Clean up + run("Delete model", ["models", "delete", "tinyllama:gguf"]) + stop_server() + + def test_models_set_alias_should_be_successful(self): + body_json = {'modelId': 'tinyllama:gguf', + 'modelAlias': 'tg'} + response = requests.post("http://localhost:3928/models/alias", json = body_json) + assert response.status_code == 200 From 5afb47078ffea0c90d54c8d5afc341b4433d35c2 Mon Sep 17 00:00:00 2001 From: vansangpfiev Date: Tue, 1 Oct 2024 11:07:05 +0700 Subject: [PATCH 05/12] feat: more --- engine/e2e-test/main.py | 40 ++++++++++++------------ engine/e2e-test/test_api_model_import.py | 22 +++++++++++++ engine/e2e-test/test_runner.py | 2 +- 3 files changed, 43 insertions(+), 21 deletions(-) create mode 100644 engine/e2e-test/test_api_model_import.py diff --git a/engine/e2e-test/main.py b/engine/e2e-test/main.py index 4c7f531c3..4b67c19dc 100644 --- a/engine/e2e-test/main.py +++ b/engine/e2e-test/main.py @@ -1,29 +1,29 @@ import pytest import sys ### e2e tests are expensive, have to keep engines tests in order -# from test_api_engine_list import TestApiEngineList -# from test_api_engine_install import TestApiEngineInstall -# from test_api_engine_get import TestApiEngineGet -# from test_api_engine_uninstall import TestApiEngineUninstall -### +from test_api_engine_list import TestApiEngineList +from test_api_engine_install import TestApiEngineInstall +from test_api_engine_get import TestApiEngineGet +from test_api_engine_uninstall import TestApiEngineUninstall ### models -# from test_api_model_pull_direct_url import TestApiModelPullDirectUrl -# from test_api_model_delete import TestApiModelDelete -# from test_api_model_list import TestApiModelList -# from test_api_model_get import TestApiModelGet -# from test_api_model_update import TestApiModelUpdate +from test_api_model_pull_direct_url import TestApiModelPullDirectUrl +from test_api_model_delete import TestApiModelDelete +from test_api_model_list import TestApiModelList +from test_api_model_get import TestApiModelGet from test_api_model_alias import TestApiModelAlias +from test_api_model_update import TestApiModelUpdate +from test_api_model_import import TestApiModelImport ### -# from test_cli_engine_get import TestCliEngineGet -# from test_cli_engine_install import TestCliEngineInstall -# from test_cli_engine_list import TestCliEngineList -# from test_cli_engine_uninstall import TestCliEngineUninstall -# from test_cli_model_delete import TestCliModelDelete -# from test_cli_model_pull_direct_url import TestCliModelPullDirectUrl -# from test_cli_server_start import TestCliServerStart -# from test_cortex_update import TestCortexUpdate -# from test_create_log_folder import TestCreateLogFolder -# from test_cli_model_import import TestCliModelImport +from test_cli_engine_get import TestCliEngineGet +from test_cli_engine_install import TestCliEngineInstall +from test_cli_engine_list import TestCliEngineList +from test_cli_engine_uninstall import TestCliEngineUninstall +from test_cli_model_delete import TestCliModelDelete +from test_cli_model_pull_direct_url import TestCliModelPullDirectUrl +from test_cli_server_start import TestCliServerStart +from test_cortex_update import TestCortexUpdate +from test_create_log_folder import TestCreateLogFolder +from test_cli_model_import import TestCliModelImport if __name__ == "__main__": sys.exit(pytest.main([__file__, "-v"])) diff --git a/engine/e2e-test/test_api_model_import.py b/engine/e2e-test/test_api_model_import.py new file mode 100644 index 000000000..086f90df6 --- /dev/null +++ b/engine/e2e-test/test_api_model_import.py @@ -0,0 +1,22 @@ +import pytest +import requests +from test_runner import start_server, stop_server + +class TestApiModelImport: + @pytest.fixture(autouse=True) + def setup_and_teardown(self): + # Setup + success = start_server() + if not success: + raise Exception("Failed to start server") + + yield + + stop_server() + + @pytest.mark.skipif(True, reason="Expensive test. Only test when you have local gguf file.") + def test_model_import_should_be_success(self): + body_json = {'modelId': 'tinyllama:gguf', + 'modelPath': '/path/to/local/gguf'} + response = requests.post("http://localhost:3928/models/import", json = body_json) + assert response.status_code == 200 \ No newline at end of file diff --git a/engine/e2e-test/test_runner.py b/engine/e2e-test/test_runner.py index f25f145eb..7716c55a0 100644 --- a/engine/e2e-test/test_runner.py +++ b/engine/e2e-test/test_runner.py @@ -7,7 +7,7 @@ from typing import List # You might want to change the path of the executable based on your build directory -executable_windows_path = "build\\Debug\\cortex-nightly.exe" +executable_windows_path = "build\\Debug\\cortex.exe" executable_unix_path = "build/cortex" # Timeout From 9066ac4a39c1cb1bcd799c5d8be32f95bc6545ff Mon Sep 17 00:00:00 2001 From: vansangpfiev Date: Tue, 1 Oct 2024 16:23:02 +0700 Subject: [PATCH 06/12] feat: more --- engine/e2e-test/main.py | 8 ++++--- engine/e2e-test/test_api_model_alias.py | 6 ------ engine/e2e-test/test_api_model_delete.py | 5 ----- engine/e2e-test/test_api_model_get.py | 6 ------ engine/e2e-test/test_api_model_start.py | 27 ++++++++++++++++++++++++ engine/e2e-test/test_api_model_stop.py | 25 ++++++++++++++++++++++ engine/e2e-test/test_api_model_update.py | 6 ------ 7 files changed, 57 insertions(+), 26 deletions(-) create mode 100644 engine/e2e-test/test_api_model_start.py create mode 100644 engine/e2e-test/test_api_model_stop.py diff --git a/engine/e2e-test/main.py b/engine/e2e-test/main.py index 4b67c19dc..62d921777 100644 --- a/engine/e2e-test/main.py +++ b/engine/e2e-test/main.py @@ -5,13 +5,15 @@ from test_api_engine_install import TestApiEngineInstall from test_api_engine_get import TestApiEngineGet from test_api_engine_uninstall import TestApiEngineUninstall -### models +### models, keeps in order from test_api_model_pull_direct_url import TestApiModelPullDirectUrl -from test_api_model_delete import TestApiModelDelete -from test_api_model_list import TestApiModelList +from test_api_model_start import TestApiModelStart +from test_api_model_stop import TestApiModelStop from test_api_model_get import TestApiModelGet from test_api_model_alias import TestApiModelAlias +from test_api_model_list import TestApiModelList from test_api_model_update import TestApiModelUpdate +from test_api_model_delete import TestApiModelDelete from test_api_model_import import TestApiModelImport ### from test_cli_engine_get import TestCliEngineGet diff --git a/engine/e2e-test/test_api_model_alias.py b/engine/e2e-test/test_api_model_alias.py index 12c26d7de..24796b3ba 100644 --- a/engine/e2e-test/test_api_model_alias.py +++ b/engine/e2e-test/test_api_model_alias.py @@ -13,14 +13,8 @@ def setup_and_teardown(self): if not success: raise Exception("Failed to start server") - # TODO: using pull with branch for easy testing tinyllama:gguf for example - popen(["pull", "tinyllama"], "1\n") - yield - # Teardown - # Clean up - run("Delete model", ["models", "delete", "tinyllama:gguf"]) stop_server() def test_models_set_alias_should_be_successful(self): diff --git a/engine/e2e-test/test_api_model_delete.py b/engine/e2e-test/test_api_model_delete.py index 544cc9117..f45768c66 100644 --- a/engine/e2e-test/test_api_model_delete.py +++ b/engine/e2e-test/test_api_model_delete.py @@ -13,14 +13,9 @@ def setup_and_teardown(self): if not success: raise Exception("Failed to start server") - # TODO: using pull with branch for easy testing tinyllama:gguf for example - popen(["pull", "tinyllama"], "1\n") - yield # Teardown - # Clean up - run("Delete model", ["models", "delete", "tinyllama"]) stop_server() def test_models_delete_should_be_successful(self): diff --git a/engine/e2e-test/test_api_model_get.py b/engine/e2e-test/test_api_model_get.py index 9b50636ed..8d5360f67 100644 --- a/engine/e2e-test/test_api_model_get.py +++ b/engine/e2e-test/test_api_model_get.py @@ -13,14 +13,8 @@ def setup_and_teardown(self): if not success: raise Exception("Failed to start server") - # TODO: using pull with branch for easy testing tinyllama:gguf for example - popen(["pull", "tinyllama"], "1\n") - yield - # Teardown - # Clean up - run("Delete model", ["models", "delete", "tinyllama:gguf"]) stop_server() def test_models_get_should_be_successful(self): diff --git a/engine/e2e-test/test_api_model_start.py b/engine/e2e-test/test_api_model_start.py new file mode 100644 index 000000000..ca923a4fb --- /dev/null +++ b/engine/e2e-test/test_api_model_start.py @@ -0,0 +1,27 @@ +import pytest +import requests +from test_runner import popen +from test_runner import start_server, stop_server + + +class TestApiModelStart: + + @pytest.fixture(autouse=True) + def setup_and_teardown(self): + # Setup + success = start_server() + if not success: + raise Exception("Failed to start server") + + # TODO: using pull with branch for easy testing tinyllama:gguf for example + popen(["pull", "tinyllama"], "1\n") + + yield + + # Teardown + stop_server() + + def test_models_start_should_be_successful(self): + json_body = {"model": "tinyllama:gguf"} + response = requests.post("http://localhost:3928/models/start", json = json_body) + assert response.status_code == 200 diff --git a/engine/e2e-test/test_api_model_stop.py b/engine/e2e-test/test_api_model_stop.py new file mode 100644 index 000000000..3370e9c92 --- /dev/null +++ b/engine/e2e-test/test_api_model_stop.py @@ -0,0 +1,25 @@ +import pytest +import requests +from test_runner import start_server, stop_server + + +class TestApiModelStop: + + @pytest.fixture(autouse=True) + def setup_and_teardown(self): + # Setup + success = start_server() + if not success: + raise Exception("Failed to start server") + + yield + + # Teardown + stop_server() + + def test_models_stop_should_be_successful(self): + json_body = {"model": "tinyllama:gguf"} + response = requests.post("http://localhost:3928/models/start", json = json_body) + assert response.status_code == 200 + response = requests.post("http://localhost:3928/models/stop", json = json_body) + assert response.status_code == 200 diff --git a/engine/e2e-test/test_api_model_update.py b/engine/e2e-test/test_api_model_update.py index 727d56782..476936917 100644 --- a/engine/e2e-test/test_api_model_update.py +++ b/engine/e2e-test/test_api_model_update.py @@ -13,14 +13,8 @@ def setup_and_teardown(self): if not success: raise Exception("Failed to start server") - # TODO: using pull with branch for easy testing tinyllama:gguf for example - popen(["pull", "tinyllama"], "1\n") - yield - # Teardown - # Clean up - run("Delete model", ["models", "delete", "tinyllama:gguf"]) stop_server() def test_models_update_should_be_successful(self): From 42465c9ff0c75a5d556cdf55d3b5cbeb4dd38431 Mon Sep 17 00:00:00 2001 From: vansangpfiev Date: Wed, 2 Oct 2024 11:15:58 +0700 Subject: [PATCH 07/12] chore: rename modelId to model --- engine/commands/cmd_info.cc | 55 ------------------- engine/commands/cmd_info.h | 14 ----- engine/commands/model_get_cmd.cc | 1 - engine/commands/model_list_cmd.cc | 2 +- engine/controllers/command_line_parser.cc | 1 - engine/controllers/models.cc | 20 +++---- engine/controllers/models.h | 6 +- engine/controllers/swagger.cc | 22 ++++---- engine/database/models.cc | 16 +++--- engine/database/models.h | 2 +- engine/e2e-test/test_api_model_alias.py | 2 +- engine/e2e-test/test_api_model_import.py | 2 +- .../test_api_model_pull_direct_url.py | 2 +- engine/e2e-test/test_api_model_update.py | 2 +- engine/services/model_service.cc | 8 ++- 15 files changed, 44 insertions(+), 111 deletions(-) delete mode 100644 engine/commands/cmd_info.cc delete mode 100644 engine/commands/cmd_info.h diff --git a/engine/commands/cmd_info.cc b/engine/commands/cmd_info.cc deleted file mode 100644 index a25697b8d..000000000 --- a/engine/commands/cmd_info.cc +++ /dev/null @@ -1,55 +0,0 @@ -#include "cmd_info.h" -#include -#include "trantor/utils/Logger.h" -#include "utils/logging_utils.h" - -namespace commands { -namespace { -constexpr const char* kDelimiter = ":"; - -std::vector split(std::string& s, const std::string& delimiter) { - std::vector tokens; - size_t pos = 0; - std::string token; - while ((pos = s.find(delimiter)) != std::string::npos) { - token = s.substr(0, pos); - tokens.push_back(token); - s.erase(0, pos + delimiter.length()); - } - tokens.push_back(s); - - return tokens; -} -} // namespace - -CmdInfo::CmdInfo(std::string model_id) { - Parse(std::move(model_id)); -} - -void CmdInfo::Parse(std::string model_id) { - if (model_id.find(kDelimiter) == std::string::npos) { - engine_name = "cortex.llamacpp"; - model_name = std::move(model_id); - branch = "main"; - } else { - auto res = split(model_id, kDelimiter); - if (res.size() != 2) { - CTL_ERR(" does not valid"); - return; - } else { - model_name = std::move(res[0]); - branch = std::move(res[1]); - if (branch.find("onnx") != std::string::npos) { - engine_name = "cortex.onnx"; - } else if (branch.find("tensorrt") != std::string::npos) { - engine_name = "cortex.tensorrt-llm"; - } else if (branch.find("gguf") != std::string::npos) { - engine_name = "cortex.llamacpp"; - } else { - CTL_ERR("Not a valid branch model_name " << branch); - } - } - } -} - -} // namespace commands \ No newline at end of file diff --git a/engine/commands/cmd_info.h b/engine/commands/cmd_info.h deleted file mode 100644 index 460990757..000000000 --- a/engine/commands/cmd_info.h +++ /dev/null @@ -1,14 +0,0 @@ -#pragma once -#include -namespace commands { -struct CmdInfo { - explicit CmdInfo(std::string model_id); - - std::string engine_name; - std::string model_name; - std::string branch; - - private: - void Parse(std::string model_id); -}; -} // namespace commands \ No newline at end of file diff --git a/engine/commands/model_get_cmd.cc b/engine/commands/model_get_cmd.cc index 5f6658cba..47931ee89 100644 --- a/engine/commands/model_get_cmd.cc +++ b/engine/commands/model_get_cmd.cc @@ -3,7 +3,6 @@ #include #include #include -#include "cmd_info.h" #include "config/yaml_config.h" #include "database/models.h" #include "utils/file_manager_utils.h" diff --git a/engine/commands/model_list_cmd.cc b/engine/commands/model_list_cmd.cc index 3fe0b4700..7ac538cdf 100644 --- a/engine/commands/model_list_cmd.cc +++ b/engine/commands/model_list_cmd.cc @@ -31,7 +31,7 @@ void ModelListCmd::Exec() { count += 1; yaml_handler.ModelConfigFromFile(model_entry.path_to_model_yaml); auto model_config = yaml_handler.GetModelConfig(); - table.add_row({std::to_string(count), model_entry.model_id, + table.add_row({std::to_string(count), model_entry.model, model_entry.model_alias, model_config.engine, model_config.version}); yaml_handler.Reset(); diff --git a/engine/controllers/command_line_parser.cc b/engine/controllers/command_line_parser.cc index 1ae32b473..2de603fd0 100644 --- a/engine/controllers/command_line_parser.cc +++ b/engine/controllers/command_line_parser.cc @@ -1,7 +1,6 @@ #include "command_line_parser.h" #include "commands/chat_cmd.h" #include "commands/chat_completion_cmd.h" -#include "commands/cmd_info.h" #include "commands/cortex_upd_cmd.h" #include "commands/engine_get_cmd.h" #include "commands/engine_install_cmd.h" diff --git a/engine/controllers/models.cc b/engine/controllers/models.cc index 4bd7259f5..b7c0bc809 100644 --- a/engine/controllers/models.cc +++ b/engine/controllers/models.cc @@ -12,11 +12,11 @@ void Models::PullModel(const HttpRequestPtr& req, std::function&& callback) { - if (!http_util::HasFieldInReq(req, callback, "modelId")) { + if (!http_util::HasFieldInReq(req, callback, "model")) { return; } - auto model_handle = (*(req->getJsonObject())).get("modelId", "").asString(); + auto model_handle = (*(req->getJsonObject())).get("model", "").asString(); if (model_handle.empty()) { Json::Value ret; ret["result"] = "Bad Request"; @@ -105,8 +105,8 @@ void Models::ListModel( void Models::GetModel(const HttpRequestPtr& req, std::function&& callback, - const std::string& model_handle) const { - LOG_DEBUG << "GetModel, Model handle: " << model_handle; + const std::string& model_id) const { + LOG_DEBUG << "GetModel, Model handle: " << model_id; Json::Value ret; ret["object"] = "list"; Json::Value data(Json::arrayValue); @@ -114,7 +114,7 @@ void Models::GetModel(const HttpRequestPtr& req, try { cortex::db::Models modellist_handler; config::YamlHandler yaml_handler; - auto model_entry = modellist_handler.GetModelInfo(model_handle); + auto model_entry = modellist_handler.GetModelInfo(model_id); if (model_entry.has_error()) { // CLI_LOG("Error: " + model_entry.error()); ret["data"] = data; @@ -138,7 +138,7 @@ void Models::GetModel(const HttpRequestPtr& req, callback(resp); } catch (const std::exception& e) { std::string message = "Fail to get model information with ID '" + - model_handle + "': " + e.what(); + model_id + "': " + e.what(); LOG_ERROR << message; ret["data"] = data; ret["result"] = "Fail to get model information"; @@ -210,11 +210,11 @@ void Models::UpdateModel(const HttpRequestPtr& req, void Models::ImportModel( const HttpRequestPtr& req, std::function&& callback) const { - if (!http_util::HasFieldInReq(req, callback, "modelId") || + if (!http_util::HasFieldInReq(req, callback, "model") || !http_util::HasFieldInReq(req, callback, "modelPath")) { return; } - auto modelHandle = (*(req->getJsonObject())).get("modelId", "").asString(); + auto modelHandle = (*(req->getJsonObject())).get("model", "").asString(); auto modelPath = (*(req->getJsonObject())).get("modelPath", "").asString(); config::GGUFHandler gguf_handler; config::YamlHandler yaml_handler; @@ -280,11 +280,11 @@ void Models::ImportModel( void Models::SetModelAlias( const HttpRequestPtr& req, std::function&& callback) const { - if (!http_util::HasFieldInReq(req, callback, "modelId") || + if (!http_util::HasFieldInReq(req, callback, "model") || !http_util::HasFieldInReq(req, callback, "modelAlias")) { return; } - auto model_handle = (*(req->getJsonObject())).get("modelId", "").asString(); + auto model_handle = (*(req->getJsonObject())).get("model", "").asString(); auto model_alias = (*(req->getJsonObject())).get("modelAlias", "").asString(); LOG_DEBUG << "GetModel, Model handle: " << model_handle << ", Model alias: " << model_alias; diff --git a/engine/controllers/models.h b/engine/controllers/models.h index df926f8b2..df3466bfe 100644 --- a/engine/controllers/models.h +++ b/engine/controllers/models.h @@ -28,16 +28,16 @@ class Models : public drogon::HttpController { std::function&& callback) const; void GetModel(const HttpRequestPtr& req, std::function&& callback, - const std::string& model_handle) const; + const std::string& model_id) const; void UpdateModel(const HttpRequestPtr& req, std::function&& callback, - const std::string& model_handle) const; + const std::string& model_id) const; void ImportModel( const HttpRequestPtr& req, std::function&& callback) const; void DeleteModel(const HttpRequestPtr& req, std::function&& callback, - const std::string& model_handle); + const std::string& model_id); void SetModelAlias( const HttpRequestPtr& req, std::function&& callback) const; diff --git a/engine/controllers/swagger.cc b/engine/controllers/swagger.cc index 2ef36dace..c12dc4823 100644 --- a/engine/controllers/swagger.cc +++ b/engine/controllers/swagger.cc @@ -174,11 +174,11 @@ Json::Value SwaggerController::generateOpenAPISpec() { pull["requestBody"]["content"]["application/json"]["schema"]["type"] = "object"; pull["requestBody"]["content"]["application/json"]["schema"]["properties"] - ["modelId"]["type"] = "string"; + ["model"]["type"] = "string"; pull["requestBody"]["content"]["application/json"]["schema"]["required"] = Json::Value(Json::arrayValue); pull["requestBody"]["content"]["application/json"]["schema"]["required"] - .append("modelId"); + .append("model"); pull["responses"]["200"]["description"] = "Model start downloading"; pull["responses"]["400"]["description"] = "Bad request"; @@ -196,11 +196,11 @@ Json::Value SwaggerController::generateOpenAPISpec() { get["requestBody"]["content"]["application/json"]["schema"]["type"] = "object"; get["requestBody"]["content"]["application/json"]["schema"]["properties"] - ["modelId"]["type"] = "string"; + ["model"]["type"] = "string"; get["requestBody"]["content"]["application/json"]["schema"]["required"] = Json::Value(Json::arrayValue); get["requestBody"]["content"]["application/json"]["schema"]["required"] - .append("modelId"); + .append("model"); get["responses"]["200"]["description"] = "Model details retrieved successfully"; get["responses"]["400"]["description"] = "Failed to get model information"; @@ -216,11 +216,11 @@ Json::Value SwaggerController::generateOpenAPISpec() { update["requestBody"]["content"]["application/json"]["schema"]; updateSchema["type"] = "object"; updateSchema["required"] = Json::Value(Json::arrayValue); - updateSchema["required"].append("modelId"); + updateSchema["required"].append("model"); Json::Value& properties = updateSchema["properties"]; - properties["modelId"]["type"] = "string"; - properties["modelId"]["description"] = + properties["model"]["type"] = "string"; + properties["model"]["description"] = "Unique identifier for the model (cannot be updated)"; properties["name"]["type"] = "string"; @@ -396,13 +396,13 @@ Json::Value SwaggerController::generateOpenAPISpec() { import["requestBody"]["content"]["application/json"]["schema"]["type"] = "object"; import["requestBody"]["content"]["application/json"]["schema"]["properties"] - ["modelId"]["type"] = "string"; + ["model"]["type"] = "string"; import["requestBody"]["content"]["application/json"]["schema"]["properties"] ["modelPath"]["type"] = "string"; import["requestBody"]["content"]["application/json"]["schema"]["required"] = Json::Value(Json::arrayValue); import["requestBody"]["content"]["application/json"]["schema"]["required"] - .append("modelId"); + .append("model"); import["requestBody"]["content"]["application/json"]["schema"]["required"] .append("modelPath"); import["responses"]["200"]["description"] = "Model imported successfully"; @@ -424,13 +424,13 @@ Json::Value SwaggerController::generateOpenAPISpec() { alias["requestBody"]["content"]["application/json"]["schema"]["type"] = "object"; alias["requestBody"]["content"]["application/json"]["schema"]["properties"] - ["modelId"]["type"] = "string"; + ["model"]["type"] = "string"; alias["requestBody"]["content"]["application/json"]["schema"]["properties"] ["modelAlias"]["type"] = "string"; alias["requestBody"]["content"]["application/json"]["schema"]["required"] = Json::Value(Json::arrayValue); alias["requestBody"]["content"]["application/json"]["schema"]["required"] - .append("modelId"); + .append("model"); alias["requestBody"]["content"]["application/json"]["schema"]["required"] .append("modelAlias"); alias["responses"]["200"]["description"] = "Model alias set successfully"; diff --git a/engine/database/models.cc b/engine/database/models.cc index 5c637f1a2..3a39fd310 100644 --- a/engine/database/models.cc +++ b/engine/database/models.cc @@ -51,8 +51,8 @@ bool Models::IsUnique(const std::vector& entries, const std::string& model_alias) const { return std::none_of( entries.begin(), entries.end(), [&](const ModelEntry& entry) { - return entry.model_id == model_id || entry.model_alias == model_id || - entry.model_id == model_alias || + return entry.model == model_id || entry.model_alias == model_id || + entry.model == model_alias || entry.model_alias == model_alias; }); } @@ -67,7 +67,7 @@ cpp::result, std::string> Models::LoadModelListNoLock() while (query.executeStep()) { ModelEntry entry; - entry.model_id = query.getColumn(0).getString(); + entry.model = query.getColumn(0).getString(); entry.author_repo_id = query.getColumn(1).getString(); entry.branch_name = query.getColumn(2).getString(); entry.path_to_model_yaml = query.getColumn(3).getString(); @@ -153,7 +153,7 @@ cpp::result Models::GetModelInfo( query.bind(2, identifier); if (query.executeStep()) { ModelEntry entry; - entry.model_id = query.getColumn(0).getString(); + entry.model = query.getColumn(0).getString(); entry.author_repo_id = query.getColumn(1).getString(); entry.branch_name = query.getColumn(2).getString(); entry.path_to_model_yaml = query.getColumn(3).getString(); @@ -168,7 +168,7 @@ cpp::result Models::GetModelInfo( } void Models::PrintModelInfo(const ModelEntry& entry) const { - LOG_INFO << "Model ID: " << entry.model_id; + LOG_INFO << "Model ID: " << entry.model; LOG_INFO << "Author/Repo ID: " << entry.author_repo_id; LOG_INFO << "Branch Name: " << entry.branch_name; LOG_INFO << "Path to model.yaml: " << entry.path_to_model_yaml; @@ -186,11 +186,11 @@ cpp::result Models::AddModelEntry(ModelEntry new_entry, std::cout << "Test: " << model_list.error(); return cpp::fail(model_list.error()); } - if (IsUnique(model_list.value(), new_entry.model_id, + if (IsUnique(model_list.value(), new_entry.model, new_entry.model_alias)) { if (use_short_alias) { new_entry.model_alias = - GenerateShortenedAlias(new_entry.model_id, model_list.value()); + GenerateShortenedAlias(new_entry.model, model_list.value()); } SQLite::Statement insert( @@ -198,7 +198,7 @@ cpp::result Models::AddModelEntry(ModelEntry new_entry, "INSERT INTO models (model_id, author_repo_id, " "branch_name, path_to_model_yaml, model_alias) VALUES (?, ?, " "?, ?, ?)"); - insert.bind(1, new_entry.model_id); + insert.bind(1, new_entry.model); insert.bind(2, new_entry.author_repo_id); insert.bind(3, new_entry.branch_name); insert.bind(4, new_entry.path_to_model_yaml); diff --git a/engine/database/models.h b/engine/database/models.h index 184f1c6a6..f3ff99faa 100644 --- a/engine/database/models.h +++ b/engine/database/models.h @@ -8,7 +8,7 @@ namespace cortex::db { struct ModelEntry { - std::string model_id; + std::string model; std::string author_repo_id; std::string branch_name; std::string path_to_model_yaml; diff --git a/engine/e2e-test/test_api_model_alias.py b/engine/e2e-test/test_api_model_alias.py index 24796b3ba..1a17ad9e0 100644 --- a/engine/e2e-test/test_api_model_alias.py +++ b/engine/e2e-test/test_api_model_alias.py @@ -18,7 +18,7 @@ def setup_and_teardown(self): stop_server() def test_models_set_alias_should_be_successful(self): - body_json = {'modelId': 'tinyllama:gguf', + body_json = {'model': 'tinyllama:gguf', 'modelAlias': 'tg'} response = requests.post("http://localhost:3928/models/alias", json = body_json) assert response.status_code == 200 diff --git a/engine/e2e-test/test_api_model_import.py b/engine/e2e-test/test_api_model_import.py index 086f90df6..8dd34ea7a 100644 --- a/engine/e2e-test/test_api_model_import.py +++ b/engine/e2e-test/test_api_model_import.py @@ -16,7 +16,7 @@ def setup_and_teardown(self): @pytest.mark.skipif(True, reason="Expensive test. Only test when you have local gguf file.") def test_model_import_should_be_success(self): - body_json = {'modelId': 'tinyllama:gguf', + body_json = {'model': 'tinyllama:gguf', 'modelPath': '/path/to/local/gguf'} response = requests.post("http://localhost:3928/models/import", json = body_json) assert response.status_code == 200 \ No newline at end of file diff --git a/engine/e2e-test/test_api_model_pull_direct_url.py b/engine/e2e-test/test_api_model_pull_direct_url.py index 177be42ab..b39227847 100644 --- a/engine/e2e-test/test_api_model_pull_direct_url.py +++ b/engine/e2e-test/test_api_model_pull_direct_url.py @@ -17,7 +17,7 @@ def setup_and_teardown(self): stop_server() def test_model_pull_with_direct_url_should_be_success(self): - myobj = {'modelId': 'https://huggingface.co/TheBloke/TinyLlama-1.1B-Chat-v0.3-GGUF/blob/main/tinyllama-1.1b-chat-v0.3.Q2_K.gguf'} + myobj = {'model': 'https://huggingface.co/TheBloke/TinyLlama-1.1B-Chat-v0.3-GGUF/blob/main/tinyllama-1.1b-chat-v0.3.Q2_K.gguf'} response = requests.post("http://localhost:3928/models/pull", json = myobj) assert response.status_code == 200 diff --git a/engine/e2e-test/test_api_model_update.py b/engine/e2e-test/test_api_model_update.py index 476936917..8d28d412a 100644 --- a/engine/e2e-test/test_api_model_update.py +++ b/engine/e2e-test/test_api_model_update.py @@ -18,6 +18,6 @@ def setup_and_teardown(self): stop_server() def test_models_update_should_be_successful(self): - body_json = {'modelId': 'tinyllama:gguf'} + body_json = {'model': 'tinyllama:gguf'} response = requests.post("http://localhost:3928/models/tinyllama:gguf", json = body_json) assert response.status_code == 200 diff --git a/engine/services/model_service.cc b/engine/services/model_service.cc index 658534fed..6342c8a0a 100644 --- a/engine/services/model_service.cc +++ b/engine/services/model_service.cc @@ -25,6 +25,7 @@ void ParseGguf(const DownloadItem& ggufDownloadItem, model_config.id = ggufDownloadItem.localPath.parent_path().filename().string(); model_config.files = {ggufDownloadItem.localPath.string()}; + model_config.model = ggufDownloadItem.id; yaml_handler.UpdateModelConfig(model_config); auto yaml_path{ggufDownloadItem.localPath}; @@ -40,7 +41,7 @@ void ParseGguf(const DownloadItem& ggufDownloadItem, auto author_id = author.has_value() ? author.value() : "cortexso"; cortex::db::Models modellist_utils_obj; - cortex::db::ModelEntry model_entry{.model_id = ggufDownloadItem.id, + cortex::db::ModelEntry model_entry{.model = ggufDownloadItem.id, .author_repo_id = author_id, .branch_name = branch, .path_to_model_yaml = yaml_name.string(), @@ -290,10 +291,13 @@ cpp::result ModelService::DownloadModelFromCortexso( config::YamlHandler yaml_handler; yaml_handler.ModelConfigFromFile(model_yml_item->localPath.string()); auto mc = yaml_handler.GetModelConfig(); + mc.model = model_id; + yaml_handler.UpdateModelConfig(mc); + yaml_handler.WriteYamlFile(model_yml_item->localPath.string()); cortex::db::Models modellist_utils_obj; cortex::db::ModelEntry model_entry{ - .model_id = model_id, + .model = model_id, .author_repo_id = "cortexso", .branch_name = branch, .path_to_model_yaml = model_yml_item->localPath.string(), From 43a6c9e1efa9816748d9b2cc38bdfcf3a84fe98e Mon Sep 17 00:00:00 2001 From: vansangpfiev Date: Wed, 2 Oct 2024 11:25:04 +0700 Subject: [PATCH 08/12] fix: unit tests --- engine/test/components/test_models_db.cc | 52 ++++++++++++------------ 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/engine/test/components/test_models_db.cc b/engine/test/components/test_models_db.cc index fa5bb16ef..6be0f6ae9 100644 --- a/engine/test/components/test_models_db.cc +++ b/engine/test/components/test_models_db.cc @@ -31,30 +31,30 @@ class ModelsTestSuite : public ::testing::Test { TEST_F(ModelsTestSuite, TestAddModelEntry) { EXPECT_TRUE(model_list_.AddModelEntry(kTestModel).value()); - auto retrieved_model = model_list_.GetModelInfo(kTestModel.model_id); + auto retrieved_model = model_list_.GetModelInfo(kTestModel.model); EXPECT_TRUE(retrieved_model); - EXPECT_EQ(retrieved_model.value().model_id, kTestModel.model_id); + EXPECT_EQ(retrieved_model.value().model, kTestModel.model); EXPECT_EQ(retrieved_model.value().author_repo_id, kTestModel.author_repo_id); // // Clean up - EXPECT_TRUE(model_list_.DeleteModelEntry(kTestModel.model_id).value()); + EXPECT_TRUE(model_list_.DeleteModelEntry(kTestModel.model).value()); } TEST_F(ModelsTestSuite, TestGetModelInfo) { EXPECT_TRUE(model_list_.AddModelEntry(kTestModel).value()); - auto model_by_id = model_list_.GetModelInfo(kTestModel.model_id); + auto model_by_id = model_list_.GetModelInfo(kTestModel.model); EXPECT_TRUE(model_by_id); - EXPECT_EQ(model_by_id.value().model_id, kTestModel.model_id); + EXPECT_EQ(model_by_id.value().model, kTestModel.model); auto model_by_alias = model_list_.GetModelInfo("test_alias"); EXPECT_TRUE(model_by_alias); - EXPECT_EQ(model_by_alias.value().model_id, kTestModel.model_id); + EXPECT_EQ(model_by_alias.value().model, kTestModel.model); EXPECT_TRUE(model_list_.GetModelInfo("non_existent_model").has_error()); // Clean up - EXPECT_TRUE(model_list_.DeleteModelEntry(kTestModel.model_id).value()); + EXPECT_TRUE(model_list_.DeleteModelEntry(kTestModel.model).value()); } TEST_F(ModelsTestSuite, TestUpdateModelEntry) { @@ -63,22 +63,22 @@ TEST_F(ModelsTestSuite, TestUpdateModelEntry) { cortex::db::ModelEntry updated_model = kTestModel; EXPECT_TRUE( - model_list_.UpdateModelEntry(kTestModel.model_id, updated_model).value()); + model_list_.UpdateModelEntry(kTestModel.model, updated_model).value()); - auto retrieved_model = model_list_.GetModelInfo(kTestModel.model_id); + auto retrieved_model = model_list_.GetModelInfo(kTestModel.model); EXPECT_TRUE(retrieved_model); EXPECT_TRUE( - model_list_.UpdateModelEntry(kTestModel.model_id, updated_model).value()); + model_list_.UpdateModelEntry(kTestModel.model, updated_model).value()); // Clean up - EXPECT_TRUE(model_list_.DeleteModelEntry(kTestModel.model_id).value()); + EXPECT_TRUE(model_list_.DeleteModelEntry(kTestModel.model).value()); } TEST_F(ModelsTestSuite, TestDeleteModelEntry) { EXPECT_TRUE(model_list_.AddModelEntry(kTestModel).value()); - EXPECT_TRUE(model_list_.DeleteModelEntry(kTestModel.model_id).value()); - EXPECT_TRUE(model_list_.GetModelInfo(kTestModel.model_id).has_error()); + EXPECT_TRUE(model_list_.DeleteModelEntry(kTestModel.model).value()); + EXPECT_TRUE(model_list_.GetModelInfo(kTestModel.model).has_error()); } TEST_F(ModelsTestSuite, TestGenerateShortenedAlias) { @@ -88,7 +88,7 @@ TEST_F(ModelsTestSuite, TestGenerateShortenedAlias) { "huggingface.co/bartowski/llama3.1-7b-gguf/Model_ID_Xxx.gguf", models1.value()); EXPECT_EQ(alias, "model_id_xxx"); - EXPECT_TRUE(model_list_.UpdateModelAlias(kTestModel.model_id, alias).value()); + EXPECT_TRUE(model_list_.UpdateModelAlias(kTestModel.model, alias).value()); // Test with existing entries to force longer alias auto models2 = model_list_.LoadModelList(); @@ -98,7 +98,7 @@ TEST_F(ModelsTestSuite, TestGenerateShortenedAlias) { EXPECT_EQ(alias, "llama3.1-7b-gguf:model_id_xxx"); // Clean up - EXPECT_TRUE(model_list_.DeleteModelEntry(kTestModel.model_id).value()); + EXPECT_TRUE(model_list_.DeleteModelEntry(kTestModel.model).value()); } TEST_F(ModelsTestSuite, TestPersistence) { @@ -106,11 +106,11 @@ TEST_F(ModelsTestSuite, TestPersistence) { // Create a new ModelListUtils instance to test if it loads from file cortex::db::Models new_model_list(db_); - auto retrieved_model = new_model_list.GetModelInfo(kTestModel.model_id); + auto retrieved_model = new_model_list.GetModelInfo(kTestModel.model); EXPECT_TRUE(retrieved_model); - EXPECT_EQ(retrieved_model.value().model_id, kTestModel.model_id); + EXPECT_EQ(retrieved_model.value().model, kTestModel.model); EXPECT_EQ(retrieved_model.value().author_repo_id, kTestModel.author_repo_id); - EXPECT_TRUE(model_list_.DeleteModelEntry(kTestModel.model_id).value()); + EXPECT_TRUE(model_list_.DeleteModelEntry(kTestModel.model).value()); } TEST_F(ModelsTestSuite, TestUpdateModelAlias) { @@ -124,11 +124,11 @@ TEST_F(ModelsTestSuite, TestUpdateModelAlias) { // Test successful update EXPECT_TRUE( - model_list_.UpdateModelAlias(kTestModel.model_id, kNewTestAlias).value()); + model_list_.UpdateModelAlias(kTestModel.model, kNewTestAlias).value()); auto updated_model = model_list_.GetModelInfo(kNewTestAlias); EXPECT_TRUE(updated_model); EXPECT_EQ(updated_model.value().model_alias, kNewTestAlias); - EXPECT_EQ(updated_model.value().model_id, kTestModel.model_id); + EXPECT_EQ(updated_model.value().model, kTestModel.model); // Test update with non-existent model EXPECT_TRUE(model_list_.UpdateModelAlias(kNonExistentModel, kAnotherAlias) @@ -136,32 +136,32 @@ TEST_F(ModelsTestSuite, TestUpdateModelAlias) { // Test update with non-unique alias cortex::db::ModelEntry another_model = kTestModel; - another_model.model_id = kAnotherModelId; + another_model.model = kAnotherModelId; another_model.model_alias = kAnotherAlias; ASSERT_TRUE(model_list_.AddModelEntry(another_model).value()); EXPECT_FALSE( - model_list_.UpdateModelAlias(kTestModel.model_id, kAnotherAlias).value()); + model_list_.UpdateModelAlias(kTestModel.model, kAnotherAlias).value()); // Test update using model alias instead of model ID EXPECT_TRUE(model_list_.UpdateModelAlias(kNewTestAlias, kFinalTestAlias)); updated_model = model_list_.GetModelInfo(kFinalTestAlias); EXPECT_TRUE(updated_model); EXPECT_EQ(updated_model.value().model_alias, kFinalTestAlias); - EXPECT_EQ(updated_model.value().model_id, kTestModel.model_id); + EXPECT_EQ(updated_model.value().model, kTestModel.model); // Clean up - EXPECT_TRUE(model_list_.DeleteModelEntry(kTestModel.model_id).value()); + EXPECT_TRUE(model_list_.DeleteModelEntry(kTestModel.model).value()); EXPECT_TRUE(model_list_.DeleteModelEntry(kAnotherModelId).value()); } TEST_F(ModelsTestSuite, TestHasModel) { EXPECT_TRUE(model_list_.AddModelEntry(kTestModel).value()); - EXPECT_TRUE(model_list_.HasModel(kTestModel.model_id)); + EXPECT_TRUE(model_list_.HasModel(kTestModel.model)); EXPECT_TRUE(model_list_.HasModel("test_alias")); EXPECT_FALSE(model_list_.HasModel("non_existent_model")); // Clean up - EXPECT_TRUE(model_list_.DeleteModelEntry(kTestModel.model_id).value()); + EXPECT_TRUE(model_list_.DeleteModelEntry(kTestModel.model).value()); } } // namespace cortex::db \ No newline at end of file From 7cb2e9ca8317c824699c726645605d1c65d5362f Mon Sep 17 00:00:00 2001 From: vansangpfiev Date: Wed, 2 Oct 2024 14:05:41 +0700 Subject: [PATCH 09/12] fix: swagger --- engine/controllers/models.cc | 2 +- engine/controllers/swagger.cc | 69 ++++++++++++++++++++++++++--------- 2 files changed, 53 insertions(+), 18 deletions(-) diff --git a/engine/controllers/models.cc b/engine/controllers/models.cc index b7c0bc809..b4277ac00 100644 --- a/engine/controllers/models.cc +++ b/engine/controllers/models.cc @@ -382,7 +382,7 @@ void Models::StopModel(const HttpRequestPtr& req, callback(resp); } else { Json::Value ret; - ret["message"] = "Started successfully!"; + ret["message"] = "Stopped successfully!"; auto resp = cortex_utils::CreateCortexHttpJsonResponse(ret); resp->setStatusCode(k200OK); callback(resp); diff --git a/engine/controllers/swagger.cc b/engine/controllers/swagger.cc index c12dc4823..3f7b37934 100644 --- a/engine/controllers/swagger.cc +++ b/engine/controllers/swagger.cc @@ -61,7 +61,7 @@ Json::Value SwaggerController::generateOpenAPISpec() { // Engines endpoints // Install Engine { - Json::Value& path = spec["paths"]["/engines/{engine}/install"]["post"]; + Json::Value& path = spec["paths"]["/engines/install/{engine}"]["post"]; path["summary"] = "Install an engine"; path["parameters"][0]["name"] = "engine"; path["parameters"][0]["in"] = "path"; @@ -191,32 +191,39 @@ Json::Value SwaggerController::generateOpenAPISpec() { "Failed to get list model information"; // GetModel - Json::Value& get = spec["paths"]["/models/get"]["post"]; + Json::Value& get = spec["paths"]["/models/{model}"]["get"]; get["summary"] = "Get model details"; - get["requestBody"]["content"]["application/json"]["schema"]["type"] = + get["parameters"][0]["name"] = "model"; + get["parameters"][0]["in"] = "path"; + get["parameters"][0]["required"] = true; + get["parameters"][0]["schema"]["type"] = "string"; + + Json::Value& responses = get["responses"]; + responses["200"]["description"] = "Model details retrieved successfully"; + Json::Value& schema = + responses["200"]["content"]["application/json"]["schema"]; + responses["responses"]["400"]["description"] = "Failed to get model information"; + + responses["400"]["description"] = "Failed to get model information"; + responses["400"]["content"]["application/json"]["schema"]["type"] = "object"; - get["requestBody"]["content"]["application/json"]["schema"]["properties"] - ["model"]["type"] = "string"; - get["requestBody"]["content"]["application/json"]["schema"]["required"] = - Json::Value(Json::arrayValue); - get["requestBody"]["content"]["application/json"]["schema"]["required"] - .append("model"); - get["responses"]["200"]["description"] = - "Model details retrieved successfully"; - get["responses"]["400"]["description"] = "Failed to get model information"; + responses["400"]["content"]["application/json"]["schema"]["properties"] + ["message"]["type"] = "string"; // UpdateModel Endpoint - Json::Value& update = spec["paths"]["/models/update"]["post"]; + Json::Value& update = spec["paths"]["/models/{model}"]["post"]; update["summary"] = "Update model details"; update["description"] = "Update various attributes of a model based on the ModelConfig " "structure"; + update["parameters"][0]["name"] = "model"; + update["parameters"][0]["in"] = "path"; + update["parameters"][0]["required"] = true; + update["parameters"][0]["schema"]["type"] = "string"; Json::Value& updateSchema = update["requestBody"]["content"]["application/json"]["schema"]; updateSchema["type"] = "object"; - updateSchema["required"] = Json::Value(Json::arrayValue); - updateSchema["required"].append("model"); Json::Value& properties = updateSchema["properties"]; properties["model"]["type"] = "string"; @@ -409,9 +416,9 @@ Json::Value SwaggerController::generateOpenAPISpec() { import["responses"]["400"]["description"] = "Failed to import model"; // DeleteModel - Json::Value& del = spec["paths"]["/models/{model_id}"]["delete"]; + Json::Value& del = spec["paths"]["/models/{model}"]["delete"]; del["summary"] = "Delete a model"; - del["parameters"][0]["name"] = "model_id"; + del["parameters"][0]["name"] = "model"; del["parameters"][0]["in"] = "path"; del["parameters"][0]["required"] = true; del["parameters"][0]["schema"]["type"] = "string"; @@ -435,6 +442,34 @@ Json::Value SwaggerController::generateOpenAPISpec() { .append("modelAlias"); alias["responses"]["200"]["description"] = "Model alias set successfully"; alias["responses"]["400"]["description"] = "Failed to set model alias"; + + // Start Model + Json::Value& start = spec["paths"]["/models/start"]["post"]; + start["summary"] = "Start model"; + start["requestBody"]["content"]["application/json"]["schema"]["type"] = + "object"; + start["requestBody"]["content"]["application/json"]["schema"]["properties"] + ["model"]["type"] = "string"; + start["requestBody"]["content"]["application/json"]["schema"]["required"] = + Json::Value(Json::arrayValue); + start["requestBody"]["content"]["application/json"]["schema"]["required"] + .append("model"); + start["responses"]["200"]["description"] = "Start model successfully"; + start["responses"]["400"]["description"] = "Failed to start model"; + + // Stop Model + Json::Value& stop = spec["paths"]["/models/stop"]["post"]; + stop["summary"] = "Stop model"; + stop["requestBody"]["content"]["application/json"]["schema"]["type"] = + "object"; + stop["requestBody"]["content"]["application/json"]["schema"]["properties"] + ["model"]["type"] = "string"; + stop["requestBody"]["content"]["application/json"]["schema"]["required"] = + Json::Value(Json::arrayValue); + stop["requestBody"]["content"]["application/json"]["schema"]["required"] + .append("model"); + stop["responses"]["200"]["description"] = "Stop model successfully"; + stop["responses"]["400"]["description"] = "Failed to stop model"; } // OpenAI Compatible Endpoints From 90848909e6656faeaeed071d730c2c5e7b07796e Mon Sep 17 00:00:00 2001 From: vansangpfiev Date: Wed, 2 Oct 2024 16:12:06 +0700 Subject: [PATCH 10/12] fix: e2e --- .../test_api_model_pull_direct_url.py | 27 +++++++++++++++---- engine/e2e-test/test_api_model_start.py | 3 ++- engine/e2e-test/test_cli_model_delete.py | 2 +- 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/engine/e2e-test/test_api_model_pull_direct_url.py b/engine/e2e-test/test_api_model_pull_direct_url.py index b39227847..aa78f4026 100644 --- a/engine/e2e-test/test_api_model_pull_direct_url.py +++ b/engine/e2e-test/test_api_model_pull_direct_url.py @@ -1,6 +1,6 @@ import pytest import requests -from test_runner import start_server, stop_server +from test_runner import start_server, stop_server, run class TestApiModelPullDirectUrl: @@ -10,14 +10,31 @@ def setup_and_teardown(self): success = start_server() if not success: raise Exception("Failed to start server") - + # Delete model if exists + run( + "Delete model", + [ + "models", + "delete", + "TheBloke:TinyLlama-1.1B-Chat-v0.3-GGUF:tinyllama-1.1b-chat-v0.3.Q2_K.gguf", + ], + ) yield # Teardown stop_server() + run( + "Delete model", + [ + "models", + "delete", + "TheBloke:TinyLlama-1.1B-Chat-v0.3-GGUF:tinyllama-1.1b-chat-v0.3.Q2_K.gguf", + ], + ) def test_model_pull_with_direct_url_should_be_success(self): - myobj = {'model': 'https://huggingface.co/TheBloke/TinyLlama-1.1B-Chat-v0.3-GGUF/blob/main/tinyllama-1.1b-chat-v0.3.Q2_K.gguf'} - response = requests.post("http://localhost:3928/models/pull", json = myobj) + myobj = { + "model": "https://huggingface.co/TheBloke/TinyLlama-1.1B-Chat-v0.3-GGUF/blob/main/tinyllama-1.1b-chat-v0.3.Q2_K.gguf" + } + response = requests.post("http://localhost:3928/models/pull", json=myobj) assert response.status_code == 200 - diff --git a/engine/e2e-test/test_api_model_start.py b/engine/e2e-test/test_api_model_start.py index ca923a4fb..8c16eb90d 100644 --- a/engine/e2e-test/test_api_model_start.py +++ b/engine/e2e-test/test_api_model_start.py @@ -1,7 +1,7 @@ import pytest import requests from test_runner import popen -from test_runner import start_server, stop_server +from test_runner import start_server, stop_server, run class TestApiModelStart: @@ -14,6 +14,7 @@ def setup_and_teardown(self): raise Exception("Failed to start server") # TODO: using pull with branch for easy testing tinyllama:gguf for example + run("Delete model", ["models", "delete", "tinyllama:gguf"]) popen(["pull", "tinyllama"], "1\n") yield diff --git a/engine/e2e-test/test_cli_model_delete.py b/engine/e2e-test/test_cli_model_delete.py index 23534dd4c..3ff7ef61d 100644 --- a/engine/e2e-test/test_cli_model_delete.py +++ b/engine/e2e-test/test_cli_model_delete.py @@ -16,7 +16,7 @@ def setup_and_teardown(self): # Teardown # Clean up - run("Delete model", ["models", "delete", "tinyllama"]) + run("Delete model", ["models", "delete", "tinyllama:gguf"]) def test_models_delete_should_be_successful(self): exit_code, output, error = run( From 7fc7d38af55f5af31f2a1a48aadf410f63a39cf9 Mon Sep 17 00:00:00 2001 From: vansangpfiev Date: Wed, 2 Oct 2024 16:28:02 +0700 Subject: [PATCH 11/12] fix: e2e tests --- engine/e2e-test/test_api_model_start.py | 4 ++-- engine/e2e-test/test_api_model_stop.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/engine/e2e-test/test_api_model_start.py b/engine/e2e-test/test_api_model_start.py index 8c16eb90d..fe2a80ceb 100644 --- a/engine/e2e-test/test_api_model_start.py +++ b/engine/e2e-test/test_api_model_start.py @@ -23,6 +23,6 @@ def setup_and_teardown(self): stop_server() def test_models_start_should_be_successful(self): - json_body = {"model": "tinyllama:gguf"} + json_body = {'model': 'tinyllama:gguf'} response = requests.post("http://localhost:3928/models/start", json = json_body) - assert response.status_code == 200 + assert response.status_code == 200, f"status_code: {response.status_code}" diff --git a/engine/e2e-test/test_api_model_stop.py b/engine/e2e-test/test_api_model_stop.py index 3370e9c92..a787be276 100644 --- a/engine/e2e-test/test_api_model_stop.py +++ b/engine/e2e-test/test_api_model_stop.py @@ -18,8 +18,8 @@ def setup_and_teardown(self): stop_server() def test_models_stop_should_be_successful(self): - json_body = {"model": "tinyllama:gguf"} + json_body = {'model': 'tinyllama:gguf'} response = requests.post("http://localhost:3928/models/start", json = json_body) - assert response.status_code == 200 + assert response.status_code == 200, f"status_code: {response.status_code}" response = requests.post("http://localhost:3928/models/stop", json = json_body) - assert response.status_code == 200 + assert response.status_code == 200, f"status_code: {response.status_code}" From 4235a322690021970a3cf1c21dbb65b89fd902bb Mon Sep 17 00:00:00 2001 From: vansangpfiev Date: Wed, 2 Oct 2024 17:00:02 +0700 Subject: [PATCH 12/12] fix: e2e tests --- engine/e2e-test/main.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/engine/e2e-test/main.py b/engine/e2e-test/main.py index 62d921777..f814c45a5 100644 --- a/engine/e2e-test/main.py +++ b/engine/e2e-test/main.py @@ -1,11 +1,12 @@ import pytest import sys + ### e2e tests are expensive, have to keep engines tests in order from test_api_engine_list import TestApiEngineList from test_api_engine_install import TestApiEngineInstall from test_api_engine_get import TestApiEngineGet -from test_api_engine_uninstall import TestApiEngineUninstall -### models, keeps in order + +### models, keeps in order, note that we only uninstall engine after finishing all models test from test_api_model_pull_direct_url import TestApiModelPullDirectUrl from test_api_model_start import TestApiModelStart from test_api_model_stop import TestApiModelStop @@ -15,6 +16,8 @@ from test_api_model_update import TestApiModelUpdate from test_api_model_delete import TestApiModelDelete from test_api_model_import import TestApiModelImport +from test_api_engine_uninstall import TestApiEngineUninstall + ### from test_cli_engine_get import TestCliEngineGet from test_cli_engine_install import TestCliEngineInstall