Skip to content
mistercyber

mistercyber

Builder · AI · Linux · Games

AI 16 min read

I Turned My CachyOS PC Into a Practical AI Coding Workstation

How I combined CachyOS, OpenCode, llama.cpp, a local 9B model, OpenRouter, MCP tools and strict Git checkpoints without turning my gaming PC into an AI-only server.

16 min read words
I Turned My CachyOS PC Into a Practical AI Coding Workstation

I Turned My CachyOS PC Into a Practical AI Coding Workstation

I moved my main desktop from Windows 11 to CachyOS because I wanted one machine for gaming, Linux experimentation, software development and the increasingly odd collection of AI tools I was testing. I did not want a dedicated inference server humming in a corner. I wanted my normal PC to remain a normal PC, just one that could also run a local language model and hand harder coding jobs to cloud models when necessary.

The result is a hybrid setup built around OpenCode. Routine work can stay on a local llama.cpp server. More demanding work can go through OpenRouter. Specialised agents split research, planning, implementation and review. MCP tools provide documentation and GitHub access. Git provides the recovery points when an agent confidently does the wrong thing.

That last part is not hypothetical in the broad sense: language models make incorrect assumptions. I am not going to decorate this article with a dramatic invented story about one deleting my project. The real lesson is less cinematic and more useful. AI-generated code still needs scope, tests, diff review and a human who knows when to stop.

This is not a claim that I found the perfect model, distribution or autonomous workflow. It is the configuration I arrived at after treating model choice, context, cost and tool permissions as engineering decisions rather than one big “AI coding” switch.

The PC still has to be a PC

The machine is fairly capable, but it is not a datacentre disguised as a desktop:

  • AMD Ryzen 7 5700X
  • NVIDIA RTX 5070 Ti with 16 GB of VRAM
  • 32 GB RAM
  • 1 TB Samsung 970 EVO Plus
  • CachyOS on Btrfs with systemd-boot
  • KDE Plasma on Wayland
  • Fish shell in Ghostty

The 16 GB of VRAM is enough to make local inference useful, but it sets a real boundary. Model weights, the KV cache and runtime overhead all need memory. A larger context window is not a free number in a configuration file; it consumes resources. A bigger quantised model may fit only with compromises, and a model that technically loads is not automatically pleasant to use.

The other constraint is gaming. The GPU cannot be treated as permanently reserved for llama.cpp, so my Fish helpers include ai-coding to start the selected model and ai-gaming to stop the server and release VRAM. Only one local model is loaded at a time. That is not elaborate orchestration, but it reflects what this computer actually is: a daily-use desktop shared by several workloads.

CachyOS suits this arrangement because it gives me an Arch-based environment with modern packages and a performance-oriented bias without preventing me from changing almost anything. KDE and Wayland provide the desktop; Fish and Ghostty provide the environment where I spend most development time. None of those makes a model more intelligent. They make the workstation comfortable to operate and inspect.

There is a trade-off in running a rolling, highly configurable Linux system. Current packages are useful for recent NVIDIA hardware and development tools, but system changes deserve recovery points too. Btrfs is helpful, but snapshots are not permission to apply mysterious commands. The same rule I use for repositories applies to the operating system: inspect first, keep the mission bounded and know how to recover.

Ghostty is not central to the AI architecture, but terminal ergonomics matter when OpenCode, Git, test runners, server logs and nvidia-smi are where the work happens. Readable output and a keyboard-driven workflow save more friction than adding another decorative dashboard.

OpenCode became the control centre

I chose OpenCode because it fits the way I already work: in a terminal, inside a project, with Git and project-specific commands close at hand. It is provider-agnostic enough for me to put a local OpenAI-compatible endpoint beside OpenRouter instead of organising my workflow around one vendor.

The useful distinction is between an AI that can emit code in a chat box and an agent that can inspect a repository, search files, consult current documentation, edit a constrained set of files, run a test and report the result. Tool access changes the job completely. It also changes the risk.

I do not use OpenCode as an autonomous programmer. I use it as an engineering assistant with a loop that developed from practical use:

THINK → SPECIFY → PLAN → DELEGATE → EXECUTE → VERIFY → REVIEW → FIX → COMMIT → REPEAT

THINK means understanding the problem before asking for edits. I want to know what currently owns the behaviour, whether the issue is reproducible and what facts are still missing.

