Extending Loomcast: adding a new runtime
Loomcast is built on Clean Architecture principles (ports and adapters). That architecture lets you add a new LLM runtime (vLLM, Text Generation WebUI, LocalAI, and so on) in a highly modular way, without touching the core application logic.
1. The RuntimePort protocol
Every interaction with an LLM runtime goes through the RuntimePort port
defined in loomcast/ports/runtime.py.
Here is the protocol definition:
from typing import Any, Protocol, runtime_checkable
@runtime_checkable
class RuntimePort(Protocol):
"""Abstract port definition for local LLM runtime interactions."""
def is_available(self) -> bool:
"""Check if the local runtime daemon/server is running and reachable."""
...
def is_model_installed(self, model: str) -> bool:
"""Check if a specific model is installed/loaded in this runtime."""
...
def install(self, model: str, progress: Any | None = None) -> None:
"""Install or pull a model into the local runtime."""
...
def chat(self, model: str, messages: list[dict[str, Any]]) -> str:
"""Send a list of messages to a model and return the generated text response."""
...
def list_installed(self) -> list[str]:
"""List all installed/available model names in this runtime."""
...
def remove(self, model: str) -> None:
"""Remove or delete an installed model from the local runtime."""
...
def update(self, model: str, progress: Any | None = None) -> bool:
"""Update an installed model in the local runtime and return True if digest changed."""
...
2. Reference example: LlamaCppRuntime
The most recent and simplest adapter to model your own on is
loomcast/adapters/runtimes/llamacpp.py.
Here is a simplified version illustrating the implementation:
from typing import Any
import httpx
from loomcast.common.exceptions import RuntimeExecutionError, RuntimeUnavailableError
from loomcast.ports.runtime import RuntimePort
class CustomRuntime(RuntimePort):
def __init__(self, base_url: str = "http://localhost:5000", timeout: float = 30.0):
self.base_url = base_url.rstrip("/")
self.timeout = timeout
def is_available(self) -> bool:
try:
response = httpx.get(f"{self.base_url}/health", timeout=2.0)
return response.status_code == 200
except httpx.HTTPError:
return False
def list_installed(self) -> list[str]:
if not self.is_available():
raise RuntimeUnavailableError("custom", self.base_url)
# Query the runtime API
...
def is_model_installed(self, model: str) -> bool:
return model in self.list_installed()
def install(self, model: str, progress: Any | None = None) -> None:
# Download the model, or validate that it is present
...
def chat(self, model: str, messages: list[dict[str, Any]]) -> str:
# Send the generation request
...
def remove(self, model: str) -> None:
# Delete the model, or raise an explanatory error if the runtime
# exposes no deletion endpoint
...
def update(self, model: str, progress: Any | None = None) -> bool:
# Re-pull the model and report whether its digest changed
...
3. Steps to integrate a fourth runtime
To add a new runtime (for example vllm), follow these steps:
Step 1: declare the type in the domain
Add the enum value in loomcast/domain/entities.py:
class RuntimeType(str, Enum):
OLLAMA = "ollama"
LMSTUDIO = "lmstudio"
LLAMACPP = "llamacpp"
VLLM = "vllm" # New runtime
Step 2: add the configuration URL
Add the setting and its environment mapping in loomcast/common/config.py:
class LoomcastConfig(BaseModel):
# ...
vllm_url: str = Field(default="http://localhost:8000")
Step 3: create the adapter
Create loomcast/adapters/runtimes/vllm.py, implementing RuntimePort.
Step 4: register the adapter in LoomcastManager
Update the self.runtimes dictionary in loomcast/core/manager.py:
self.runtimes = {
RuntimeType.OLLAMA: OllamaRuntime(base_url=self.config.ollama_url),
RuntimeType.LMSTUDIO: LMStudioRuntime(base_url=self.config.lmstudio_url),
RuntimeType.LLAMACPP: LlamaCppRuntime(base_url=self.config.llamacpp_url),
RuntimeType.VLLM: VllmRuntime(base_url=self.config.vllm_url),
}
4. The strict Clean Architecture rule
[!IMPORTANT] Separation of concerns: never make the
domain/orports/layer depend on a third-party network or I/O library (httpx,requests,aiohttp, and so on).
- The domain layer (
loomcast/domain/) contains pure entities only (dataclasses / Pydantic models). - The ports layer (
loomcast/ports/) contains interfaces and type protocols only. - All network logic and HTTP calls must live exclusively in the adapters
layer (
loomcast/adapters/runtimes/).