mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-11-02 11:12:21 +01:00
fix(cookbook): preserve endpoints across model launches (#6969)
* fix(cookbook): match stale endpoints by launched model, not display name After registering an LLM endpoint, the Cookbook deleted every other local-* endpoint with the same display name at a different URL. The display name is the repo short name, which repos from different orgs share (for example meta-llama/Llama-3.1-8B-Instruct and unsloth/Llama-3.1-8B-Instruct), so launching one deleted the endpoint of the other while its server was running. Match stale endpoints by the full model id they were last launched for, which is the last entry of their pinned_models. Reusing an endpoint now moves the launched model to the end of its pins so that entry stays current. * fix(cookbook): guard endpoint cleanup by serve ownership Preserve independently launched models instead of sweeping endpoints by display name or mutable model pins. Bind Cookbook endpoint registrations to their serve sessions and prevent stale watchdogs or scheduled stops from deleting newer registrations. Reuse the existing endpoint deletion contract to preserve settings, preferences, session cleanup, and cache invalidation. Cover LLM and image endpoint lifecycle regressions. --------- Co-authored-by: Alexandre Teixeira <general@alexandreteixeira.dev>
This commit is contained in:
co-authored by
Alexandre Teixeira
parent
39303f19cd
commit
6f2b58c896
@@ -4,6 +4,13 @@ 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):
|
||||
@@ -48,3 +55,219 @@ async def test_tick_persists_only_successfully_stopped_serves(tmp_path, monkeypa
|
||||
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):
|
||||
import routes.cookbook_routes as cookbook_routes
|
||||
from routes.cookbook_helpers import ServeRequest
|
||||
|
||||
router = cookbook_routes.setup_cookbook_routes()
|
||||
|
||||
serve = next(
|
||||
route.endpoint
|
||||
for route in router.routes
|
||||
if route.path == "/api/model/serve"
|
||||
and "POST" in route.methods
|
||||
)
|
||||
|
||||
cells = dict(
|
||||
zip(
|
||||
serve.__code__.co_freevars,
|
||||
serve.__closure__ or (),
|
||||
)
|
||||
)
|
||||
|
||||
register = cells["_auto_register_image_endpoint"].cell_contents
|
||||
|
||||
endpoint_id = register(
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user