- How 27B fits in 16 GB
- Why CachyOS
- What this local chat on CachyOS gives you
- Addresses
- 1. GPU and packages
- 2. Ollama service
- 3. Weights and the jinx model
- 4. SearXNG search
- 5. Open WebUI chat
- 5.1. Environment
- 5.2. SearXNG client patch (SafeSearch)
- 5.3. Start the container
- 5.4. Modes filter (Jinx Modes)
- 6. On/off launcher
- Checks: local chat works
- What this setup deliberately skips
A local chat on one PC: the model runs on the GPU, the window opens in the browser, and search goes through local SearXNG. There is no separate agent (Telegram bot, external orchestrator): Open WebUI talks to Ollama directly. That is a practical local chat without the cloud.
Verified on CachyOS, NVIDIA GeForce RTX 5060 Ti 16 GB, Ollama 0.40.6 (Vulkan). Model: Qwen3.8-27B Uncensored, quantization IQ4_XS. You need Ollama 0.40+: in the GGUF the architecture is named qwen35.
An earlier version of this guide used Qwen3-14B Q5_K_M. It was a workable minimum, but answers were clearly weaker. The canonical local chat is now Qwen3.8-27B on the same 16 GB card. If you already built the stack: swap weights and Modelfile and add the modes filter — you can leave Docker chat and SearXNG as they are.
Removing refusals does not cancel the law. Do not ask for text that harms people, helps fraud, or involves minors.
How 27B fits in 16 GB
On paper a “full” 27B at a thick quant does not fit 16 GB. In the Qwen3.8-27B on clore.ai guide, Q6_K pointed at 32 GB cards. At home on an RTX 5060 Ti 16 GB it worked through four deliberate compromises — not magic. Broader hardware options are in the local AI hardware guide.
- IQ4_XS quantization instead of Q5/Q6/Q8. The weight file is about 14.3 GiB. Quality is clearly above the old 14B Q5_K_M; speed and VRAM headroom beat thicker quants of the same 27B.
- 8192-token context (
num_ctx), not 16384 and never 64k. KV cache grows with window length: on 16 GB extra context burns the card faster than long history helps. - A tail of layers in RAM. While the model is loaded, the GPU is nearly full (~15 GB); Ollama keeps a small tail in system RAM.
ollama psshows GPU plus partial offload. - No Hermes and no “think always”. A separate agent with a huge system prompt and a 64k window breaks both VRAM and answer quality on this card with 27B. The Jinx Modes filter turns thinking on only where it helps (tech/research); everyday chat and “write a school text” stay short.
Bottom line: on 16 GB you get a strong home 27B chat, not the thickest quant and not the long context of a rented 32–96 GB box. For gaming, turn the stack off with the launcher — the card is almost fully occupied.
Why CachyOS
I really like CachyOS. It is Arch Linux focused on speed: optimized kernel and packages for modern CPUs, NVIDIA without a circus, rolling updates — fresh drivers and Ollama with the system.
- Fast kernel and packages for modern CPUs.
- A comfortable Arch install without building everything by hand.
- Decent NVIDIA support: this guide uses
ollama-vulkanand the module for the current CachyOS kernel. - Rolling release: you do not wait for the next distro version.
When the previous revision was written, CachyOS led DistroWatch 12-month page hits — not install counts, but interest in the distro pages.
What this local chat on CachyOS gives you
In short, this local chat on CachyOS is Ollama with the jinx model, browser Open WebUI and SearXNG on one machine. Below is how to build that local chat and fit Qwen3.8-27B into 16 GB.
| Part | Role |
|---|---|
| Ollama + model jinx | Runs on the GPU. In chat it is Jinx (Ollama rejects Cyrillic model names). |
| Open WebUI | Browser chat on port 8080. |
| SearXNG | Search for the globe button, localhost only, port 8888. |
| Jinx launcher | Shows whether the stack is up and stops or starts everything. |
Model context is 8192 tokens, answer cap 6144. Thinking is not always on: the Jinx Modes filter picks chat / school / tech / research and so on. Search is on by default in new chats; for everyday and school “write a text” prompts the filter may turn it off.
Weights load after the first message and unload after about five minutes of silence. That keeps this local chat on CachyOS usable between gaming sessions: the local chat wakes on demand.
Addresses
| Address | Meaning |
|---|---|
127.0.0.1 | This machine. Ollama and SearXNG listen only here. |
http://127.0.0.1:8080 | Chat on this PC. |
http://<LAN-IP>:8080 | Chat from a phone/laptop on the home network. |
http://127.0.0.1:11434 | Ollama API (local only). |
http://127.0.0.1:8888 | SearXNG (local only; from a phone, search still goes through Open WebUI on the PC). |
Paths in this guide: ~/.config/jinx and ~/.local/share/jinx. On an existing box the folder names may differ — services and parameters matter.
1. GPU and packages
nvidia-smi should show the card and memory size. On CachyOS the open NVIDIA module usually needs something like linux-cachyos-nvidia-open for the current kernel.
sudo pacman -S ollama ollama-vulkan docker python-pyqt6 kdialog nvidia-utils
sudo systemctl enable --now docker
sudo usermod -aG docker "$USER"
After adding the docker group, log in again. The backend here is Vulkan (ollama-vulkan); a separate CUDA package is not required.
2. Ollama service
File /etc/systemd/system/ollama.service.d/context.conf:
[Service]
Environment="OLLAMA_KV_CACHE_TYPE=q8_0"
Environment="OLLAMA_FLASH_ATTENTION=1"
Environment="OLLAMA_CONTEXT_LENGTH=65536"
Environment="OLLAMA_GPU_OVERHEAD=1610612736"
OLLAMA_CONTEXT_LENGTHis the service ceiling. The model window is set in the Modelfile (num_ctx 8192). Do not raisenum_ctxmuch on 16 GB.OLLAMA_GPU_OVERHEADkeeps ~1.5 GiB for the desktop.
sudo mkdir -p /etc/systemd/system/ollama.service.d
# положите context.conf, затем:
sudo systemctl daemon-reload
sudo systemctl enable --now ollama
The service listens on 127.0.0.1:11434.
3. Weights and the jinx model
File: Qwen3.8-27B-Uncensored-IQ4_XS.gguf (~14.3 GiB), repository JonathanColetti/Qwen3.8-27B-Uncensored-GGUF on Hugging Face. IQ4_XS balances quality and speed on 16 GB.
mkdir -p ~/.local/share/jinx ~/.config/jinx
curl -L --fail -o ~/.local/share/jinx/Qwen3.8-27B-Uncensored-IQ4_XS.gguf \
https://huggingface.co/JonathanColetti/Qwen3.8-27B-Uncensored-GGUF/resolve/main/Qwen3.8-27B-Uncensored-IQ4_XS.gguf
File ~/.config/jinx/Modelfile (FROM needs the full path to the GGUF):
FROM /FULL/PATH/TO/HOME/.local/share/jinx/Qwen3.8-27B-Uncensored-IQ4_XS.gguf
SYSTEM """You are Jinx — a home assistant on this PC. Answer in clear language.
Each turn may include a [Mode: …] tag — follow it strictly (it overrides general habits).
Modes:
• chat — short and human, no lectures or lists for their own sake.
• quick — only the point, 1–5 sentences.
• school — finished text in simple sentences, as a pupil would hand in; not a teaching guide. Math: short steps and the answer.
• tech — point → exact steps/commands → short “if it fails”. Do not invent versions, flags, paths, APIs or command output; if data is missing, say what to check. Search snippets beat guesses.
• research — conclusion first, then arguments and caveats; mark factual gaps; lean on snippets when present.
• general — clear answer; length ≈ question complexity.
Shared rules:
• Keep inner reasoning short, then always give the user an answer (never end on reasoning alone or “I’ll answer now”).
• If corrected — rewrite the needed part, do not repeat the whole old reply.
• Do not repeat lines or print empty numbers (“1.” with no text).
• Code, commands, paths, URLs — as-is, in code blocks.
• Do not pretend you opened a site without a snippet. An honest gap beats a pretty invention."""
PARAMETER temperature 0.6
PARAMETER top_p 0.95
PARAMETER top_k 20
PARAMETER repeat_penalty 1.15
PARAMETER repeat_last_n 256
PARAMETER num_predict 6144
PARAMETER num_ctx 8192
Do not set a separate TEMPLATE: it comes from the GGUF. Do not inject empty <think></think> — that forces thinking off. num_predict 6144 keeps long reasoning from eating the whole budget before the answer text.
ollama create jinx -f ~/.config/jinx/Modelfile
ollama list
4. SearXNG search
Directory: ~/.config/jinx/searxng/settings.yml.
use_default_settings: true
server:
secret_key: "ЗАМЕНИТЕ"
limiter: false
image_proxy: false
search:
safe_search: 0
default_lang: "all"
formats:
- html
- json
ui:
default_locale: "ru"
# Техдокументация выше; витрины-конвертеры и Pinterest — ниже или вырезаются.
# Школа и быт остаются на обычном general (google/ddg и т.п.).
hostnames:
high_priority:
- '(.*\.)?wikipedia\.org$'
- '(.*\.)?github\.com$'
- '(.*\.)?gitlab\.com$'
- '(.*\.)?stackoverflow\.com$'
- '(.*\.)?stackexchange\.com$'
- '(.*\.)?askubuntu\.com$'
- '(.*\.)?superuser\.com$'
- '(.*\.)?developer\.mozilla\.org$'
- '(.*\.)?docs\.python\.org$'
- '(.*\.)?docs\.ollama\.com$'
- '(.*\.)?readthedocs\.io$'
- '(.*\.)?readthedocs\.org$'
- '(.*\.)?archlinux\.org$'
- '(.*\.)?wiki\.archlinux\.org$'
- '(.*\.)?kernel\.org$'
- '(.*\.)?learn\.microsoft\.com$'
- '(.*\.)?ietf\.org$'
- '(.*\.)?rfc-editor\.org$'
- '(.*\.)?arxiv\.org$'
- '(.*\.)?npmjs\.com$'
- '(.*\.)?pypi\.org$'
- '(.*\.)?crates\.io$'
- '(.*\.)?pkg\.go\.dev$'
- '(.*\.)?go\.dev$'
low_priority:
- '(.*\.)?facebook\.com$'
- '(.*\.)?instagram\.com$'
- '(.*\.)?tiktok\.com$'
- '(.*\.)?quora\.com$'
- '(.*\.)?baidu\.com$'
- '(.*\.)?coinbase\.com$'
- '(.*\.)?binance\.com$'
remove:
- '(.*\.)?pinterest\..*'
- '(.*\.)?investing\.com$'
- '(.*\.)?alegsaonline\.com$'
engines:
- name: wikipedia
engine: wikipedia
shortcut: wp
display_type: ["infobox", "list"]
categories: [general]
- name: github
engine: github
shortcut: gh
categories: [it, general]
- name: stackoverflow
engine: stackexchange
shortcut: st
api_site: 'stackoverflow'
categories: [it, q&a, general]
- name: askubuntu
engine: stackexchange
shortcut: ubuntu
api_site: 'askubuntu'
categories: [it, q&a, general]
openssl rand -hex 32 # put the value instead of REPLACE / ЗАМЕНИТЕ
limiter: false lets chat on this PC query search. json is required by Open WebUI. default_lang: "all" does not clamp results to one language.
Do not attach the whole SearXNG it tab to every chat query: MDN noise often hurts everyday answers. hostnames plus GitHub/SO in general is enough.
mkdir -p ~/.config/jinx/searxng
# положите settings.yml, затем:
docker run -d \
--name searxng \
--network host \
--restart unless-stopped \
-e SEARXNG_PORT=8888 \
-e GRANIAN_HOST=127.0.0.1 \
-v "$HOME/.config/jinx/searxng:/etc/searxng" \
searxng/searxng:latest
GRANIAN_HOST=127.0.0.1 keeps search off the LAN.
5. Open WebUI chat
5.1. Environment
mkdir -p ~/.config/jinx/open-webui-patches
install -m 600 /dev/null ~/.config/jinx/open-webui.env
File ~/.config/jinx/open-webui.env:
DEFAULT_MODEL_PARAMS={"function_calling":"legacy","repeat_penalty":1.15,"num_predict":6144}
DEFAULT_MODEL_METADATA={"capabilities":{"web_search":true},"defaultFeatureIds":["web_search"]}
ENABLE_OLLAMA_API=true
OLLAMA_BASE_URL=http://127.0.0.1:11434
DEFAULT_MODELS=jinx
ENABLE_OPENAI_API=false
DEFAULT_LOCALE=en
ENABLE_WEB_SEARCH=true
WEB_SEARCH_ENGINE=searxng
SEARXNG_QUERY_URL=http://127.0.0.1:8888/search?q=<query>&format=json&language=all
SEARXNG_LANGUAGE=all
WEB_SEARCH_RESULT_COUNT=8
BYPASS_WEB_SEARCH_WEB_LOADER=true
BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL=true
function_calling":"legacy"— otherwise built-in tools on a local model loop easily. Search uses the globe.BYPASS_WEB_SEARCH_WEB_LOADER— do not download every full page; snippets are enough.DEFAULT_MODEL_PARAMShas nothink: true: the Jinx Modes filter owns that.- .env values apply when the container is created; history lives in the
open-webuivolume — do not delete it.
5.2. SearXNG client patch (SafeSearch)
Open WebUI defaults to SafeSearch=1 toward SearXNG. You need default safesearch: '0'.
# вытащить файл из образа и поправить default
cid=$(docker create ghcr.io/open-webui/open-webui:main)
docker cp "$cid":/app/backend/open_webui/retrieval/web/searxng.py \
~/.config/jinx/open-webui-patches/searxng.py
docker rm "$cid"
# в файле найти safesearch / kwargs.get('safesearch' …) и поставить default '0'
In the file find safesearch / kwargs.get('safesearch' …) and set default '0'. Mount that file into the container as below.
5.3. Start the container
docker run -d \
--name open-webui \
--network host \
--restart unless-stopped \
--env-file "$HOME/.config/jinx/open-webui.env" \
-v open-webui:/app/backend/data \
-v "$HOME/.config/jinx/open-webui-patches/searxng.py:/app/backend/open_webui/retrieval/web/searxng.py:ro" \
ghcr.io/open-webui/open-webui:main
On the PC: http://127.0.0.1:8080. From another device at home: http://<LAN-IP>:8080. First account is admin. Model: jinx. Search is the globe. Chat and search containers do not use the GPU.
5.4. Modes filter (Jinx Modes)
Without a filter you would have to keep thinking always on or always off. The filter picks chat / quick / school / tech / research / general, toggles think, disables web search for chat and school “write an essay” prompts, and adds a [Mode: …] mark.
Install: Workspace → Functions → New Function, type Filter, paste the code below, enable Active and Global. Force in chat: режим school, режим tech, подумай, без рассуждений, кратко (or the English equivalents in the filter). After edits, sometimes restart the open-webui container.
"""
title: Jinx Modes
author: kiberlis
version: 1.0.0
required_open_webui_version: 0.4.0
description: Режим чата, think и web search для модели jinx.
"""
from __future__ import annotations
import re
from typing import Optional
from pydantic import BaseModel, Field
MODE_MARK = re.compile(r"^\s*режим\s+(\w+)\s*$", re.I | re.U)
THINK_ON = re.compile(r"^\s*(подумай|с рассуждением|think)\s*$", re.I | re.U)
THINK_OFF = re.compile(r"^\s*(без рассуждений|без think|no.?think)\s*$", re.I | re.U)
SHORT = re.compile(r"^\s*(кратко|коротко)\s*$", re.I | re.U)
SCHOOL_WRITE = re.compile(
r"(сочинен|изложен|напиши\s+текст|для\s+\d+\s*класс|школьн)",
re.I | re.U,
)
TECH = re.compile(
r"(ошибк|traceback|exception|docker|systemd|nvidia|cuda|ollama|"
r"конфиг|config|api|github|stack\s*overflow|установи|команду|"
r"pacman|apt |ssh |yaml|json|python|bash|скрипт)",
re.I | re.U,
)
RESEARCH = re.compile(
r"(сравни|обзор|почему|разбер|исследу|проанализир|что лучше|"
r"плюсы и минусы|research)",
re.I | re.U,
)
QUICK = re.compile(r"^(да|нет|ок|спасибо|привет|здравствуй)\b", re.I | re.U)
class Filter:
class Valves(BaseModel):
priority: int = Field(default=0)
def __init__(self):
self.valves = self.Valves()
def _last_user(self, messages: list) -> str:
for m in reversed(messages or []):
if m.get("role") == "user":
c = m.get("content")
if isinstance(c, str):
return c
if isinstance(c, list):
parts = [
x.get("text", "")
for x in c
if isinstance(x, dict) and x.get("type") == "text"
]
return "\n".join(parts)
return ""
def _pick_mode(self, text: str, forced: Optional[str]) -> str:
if forced:
return forced
t = (text or "").strip()
if SHORT.search(t) or (len(t) < 40 and QUICK.search(t)):
return "quick"
if SCHOOL_WRITE.search(t):
return "school"
if TECH.search(t):
return "tech"
if RESEARCH.search(t) or len(t) > 400:
return "research"
if len(t) < 120:
return "chat"
return "general"
def _think_for(self, mode: str, force_on: bool, force_off: bool) -> bool:
if force_off:
return False
if force_on:
return True
return mode in ("tech", "research")
def _search_for(self, mode: str, text: str) -> bool:
if mode in ("chat", "quick"):
return False
if mode == "school" and SCHOOL_WRITE.search(text or ""):
return False
return mode in ("tech", "research", "general", "school")
def inlet(self, body: dict, __user__: Optional[dict] = None) -> dict:
messages = list(body.get("messages") or [])
forced = None
force_think_on = False
force_think_off = False
cleaned = []
for m in messages:
if m.get("role") != "user" or not isinstance(m.get("content"), str):
cleaned.append(m)
continue
raw = m["content"]
lines = []
for line in raw.splitlines():
mm = MODE_MARK.match(line)
if mm:
forced = mm.group(1).lower()
if forced == "подумай":
force_think_on = True
forced = None
continue
if THINK_ON.match(line):
force_think_on = True
continue
if THINK_OFF.match(line):
force_think_off = True
continue
if SHORT.match(line) and len(raw.strip()) < 20:
forced = forced or "quick"
continue
lines.append(line)
nm = dict(m)
nm["content"] = "\n".join(lines).strip() or raw
cleaned.append(nm)
text = self._last_user(cleaned)
mode = self._pick_mode(text, forced)
if mode not in ("chat", "quick", "school", "tech", "research", "general"):
mode = "general"
think = self._think_for(mode, force_think_on, force_think_off)
want_search = self._search_for(mode, text)
mark = f"[Режим: {mode}]"
sys_msg = {
"role": "system",
"content": (
f"{mark} Следуй правилам этого режима из SYSTEM модели jinx. "
f"Рассуждения (think): {'включены' if think else 'выключены'}."
),
}
# Put mode mark near the end so it wins over older system fluff.
out_msgs = []
inserted = False
for m in cleaned:
out_msgs.append(m)
if m.get("role") == "user" and not inserted:
# insert system just before last user later
pass
# Rebuild: all non-trailing-user, then sys_msg, then last user
if cleaned and cleaned[-1].get("role") == "user":
out_msgs = cleaned[:-1] + [sys_msg, cleaned[-1]]
else:
out_msgs = cleaned + [sys_msg]
body = dict(body)
body["messages"] = out_msgs
opts = dict(body.get("options") or {})
opts["think"] = think
body["options"] = opts
features = dict(body.get("features") or {})
if not want_search:
features["web_search"] = False
body["features"] = features
return body
6. On/off launcher
You need python-pyqt6 and kdialog. Turning off stops containers and Ollama. Turning on brings services back without loading weights into VRAM.
Save as ~/.local/bin/jinx-toggle, chmod 755:
#!/usr/bin/env python3
"""Show whether Jinx is running and turn the whole local stack on or off."""
import os
import subprocess
import tempfile
from PyQt6.QtCore import Qt, QThread, QTimer, pyqtSignal
from PyQt6.QtGui import QFont
from PyQt6.QtWidgets import (
QApplication,
QHBoxLayout,
QLabel,
QPushButton,
QVBoxLayout,
QWidget,
)
OLLAMA = "ollama"
OFF_LABEL = {"ollama": "stopped", "chat": "stopped", "search": "stopped"}
def container_running(name):
result = subprocess.run(
["docker", "inspect", "--format", "{{.State.Running}}", name],
capture_output=True,
text=True,
)
return result.returncode == 0 and result.stdout.strip() == "true"
def service_state():
active = subprocess.run(
["systemctl", "is-active", "--quiet", OLLAMA]
).returncode == 0
return {
"ollama": active,
"chat": container_running("open-webui"),
"search": container_running("searxng"),
}
def stack_on(state):
return state["ollama"] or state["chat"] or state["search"]
class SwitchWorker(QThread):
finished_ok = pyqtSignal(bool, str)
def __init__(self, turn_off):
super().__init__()
self.turn_off = turn_off
def run(self):
ask = tempfile.NamedTemporaryFile("w", delete=False)
ask.write(
"#!/bin/sh\n"
"exec /usr/bin/kdialog --password "
'"Password to turn Jinx on or off"\n'
)
ask.close()
os.chmod(ask.name, 0o700)
env = os.environ.copy()
env["SUDO_ASKPASS"] = ask.name
env.setdefault("DISPLAY", ":0")
env.setdefault("WAYLAND_DISPLAY", "wayland-1")
env.setdefault("XDG_RUNTIME_DIR", f"/run/user/{os.getuid()}")
try:
if subprocess.run(["sudo", "-A", "-v"], env=env).returncode != 0:
self.finished_ok.emit(False, "Password rejected. Nothing changed.")
return
if self.turn_off:
ok, message = self._stop(env)
else:
ok, message = self._start(env)
self.finished_ok.emit(ok, message)
finally:
os.unlink(ask.name)
def _systemctl(self, env, *args):
return subprocess.run(
["sudo", "-A", "/usr/bin/systemctl", *args],
env=env,
capture_output=True,
text=True,
)
def _stop(self, env):
subprocess.run(
["docker", "stop", "open-webui", "searxng", "hermes"],
capture_output=True,
text=True,
)
disabled = self._systemctl(env, "disable", OLLAMA)
stopped = self._systemctl(env, "stop", OLLAMA)
if disabled.returncode != 0 or stopped.returncode != 0:
return False, "Could not stop Ollama."
return True, "Ollama, chat and search are stopped. The GPU is free."
def _start(self, env):
enabled = self._systemctl(env, "enable", OLLAMA)
started = self._systemctl(env, "start", OLLAMA)
chat = subprocess.run(
["docker", "start", "searxng", "open-webui"],
capture_output=True,
text=True,
)
if enabled.returncode != 0 or started.returncode != 0 or chat.returncode != 0:
return False, "Could not start the services."
return True, "Chat is available again. The GPU is used only after a chat message."
class JinxWindow(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle("Jinx")
self.setFixedWidth(440)
self.worker = None
self.status = QLabel("Checking…")
self.status.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.status.setFont(QFont("Sans Serif", 26, QFont.Weight.Bold))
self.rows = {}
details = QVBoxLayout()
details.setSpacing(6)
for key, title in (("ollama", "Ollama"), ("chat", "Chat"), ("search", "Search")):
row = QHBoxLayout()
name = QLabel(title)
name.setFont(QFont("Sans Serif", 12))
value = QLabel("—")
value.setFont(QFont("Sans Serif", 12, QFont.Weight.DemiBold))
value.setAlignment(Qt.AlignmentFlag.AlignRight)
row.addWidget(name)
row.addStretch(1)
row.addWidget(value)
details.addLayout(row)
self.rows[key] = value
self.note = QLabel("")
self.note.setWordWrap(True)
self.note.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.button = QPushButton("Please wait")
self.button.setMinimumHeight(44)
self.button.clicked.connect(self.toggle)
layout = QVBoxLayout(self)
layout.setContentsMargins(28, 24, 28, 24)
layout.setSpacing(16)
layout.addWidget(self.status)
layout.addLayout(details)
layout.addWidget(self.note)
layout.addWidget(self.button)
self.timer = QTimer(self)
self.timer.setInterval(2000)
self.timer.timeout.connect(self.refresh)
self.refresh()
self.timer.start()
def refresh(self):
if self.worker is not None:
return
state = service_state()
on = stack_on(state)
complete = all(state.values()) if on else not any(state.values())
if on and complete:
self.status.setText("On")
self.status.setStyleSheet("color: #1b7f3a;")
self.button.setText("Turn off")
self.note.setText("The GPU is used only after a chat message.")
elif on:
self.status.setText("Partially on")
self.status.setStyleSheet("color: #9a6700;")
self.button.setText("Turn everything off")
self.note.setText("Some services are still running. The button will stop all of them.")
else:
self.status.setText("Off")
self.status.setStyleSheet("color: #8a8f98;")
self.button.setText("Turn on")
self.note.setText("Ollama, chat and search are stopped. The GPU is free.")
for key, value in state.items():
label = self.rows[key]
label.setText("running" if value else OFF_LABEL[key])
label.setStyleSheet("color: #1b7f3a;" if value else "color: #8a8f98;")
self.button.setEnabled(True)
def toggle(self):
state = service_state()
self.button.setEnabled(False)
self.timer.stop()
self.worker = SwitchWorker(stack_on(state))
self.worker.finished_ok.connect(self._finished)
self.worker.start()
def _finished(self, ok, message):
self.worker = None
self.note.setText(message)
if not ok:
self.status.setText("Unchanged")
self.status.setStyleSheet("color: #9a6700;")
self.refresh()
self.timer.start()
def main():
app = QApplication([])
window = JinxWindow()
window.show()
raise SystemExit(app.exec())
if __name__ == "__main__":
main()
Launchers: ~/.local/share/applications/jinx-toggle.desktop and optionally a desktop file.
[Desktop Entry]
Type=Application
Name=Jinx
Comment=Show whether Jinx is on and toggle Ollama, chat and search
Exec=/home/USER/.local/bin/jinx-toggle
Icon=cpu
Terminal=false
Categories=Utility;
StartupNotify=false
In Exec replace USER with your account name. On the desktop: chmod +x.
Checks: local chat works
- ollama list shows jinx.
- A short question gets an answer; nvidia-smi shows used VRAM; ollama ps shows the model on GPU (some layers may be in RAM).
- “Hi” answers quickly (no long think); a hard technical question may turn thinking on.
- The globe returns snippets; tech queries favor docs/GitHub over currency converters.
- The Jinx launcher shows On / Off. After off, port 8080 is down and the GPU is free.
What this setup deliberately skips
- A separate agent with a huge system prompt and a 64k window.
- Forced think: true on every message.
- Downloading full pages on every search.
- Public SearXNG on the internet (127.0.0.1 only).
Bottom line: a local chat on CachyOS with Qwen3.8-27B on 16 GB works if you accept IQ4_XS and an 8192 window. Full up-to-date guide: en.kiberlis.ru.
No email, no trackers — just the update feed.