SPECIFY turns an idea into observable acceptance criteria. “Clean this up” is not a specification. “Preserve the existing output, remove this duplicated branch and keep these tests passing” is at least reviewable.

PLAN is a cheap place to reject a bad interpretation. A useful plan names the likely files, explains the change and says how it will be verified. It does not need to be an essay.

DELEGATE means selecting a model, agent and permission boundary. Research does not need write access. A reviewer should not quietly become the builder. A Linux diagnostic agent should not begin changing system configuration because it found something interesting.

EXECUTE creates the smallest diff that should satisfy the specification. If the diff becomes too broad to understand, I stop rather than praising the agent for its initiative.

VERIFY means running deterministic project checks and reading their output. The model saying “all tests pass” is not evidence unless the tests actually ran and the exit status is visible.

REVIEW compares the diff with the original requirement. I look for accidental edits, invented APIs, unnecessary abstraction, changed error behaviour and sensitive values in logs.

FIX returns to the smallest failed stage. Sometimes the code is wrong. Sometimes the test exposes a bad assumption in the specification. Blindly changing the assertion to make green text appear is not fixing anything.

COMMIT creates a coherent recovery point after review. OpenCode does not get to decide that a plausible diff deserves to enter history.

REPEAT acknowledges that development is iterative. The point is not ceremony for a one-line typo. It is retaining control when an assistant can modify a repository much faster than I can read it.

The dangerous prompt is still the tempting one: “build the whole application”. It combines product decisions, architecture, implementation and acceptance into one uncontrolled mission. A bounded prompt is less exciting and far more useful: “Inspect this repository, identify the handler for this behaviour, propose a plan, change only the required files, run the relevant checks, show me the diff and stop.”

That is my definition of controlled vibe coding.

The local llama.cpp path

My default local provider in OpenCode is named llama-local. It points to a llama.cpp server listening only on loopback:

http://127.0.0.1:8080/v1

The everyday model is:

llama-local/ornith-9b-q8

It is configured for a 65,536-token context limit and an 8,192-token output limit. That describes the configured ceiling, not a promise that filling the entire context produces equally good reasoning. Long context can be useful, but irrelevant files and a sprawling conversation can make the signal worse. Context management is still part of the job.

The server uses llama.cpp with NVIDIA acceleration, Flash Attention and Q8 KV caches. It runs one slot and one model at a time. Vision projectors are disabled for coding to preserve VRAM. The launcher binds to 127.0.0.1:8080, restricts CORS to localhost and does not use an API key because it is intended for one user on one host. I would not expose that port to the network in this form.

llama.cpp presents OpenAI-compatible routes, which is the useful architectural trick. OpenCode does not need a one-off integration for every local runtime. It talks to a familiar /v1 API while llama.cpp handles the model and GPU underneath.

I can check the two relevant layers separately:

curl http://127.0.0.1:8080/health
curl http://127.0.0.1:8080/v1/models

Then I can inspect OpenCode's view:

opencode models llama-local
opencode debug config

That separation matters when something fails. If /v1/models does not answer, the problem is below OpenCode. If llama.cpp is healthy but the model is absent in OpenCode, the provider configuration is the better place to look. Debugging the stack one boundary at a time is much quicker than repeatedly changing everything.

What the local model is good for

I use the local model for everyday coding, tests, exploration and documentation. It is well suited to bounded, low-risk tasks: locating code, explaining a function, formatting, repetitive changes, small fixes and first-pass investigation. Local requests avoid per-token API charges and keep that request on my machine.

Privacy is an advantage, but only within limits. A local model does not send the prompt to an inference provider, yet OpenCode may still have other enabled tools capable of external communication. “The selected model is local” and “the entire session is offline” are different claims. My /local command explicitly pins Ornith and prohibits cloud and web use when that boundary matters.

Latency can feel direct because there is no remote provider round-trip, but I have not published measured timing, so I will not invent tokens-per-second figures. The practical advantage is predictability and control rather than a benchmark trophy.

What it is not good for

A quantised 9B model is not a frontier model in miniature. It can miss interactions across a large codebase, struggle with ambiguous architecture and produce convincing tool plans that do not match the repository. More context does not repair weak reasoning. Repeating a failed prompt five times can waste more time than escalating once.

I have other local models configured for experiments, including larger Qwen, Gemma, Devstral and DeepSeek variants, but “configured” or “stored on disk” does not mean “currently served”. I verify the active model through /v1/models rather than relying on a name in a config file.

