Local chat on CachyOS: Qwen3.8-27B on 16 GB

local chat on CachyOS with Qwen3.8-27B AI models
Local chat on CachyOS: Qwen3.8-27B Uncensored IQ4_XS in Ollama and Open WebUI on a 16 GB RTX. How 27B fits, Jinx Modes and SearXNG.

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 ps shows 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-vulkan and 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.

PartRole
Ollama + model jinxRuns on the GPU. In chat it is Jinx (Ollama rejects Cyrillic model names).
Open WebUIBrowser chat on port 8080.
SearXNGSearch for the globe button, localhost only, port 8888.
Jinx launcherShows 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

AddressMeaning
127.0.0.1This machine. Ollama and SearXNG listen only here.
http://127.0.0.1:8080Chat on this PC.
http://<LAN-IP>:8080Chat from a phone/laptop on the home network.
http://127.0.0.1:11434Ollama API (local only).
http://127.0.0.1:8888SearXNG (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_LENGTH is the service ceiling. The model window is set in the Modelfile (num_ctx 8192). Do not raise num_ctx much on 16 GB.
  • OLLAMA_GPU_OVERHEAD keeps ~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

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_PARAMS has no think: true: the Jinx Modes filter owns that.
  • .env values apply when the container is created; history lives in the open-webui volume — 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

  1. ollama list shows jinx.
  2. A short question gets an answer; nvidia-smi shows used VRAM; ollama ps shows the model on GPU (some layers may be in RAM).
  3. “Hi” answers quickly (no long think); a hard technical question may turn thinking on.
  4. The globe returns snippets; tech queries favor docs/GitHub over currency converters.
  5. 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.

Categories: AI models Docker Apps & services
Subscribe to new posts (RSS)

No email, no trackers — just the update feed.

Also available in Russian

Rate this article
Leave a comment