Issue #2: Manifest schema + Sidecar foundation - sidecar/manifest.py: YAML manifest loading and profile validation - sidecar/app.py: FastAPI sidecar service with /models/available, /models/status endpoints - Router GET /v1/models: proxies to sidecar, returns OpenAI-compatible model list - Tests: 12 manifest tests, 6 sidecar endpoint tests, 3 router tests (21 total) Issue #3: Sidecar model switch + Router request queue - Sidecar POST /models/switch: stops current llama-server, starts new one, polls for readiness - Switch lock prevents concurrent switches (threading.Lock for TestClient compatibility) - Router request queue: max 10 requests, 120s hard timeout, 429 when full - Router automatic model detection: extracts model from chat body, matches against sidecar status - Full proxy endpoint with Sidecar → Main PC routing and fallback chain - Tests: 5 sidecar switch tests, 4 queue tests, 3 router integration tests (12 total) Total: 33 tests, all passing
58 lines
1.4 KiB
Python
58 lines
1.4 KiB
Python
"""Manifest loading and validation — Issue #2."""
|
|
import yaml
|
|
from typing import Optional
|
|
|
|
|
|
def validate_profile(profile: dict) -> dict:
|
|
"""Validate and normalize a single manifest profile entry.
|
|
|
|
Required fields: id, name, model_path.
|
|
Optional field: flags (defaults to {}).
|
|
"""
|
|
for field in ("id", "name", "model_path"):
|
|
if field not in profile:
|
|
raise ValueError(f"Missing required field: {field}")
|
|
|
|
return {
|
|
"id": profile["id"],
|
|
"name": profile["name"],
|
|
"model_path": profile["model_path"],
|
|
"flags": profile.get("flags", {}),
|
|
}
|
|
|
|
|
|
def load_manifest(path: str) -> Optional[list]:
|
|
"""Load and validate profiles from a YAML manifest file.
|
|
|
|
Returns a list of validated profile dicts, or None on any error.
|
|
"""
|
|
try:
|
|
with open(path, "r") as f:
|
|
content = f.read()
|
|
except (FileNotFoundError, OSError):
|
|
return None
|
|
|
|
if not content.strip():
|
|
return []
|
|
|
|
try:
|
|
data = yaml.safe_load(content)
|
|
except yaml.YAMLError:
|
|
return None
|
|
|
|
if data is None or data == []:
|
|
return []
|
|
|
|
if not isinstance(data, list):
|
|
return None
|
|
|
|
profiles = []
|
|
for item in data:
|
|
try:
|
|
profiles.append(validate_profile(item))
|
|
except ValueError:
|
|
# Skip invalid profiles rather than failing the whole manifest
|
|
continue
|
|
|
|
return profiles
|