The Fish commands make model operation deliberately boring:

llama-use ornith
llama-start
llama-status
llama-logs
ai-coding
ai-gaming

llama-use selects a model and stops the current server; it does not start the replacement until llama-start or ai-coding runs. That detail prevents me from assuming a model switch succeeded merely because I typed its alias.

OpenRouter is the external model layer

Local-first does not mean forcing every task through the local model. OpenRouter is my external layer because it provides one interface to multiple models and providers. My configuration includes an automatic route, a free route and named cloud options for cheap work, long-context fallback, architecture, review and difficult implementation.

The exact catalogue changes over time. At the time of this configuration it includes DeepSeek V4 Flash, GLM 5.3 Flash, Qwen 3.8 Flash, Claude Sonnet 5, GPT-5.3 Codex and Claude Opus 5. Those identifiers are configuration facts, not endorsements and not a promise that every model remains available under the same name.

OpenRouter also has provider-level routing concerns. Two endpoints serving the nominally same model can differ in price, latency, throughput, supported parameters and reliability. Its documented default provider selection favours stable endpoints, weights lower-priced candidates and retains fallbacks unless preferences override that behaviour. That is more nuanced than “pick the biggest model”.

I deliberately do not quote a table of token prices here. API pricing and availability change, and a stale price presented without a date is worse than no price. Before budgeting I check the live model catalogue. The durable facts are that input and output tokens can be priced differently, long prompts cost more, repeated context is billed again, retries add usage and multi-agent workflows multiply calls.

A long conversation can become expensive quietly. Every agent hand-off may include research, repository context, tool results and previous decisions. Asking a premium model to re-read all of that for a formatting change makes no sense. “Use the best model” is not a strategy unless cost and task difficulty are irrelevant.

Low, medium and high: routing by consequence

I use three levels as a decision aid. They are not benchmark categories, and the current automatic aliases do not prove that every request is classified perfectly.

Low

Low covers simple edits, formatting, straightforward refactors, repetitive work, basic investigation, small fixes and explanations where an error is cheap to notice. This is the local model's normal territory. A cheap cloud worker can also make sense if the local server is stopped or the task needs a capability it lacks.

Medium

Medium covers ordinary feature implementation, debugging, multi-file changes, moderate architecture and contained but reasoning-heavy review. I want a plan first, clearer acceptance criteria and stronger verification. Depending on the task, I may use a larger local experiment or one of the cloud fallbacks.

High

High is reserved for difficult debugging, unfamiliar architecture, major refactors, security-sensitive code and cases where being wrong will cost more time than the model call. A premium model can help analyse such work, but it does not receive automatic permission to implement or publish it. Higher capability should increase scrutiny, not remove it.

This works better than putting the most expensive model behind every request because most repository work is not equally difficult. File discovery and a dangerous migration do not deserve the same budget or permission boundary.

What automatic selection means here

My OpenRouter aliases auto-low through auto-max currently resolve to openrouter/auto through the auto-router plugin. That is what is configured. The broader low/medium/high policy is the design principle around it. A complete local classifier that inspects repository sensitivity and guarantees the ideal tier is something that could be developed further; I do not pretend it already exists.

Good automatic routing should be visible and overridable. It should select from allowed models, report the route and escalate when evidence justifies it. Verification failure is a useful signal: if a cheaper model cannot form a coherent plan, contradicts repository conventions or repeatedly fails tests without understanding why, move up. An ambiguous prompt should be clarified rather than merely sent to a more expensive model.

My documented escalation order is local Ornith, then a stronger local option, then cheap cloud, then strong cloud. Premium use requires explicit approval. There is also a retry limit: one materially different attempt is more useful than an endless loop of slight prompt variations.

Agents: separation of concerns, not a role-playing game

My OpenCode setup includes builder, researcher, planner, reviewer, debugger, Linux, general, verifier, fixer and economical-manager roles. The names matter less than their boundaries.

Researcher is not builder. Planner is not executor. Reviewer is not builder. Verifier is not fixer.

That separation reduces a common failure mode: the same agent makes an assumption, implements it and then reviews its own output using the same mistaken frame. A fresh reviewer can catch scope creep or missing cases. A read-only verifier can report failing commands without “helpfully” changing code to hide them.

