"""Deploy llm-service to Hugging Face Spaces. Stages the Docker Space files, sets API key secrets from local secrets.toml, and uploads everything via the huggingface_hub API. Usage: python -m scripts.hf_deploy Example: python -m scripts.hf_deploy ronheichman/llm-service """ from __future__ import annotations import os import shutil import sys import tempfile import tomllib from datetime import date from pathlib import Path from huggingface_hub import HfApi REPO_ROOT = Path(__file__).resolve().parent.parent STAGE_FILES = { "Dockerfile.hf": "Dockerfile", "hf_readme.md": "README.md", "hf_entrypoint.sh": "hf_entrypoint.sh", "free_llm_providers.yaml": "free_llm_providers.yaml", "secrets.example.toml": "secrets.example.toml", } STAGE_DIRS = { "scripts": "scripts", } def load_secrets(secrets_path: Path) -> dict[str, dict]: """Parse secrets.toml and return the keys section.""" return tomllib.loads(secrets_path.read_text()).get("keys", {}) def _as_date(value: object) -> date | None: if value is None: return None return date.fromisoformat(str(value)) if isinstance(value, str) else value def provider_is_active(config: dict) -> bool: """Mirror generate_config's expires/disabled_until checks. hf_inject_secrets.py rebuilds secrets.toml on the Space from env vars and cannot carry disabled_until/expires metadata. Disabled providers must therefore be excluded at deploy time, and their stale Space secrets deleted, or dead providers resurrect on the Space (this is how scitely and iflow kept serving 4xx/5xx from the live proxy after being disabled locally). """ today = date.today() expires = _as_date(config.get("expires")) if expires is not None and expires < today: return False disabled_until = _as_date(config.get("disabled_until")) return not (disabled_until is not None and disabled_until > today) def secrets_to_env_vars(keys: dict[str, dict]) -> dict[str, str]: """Map active secrets.toml entries to HF Space secret env var names.""" env_vars: dict[str, str] = {} for provider, config in keys.items(): if not provider_is_active(config): continue upper = provider.upper() for i, key in enumerate(config.get("keys", [])): if i == 0: env_vars[f"LLM_KEY_{upper}"] = key else: env_vars[f"LLM_KEY_{upper}_{i + 1}"] = key account_id = config.get("account_id") if account_id: env_vars[f"LLM_ACCOUNT_ID_{upper}"] = str(account_id) return env_vars def stale_env_var_names(keys: dict[str, dict]) -> list[str]: """Space secret names to delete for providers that are not active.""" names: list[str] = [] for provider, config in keys.items(): if provider_is_active(config): continue upper = provider.upper() names.append(f"LLM_KEY_{upper}") names.extend(f"LLM_KEY_{upper}_{i}" for i in range(2, 2 + len(config.get("keys", [])))) if config.get("account_id"): names.append(f"LLM_ACCOUNT_ID_{upper}") return names def stage_files(staging_dir: Path) -> None: """Copy all deployment files into the staging directory.""" for src_name, dst_name in STAGE_FILES.items(): src = REPO_ROOT / src_name if not src.exists(): print(f"WARNING: {src} not found, skipping") continue shutil.copy2(src, staging_dir / dst_name) for src_name, dst_name in STAGE_DIRS.items(): src = REPO_ROOT / src_name if not src.exists(): print(f"WARNING: {src}/ not found, skipping") continue shutil.copytree(src, staging_dir / dst_name, dirs_exist_ok=True) SERVICE_SECRETS = { "STORE_MODEL_IN_DB": "True", } def deploy( space_id: str, secrets_path: Path | None, database_url: str | None, master_key: str | None, ) -> str: """Deploy to HF Spaces. Returns the Space URL.""" token = os.environ.get("HF_TOKEN") if token is None and secrets_path and secrets_path.exists(): hf_entry = load_secrets(secrets_path).get("huggingface", {}) hf_keys = hf_entry.get("keys", []) if hf_keys: token = hf_keys[0] print("Using huggingface key from secrets.toml as deploy token") api = HfApi(token=token) api.create_repo(repo_id=space_id, repo_type="space", space_sdk="docker", exist_ok=True) for name, value in SERVICE_SECRETS.items(): api.add_space_secret(repo_id=space_id, key=name, value=value) if database_url: api.add_space_secret(repo_id=space_id, key="DATABASE_URL", value=database_url) print("Set DATABASE_URL Space secret") if master_key: api.add_space_secret(repo_id=space_id, key="LITELLM_MASTER_KEY", value=master_key) print("Set LITELLM_MASTER_KEY Space secret") if secrets_path and secrets_path.exists(): keys = load_secrets(secrets_path) env_vars = secrets_to_env_vars(keys) for name, value in env_vars.items(): api.add_space_secret(repo_id=space_id, key=name, value=value) print(f"Set {len(env_vars)} LLM key secret(s)") stale = stale_env_var_names(keys) for name in stale: api.delete_space_secret(repo_id=space_id, key=name) if stale: print(f"Deleted {len(stale)} stale secret(s) for disabled/expired providers: " + ", ".join(stale)) else: print("No secrets.toml found, skipping LLM key injection") with tempfile.TemporaryDirectory() as tmp: staging = Path(tmp) stage_files(staging) api.upload_folder( repo_id=space_id, repo_type="space", folder_path=str(staging), ) url = f"https://huggingface.co/spaces/{space_id}" print(f"Deployed to {url}") return url def main() -> None: if len(sys.argv) < 2: print("Usage: python -m scripts.hf_deploy [--database-url URL] [--master-key KEY]") print("Example: python -m scripts.hf_deploy ronheichman/llm-service --database-url postgresql://...") sys.exit(1) space_id = sys.argv[1] secrets_path = REPO_ROOT / "secrets.toml" db_url = None mk = None args = sys.argv[2:] i = 0 while i < len(args): if args[i] == "--database-url" and i + 1 < len(args): db_url = args[i + 1] i += 2 elif args[i] == "--master-key" and i + 1 < len(args): mk = args[i + 1] i += 2 else: i += 1 deploy(space_id, secrets_path, db_url, mk) if __name__ == "__main__": main()