diff --git a/README.md b/README.md
index 8683bcce3..03d9ffd04 100644
--- a/README.md
+++ b/README.md
@@ -301,7 +301,7 @@ ARTEMIS supports two execution profiles tailored for different automation requir
- [ ] **Android Studio Integration**: Native IDE plugin and workflow integration to enable in-editor debugging, test recording, and automated device control directly within Android Studio.
- [ ] **iOS Platform Expansion**: Extending multimodal perception and mobile automation to iOS devices and simulators.
-- [ ] **On-Device Lightweight VLMs**: Local execution with lightweight edge vision models for low-latency, privacy-first automation.
+- [x] **On-Device Lightweight VLMs**: Local execution with lightweight edge vision models for low-latency, privacy-first automation via OpenAI-compatible local providers — see the [local models guide](./docs/local_models.md) for install/pull/run steps (`config/artemis.gemma4.jsonc` is the tested Gemma 4 E4B setup).
- [ ] **Real-time Duplex Voice Interaction**: Voice-driven task dispatch with real-time conversational control and interruption handling.
## Community & Contributing
diff --git a/apps/admin_console/routers/system.py b/apps/admin_console/routers/system.py
index c8ce18343..be27dbeef 100644
--- a/apps/admin_console/routers/system.py
+++ b/apps/admin_console/routers/system.py
@@ -16,6 +16,7 @@
import ipaddress
import os
+import re
import secrets
from urllib.parse import urlsplit
@@ -624,3 +625,188 @@ async def _shutdown_after_response() -> None:
"message": "Artemis server is shutting down.",
"pid": os.getpid(),
}
+
+
+# --- Local model lifecycle (artemis model pull/serve/stop) ------------------
+#
+# The console surfaces the local_model_endpoint probe's guided actions; these
+# endpoints let the user run them without leaving the browser. Each action
+# spawns `python -m artemis model ...` and streams its output into a capped
+# in-memory log the frontend polls.
+
+
+class LocalModelActionRequest(BaseModel):
+ """Payload for a local-model lifecycle action."""
+
+ alias: str = Field(..., description="Catalog alias or org/repo id (e.g. gemma4-e4b)")
+ port: int | None = Field(
+ default=None,
+ ge=1,
+ le=65535,
+ description="Serve/stop port; the CLI default (8080) applies when omitted",
+ )
+
+
+_MODEL_TASKS: dict[str, dict] = {}
+_MODEL_TASK_LOG_LINES = 80
+
+
+def _model_cli_available() -> bool:
+ """Whether the ``artemis model`` command group exists in this build."""
+ import importlib.util
+
+ try:
+ return importlib.util.find_spec("artemis.interfaces.cli.commands.model") is not None
+ except Exception:
+ return False
+
+
+def _task_view(key: str, task: dict) -> dict:
+ return {
+ "key": key,
+ "action": task["action"],
+ "alias": task["alias"],
+ "cmd": task["cmd"],
+ "status": task["status"],
+ "returncode": task["returncode"],
+ "started_at": task["started_at"],
+ "log": task["log"][-12:],
+ }
+
+
+def _task_snapshot() -> dict:
+ return {k: _task_view(k, t) for k, t in _MODEL_TASKS.items()}
+
+
+async def _drain_model_task(task: dict) -> None:
+ proc = task["process"]
+ try:
+ if proc.stdout is not None:
+ async for raw in proc.stdout:
+ line = raw.decode(errors="replace").rstrip()
+ if line:
+ task["log"].append(line)
+ del task["log"][:-_MODEL_TASK_LOG_LINES]
+ rc = await proc.wait()
+ except Exception as e: # pylint: disable=broad-exception-caught
+ rc = -1
+ task["log"].append(f"task monitor error: {e}")
+ task["returncode"] = rc
+ task["status"] = "done" if rc == 0 else "failed"
+
+
+async def _start_model_task(action: str, alias: str, extra_args: list[str]) -> dict:
+ """Spawn `artemis model Define models in AI Model Setup
Custom Configuration
config/artemis.jsonc & keys in .env
- (OpenAI, Claude, Ollama).artemis model serve).
{{ lm.description }}
+ + + @for (act of lm.actions; track act.label) { + @if (act.action_type === 'command') { +{{ act.payload }}
+ @if (modelActionArgs(act.payload); as args) {
+ @if (localModelCommandsAvailable()) {
+ @if (localModelTaskFor(args.action, args.alias)?.status === 'running') {
+ progress_activity
+ Running…
+ } @else {
+
+ }
+ }
+ }
+
+ {{ act.label }}: {{ act.payload }}
+ } + } +