It is still AI checking AI. It is not independent proof. Different agents may share model weaknesses, and hand-offs can lose nuance. More agents also mean more context, more tool calls, more tokens and more opportunities for one model to misunderstand another. A one-line change does not need a committee.

I use specialised roles when the task genuinely benefits from separation: research against authoritative sources, planning a cross-file change, independent review, Linux diagnostics or deterministic verification. For small work, direct execution followed by human diff review is simpler.

The economical-manager role reflects the routing principle: spend capability where it changes the outcome, not everywhere. The Linux agent has stricter rules because changing system configuration carries a different blast radius from editing a Markdown file. The builder is restricted to approved scope and must verify changes; it does not commit or push.

MCP turns text generation into tool-assisted work

Context7 and GitHub MCP are enabled globally in this setup. Blender and Godot are disabled globally; Godot can be enabled at project level when a game-development experiment actually needs it.

Context7 lets an agent query current library documentation instead of inventing an option that sounds right. GitHub MCP can inspect repository, issue and pull-request information and, where permissions allow, perform external actions. Project-specific servers can expose still more context.

This is the difference between “AI that writes code” and “AI that can investigate and operate”: the latter can read the project, fetch current documentation, interact with development systems, make a patch, run checks and report concrete results.

It is also why security cannot be an afterthought. An agent with shell and tool access may modify or delete files, change configuration, install packages, alter a repository or trigger an external operation. MCP credentials determine what the tool can reach. A mistaken assumption attached to a powerful tool is much more consequential than a mistaken paragraph.

I keep unrelated servers disabled because their schemas consume context and enlarge the permission surface. I do not add filesystem, database, Docker, browser or system-control MCP globally merely because it exists. Least privilege is less glamorous than a demo where the agent controls everything, but it is easier to live with.

Secrets stay in environment variables or the appropriate credential store. They do not belong in prompts, screenshots, articles or commits. A loopback server without authentication must remain loopback-only.

Git is the safety system I actually trust

Never give an AI an enormous uncontrolled mission without a recovery point.

Before risky work, I want a known Git state. I inspect git status, create a checkpoint when appropriate and use a branch for changes that deserve isolation. During the task I keep the diff small. Afterwards I inspect every changed file and run the relevant tests, type checks, linters or build commands for that repository.

I do not trust “it looks finished”. I do not let an agent convert unrelated formatting, dependency churn and generated files into one convenient commit. I investigate unexpected files rather than deleting them. Destructive operations such as force-pushing, rewriting history, resetting or removing untracked work require deliberate human approval.

A commit is a recovery and review boundary, not proof of correctness. Tests can miss the requirement. A reviewer can miss a subtle bug. But small, coherent commits make errors easier to identify, revert and repair than one giant AI-generated dump.

The practical sequence looks like this:

  1. Inspect the repository and working tree.
  2. Define one bounded mission and its acceptance criteria.
  3. Create a checkpoint or branch where the risk justifies it.
  4. Let the agent inspect and plan before editing.
  5. Review the proposed scope.
  6. Execute the smallest useful change.
  7. Run deterministic checks and record failures honestly.
  8. Inspect the diff as if it came from another developer.
  9. Fix issues without broadening the mission unnecessarily.
  10. Commit only after human review.

That process is not anti-AI. It is what lets me use AI without surrendering the repository.

A realistic task moving through the system

Suppose I am changing a Ghost theme or its publishing tooling. I do not begin with “improve the blog”. I ask the researcher to locate the relevant templates, scripts and documentation without editing. Context7 or official documentation can resolve current API behaviour. The planner then proposes a file-level change and verification path.

For a contained edit, Ornith can make the first patch locally. The verifier runs the project's actual checks and reports exit statuses. A reviewer reads the diff from a fresh context. If the local model has misunderstood the Ghost API or cannot reason across the affected files, I escalate the analysis through OpenRouter. I do not escalate because the first answer lacks impressive prose.

The same pattern applies to OpenCode configuration and Linux work, with different risk. A configuration change should be checked against current OpenCode documentation and the resolved config. A CachyOS system change needs diagnosis, a clear confirmation boundary and post-change verification. A game-development experiment may enable the Godot MCP for that project rather than making it available everywhere.

