| """Real backends — small GGUF LLMs via llama-cpp-python. |
| |
| Image generation was dropped (see backend/factory.py docstring). Only the |
| LLM path remains real on Spaces. |
| |
| LLM loader follows the verified Build Small Discord recipe (Dean [UNRL], |
| 2026-05-06): `Llama(...)` is constructed INSIDE the `@spaces.GPU` boundary, |
| not at module level. |
| |
| Each `RealLLMBackend(model_id)` loads a different GGUF from the roster in |
| `game/models.py`. Per-model_id instances are cached by `backend/factory.py`. |
| |
| Model card references (per CLAUDE.md vendor-citation rule): |
| https://huggingface.co/nvidia/NVIDIA-Nemotron-3-Nano-4B-GGUF |
| Additional roster URLs live in `game/models.py`. |
| """ |
|
|
| from __future__ import annotations |
|
|
| import os |
| from typing import TYPE_CHECKING |
|
|
| from game.models import get_spec |
|
|
| from . import config |
|
|
| if TYPE_CHECKING: |
| from llama_cpp import Llama |
|
|
|
|
| def force_cpu_mode() -> bool: |
| """When set, RealLLMBackend loads with `n_gpu_layers=0` and reduced |
| context — usable on the Space's 2 vCPU when ZeroGPU quota is gone. |
| Toggled by app.py's tick_decisions fallback path.""" |
| return os.getenv("TOWNLET_FORCE_CPU") == "1" |
|
|
|
|
| class RealLLMBackend: |
| """A small GGUF LLM via llama-cpp-python, keyed by `model_id` in the roster. |
| |
| First call downloads the GGUF (cached after that). On Spaces with |
| `preload_from_hub` in README.md, the cache is warm at boot. |
| """ |
|
|
| |
| |
| |
| |
| _model_paths: dict[str, str] = {} |
|
|
| @classmethod |
| def register_model_path(cls, model_id: str, path: str) -> None: |
| cls._model_paths[model_id] = path |
|
|
| def __init__(self, model_id: str) -> None: |
| self.model_id = model_id |
| self._spec = get_spec(model_id) |
| self._llm: Llama | None = None |
|
|
| def _ensure_loaded(self) -> "Llama": |
| if self._llm is None: |
| try: |
| from llama_cpp import Llama |
| except ImportError as e: |
| raise RuntimeError( |
| "llama-cpp-python is not installed. To run with real models " |
| "locally, run `.venv/bin/pip install llama-cpp-python` " |
| "(first time compiles from source, takes 5-15 min on Mac). " |
| "Or run with mocks (no FORCE_REAL_MODELS env var set)." |
| ) from e |
|
|
| |
| |
| |
| |
| path = self._model_paths.get(self.model_id) |
| if path is None: |
| from huggingface_hub import hf_hub_download |
| path = hf_hub_download( |
| repo_id=self._spec.repo_id, |
| filename=self._spec.gguf_filename, |
| ) |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| on_spaces = config.IS_SPACES |
| cpu_only = force_cpu_mode() |
| if cpu_only: |
| gpu_layers = 0 |
| n_ctx = 4096 |
| use_flash = False |
| elif on_spaces: |
| gpu_layers = -1 |
| n_ctx = 8192 |
| use_flash = True |
| else: |
| gpu_layers = 0 |
| n_ctx = 6144 |
| use_flash = False |
|
|
| self._llm = Llama( |
| model_path=path, |
| n_gpu_layers=gpu_layers, |
| n_ctx=n_ctx, |
| flash_attn=use_flash, |
| verbose=False, |
| ) |
| return self._llm |
|
|
| def generate(self, system_prompt: str, user_prompt: str) -> str: |
| return self.generate_messages( |
| messages=[ |
| {"role": "system", "content": system_prompt}, |
| {"role": "user", "content": user_prompt}, |
| ], |
| max_tokens=256, |
| ) |
|
|
| def generate_messages( |
| self, |
| messages: list[dict], |
| stop_sequences: list[str] | None = None, |
| max_tokens: int = 1024, |
| grammar: str | None = None, |
| ) -> str: |
| |
| |
| |
| |
| |
| llm = self._ensure_loaded() |
| kwargs: dict = { |
| "messages": messages, |
| "max_tokens": max_tokens, |
| "temperature": 0.8, |
| } |
| if stop_sequences: |
| kwargs["stop"] = stop_sequences |
| if grammar: |
| from llama_cpp import LlamaGrammar |
|
|
| kwargs["grammar"] = LlamaGrammar.from_string(grammar) |
| result = llm.create_chat_completion(**kwargs) |
| return result["choices"][0]["message"]["content"].strip() |
|
|