mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-11-06 13:12:27 +01:00
* refactor(cookbook): move endpoint registration out of the routes closure _auto_register_llm_endpoint and _auto_register_image_endpoint use no closure state and model_serve is their only caller, so they can live in a plain module and be imported and tested directly. Bodies are unchanged apart from indentation. Tests now import the helpers instead of reading them from the route's closure. That lookup also failed if the route stopped referencing a helper, so add a test that pins which helper model_serve calls. Part of #4071 * fix(cookbook): bound the OLLAMA_HOST port scan The pattern rescanned the rest of the token from every OLLAMA_HOST= occurrence, so a long serve command with repeated assignments took quadratic time. model_serve runs this on the event loop, and a 192 KB command stalled the whole app for about 9s. Cap the host at 253 characters, the longest DNS name, so every valid host still matches. Part of #4071
256 lines
6.2 KiB
Python
256 lines
6.2 KiB
Python
import json
|
|
|
|
import pytest
|
|
|
|
from src import cookbook_serve_lifecycle as lifecycle
|
|
|
|
from tests.test_cookbook_endpoint_registration import (
|
|
endpoint_db,
|
|
endpoint_http_api,
|
|
_endpoint,
|
|
_launch_llm,
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_tick_persists_only_successfully_stopped_serves(tmp_path, monkeypatch):
|
|
state_path = tmp_path / "cookbook_state.json"
|
|
state_path.write_text(
|
|
json.dumps({
|
|
"tasks": [
|
|
{
|
|
"id": "stop-succeeds",
|
|
"type": "serve",
|
|
"status": "running",
|
|
"_scheduledStopAtMs": 0,
|
|
},
|
|
{
|
|
"id": "stop-fails",
|
|
"type": "serve",
|
|
"status": "running",
|
|
"_scheduledStopAtMs": 0,
|
|
},
|
|
]
|
|
}),
|
|
encoding="utf-8",
|
|
)
|
|
|
|
async def fake_stop_serve(session_id, remote_host="", ssh_port=""):
|
|
return session_id == "stop-succeeds"
|
|
|
|
async def fake_delete_endpoint(task):
|
|
return None
|
|
|
|
monkeypatch.setattr(lifecycle, "COOKBOOK_STATE_FILE", str(state_path))
|
|
monkeypatch.setattr(lifecycle, "_stop_serve", fake_stop_serve)
|
|
monkeypatch.setattr(lifecycle, "_delete_endpoint_for_task", fake_delete_endpoint)
|
|
|
|
await lifecycle._tick()
|
|
|
|
tasks = {
|
|
task["id"]: task
|
|
for task in json.loads(state_path.read_text(encoding="utf-8"))["tasks"]
|
|
}
|
|
assert tasks["stop-succeeds"]["status"] == "stopped"
|
|
assert tasks["stop-succeeds"]["_scheduledStopAtMs"] is None
|
|
assert tasks["stop-fails"]["status"] == "running"
|
|
assert tasks["stop-fails"]["_scheduledStopAtMs"] == 0
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_scheduled_stop_preserves_endpoint_reused_by_newer_serve(
|
|
endpoint_db,
|
|
endpoint_http_api,
|
|
):
|
|
original = _launch_llm(
|
|
"organization-a/Model-A",
|
|
8001,
|
|
serve_session_id="serve-old",
|
|
)
|
|
|
|
reused = _launch_llm(
|
|
"organization-b/Model-B",
|
|
8001,
|
|
serve_session_id="serve-new",
|
|
)
|
|
|
|
assert original == reused
|
|
|
|
await lifecycle._delete_endpoint_for_task({
|
|
"_endpointId": original,
|
|
"sessionId": "serve-old",
|
|
"type": "serve",
|
|
})
|
|
|
|
assert _endpoint(endpoint_db, reused) is not None
|
|
|
|
await lifecycle._delete_endpoint_for_task({
|
|
"_endpointId": reused,
|
|
"sessionId": "serve-new",
|
|
"type": "serve",
|
|
})
|
|
|
|
assert _endpoint(endpoint_db, reused) is None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_scheduled_stop_preserves_legacy_endpoint_without_owner(
|
|
endpoint_db,
|
|
endpoint_http_api,
|
|
):
|
|
endpoint_id = _launch_llm(
|
|
"organization-a/Model-A",
|
|
8002,
|
|
)
|
|
|
|
await lifecycle._delete_endpoint_for_task({
|
|
"_endpointId": endpoint_id,
|
|
"sessionId": "serve-old",
|
|
"type": "serve",
|
|
})
|
|
|
|
assert _endpoint(endpoint_db, endpoint_id) is not None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_scheduled_stop_cannot_delete_manual_endpoint(
|
|
endpoint_db,
|
|
endpoint_http_api,
|
|
):
|
|
from core.database import ModelEndpoint
|
|
|
|
with endpoint_db() as db:
|
|
db.add(ModelEndpoint(
|
|
id="manual-endpoint",
|
|
name="Manual",
|
|
base_url="http://localhost:9000/v1",
|
|
cookbook_serve_session_id="serve-old",
|
|
))
|
|
db.commit()
|
|
|
|
await lifecycle._delete_endpoint_for_task({
|
|
"_endpointId": "manual-endpoint",
|
|
"sessionId": "serve-old",
|
|
"type": "serve",
|
|
})
|
|
|
|
assert _endpoint(endpoint_db, "manual-endpoint") is not None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_scheduled_stop_requires_session_identity(endpoint_db, endpoint_http_api):
|
|
endpoint_id = _launch_llm(
|
|
"organization-a/Model-A",
|
|
8003,
|
|
serve_session_id="serve-current",
|
|
)
|
|
|
|
await lifecycle._delete_endpoint_for_task({
|
|
"_endpointId": endpoint_id,
|
|
"type": "serve",
|
|
})
|
|
|
|
assert _endpoint(endpoint_db, endpoint_id) is not None
|
|
|
|
|
|
|
|
def _launch_image(repo_id, port, serve_session_id):
|
|
from routes.cookbook.endpoint_registration import _auto_register_image_endpoint
|
|
from routes.cookbook_helpers import ServeRequest
|
|
|
|
endpoint_id = _auto_register_image_endpoint(
|
|
ServeRequest(
|
|
repo_id=repo_id,
|
|
cmd=f"python scripts/diffusion_server.py --port {port}",
|
|
),
|
|
None,
|
|
serve_session_id=serve_session_id,
|
|
)
|
|
|
|
assert endpoint_id
|
|
return endpoint_id
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_scheduled_stop_deletes_owned_image_endpoint(
|
|
endpoint_db,
|
|
monkeypatch,
|
|
endpoint_http_api,
|
|
):
|
|
import src.settings as settings
|
|
from core.database import ModelEndpoint
|
|
|
|
monkeypatch.setattr(settings, "load_settings", lambda: {})
|
|
monkeypatch.setattr(
|
|
settings,
|
|
"save_settings",
|
|
lambda value: None,
|
|
)
|
|
|
|
endpoint_id = _launch_image(
|
|
"organization-a/Image-A",
|
|
8101,
|
|
"serve-image-a",
|
|
)
|
|
|
|
assert endpoint_id.startswith("img-")
|
|
|
|
with endpoint_db() as db:
|
|
endpoint = db.get(ModelEndpoint, endpoint_id)
|
|
assert endpoint.cookbook_serve_session_id == "serve-image-a"
|
|
|
|
await lifecycle._delete_endpoint_for_task({
|
|
"_endpointId": endpoint_id,
|
|
"sessionId": "serve-image-a",
|
|
"type": "serve",
|
|
})
|
|
|
|
assert _endpoint(endpoint_db, endpoint_id) is None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_old_image_serve_cannot_delete_reused_endpoint(
|
|
endpoint_db,
|
|
monkeypatch,
|
|
endpoint_http_api,
|
|
):
|
|
import src.settings as settings
|
|
|
|
monkeypatch.setattr(settings, "load_settings", lambda: {})
|
|
monkeypatch.setattr(
|
|
settings,
|
|
"save_settings",
|
|
lambda value: None,
|
|
)
|
|
|
|
original = _launch_image(
|
|
"organization-a/Image-A",
|
|
8102,
|
|
"serve-old",
|
|
)
|
|
|
|
reused = _launch_image(
|
|
"organization-b/Image-B",
|
|
8102,
|
|
"serve-new",
|
|
)
|
|
|
|
assert original == reused
|
|
|
|
await lifecycle._delete_endpoint_for_task({
|
|
"_endpointId": original,
|
|
"sessionId": "serve-old",
|
|
"type": "serve",
|
|
})
|
|
|
|
assert _endpoint(endpoint_db, reused) is not None
|
|
|
|
await lifecycle._delete_endpoint_for_task({
|
|
"_endpointId": reused,
|
|
"sessionId": "serve-new",
|
|
"type": "serve",
|
|
})
|
|
|
|
assert _endpoint(endpoint_db, reused) is None
|