Skip to content
This repository was archived by the owner on Jul 4, 2025. It is now read-only.

Commit 8a99ff8

Browse files
committed
feat: automation test docker
1 parent 505cac8 commit 8a99ff8

13 files changed

+139
-26
lines changed

.github/workflows/cortex-cpp-quality-gate.yml

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,3 +189,41 @@ jobs:
189189
AWS_ACCESS_KEY_ID: "${{ secrets.MINIO_ACCESS_KEY_ID }}"
190190
AWS_SECRET_ACCESS_KEY: "${{ secrets.MINIO_SECRET_ACCESS_KEY }}"
191191
AWS_DEFAULT_REGION: "${{ secrets.MINIO_REGION }}"
192+
193+
build-docker-and-test:
194+
runs-on: ubuntu-latest
195+
steps:
196+
- name: Getting the repo
197+
uses: actions/checkout@v3
198+
with:
199+
submodules: 'recursive'
200+
201+
- name: Set up QEMU
202+
uses: docker/setup-qemu-action@v3
203+
204+
- name: Set up Docker Buildx
205+
uses: docker/setup-buildx-action@v3
206+
207+
- name: Run Docker
208+
run: |
209+
docker build -t menloltd/cortex:test -f docker/Dockerfile .
210+
docker run -it -d -p 3928:39281 --name cortex menloltd/cortex:test
211+
212+
- name: use python
213+
uses: actions/setup-python@v5
214+
with:
215+
python-version: "3.10"
216+
217+
- name: Run e2e tests
218+
run: |
219+
cd engine
220+
python -m pip install --upgrade pip
221+
python -m pip install -r e2e-test/requirements.txt
222+
pytest e2e-test/test_api_docker.py
223+
224+
- name: Run Docker
225+
continue-on-error: true
226+
if: always()
227+
run: |
228+
docker stop cortex
229+
docker rm cortex

docker/entrypoint.sh

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@
22

33
# Install cortex.llamacpp engine
44

5+
echo "apiServerHost: 0.0.0.0" > /root/.cortexrc
6+
echo "enableCors: true" >> /root/.cortexrc
7+
58
cortex engines install llama-cpp -s /opt/cortex.llamacpp
6-
cortex -v
79

810
# Start the cortex server
911

10-
sed -i 's/apiServerHost: 127.0.0.1/apiServerHost: 0.0.0.0/' /root/.cortexrc
11-
1212
cortex start
1313

1414
# Keep the container running by tailing the log files

docs/docs/capabilities/models/sources/index.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,7 @@ curl --request POST \
272272
Clients can abort a downloading task using the task ID. Below is a sample `curl` command to abort a download task:
273273

