From the depths of gaming …

I’m a nerd, I play (played…) in my spare time. So I’ve got a “gamer” type PC, with a solid graphics card, a beefy CPU and lots of RAM. It’s a build I started putting together during covid, before prices back then (already going off the rails at that point) went completely crazy.

An rtx 3080, and I was doing pretty well gaming (Watch Dogs, Satisfactory, and other wonders).

Then I swapped the GPU, to do a bit of ML on it. Added more RAM too. And little by little my gaming PC turns into an ML workstation. Cool.

Then we entered the era of AI agents, local LLMs (thanks China), OSS projects to run AI agents and so on (hello OpenClaw, Hermes, etc), and I wanted to play with all of that at home. The workstation was just waiting for it. Perfect timing.

First attempts with local AIs, and you already start seeing the limits of the rtx 3090, despite its 24 GB of VRAM.

Being a proper “tryharder”, I decide to find a second GPU, the exact same one, for peanuts (well okay, 650 euros still). And then I have to swap the motherboard to actually take advantage of both GPUs (PCI lane count, etc, basically big nerd stuff). Found a motherboard on leboncoin, the GPU too. Built the whole thing. The second GPU overheats (on its own or with both, cross tests, double check). I got scammed by a 60-year-old dude who, and I quote, only played Flight Simulator on it (and had left the plastic film on the cooling plate!). I should’ve been more careful.

Still tryharding, I decide to open up the card: thermal paste bone DRY.

Dried thermal paste

I redo all of that (I only have one photo, unfortunately). We put everything back together.. it rips and it’s stable temperature-wise.

The full build:

The full build

  • Amd Ryzen 3900x
  • 64 GB DDR4 RAM
  • Asus ROG X570
  • 2x Asus TUF RTX 3090

… to a local AI

Let’s go. Step by step.

Preliminary tests

The machine is ready, I install a linux distro on it and run my first convincing tests, notably with gemma 4.

That’s all nice and cool, but I don’t want to manage my workstation as a standalone, with everything that implies.

Simple solution, it was right in front of me the whole time! Integrate the workstation into my homelab. Just like with the rpis, my workstation joins my cluster with a single command.

Now, what to do?

First step: have your own LLM served properly on this node.

At that point, gemma-4 is all over the LLM threads on OW, so I test serving it here.

For that, we’ll use vllm, configure it to load the right gemma-4 that fits across the 2 GPUs (48GB total). From memory, we use a quantized model + a 200k context pre-allocation to make things interesting.

Blown away by the results. Local LLMs are starting to get serious.

I let it run locally, without using it too much.. a bit of opencode on a “fake” project to see how it handles itself. Nothing more.

Then… the latest Qwen model drops: qwen3.6. The benchmarks are claiming insane numbers, especially in agentic coding.

Qwen3.6: just thank you.

You know the guy by now: FOMO makes me switch from Gemma to QWEN. But this time we go further: I want to turn it into a personal agent, available on my phone everywhere, all the time. With voice. All private, local, no data leaves the house.

To pull this off, we’re going with a complete stack that fits in the workstation:

  • vllm serving qwen3.6 27B, the exact version being cyankiwi/Qwen3.6-27B-AWQ-BF16-INT4
  • open-webui for a clean and configurable chat interface
  • STT and TTS to chat with Qwen smoothly.
    • STT: Speaches (faster whisper) locked to FR
    • TTS: Kokoro
  • WebSearch SearXNG for web searches without external APIs.

All of it reachable only through my private tailscale network.

Each AI building block is served via an openai-compliant API, for maximum interconnectivity.

A quick look at open-webui:

open-webui preview

Performance

First simple case: web search and analysis.

Performance:

Engine 000: Avg prompt throughput: 531.9 tokens/s, Avg generation throughput: 120.5 tokens/s, Running: 1 reqs, Waiting: 0 reqs,

Demo of an audio conversation:

It works, the UI responds well and it’s pretty fluid. Sure it’s slow, but there’s got to be a way to optimize that. A solid v1.

Agentic coding

For this test, we’ll use an open source project: opencode.

https://opencode.ai/

For those who don’t know what it is: it’s kind of the equivalent of claude code for anthropic, and codex for openai. Software that fully assists you in understanding, managing, and evolving a codebase.

We plug it into our local llm via a simple configuration:

{
  "$schema": "https://opencode.ai/config.json",
  "model": "vllm/qwen3.6-27b",
  "small_model": "vllm/qwen3.6-27b",
  "provider": {
    "vllm": {
      "npm": "@ai-sdk/openai-compatible",
      "name": "vLLM (local)",
      "options": {
        "baseURL": "https://my.endpoint.com"
      },
      "models": {
        "qwen3.6-27b": {
          "name": "Qwen 3.6 27B"
        }
      }
    }
  }
}

The interface looks like this:

opencode interface

Everything is set up, we’re good. Now, what are we going to make it code to test its performance?

First test: tic-tac-toe.

Prompt:

Let's build a well known game: tictactoe.
The goal is quite simple: a small React App, Typescript, Tailwind, served with a local dev server. 2 players can play the game locally on the same computer. Rules are the game rules, you know them.
Simple yet beautiful design, keep it simple and UX friendly.
Plan and go !

One-shot:

Code available here:

https://github.com/ademoverflow/tictactoe-qwen/

What opencode did:

Summary: Tic-Tac-Toe Build

1. Project Scaffolding
- Ran npm create vite@latest . -- --template react-ts to scaffold a Vite + React + TypeScript project in the existing empty git repo
- Installed all dependencies with npm install
- Added Tailwind CSS 4 via npm install -D tailwindcss @tailwindcss/vite
2. Configuration
- vite.config.ts — Added @tailwindcss/vite plugin alongside the React plugin
- index.html — Updated <title> from "tictactoe" to "Tic-Tac-Toe"
- Cleaned up default template assets (App.css, hero.png, react.svg, vite.svg, favicon.svg)
3. Styles — src/index.css
- Replaced the Vite template CSS entirely
- Imported Tailwind via @import "tailwindcss"
- Defined 4 custom @keyframes:
- cell-pop — scale + fade animation for marks appearing
- glow — pulsing box-shadow for winning cells
- fade-in — slide-up + fade for result banners
- shimmer — (available for future use)
- Mapped keyframes to utility classes (.cell-pop, .cell-glow, .fade-in)
4. Game Logic — src/App.tsx
- Win detection — checkWinner() iterates 8 possible lines (3 rows, 3 cols, 2 diagonals)
- Draw detection — isDraw() checks if all 9 cells are filled
- AI (minimax with alpha-beta pruning) — getBestMove() evaluates all possible moves recursively, making the computer unbeatable
- Game state — board (9 cells), xIsNext, mode (2p / ai), status (playing / won / draw), winningLine, thinking flag
- AI move trigger — useEffect fires on AI's turn with a 400ms delay for natural feel
- Components — CellComponent renders each cell with SVG X/O, hover states, and winning glow
5. UI Design
- Gradient background (slate-100 → slate-200), centered layout
- Mode toggle buttons (pill-shaped, indigo active state)
- Turn indicator with animated dots during AI thinking
- 3×3 grid with rounded cells, shadow, hover scale effect
- X in indigo, O in rose/red
- Winning cells get a pulsing glow animation
- Result banner with fade-in animation
- "New Game" button after game ends, "Reset" button during play
6. Verification
- npx tsc --noEmit — zero TypeScript errors
- npm run dev — Vite dev server started successfully on http://localhost:5173/

Functional, no issues, no bugs.

Second test: a real app

That’ll be the topic of the next article.

Until then, patience.

See you next time,

Adem.