from __future__ import annotations import argparse import sys from urllib.parse import urlparse from common import ROOT, read_json GPU_PROFILES = ROOT / "config" / "gpu_profiles.json" KNOWN_SERVICES = { "vllm-text", "llama-gguf", "translation-api", "audio-api", "video-api", "image-api", "model-chat-ui", } REQUIRED_PROFILES = {"default", "text", "audio", "video", "image", "gguf-1c"} REQUIRED_FIELDS = {"id", "label", "command", "starts", "stops", "wait", "notes"} def require(condition: bool, message: str, errors: list[str]) -> None: if not condition: errors.append(message) def validate_service_list(profile_id: str, field: str, value: object, errors: list[str]) -> list[str]: require(isinstance(value, list), f"{profile_id}.{field} must be a list", errors) if not isinstance(value, list): return [] services = [] for index, service in enumerate(value): if not isinstance(service, str) or not service: errors.append(f"{profile_id}.{field}[{index}] must be a non-empty string") continue services.append(service) if service not in KNOWN_SERVICES: errors.append(f"{profile_id}.{field}[{index}] has unknown service `{service}`") if len(services) != len(set(services)): errors.append(f"{profile_id}.{field} contains duplicate service ids") return services def validate_wait(profile_id: str, value: object, errors: list[str]) -> None: require(isinstance(value, list), f"{profile_id}.wait must be a list", errors) if not isinstance(value, list): return for index, item in enumerate(value): if not isinstance(item, dict): errors.append(f"{profile_id}.wait[{index}] must be an object") continue name = item.get("name") url = item.get("url") if not isinstance(name, str) or not name: errors.append(f"{profile_id}.wait[{index}].name must be a non-empty string") if not isinstance(url, str) or not url: errors.append(f"{profile_id}.wait[{index}].url must be a non-empty string") continue parsed = urlparse(url) if parsed.scheme not in {"http", "https"} or not parsed.netloc: errors.append(f"{profile_id}.wait[{index}].url is not an HTTP URL: {url}") def validate_profile(profile_id: str, profile: object, errors: list[str]) -> None: if not isinstance(profile, dict): errors.append(f"{profile_id} must be an object") return missing = sorted(REQUIRED_FIELDS - set(profile)) if missing: errors.append(f"{profile_id} missing field(s): {', '.join(missing)}") require(profile.get("id") == profile_id, f"{profile_id}.id must equal `{profile_id}`", errors) for field in ("label", "command", "notes"): require(isinstance(profile.get(field), str) and bool(profile.get(field)), f"{profile_id}.{field} must be a non-empty string", errors) command = str(profile.get("command") or "") require(f"-Profile {profile_id}" in command, f"{profile_id}.command must include `-Profile {profile_id}`", errors) starts = validate_service_list(profile_id, "starts", profile.get("starts"), errors) stops = validate_service_list(profile_id, "stops", profile.get("stops"), errors) overlap = sorted(set(starts) & set(stops)) if overlap: errors.append(f"{profile_id} has service(s) in both starts and stops: {', '.join(overlap)}") validate_wait(profile_id, profile.get("wait"), errors) def main() -> int: parser = argparse.ArgumentParser(description="Validate GPU profile config.") parser.add_argument("--print", action="store_true", help="Print valid profile ids.") args = parser.parse_args() try: profiles = read_json(GPU_PROFILES) except (OSError, ValueError) as exc: print(f"GPU profile config error: {exc}", file=sys.stderr) return 1 errors: list[str] = [] missing_profiles = sorted(REQUIRED_PROFILES - set(profiles)) if missing_profiles: errors.append(f"missing required profile(s): {', '.join(missing_profiles)}") for profile_id, profile in sorted(profiles.items()): validate_profile(str(profile_id), profile, errors) if errors: print("GPU profile validation failed:", file=sys.stderr) for error in errors: print(f"- {error}", file=sys.stderr) return 1 print(f"Validated {len(profiles)} GPU profile(s).") if args.print: for profile_id in sorted(profiles): print(f"- {profile_id}") return 0 if __name__ == "__main__": raise SystemExit(main())