274274
```sh
275-
curl --location --request DELETE 'http://127.0.0.1:3928/models/pull' \
275+
curl --location --request DELETE 'http://127.0.0.1:39281/v1/models/pull' \
276276
--header 'Content-Type: application/json' \
277277
--data '{
278278
"taskId": "tinyllama:1b-gguf-q2-k"

engine/e2e-test/test_api_cortexso_hub_llamacpp_engine.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -100,22 +100,22 @@ async def test_models_on_cortexso_hub(self, model_url):
100100
json_body = {
101101
"model": model_url
102102
}
103-
response = requests.post("http://localhost:3928/models/pull", json=json_body)
103+
response = requests.post("http://localhost:3928/v1/models/pull", json=json_body)
104104
assert response.status_code == 200, f"Failed to pull model: {model_url}"
105105

106106
await wait_for_websocket_download_success_event(timeout=None)
107107

108108
# Check if the model was pulled successfully
109109
get_model_response = requests.get(
110-
f"http://127.0.0.1:3928/models/{model_url}"
110+
f"http://127.0.0.1:3928/v1/models/{model_url}"
111111
)
112112
assert get_model_response.status_code == 200, f"Failed to fetch model: {model_url}"
113113
assert (
114114
get_model_response.json()["model"] == model_url
115115
), f"Unexpected model name for: {model_url}"
116116

117117
# Check if the model is available in the list of models
118-
response = requests.get("http://localhost:3928/models")
118+
response = requests.get("http://localhost:3928/v1/models")
119119
assert response.status_code == 200
120120
models = [i["id"] for i in response.json()["data"]]
121121
assert model_url in models, f"Model not found in list: {model_url}"
@@ -129,7 +129,7 @@ async def test_models_on_cortexso_hub(self, model_url):
129129
assert exit_code == 0, f"Install engine failed with error: {error}"
130130

131131
# Start the model
132-
response = requests.post("http://localhost:3928/models/start", json=json_body)
132+
response = requests.post("http://localhost:3928/v1/models/start", json=json_body)
133133
assert response.status_code == 200, f"status_code: {response.status_code}"
134134

135135
# Send an inference request
@@ -155,7 +155,7 @@ async def test_models_on_cortexso_hub(self, model_url):
155155
assert response.status_code == 200, f"status_code: {response.status_code} response: {response.json()}"
156156

157157
# Stop the model
158-
response = requests.post("http://localhost:3928/models/stop", json=json_body)
158+
response = requests.post("http://localhost:3928/v1/models/stop", json=json_body)
159159
assert response.status_code == 200, f"status_code: {response.status_code}"
160160

161161
# Uninstall Engine

engine/e2e-test/test_api_docker.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import pytest
2+
import requests
3+
import os
4+
5+
from pathlib import Path
6+
from test_runner import (
7+
wait_for_websocket_download_success_event
8+
)
9+
10+
repo_branches = ["tinyllama:1b-gguf"]
11+
12+
class TestCortexsoModels:
13+
14+
@pytest.fixture(autouse=True)
15+
def setup_and_teardown(self, request):
16+
yield
17+
18+
@pytest.mark.parametrize("model_url", repo_branches)
19+
@pytest.mark.asyncio
20+
async def test_models_on_cortexso_hub(self, model_url):
21+
22+
# Pull model from cortexso hub
23+
json_body = {
24+
"model": model_url
25+
}
26+
response = requests.post("http://localhost:3928/v1/models/pull", json=json_body)
27+
assert response.status_code == 200, f"Failed to pull model: {model_url}"
28+
29+
await wait_for_websocket_download_success_event(timeout=None)
30+
31+
# Check if the model was pulled successfully
32+
get_model_response = requests.get(
33+
f"http://127.0.0.1:3928/v1/models/{model_url}"
34+
)
35+
assert get_model_response.status_code == 200, f"Failed to fetch model: {model_url}"
36+
assert (
37+
get_model_response.json()["model"] == model_url
38+
), f"Unexpected model name for: {model_url}"
39+
40+
# Check if the model is available in the list of models
41+
response = requests.get("http://localhost:3928/v1/models")
42+
assert response.status_code == 200
43+
models = [i["id"] for i in response.json()["data"]]
44+
assert model_url in models, f"Model not found in list: {model_url}"
45+
46+
# Start the model
47+
response = requests.post("http://localhost:3928/v1/models/start", json=json_body)
48+
assert response.status_code == 200, f"status_code: {response.status_code}"
49+
50+
# Send an inference request
51+
inference_json_body = {
52+
"frequency_penalty": 0.2,
53+
"max_tokens": 4096,
54+
"messages": [
55+
{
56+
"content": "",
57+
"role": "user"
58+
}
59+
],
60+
"model": model_url,
61+
"presence_penalty": 0.6,
62+
"stop": [
63+
"End"
64+
],
65+
"stream": False,
66+
"temperature": 0.8,
67+
"top_p": 0.95
68+
}
69+
response = requests.post("http://localhost:3928/v1/chat/completions", json=inference_json_body, headers={"Content-Type": "application/json"})
70+
assert response.status_code == 200, f"status_code: {response.status_code} response: {response.json()}"
71+
72+
# Stop the model
73+
response = requests.post("http://localhost:3928/v1/models/stop", json=json_body)
74+
assert response.status_code == 200, f"status_code: {response.status_code}"
75+

engine/e2e-test/test_api_model_delete.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,5 @@ def setup_and_teardown(self):
1818
stop_server()
1919

2020
def test_models_delete_should_be_successful(self):
21-
response = requests.delete("http://localhost:3928/models/tinyllama:gguf")
21+
response = requests.delete("http://localhost:3928/v1/models/tinyllama:gguf")
2222
assert response.status_code == 200

engine/e2e-test/test_api_model_get.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,5 @@ def setup_and_teardown(self):
1818
stop_server()
1919

2020
def test_models_get_should_be_successful(self):
21-
response = requests.get("http://localhost:3928/models/tinyllama:gguf")
21+
response = requests.get("http://localhost:3928/v1/models/tinyllama:gguf")
2222
assert response.status_code == 200

engine/e2e-test/test_api_model_import.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,15 @@ def setup_and_teardown(self):
1818
def test_model_import_should_be_success(self):
1919
body_json = {'model': 'tinyllama:gguf',
2020
'modelPath': '/path/to/local/gguf'}
21-
response = requests.post("http://localhost:3928/models/import", json=body_json)
21+
response = requests.post("http://localhost:3928/v1/models/import", json=body_json)
2222
assert response.status_code == 200
2323

2424
@pytest.mark.skipif(True, reason="Expensive test. Only test when you have local gguf file.")
2525
def test_model_import_with_name_should_be_success(self):
2626
body_json = {'model': 'tinyllama:gguf',
2727
'modelPath': '/path/to/local/gguf',
2828
'name': 'test_model'}
29-
response = requests.post("http://localhost:3928/models/import", json=body_json)
29+
response = requests.post("http://localhost:3928/v1/models/import", json=body_json)
3030
assert response.status_code == 200
3131

3232
@pytest.mark.skipif(True, reason="Expensive test. Only test when you have local gguf file.")
@@ -35,10 +35,10 @@ def test_model_import_with_name_should_be_success(self):
3535
'modelPath': '/path/to/local/gguf',
3636
'name': 'test_model',
3737
'option': 'copy'}
38-
response = requests.post("http://localhost:3928/models/import", json=body_json)
38+
response = requests.post("http://localhost:3928/v1/models/import", json=body_json)
3939
assert response.status_code == 200
4040
# Test imported path
41-
response = requests.get("http://localhost:3928/models/testing-model")
41+
response = requests.get("http://localhost:3928/v1/models/testing-model")
4242
assert response.status_code == 200
4343
# Since this is a dynamic test - require actual file path
4444
# it's not safe to assert with the gguf file name
@@ -47,11 +47,11 @@ def test_model_import_with_name_should_be_success(self):
4747
def test_model_import_with_invalid_path_should_fail(self):
4848
body_json = {'model': 'tinyllama:gguf',
4949
'modelPath': '/invalid/path/to/gguf'}
50-
response = requests.post("http://localhost:3928/models/import", json=body_json)
50+
response = requests.post("http://localhost:3928/v1/models/import", json=body_json)
5151
assert response.status_code == 400
5252

5353
def test_model_import_with_missing_model_should_fail(self):
5454
body_json = {'modelPath': '/path/to/local/gguf'}
55-
response = requests.post("http://localhost:3928/models/import", json=body_json)
55+
response = requests.post("http://localhost:3928/v1/models/import", json=body_json)
5656
print(response)
5757
assert response.status_code == 409

engine/e2e-test/test_api_model_list.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,5 @@ def setup_and_teardown(self):
1818
stop_server()
1919

2020
def test_models_list_should_be_successful(self):
21-
response = requests.get("http://localhost:3928/models")
21+
response = requests.get("http://localhost:3928/v1/models")
2222
assert response.status_code == 200

engine/e2e-test/test_api_model_pull_direct_url.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,11 @@ async def test_model_pull_with_direct_url_should_be_success(self):
4242
myobj = {
4343
"model": "https://huggingface.co/afrideva/zephyr-smol_llama-100m-sft-full-GGUF/blob/main/zephyr-smol_llama-100m-sft-full.q2_k.gguf"
4444
}
45-
response = requests.post("http://localhost:3928/models/pull", json=myobj)
45+
response = requests.post("http://localhost:3928/v1/models/pull", json=myobj)
4646
assert response.status_code == 200
4747
await wait_for_websocket_download_success_event(timeout=None)
4848
get_model_response = requests.get(
49-
"http://127.0.0.1:3928/models/afrideva:zephyr-smol_llama-100m-sft-full-GGUF:zephyr-smol_llama-100m-sft-full.q2_k.gguf"
49+
"http://127.0.0.1:3928/v1/models/afrideva:zephyr-smol_llama-100m-sft-full-GGUF:zephyr-smol_llama-100m-sft-full.q2_k.gguf"
5050
)
5151
assert get_model_response.status_code == 200
5252
assert (
@@ -60,11 +60,11 @@ async def test_model_pull_with_direct_url_should_have_desired_name(self):
6060
"model": "https://huggingface.co/afrideva/zephyr-smol_llama-100m-sft-full-GGUF/blob/main/zephyr-smol_llama-100m-sft-full.q2_k.gguf",
6161
"name": "smol_llama_100m"
6262
}
63-
response = requests.post("http://localhost:3928/models/pull", json=myobj)
63+
response = requests.post("http://localhost:3928/v1/models/pull", json=myobj)
6464
assert response.status_code == 200
6565
await wait_for_websocket_download_success_event(timeout=None)
6666
get_model_response = requests.get(
67-
"http://127.0.0.1:3928/models/afrideva:zephyr-smol_llama-100m-sft-full-GGUF:zephyr-smol_llama-100m-sft-full.q2_k.gguf"
67+
"http://127.0.0.1:3928/v1/models/afrideva:zephyr-smol_llama-100m-sft-full-GGUF:zephyr-smol_llama-100m-sft-full.q2_k.gguf"
6868
)
6969
assert get_model_response.status_code == 200
7070
print(get_model_response.json()["name"])

0 commit comments

Comments
 (0)