Sometimes manual work is faster. If I already know the exact one-line fix, constructing a multi-agent pipeline is overhead. Sometimes the model produces a plausible abstraction that is more complicated than the original problem. Sometimes a tool fails authentication, an MCP server refuses to connect, or a long context contains enough stale information to confuse the next step. Sometimes I am debugging the assistant's assumptions rather than the software.

Those are not surprising edge cases. They are the normal cost of adding probabilistic components and external tools to a development loop.

What worked, and what did not

The parts that have proven structurally useful are the unexciting ones.

A local OpenAI-compatible endpoint gives OpenCode a simple integration surface. The local model is useful for inexpensive routine work, and stopping it for gaming keeps the workstation flexible. Stronger external models remain available for harder reasoning instead of forcing one model to pretend it suits every task.

Documentation lookup reduces invented APIs. Specialised agents can improve complicated work when their responsibilities are genuinely separate. Verification and fresh review catch problems that confident prose does not. Git checkpoints make experimentation reversible. Clear constraints consistently matter more than a grand prompt.

What does not work is treating orchestration as intelligence. Extra agents can consume tokens while passing the same bad assumption between them. Large context windows invite dumping entire repositories into a session instead of selecting relevant evidence. A small local model can be plainly inadequate for a difficult architectural task. Cloud calls can become expensive through repeated context, retries and premium models. Authentication and MCP connectivity add their own debugging. Manual editing can beat all of this for a trivial change.

I have no verified productivity percentage, cost saving or tokens-per-second figure to offer. The honest claim is narrower: routing gives me a practical way to match model capability and cost to the task, while the workflow makes failures easier to see.

Cost control is mostly context control

Local inference avoids a per-token API bill, but it is not free. The GPU cost money, consumes electricity and cannot simultaneously devote all its VRAM to a game. Running and maintaining the server takes time. A weak result can cost more human effort than one well-chosen cloud request.

Cloud economics depend on current model pricing, input and output volume, provider selection and retries. Large context is often the hidden expense. Re-sending repository files, tool output and a long agent history on every turn can dwarf the final answer. Multi-agent systems multiply that effect.

My practical rules are:

  • Keep missions bounded so the context stays relevant.
  • Summarise or restart when a conversation has accumulated stale assumptions.
  • Use local or cheap routes for repetitive, low-risk work.
  • Escalate on evidence, not prestige.
  • Avoid premium models for formatting, file discovery and simple edits.
  • Check live OpenRouter pricing before budgeting; do not rely on an old article.
  • Measure actual usage with tooling such as opencode stats --days 30 --models 20 rather than guessing.

“Cheap first” only works if it includes a stop condition. Ten failed cheap calls are not automatically economical. The useful policy is: use the least expensive route likely to succeed, verify it, and escalate when failure demonstrates that more capability is justified.

The setup as it stands

The current arrangement is intentionally mixed:

  • CachyOS, KDE Plasma and Wayland provide the desktop.
  • Ghostty and Fish provide the terminal workflow and model-control helpers.
  • llama.cpp serves one local model on 127.0.0.1:8080/v1 with NVIDIA acceleration.
  • llama-local/ornith-9b-q8 is the default everyday model with roughly 65K configured context.
  • OpenRouter provides automatic routing and named cloud escalation options.
  • Low, medium and high levels guide model spending and scrutiny.
  • Researcher, planner, builder, reviewer, debugger, verifier and fixer roles separate responsibilities when that separation helps.
  • Context7 and GitHub MCP are enabled; unrelated global integrations stay disabled.
  • Git checkpoints, diff inspection and deterministic checks remain mandatory before a change is accepted.

What would I change next? I would make routing more observable rather than more magical: record which model handled a task, why it escalated, how much context it used and whether verification passed. That would turn the low/medium/high strategy from a useful convention into something measurable. I would also keep pruning agents and tools that add more orchestration than value.

The part I would carry to another machine is not a particular model list. Models, prices and provider availability move too quickly. The durable idea is to keep the routes explicit and the authority limited.

Bad vibe coding is:

“AI, build me an entire application.”

Controlled AI-assisted development is:

“Here is the repository. Here is the specific goal and what must not change. Research what you need. Plan the smallest change. Implement it. Run the relevant checks. Show me the diff. Report uncertainty. Stop.”

That still leaves plenty of room for experimentation. It just refuses to confuse fast generation with finished engineering. 😉

Share this post
Back to Top

Stay in the loop

Get notified whenever I publish new technical deep-dives, experimental projects, or software insights.

Link copied