Chat with someone who isn't trying to help you.
FrigidLLM is a small full-stack app that lets you talk to a roster of deliberately unhelpful characters running on a local Ollama model. No assistants, no apologies, no "as a language model" : each character has a personality, a mood, and their own sampling parameters, and they answer in-character or not at all.
The backend is a Go HTTP server with an asynchronous job queue and Firebase Google sign-in. The frontend is a React + Vite + TypeScript app that handles auth and polls the backend for each reply.
Pick someone to talk to. Each character is a separate Go file under
backend/internal/ollama/personalities/ :
the system prompt, temperature, top-p and top-k all live in there.
┌─────────────────────────────┐ ┌─────────────────────────────┐ ┌──────────────┐
│ frontend/ (Vite, :5173) │ │ backend/ (Go, :8080) │ │ Ollama │
│ │ HTTPS │ │ HTTP │ :11434 │
│ Google sign-in (Firebase) ├────────►│ POST /generate ──┐ ├────────►│ │
│ POST prompt + character │ Bearer │ ↓ │ │ │ │
│ Poll GET /jobs/{id} │ <UID> │ worker pool (10) │ │ │ │
│ │ │ └─► chat │ │ │
└─────────────────────────────┘ └─────────────────────────────┘ └──────────────┘
The backend treats every prompt → response as an asynchronous job: clients
get a job_id immediately and poll GET /jobs/{id} until status is done or
failed. The frontend hides the polling behind a 1-second loop, so it feels
like a normal chat. Up to 10 jobs run in parallel; the 11th queues; the 101st
gets a 503.
Each character ships its own system prompt and its own sampling settings
(temperature, top-p, top-k), so the personality is doing the heavy lifting :
not the user's prompt. The handler also accepts a per-request temperature
override in [0, 2], defaulting to 0.7 when omitted.
.
├── backend/ Go HTTP server. Worker pool, Firebase ID-token auth, Ollama client.
│ See backend/README.md.
├── frontend/ React + Vite + TS web app. Firebase Google sign-in + job polling.
│ See frontend/README.md.
├── install.sh One-shot setup (installs Ollama, pulls model, npm install, go mod download).
├── launch.sh Start Ollama + backend (DEV_MODE=1) + frontend, all at once.
└── README.md you are here
The two halves communicate only over HTTP : the frontend never imports the
backend, and vice versa. CORS is enforced backend-side (single origin, set via
CORS_ORIGIN, default http://localhost:5173).
The fastest path — two scripts at the repo root handle everything:
./install.sh # installs Ollama (if missing), pulls the model,
# downloads Go deps, runs `npm install`, seeds
# frontend/.env.local with VITE_DEV_MODE=1.
./launch.sh # starts Ollama, the Go backend in DEV_MODE=1,
# and the Vite dev server. Ctrl-C stops them all.
# Open http://localhost:5173.launch.sh always runs in dev mode — Firebase is bypassed and the backend
binds loopback only. You only need Go (≥1.21) and Node (≥18) preinstalled;
install.sh takes care of Ollama and the phi3 model.
Override the model with OLLAMA_MODEL=llama3 ./install.sh (and the same on
./launch.sh).
If you'd rather run each piece by hand:
-
Ollama with a model pulled.
ollama pull phi3 # systemd usually starts ollama automatically; otherwise: ollama serve -
Backend (in dev mode — bypasses Firebase, binds loopback only):
cd backend DEV_MODE=1 go run ./cmd/httpserver -
Frontend (talks to the dev-mode backend, no Firebase setup needed):
cd frontend cp .env.example .env.local # set VITE_DEV_MODE=1 in .env.local npm install npm run dev # http://localhost:5173
For real Google sign-in, swap the backend to FIREBASE_CREDENTIALS_FILE=...
and fill in the Firebase Web App keys in frontend/.env.local. Both
sub-READMEs walk through it.
- Drop a new file in
backend/internal/ollama/personalities/following the shape offrank.go: a name, a one-line description, sampling parameters, and a system prompt. - Register it in the
byNamemap inpersonalities.go. - Add a matching portrait at
frontend/src/assets/<name>.png.
The public GET /characters endpoint will pick it up automatically and the
frontend's character picker will render it on next reload.
- backend/README.md : API contract, environment variables, auth (Firebase + dev mode), worker-pool concurrency model, graceful shutdown, intentional out-of-scope items.
- frontend/README.md : run modes, source layout, how the polling hook works, what's deliberately not implemented yet.
This is a personal project. There is no test suite yet, no production deployment, and no persistence : restarting the backend loses all jobs. The scope is deliberately small; see the "Out of scope" sections in each sub-README for what's been considered and left out (persistence, retries, streaming, multi-origin CORS, etc.).




