438 lines
16 KiB
Python
438 lines
16 KiB
Python
from types import SimpleNamespace
|
|
from unittest.mock import patch
|
|
|
|
from omlx.admin import routes as admin_routes
|
|
|
|
|
|
class FakePool:
|
|
def __init__(self, scheduler, loading_started_at=None):
|
|
core = SimpleNamespace(
|
|
_output_collectors={"gen-1": object(), "prefill-1": object(), "wait-1": object()},
|
|
scheduler=scheduler,
|
|
)
|
|
engine = SimpleNamespace(_engine=SimpleNamespace(engine=core))
|
|
self._entries = {"model-a": SimpleNamespace(engine=engine)}
|
|
self.loading_started_at = loading_started_at
|
|
|
|
def get_status(self):
|
|
return {
|
|
"current_model_memory": 1024,
|
|
"final_ceiling": 2048,
|
|
"models": [
|
|
{
|
|
"id": "model-a",
|
|
"loaded": True,
|
|
"is_loading": self.loading_started_at is not None,
|
|
"loading_started_at": self.loading_started_at,
|
|
"estimated_size": 1024,
|
|
"pinned": False,
|
|
}
|
|
],
|
|
}
|
|
|
|
|
|
class FakePrefillTracker:
|
|
def get_model_progress(self, model_id):
|
|
assert model_id == "model-a"
|
|
return [{"request_id": "prefill-1", "processed": 10, "total": 20}]
|
|
|
|
|
|
def test_active_models_generation_includes_activity_and_waiting_rows():
|
|
running_request = SimpleNamespace(
|
|
request_id="gen-1",
|
|
generation_started_at=100.0,
|
|
last_activity_at=109.5,
|
|
num_output_tokens=20,
|
|
num_prompt_tokens=12,
|
|
max_tokens=64,
|
|
)
|
|
waiting_request = SimpleNamespace(
|
|
request_id="wait-1",
|
|
arrival_time=105.0,
|
|
num_prompt_tokens=30,
|
|
)
|
|
scheduler = SimpleNamespace(
|
|
snapshot_for_admin=lambda: {
|
|
"running_by_id": {"gen-1": running_request},
|
|
"waiting": [waiting_request],
|
|
},
|
|
)
|
|
|
|
with (
|
|
patch.object(admin_routes, "_get_engine_pool", return_value=FakePool(scheduler)),
|
|
patch("omlx.admin.routes._get_server_state", return_value=None),
|
|
patch.object(admin_routes, "_get_settings_manager", return_value=None),
|
|
patch.object(admin_routes, "_get_global_settings", return_value=None),
|
|
patch("omlx.prefill_progress.get_prefill_tracker", return_value=FakePrefillTracker()),
|
|
patch("time.monotonic", return_value=110.0),
|
|
):
|
|
data = admin_routes._build_active_models_data()
|
|
|
|
model = data["models"][0]
|
|
assert data["total_active_requests"] == 2
|
|
assert model["active_requests"] == 2
|
|
assert model["waiting_requests"] == 1
|
|
assert model["prefilling"] == [
|
|
{"request_id": "prefill-1", "processed": 10, "total": 20}
|
|
]
|
|
assert model["waiting"] == [
|
|
{
|
|
"request_id": "wait-1",
|
|
"queue_position": 1,
|
|
"elapsed_seconds": 5.0,
|
|
"prompt_tokens": 30,
|
|
}
|
|
]
|
|
assert model["generating"] == [
|
|
{
|
|
"request_id": "gen-1",
|
|
"elapsed_seconds": 10.0,
|
|
"generated_tokens": 20,
|
|
"tokens_per_second": 2.0,
|
|
"last_activity_age_seconds": 0.5,
|
|
"prompt_tokens": 12,
|
|
"max_tokens": 64,
|
|
}
|
|
]
|
|
|
|
|
|
def test_active_models_does_not_count_waiting_collectors_as_active():
|
|
waiting_request = SimpleNamespace(
|
|
request_id="wait-1",
|
|
arrival_time=105.0,
|
|
num_prompt_tokens=30,
|
|
)
|
|
scheduler = SimpleNamespace(
|
|
snapshot_for_admin=lambda: {
|
|
"running_by_id": {},
|
|
"waiting": [waiting_request],
|
|
},
|
|
)
|
|
|
|
with (
|
|
patch.object(admin_routes, "_get_engine_pool", return_value=FakePool(scheduler)),
|
|
patch("omlx.admin.routes._get_server_state", return_value=None),
|
|
patch.object(admin_routes, "_get_settings_manager", return_value=None),
|
|
patch.object(admin_routes, "_get_global_settings", return_value=None),
|
|
patch("omlx.prefill_progress.get_prefill_tracker", return_value=EmptyPrefillTracker()),
|
|
patch("time.monotonic", return_value=110.0),
|
|
):
|
|
data = admin_routes._build_active_models_data()
|
|
|
|
model = data["models"][0]
|
|
assert data["total_active_requests"] == 0
|
|
assert data["total_waiting_requests"] == 1
|
|
assert model["active_requests"] == 0
|
|
assert model["waiting_requests"] == 1
|
|
assert model["generating"] == []
|
|
|
|
|
|
def test_active_models_loading_includes_elapsed_and_percent_estimate():
|
|
scheduler = SimpleNamespace(
|
|
snapshot_for_admin=lambda: {"running_by_id": {}, "waiting": []},
|
|
)
|
|
|
|
with (
|
|
patch.object(
|
|
admin_routes,
|
|
"_get_engine_pool",
|
|
return_value=FakePool(scheduler, loading_started_at=102.0),
|
|
),
|
|
patch("omlx.admin.routes._get_server_state", return_value=None),
|
|
patch.object(admin_routes, "_get_settings_manager", return_value=None),
|
|
patch.object(admin_routes, "_get_global_settings", return_value=None),
|
|
patch("omlx.prefill_progress.get_prefill_tracker", return_value=FakePrefillTracker()),
|
|
patch("time.monotonic", return_value=110.0),
|
|
):
|
|
data = admin_routes._build_active_models_data()
|
|
|
|
model = data["models"][0]
|
|
assert model["is_loading"] is True
|
|
assert model["loading_elapsed_seconds"] == 8.0
|
|
assert model["loading_estimated_seconds"] is None
|
|
assert model["loading_remaining_seconds_estimate"] is None
|
|
|
|
|
|
class FakeNonStreamingEngine:
|
|
def get_activity_snapshot(self):
|
|
return {
|
|
"active_requests": 1,
|
|
"activities": [
|
|
{
|
|
"request_id": "embed-1",
|
|
"kind": "embedding",
|
|
"detail": "Embedding",
|
|
"elapsed_seconds": 12.0,
|
|
"input_count": 200,
|
|
"token_count": 120200,
|
|
}
|
|
],
|
|
}
|
|
|
|
|
|
class FakeNonStreamingPool:
|
|
def __init__(self):
|
|
self._entries = {"embed-model": SimpleNamespace(engine=FakeNonStreamingEngine())}
|
|
|
|
def get_status(self):
|
|
return {
|
|
"current_model_memory": 1024,
|
|
"final_ceiling": 2048,
|
|
"models": [
|
|
{
|
|
"id": "embed-model",
|
|
"loaded": True,
|
|
"is_loading": False,
|
|
"estimated_size": 1024,
|
|
"pinned": False,
|
|
}
|
|
],
|
|
}
|
|
|
|
|
|
def test_active_models_includes_non_streaming_activity_rows():
|
|
class EmptyPrefillTracker:
|
|
def get_model_progress(self, model_id):
|
|
return []
|
|
|
|
with (
|
|
patch.object(admin_routes, "_get_engine_pool", return_value=FakeNonStreamingPool()),
|
|
patch("omlx.admin.routes._get_server_state", return_value=None),
|
|
patch.object(admin_routes, "_get_settings_manager", return_value=None),
|
|
patch.object(admin_routes, "_get_global_settings", return_value=None),
|
|
patch("omlx.prefill_progress.get_prefill_tracker", return_value=EmptyPrefillTracker()),
|
|
patch("time.monotonic", return_value=110.0),
|
|
):
|
|
data = admin_routes._build_active_models_data()
|
|
|
|
model = data["models"][0]
|
|
assert data["total_active_requests"] == 1
|
|
assert model["active_requests"] == 1
|
|
assert model["activities"] == [
|
|
{
|
|
"request_id": "embed-1",
|
|
"kind": "embedding",
|
|
"detail": "Embedding",
|
|
"elapsed_seconds": 12.0,
|
|
"input_count": 200,
|
|
"token_count": 120200,
|
|
}
|
|
]
|
|
|
|
|
|
# ── idle / TTL countdown (#1307) ──────────────────────────────────────────
|
|
|
|
|
|
def _make_global_settings(idle_timeout_seconds=None):
|
|
"""Build a fake global_settings SimpleNamespace with optional idle_timeout."""
|
|
idle_timeout = SimpleNamespace(idle_timeout_seconds=idle_timeout_seconds)
|
|
return SimpleNamespace(idle_timeout=idle_timeout)
|
|
|
|
|
|
def _make_settings_manager(ttl_seconds=None):
|
|
"""Build a fake settings_manager whose get_settings() returns ttl_seconds."""
|
|
settings = SimpleNamespace(ttl_seconds=ttl_seconds)
|
|
return SimpleNamespace(get_settings=lambda _mid: settings)
|
|
|
|
|
|
class FakeIdlePool:
|
|
"""Pool with a single loaded idle model that has last_access set."""
|
|
|
|
def __init__(self, last_access=100.0):
|
|
self._entries = {
|
|
"model-a": SimpleNamespace(engine=object()), # engine is not None → loaded
|
|
}
|
|
self._last_access = last_access
|
|
|
|
def get_status(self):
|
|
return {
|
|
"current_model_memory": 1024,
|
|
"final_ceiling": 2048,
|
|
"models": [
|
|
{
|
|
"id": "model-a",
|
|
"loaded": True,
|
|
"is_loading": False,
|
|
"loading_started_at": None,
|
|
"estimated_size": 1024,
|
|
"pinned": False,
|
|
"last_access": self._last_access,
|
|
}
|
|
],
|
|
}
|
|
|
|
|
|
class EmptyPrefillTracker:
|
|
def get_model_progress(self, model_id):
|
|
return []
|
|
|
|
|
|
def test_idle_seconds_computed_from_last_access():
|
|
"""idle_seconds = time.time() - last_access for a loaded model."""
|
|
with (
|
|
patch("omlx.admin.routes._get_server_state", return_value=None),
|
|
patch.object(admin_routes, "_get_engine_pool", return_value=FakeIdlePool(last_access=100.0)),
|
|
patch.object(admin_routes, "_get_settings_manager", return_value=None),
|
|
patch.object(admin_routes, "_get_global_settings", return_value=None),
|
|
patch("omlx.prefill_progress.get_prefill_tracker", return_value=EmptyPrefillTracker()),
|
|
patch("time.time", return_value=115.0),
|
|
patch("time.monotonic", return_value=115.0),
|
|
):
|
|
data = admin_routes._build_active_models_data()
|
|
|
|
model = data["models"][0]
|
|
assert model["idle_seconds"] == 15.0
|
|
assert model["ttl_remaining_seconds"] is None # no TTL configured
|
|
|
|
|
|
def test_idle_seconds_none_when_no_last_access():
|
|
"""idle_seconds is None when last_access is missing or 0."""
|
|
with (
|
|
patch("omlx.admin.routes._get_server_state", return_value=None),
|
|
patch.object(admin_routes, "_get_engine_pool", return_value=FakeIdlePool(last_access=0)),
|
|
patch.object(admin_routes, "_get_settings_manager", return_value=None),
|
|
patch.object(admin_routes, "_get_global_settings", return_value=None),
|
|
patch("omlx.prefill_progress.get_prefill_tracker", return_value=EmptyPrefillTracker()),
|
|
patch("time.time", return_value=115.0),
|
|
patch("time.monotonic", return_value=115.0),
|
|
):
|
|
data = admin_routes._build_active_models_data()
|
|
|
|
assert data["models"][0]["idle_seconds"] is None
|
|
|
|
|
|
def test_ttl_remaining_from_per_model_setting():
|
|
"""TTL countdown uses per-model ttl_seconds when available."""
|
|
with (
|
|
patch("omlx.admin.routes._get_server_state", return_value=None),
|
|
patch.object(admin_routes, "_get_engine_pool", return_value=FakeIdlePool(last_access=100.0)),
|
|
patch.object(
|
|
admin_routes,
|
|
"_get_settings_manager",
|
|
return_value=_make_settings_manager(ttl_seconds=30),
|
|
),
|
|
patch.object(admin_routes, "_get_global_settings", return_value=None),
|
|
patch("omlx.prefill_progress.get_prefill_tracker", return_value=EmptyPrefillTracker()),
|
|
patch("time.time", return_value=115.0),
|
|
patch("time.monotonic", return_value=115.0),
|
|
):
|
|
data = admin_routes._build_active_models_data()
|
|
|
|
model = data["models"][0]
|
|
assert model["idle_seconds"] == 15.0
|
|
# 30s TTL - 15s idle = 15s remaining
|
|
assert model["ttl_remaining_seconds"] == 15.0
|
|
|
|
|
|
def test_ttl_remaining_falls_back_to_global_idle_timeout():
|
|
"""When per-model ttl_seconds is None, global idle_timeout is used."""
|
|
with (
|
|
patch("omlx.admin.routes._get_server_state", return_value=None),
|
|
patch.object(admin_routes, "_get_engine_pool", return_value=FakeIdlePool(last_access=100.0)),
|
|
patch.object(
|
|
admin_routes,
|
|
"_get_settings_manager",
|
|
return_value=_make_settings_manager(ttl_seconds=None),
|
|
),
|
|
patch.object(
|
|
admin_routes,
|
|
"_get_global_settings",
|
|
return_value=_make_global_settings(idle_timeout_seconds=60),
|
|
),
|
|
patch("omlx.prefill_progress.get_prefill_tracker", return_value=EmptyPrefillTracker()),
|
|
patch("time.time", return_value=115.0),
|
|
patch("time.monotonic", return_value=115.0),
|
|
):
|
|
data = admin_routes._build_active_models_data()
|
|
|
|
model = data["models"][0]
|
|
assert model["idle_seconds"] == 15.0
|
|
# 60s global TTL - 15s idle = 45s remaining
|
|
assert model["ttl_remaining_seconds"] == 45.0
|
|
|
|
|
|
def test_ttl_remaining_per_model_takes_priority():
|
|
"""Per-model ttl_seconds overrides global idle_timeout."""
|
|
with (
|
|
patch("omlx.admin.routes._get_server_state", return_value=None),
|
|
patch.object(admin_routes, "_get_engine_pool", return_value=FakeIdlePool(last_access=100.0)),
|
|
patch.object(
|
|
admin_routes,
|
|
"_get_settings_manager",
|
|
return_value=_make_settings_manager(ttl_seconds=20),
|
|
),
|
|
patch.object(
|
|
admin_routes,
|
|
"_get_global_settings",
|
|
return_value=_make_global_settings(idle_timeout_seconds=300),
|
|
),
|
|
patch("omlx.prefill_progress.get_prefill_tracker", return_value=EmptyPrefillTracker()),
|
|
patch("time.time", return_value=115.0),
|
|
patch("time.monotonic", return_value=115.0),
|
|
):
|
|
data = admin_routes._build_active_models_data()
|
|
|
|
model = data["models"][0]
|
|
# per-model 20s wins over global 300s
|
|
assert model["ttl_remaining_seconds"] == 5.0 # 20 - 15
|
|
|
|
|
|
def test_ttl_remaining_clamped_to_zero():
|
|
"""ttl_remaining_seconds floors at 0 when TTL has expired."""
|
|
with (
|
|
patch("omlx.admin.routes._get_server_state", return_value=None),
|
|
patch.object(admin_routes, "_get_engine_pool", return_value=FakeIdlePool(last_access=100.0)),
|
|
patch.object(
|
|
admin_routes,
|
|
"_get_settings_manager",
|
|
return_value=_make_settings_manager(ttl_seconds=10),
|
|
),
|
|
patch.object(admin_routes, "_get_global_settings", return_value=None),
|
|
patch("omlx.prefill_progress.get_prefill_tracker", return_value=EmptyPrefillTracker()),
|
|
patch("time.time", return_value=130.0), # 30s idle, 10s TTL → expired
|
|
patch("time.monotonic", return_value=130.0),
|
|
):
|
|
data = admin_routes._build_active_models_data()
|
|
|
|
assert data["models"][0]["ttl_remaining_seconds"] == 0.0
|
|
|
|
|
|
def test_idle_and_ttl_not_computed_for_loading_model():
|
|
"""Loading models have no idle/ttl fields set (both None)."""
|
|
class LoadingPool:
|
|
def __init__(self):
|
|
self._entries = {"model-a": SimpleNamespace(engine=None)} # not loaded yet
|
|
|
|
def get_status(self):
|
|
return {
|
|
"current_model_memory": 0,
|
|
"final_ceiling": 0,
|
|
"models": [
|
|
{
|
|
"id": "model-a",
|
|
"loaded": False,
|
|
"is_loading": True,
|
|
"loading_started_at": 100.0,
|
|
"estimated_size": 1024,
|
|
"pinned": False,
|
|
"last_access": 0,
|
|
}
|
|
],
|
|
}
|
|
|
|
with (
|
|
patch("omlx.admin.routes._get_server_state", return_value=None),
|
|
patch.object(admin_routes, "_get_engine_pool", return_value=LoadingPool()),
|
|
patch.object(admin_routes, "_get_settings_manager", return_value=None),
|
|
patch.object(admin_routes, "_get_global_settings", return_value=None),
|
|
patch("omlx.prefill_progress.get_prefill_tracker", return_value=EmptyPrefillTracker()),
|
|
patch("time.time", return_value=115.0),
|
|
patch("time.monotonic", return_value=115.0),
|
|
):
|
|
data = admin_routes._build_active_models_data()
|
|
|
|
model = data["models"][0]
|
|
assert model["is_loading"] is True
|
|
assert model["idle_seconds"] is None
|
|
assert model["ttl_remaining_seconds"] is None
|