LoL Matchbook
Champ select gives you about 30 seconds to lock in a pick. A model generating advice on the spot can't beat that clock, and it has no business fighting the game for GPU while you're trying to play it. So I built a desktop app that watches the League client's local API, detects your matchup the moment it's locked, and answers out of a precomputed lookup table instead of generating anything live.
Precompute, don't generate live
That's the whole architecture in one sentence. A background job pulls ranked match data, aggregates it per champion pair, rank, and game phase, and writes early/mid/late-game blurbs to Postgres ahead of time. During an actual game, /advice is just a DB read: under 200ms, no GPU touched, nothing fighting the game for VRAM.
Precomputing every same-lane pair at measured throughput would take 104 to 158 days.The fix wasn't a faster model. It was giving up on precomputing everything.
6,512 pairs and a tiered fallback
Ranked-only, deduplicated match data puts the same-lane matchup space at 6,512 pairs across rank brackets and game phases. At measured generation throughput, precomputing all of it isn't an overnight job, it's a multi-month one. So instead of chasing more throughput, I split precompute into two tiers: an eager tier covering the top pairs by play volume, sized to whatever refresh window is actually tolerable, and a lazy tier that falls back to a wider rank bracket or an archetype-level blurb on a miss. Misses get logged to a background backfill queue rather than blocking the request. Champ select never waits on generation either way; the tier only changes how fresh the answer is, not how fast it shows up.
A fine-tuned model for the part that has to be live
Follow-up questions in the chat panel (/ask) can't be precomputed, since there's no way to know what someone will type. That path runs a LoRA-adapted small model through llama-cpp-python's GGUF serving, quantized and kept on CPU on purpose so it stays safe to run alongside the game instead of reaching for the same GPU the precompute batch job needs. Getting this working on Windows turned into its own side quest: llama-cpp-python has no prebuilt Windows wheel, so the full backend has to run under WSL2 with a real Ubuntu userspace (Docker Desktop's built-in WSL VM doesn't count). Even that wasn't straightforward — a fresh Ubuntu install shipped a Python too new for vllm and torch to install against.
From dev server pair to a packaged app
The FastAPI backend and React/Vite frontend started as two dev servers you'd run by hand. The last build phase wraps the backend as a Tauri sidecar with the frontend as its webview, so it's one packaged app with launch-on-login now, and you don't have to remember to start two terminals before queueing up.