01 · Overview
What this is
The whole project rests on one idea: a model file on disk becomes a live HTTP server, and everything else — every tool, every interface — is just a client of that server.
Three GGUF model files sit on disk: Llama 4 Scout,
Qwen3‑Coder‑Next, and Qwen3‑Next‑80B‑A3B‑Instruct.
A GGUF file is inert: weights plus metadata, nothing more, until
something loads it. That something is llama-server,
the inference engine from the llama.cpp
project, built from source with CUDA support rather than run
through a wrapper like Ollama, for direct control over
quantization, GPU/CPU offload, and context settings. Each running
model gets its own llama-server process, and each one
exposes an OpenAI-compatible HTTP API — the same
POST /v1/chat/completions shape used by the OpenAI
API and by most of the LLM tooling ecosystem. That compatibility
is what lets a plain HTTP client speak to a local model exactly
the way it would speak to a cloud one.
Two independent front ends sit on top of that API: a terminal chat client and a lightweight browser-based GUI. Neither one knows the other exists, and they don't share code — they share a contract. A model name always maps to one running server, and talking to it is always the same request shape. Any future front end (a different TUI, a small mobile client) would just be another client of that same contract; nothing about the inference layer would need to change.
02 · The offload trick
Fitting an 80B-parameter model into 8GB of VRAM
None of these models come close to fitting in 8GB of VRAM as a whole — their GGUF files run 43–65GB apiece. What makes them usable on a small GPU at all is that they're mixture-of-experts (MoE) architectures, combined with a specific offload recipe.
A dense model uses every one of its weights on every token it processes. An MoE model instead has a large bank of "expert" feed-forward sub-networks and a small routing mechanism that activates only a handful of them per token. Most of the model's bulk sits idle for any given token; only the attention layers and a small always-active shared/routing portion run on literally everything. Qwen3-Coder-Next and Qwen3-Next-80B-A3B-Instruct share the same underlying backbone — Alibaba's Qwen3-Next, roughly 80B total parameters with only about 3B active per token, one variant fine-tuned for code and the other instruct-tuned generally.
llama-server exposes two flags that exploit this
directly: -ngl 999 tells it to offload every
transformer layer it can to the GPU, and --cpu-moe
overrides that for one specific category of tensor, pinning the
MoE expert feed-forward weights to CPU RAM regardless. The
combined effect is a deliberate split:
Attention layers and shared/routing weights — the parts that run on every token — live in the scarce, fast memory where they benefit most from GPU throughput.
The MoE expert weights — the bulk of the parameter count, but sparsely used by any single token — sit in cheap, plentiful RAM instead of competing for VRAM.
That's the whole trick that lets an 8GB consumer card meaningfully accelerate models many times larger than its own memory. Context length trades off against the same VRAM budget, so it's tuned per model: Scout's attention layers are larger (17B active parameters per token, across 16 experts), so its context window is trimmed further in GPU-offload mode to leave headroom, while both Qwen variants keep a larger window since their much smaller active-parameter footprint leaves more room to begin with.
There's also a plain CPU-only mode (-ngl 0,
nothing offloaded) that never touches the GPU at all. It's
slow — on the order of one token every few seconds for
Scout and Qwen3-Next, and sometimes under one token per second
for Qwen3-Coder-Next specifically, since its expert routing is
expensive to do from CPU RAM — but it's the mode both
front ends use by default, and a fully isolated way to
sanity-check that a model and its chat template are working
correctly before ever touching GPU offload.
03 · Model roster
The models
All three are quantized GGUF builds, chosen to cover a general assistant, a coding specialist, and a general-purpose sibling built on the coding model's own architecture.
| Model | Role | MoE shape | Size on disk |
|---|---|---|---|
| Llama 4 Scout | General-purpose | 16 experts · 17B active/token | ~65GB (2 GGUF parts) |
| Qwen3-Coder-Next | Coding specialist | Qwen3-Next backbone · 80B total / ~3B active | ~49GB |
| Qwen3-Next-80B-A3B-Instruct | General-purpose | Same backbone as above, instruct-tuned | ~43GB |
Qwen3-Coder-Next and Qwen3-Next-80B-A3B-Instruct are the same underlying model architecture, fine-tuned for different jobs — which is why they land in the same offload recipe and the same rough speed range on this hardware.
04 · Architecture
Top to bottom
-
Model file (GGUF)Weights plus metadata on disk. Inert until loaded — does nothing by itself.
-
llama-serverLoads one GGUF file, applies the GPU/CPU offload split above, and exposes it as an OpenAI-compatible HTTP API. One process per running model.
-
Shared API contract
POST /v1/chat/completions, with the model's own embedded chat template applied automatically. Every client speaks this one shape. -
Front endsA terminal CLI and a browser-based GUI, both independent, interchangeable clients of the same contract.
05 · Terminal client
The CLI: a streaming REPL with real tool-calling
The base client is a straightforward terminal chat loop: a
you> prompt, the reply streaming back token by
token over server-sent events, and conversation history kept in
memory across turns. /clear wipes history without
restarting the server, /exit (or Ctrl+D) ends the
session cleanly, and Ctrl+C interrupts mid-stream without
leaving anything in a broken state.
An opt-in MCP tool-calling layer
A separate --tools mode layers a real
Model Context Protocol
client on top of that same chat loop, kept deliberately
non-streaming and clearly marked as best-effort. On startup it
connects, over stdio, to whichever MCP servers are enabled in a
small config file, calls list_tools() on each, and
merges the results into one tool schema the model can call
against. Every tool is namespaced {source}__{name}
— e.g. filesystem__read_file — which
isn't cosmetic: a couple of the configured servers expose tools
with names that would otherwise collide.
The safety gate is the same for every source, no exceptions: every tool call, before it executes, requires a manual y/n confirmation. There's no auto-execute path. A local filesystem server can read and write anything under its configured root, so that confirmation is the real safety boundary, not a formality. Servers that fail to connect don't take the CLI down with them — it prints one warning line and continues with whatever did connect.
An honest limitation, found rather than assumed: a merged tool schema can be bigger than these models' context windows. One configured browser-automation server alone measured out to roughly 4,400 tokens of schema; combined with a filesystem server's tool set, the merged schema came in at about 6,600 tokens against a live model running a 4,096-token context — over budget before a single word of conversation. The fix wasn't just a warning: that server now ships disabled by default until the context window is raised to accommodate it, and the CLI checks the connected server's actual context size at startup and warns proactively if the merged schema looks too large for it, instead of failing mid-conversation with a confusing error.
06 · Browser client
The GUI: a picker, not a rebuilt chat UI
Rather than build a custom chat interface, the GUI leans on the one llama.cpp already ships with each model server.
A tiny local picker page lists one entry per model. Selecting one
launches that model's server, in CPU-only mode, if it isn't
already running, then opens that model's own built-in llama.cpp
chat page in an app-mode browser window — no tabs, no
address bar, so it reads as a standalone app rather than a
browser tab. If the picker's own backend isn't reachable for any
reason, the page falls back to showing the exact command to run
the same launch by hand, as a deliberate safety net rather than a
missing feature. Once a model's chat window is open, it's just a
plain web page talking to the same
/v1/chat/completions contract as the CLI. This GUI
path only ever launches CPU-only, by design; GPU-offload mode is
something invoked directly, on purpose, not exposed through a
click.
07 · Field notes
Three bugs worth remembering
A few things that came up building this, kept here because they're the kind of thing worth remembering the next time something looks similarly broken.
-
Wrong endpoint, garbled output
llama-serverexposes both a raw completion endpoint that takes a bare prompt string and an OpenAI-compatible chat endpoint that formats structured messages through the model's embedded template automatically. Hit the raw endpoint directly with a hand-written prompt and you skip that templating entirely — literal template control tokens and role markers leak into the visible output. Not a model defect, just a mismatch between the endpoint called and the input shape it expects. The fix is the rule now: always go through the chat-completions endpoint with a proper messages array. -
Streamed emoji and accents turning to mojibake
The model server's streaming response declares
text/event-streamwith no character-set parameter, so a naive HTTP client's default encoding guess can land on the wrong single-byte codec and mangle every multi-byte UTF-8 sequence — corrupting not just what's printed to the terminal but the in-memory conversation history itself. The fix was one line: explicitly force UTF-8 decoding on the response before iterating over it. Confirmed with a round-trip test asking a model to echo back a string full of accents and emoji and checking the bytes that came back. -
A tool schema bigger than the context window
Covered above in the CLI section, but worth restating as its own lesson: it's entirely possible for the description of the tools available to a model to exceed the model's own context window, with zero conversation in it yet. That's not an edge case to handle defensively "just in case" — it actually happened, on the first real attempt to combine two tool sources against a small-context model.
08 · Maturity check
Status
Not everything here is at the same level of maturity, so here's where each piece actually stands.
Core CLI
Streaming chat loop, history, and clean interrupt handling are working end to end.
MCP tool-calling
Config loading, connecting, schema merging, and the confirmation gate are all verified directly. A live model actually driving a full multi-round tool call against the merged schema is the next thing to confirm.
GUI picker
Launch requests confirmed to correctly start the right model server as a detached background process, including the already-running case.
09 · Live demo
Load your own GGUF and chat with it, right here
Unlike everything above, this part of the page is not a description of something running elsewhere — it actually runs, in this tab, right now. Pick a small GGUF file from your own machine below and it loads and runs entirely client-side, via wllama, a WebAssembly build of llama.cpp that runs inside the browser itself. Nothing you select is uploaded anywhere — there is no server on the other end of this page to send it to. This site is static file hosting, full stop.
This is not the 43–65GB production setup described above, and it can't be. A browser tab, running a single WebAssembly thread with no GPU offload, can realistically handle only a small quantized GGUF — roughly under 2GB, and even well under that will be noticeably slow compared to the CUDA-accelerated setup above. Point this at Scout or either Qwen model from the roster above and it will not work: it will hang the tab or run the browser out of memory long before it finishes loading. Use something genuinely small instead — a "tiny"/toy GGUF, or a small quantized instruct model in the hundreds-of-MB to low-single-digit-GB range.
Browser requirements, stated plainly. This demo deliberately disables wllama's own default fallback for browsers without the WebAssembly features it needs (JSPI and Memory64), because that fallback loads its files from a CDN, and this page makes zero requests to any host you didn't ask it to. The consequence: a browser without native support for those two features simply can't run this demo. Confirmed working in a recent Chromium-based desktop browser (Chrome); older browsers, and possibly Safari, may not work here at all — that's a real gap, not a hedge.
Fixed for this demo: 2048-token context, 256-token replies, single WASM thread, no GPU offload.
10 · Try the real thing
Run the CLI yourself
The browser demo above is deliberately small and self-contained.
The actual project it's standing in for — the terminal CLI
described in section 05, talking to a real llama-server
process on your own hardware — is real, runnable code, and
the source is public. This section is the setup for running that,
not the browser demo.
Get the source
github.com/coleman-sagil/local-llm is the real repository this whole page describes.
git clone https://github.com/coleman-sagil/local-llm.git
cd local-llm
pip install requests
requests is the only third-party dependency the core
chat client (cli/llmcli.py) imports; everything else
it uses is Python standard library. The optional
--tools layer additionally needs the MCP Python SDK:
pip install mcp.
What the repo does and doesn't ship
Two directories that matter are deliberately not in the repo:
models/ (the GGUF weight files themselves —
tens of gigabytes, wrong tool for git) and llama.cpp/
(the inference engine, vendored and built from source, also
gitignored). A fresh clone gets you the CLI, the GUI picker, and
bin/start-model.sh/bin/stop-model.sh
— but not a working llama-server binary or any
models to point it at. To actually run something, you need your
own compiled llama-server (build
llama.cpp
from source, CUDA support optional) and your own GGUF file,
serving an OpenAI-compatible /v1/chat/completions
endpoint.
Point the CLI at a server
cli/llmcli.py itself is a thin, generic client: it
only knows how to POST to /v1/chat/completions on
127.0.0.1 at a fixed port per model name. It never
starts a server itself. The three names it ships with are this
project's own three models:
scout -> http://127.0.0.1:8090 (Llama-4-Scout)
qwen -> http://127.0.0.1:8091 (Qwen3-Coder-Next)
qwen-next -> http://127.0.0.1:8092 (Qwen3-Next-80B-A3B-Instruct)
Two ways to actually use it against your own setup: either edit
the MODEL_PORTS dict at the top of
cli/llmcli.py to add your own model name and port, or
get your own llama-server listening on one of the
three ports above and just use that model name as-is. If you have
your own GGUF files at the exact paths
bin/start-model.sh expects, that script will also
start/stop the server for you:
bin/start-model.sh scout --cpu-only # or: qwen, qwen-next
python3 cli/llmcli.py --model scout
bin/stop-model.sh scout
Once connected, it's the same REPL described in section 05:
type at the you> prompt, the reply streams back
token by token, /clear wipes conversation history
without restarting the server, /exit (or Ctrl+D)
ends the session, and Ctrl+C interrupts cleanly mid-stream. Add
--tools to layer on the MCP tool-calling REPL
described in the same section — non-streaming, every tool
call gated behind a manual y/n confirmation, configured through
your own cli/mcp_servers.json.
python3 cli/llmcli.py --model qwen --tools
If the server isn't reachable, the CLI fails fast with the exact command to start it, rather than hanging or printing a stack trace — that behavior is real and unchanged from section 05.