<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Local-Ai | Bas Nijholt</title><link>https://www.nijho.lt/tag/local-ai/</link><atom:link href="https://www.nijho.lt/tag/local-ai/index.xml" rel="self" type="application/rss+xml"/><description>Local-Ai</description><generator>Hugo</generator><language>en-us</language><copyright>© 2026</copyright><lastBuildDate>Fri, 02 Oct 2026 00:00:00 +0000</lastBuildDate><image><url>https://www.nijho.lt/media/icon_hu_408a8f6cef43e327.png</url><title>Local-Ai</title><link>https://www.nijho.lt/tag/local-ai/</link></image><item><title>Self-hosting AI does not save money, and I do it anyway</title><link>https://www.nijho.lt/post/self-hosting-ai-is-not-cheaper/</link><pubDate>Fri, 02 Oct 2026 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/self-hosting-ai-is-not-cheaper/</guid><description>&lt;p&gt;I have had this debate many times, so I am finally writing it down.&lt;/p&gt;&#10;&lt;p&gt;Whenever I say that self-hosting AI does not save money, people hear that I am against self-hosting.&#10;That is not the point at all.&#10;I am a massive fan of open-weight models.&#10;&lt;a href="https://github.com/basnijholt/dotfiles/blob/e63a3f341ff36b7b57bf31361c2844e1d8b78e95/configs/nixos/hosts/pc/ai.nix#L43" target="_blank" rel="noopener"&gt;Qwen3.8 27B runs on two RTX 3090s at home&lt;/a&gt; (and 15 more models), and my phone dictation goes to &lt;a href="https://www.nijho.lt/post/diction-agent-cli-qwen/"&gt;Qwen3-ASR on the same machine&lt;/a&gt;.&#10;I think open-source AI is the best thing since sliced bread.&lt;/p&gt;&#10;&lt;p&gt;I don&amp;rsquo;t pretend it saves money.&#10;I do it for fun, for sovereignty, and for privacy, which I come back to at the end.&lt;/p&gt;&#10;&lt;p&gt;All benchmark numbers and prices below come from &lt;a href="https://artificialanalysis.ai/" target="_blank" rel="noopener"&gt;Artificial Analysis&lt;/a&gt; and &lt;a href="https://openrouter.ai/" target="_blank" rel="noopener"&gt;OpenRouter&lt;/a&gt; as of September 30, 2026.&#10;I did the math for the hardware I own, and for the counterarguments I hear most.&#10;All assumptions are listed &lt;a href="#appendix-all-assumptions"&gt;at the end&lt;/a&gt;.&lt;/p&gt;&#10;&lt;p&gt;Benchmarks are not everything, and a high score does not reliably predict how a model does on real work.&#10;They are still the best we have: all models are benchmaxed, tuned to do well on the popular benchmarks, so the scores at least work as a reference frame for comparing them with each other.&lt;/p&gt;&#10;&lt;p&gt;&lt;em&gt;Update, October 2, 2026: after the discussion on r/LocalLLaMA, I added &lt;a href="#5-but-my-mac-dgx-spark-or-ai-max-does-save-money"&gt;section 5&lt;/a&gt; on Macs, DGX Sparks, and AMD AI Max machines.&lt;/em&gt;&lt;/p&gt;&#10;&lt;details class="toc" open&gt;&lt;summary&gt;Table of contents&lt;/summary&gt;&lt;nav id="TableOfContents"&gt;&#10; &lt;ul&gt;&#10; &lt;li&gt;&lt;a href="#1-the-model-i-love-and-the-one-that-beats-it-on-price"&gt;1. The model I love, and the one that beats it on price&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#2-but-my-gpus-are-already-paid-for"&gt;2. &amp;ldquo;But my GPUs are already paid for&amp;rdquo;&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#3-why-datacenters-are-so-much-better-at-this"&gt;3. Why datacenters are so much better at this&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#4-but-i-have-solar-panels"&gt;4. &amp;ldquo;But I have solar panels&amp;rdquo;&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#5-but-my-mac-dgx-spark-or-ai-max-does-save-money"&gt;5. &amp;ldquo;But my Mac, DGX Spark, or AI Max does save money&amp;rdquo;&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#6-the-bar-keeps-moving"&gt;6. The bar keeps moving&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#7-a-small-company-has-it-worse"&gt;7. A small company has it worse&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#8-they-lose-money-on-inference-so-prices-will-go-up"&gt;8. &amp;ldquo;They lose money on inference, so prices will go up&amp;rdquo;&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#9-but-they-sell-your-data"&gt;9. &amp;ldquo;But they sell your data&amp;rdquo;&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#10-where-self-hosting-does-win-the-same-model"&gt;10. Where self-hosting does win: the same model&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#11-so-why-do-i-do-it"&gt;11. So why do I do it?&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#appendix-all-assumptions"&gt;Appendix: all assumptions&lt;/a&gt;&lt;/li&gt;&#10; &lt;/ul&gt;&#10;&lt;/nav&gt;&lt;/details&gt;&#10;&#10;&lt;h2 id="1-the-model-i-love-and-the-one-that-beats-it-on-price"&gt;1. The model I love, and the one that beats it on price &lt;a class="anchor" href="#1-the-model-i-love-and-the-one-that-beats-it-on-price" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;My favorite local model right now is &lt;a href="https://artificialanalysis.ai/models/qwen3-8-27b" target="_blank" rel="noopener"&gt;Qwen3.8 27B&lt;/a&gt;.&#10;It came out in August and it is Apache-2.0.&#10;People often say models like this run on a single gaming GPU, but that is only true after quantizing them.&#10;At full precision, Qwen3.8 27B needs about 54 GB of memory.&#10;Quantized to about 4 bits per weight, it fits on one 24 GB card, at some cost in quality.&#10;I still &lt;a href="https://github.com/basnijholt/dotfiles/blob/e63a3f341ff36b7b57bf31361c2844e1d8b78e95/configs/nixos/hosts/pc/ai.nix#L56-L65" target="_blank" rel="noopener"&gt;split it across both of my 3090s&lt;/a&gt;, because the second card leaves room for a larger context window.&lt;/p&gt;&#10;&lt;p&gt;I compare it with OpenAI&amp;rsquo;s &lt;a href="https://artificialanalysis.ai/models/gpt-6-luna" target="_blank" rel="noopener"&gt;GPT-6 Luna&lt;/a&gt;, the cheap tier released on September 22.&#10;Both models let you choose how long they think, but the settings do not mean the same thing for both.&#10;Luna&amp;rsquo;s token use grows more than 20 times from its lowest setting to its highest, while Qwen&amp;rsquo;s barely changes.&#10;Qwen on low already writes more tokens than Luna on xhigh.&lt;/p&gt;&#10;&lt;figure class="plot-figure"&gt;&#10; &lt;div class="plot-container" role="img" aria-label="Average output tokens per Intelligence Index task at each reasoning setting. Qwen3.8 27B has no high or max setting. Hover for scores." data-plot-module="/post/self-hosting-ai-is-not-cheaper/plots.js" data-plot-name="effortTokens"&gt;&lt;/div&gt;&#10; &lt;figcaption&gt;Average output tokens per Intelligence Index task at each reasoning setting. Qwen3.8 27B has no high or max setting. Hover for scores.&lt;/figcaption&gt;&#10; &lt;noscript&gt;This chart needs JavaScript.&lt;/noscript&gt;&#10;&lt;/figure&gt;&#10;&#10;&lt;script type="module" src="https://www.nijho.lt/js/plot.7cb86dc2e3ef25b56bc93730b2620745a7f02b5715de87f94e61908f60e51690.js" integrity="sha256-fLhtwuPvJbVryTcwsmIHRafwK1cV3of5TmGQj2DlFpA=" crossorigin="anonymous"&gt;&lt;/script&gt;&#10;&#10;&#10;&lt;p&gt;So the name of a setting says little on its own.&#10;I compare both models at xhigh, the highest setting Qwen offers, and count the tokens each one actually uses.&#10;The extra tokens Qwen needs are part of what I am measuring.&#10;There, Qwen scores 33.7 on the &lt;a href="https://artificialanalysis.ai/methodology/intelligence-benchmarking" target="_blank" rel="noopener"&gt;Artificial Analysis Intelligence Index&lt;/a&gt; and Luna scores 34.6.&#10;For reference, Claude Opus 4.6, a frontier model from February, scores 31.9.&#10;That is an amazing feat in itself.&#10;When Opus 4.6 was the best model we had, I never thought that within the same year I could run essentially that level of intelligence in my own house.&lt;/p&gt;&#10;&lt;p&gt;Artificial Analysis also &lt;a href="https://artificialanalysis.ai/leaderboards/models" target="_blank" rel="noopener"&gt;publishes&lt;/a&gt; how many tokens each model used to run the index, and what that cost.&#10;So the question is simple: what does it cost to run the whole benchmark once?&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;&#10;&lt;figure class="plot-figure"&gt;&#10; &lt;div class="plot-container" role="img" aria-label="Cost of one run of the Artificial Analysis Intelligence Index. The two bars for hardware at home are electricity only: measured on my 3090s, estimated for the RTX PRO 6000." data-plot-module="/post/self-hosting-ai-is-not-cheaper/plots.js" data-plot-name="runCost"&gt;&lt;/div&gt;&#10; &lt;figcaption&gt;Cost of one run of the Artificial Analysis Intelligence Index. The two bars for hardware at home are electricity only: measured on my 3090s, estimated for the RTX PRO 6000.&lt;/figcaption&gt;&#10; &lt;noscript&gt;This chart needs JavaScript.&lt;/noscript&gt;&#10;&lt;/figure&gt;&#10;&#10;&lt;script type="module" src="https://www.nijho.lt/js/plot.7cb86dc2e3ef25b56bc93730b2620745a7f02b5715de87f94e61908f60e51690.js" integrity="sha256-fLhtwuPvJbVryTcwsmIHRafwK1cV3of5TmGQj2DlFpA=" crossorigin="anonymous"&gt;&lt;/script&gt;&#10;&#10;&#10;&lt;p&gt;Luna runs the whole benchmark for $67.&#10;Qwen at full precision, through the &lt;a href="https://openrouter.ai/qwen/qwen3.8-27b/providers" target="_blank" rel="noopener"&gt;cheapest provider&lt;/a&gt; that does not keep your data (ZDR), costs $619.&#10;Two things cause that gap: providers charge almost four times as much per output token for Qwen, and Qwen generates almost three times as many tokens to get the same work done.&lt;/p&gt;&#10;&lt;p&gt;The second bar is my own machine: the electricity alone for running Qwen on my GPUs costs more than Luna&amp;rsquo;s entire bill.&lt;/p&gt;&#10;&lt;p&gt;The third bar is what it takes to match the API&amp;rsquo;s full precision at home: an RTX PRO 6000, a workstation card with 96 GB of memory, enough for the full model.&lt;sup id="fnref:2"&gt;&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref"&gt;2&lt;/a&gt;&lt;/sup&gt;&#10;Its electricity alone costs twice Luna&amp;rsquo;s bill, and one run takes more than seven weeks.&#10;The card by itself sells for $10,000 to $20,000, so with a computer around it, you are paying for a pretty nice car.&#10;Buy a few of them to run agents in parallel, and you are building a small data center of your own.&lt;/p&gt;&#10;&lt;p&gt;The bars for hardware at home are electricity only, so they are the lowest these runs can cost: the cards come on top, and how much depends on how busy you keep them, which is what section 4 is about.&lt;/p&gt;&#10;&lt;p&gt;What I care about is the cheapest way to get answers of Qwen&amp;rsquo;s quality, from whichever model gives them.&lt;/p&gt;&#10;&lt;h2 id="2-but-my-gpus-are-already-paid-for"&gt;2. &amp;ldquo;But my GPUs are already paid for&amp;rdquo; &lt;a class="anchor" href="#2-but-my-gpus-are-already-paid-for" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;This is the argument I hear most, so assume the hardware is free and only the electricity counts.&lt;/p&gt;&#10;&lt;p&gt;I measured my own machine.&#10;It runs Qwen in vLLM, split across both cards, at about 107 tokens per second.&lt;sup id="fnref:3"&gt;&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref"&gt;3&lt;/a&gt;&lt;/sup&gt;&#10;The cards are capped at 270 W each,&lt;sup id="fnref:4"&gt;&lt;a href="#fn:4" class="footnote-ref" role="doc-noteref"&gt;4&lt;/a&gt;&lt;/sup&gt; and the whole machine draws about 700 W.&#10;It then needs four weeks, running day and night, to get through the benchmark once.&#10;I also give my quantized copy the score Artificial Analysis measured through Alibaba&amp;rsquo;s API.&#10;I have not rerun the benchmark on it, and anything lost to quantization makes my machine look worse.&lt;/p&gt;&#10;&lt;figure class="plot-figure"&gt;&#10; &lt;div class="plot-container" role="img" aria-label="Electricity for one benchmark run of Qwen3.8 27B on my two 3090s, by electricity price. The dashed line is what Luna charges for the same work, with OpenAI&amp;rsquo;s hardware and margin included. Hover for values." data-plot-module="/post/self-hosting-ai-is-not-cheaper/plots.js" data-plot-name="electricityCost"&gt;&lt;/div&gt;&#10; &lt;figcaption&gt;Electricity for one benchmark run of Qwen3.8 27B on my two 3090s, by electricity price. The dashed line is what Luna charges for the same work, with OpenAI&amp;rsquo;s hardware and margin included. Hover for values.&lt;/figcaption&gt;&#10; &lt;noscript&gt;This chart needs JavaScript.&lt;/noscript&gt;&#10;&lt;/figure&gt;&#10;&#10;&lt;script type="module" src="https://www.nijho.lt/js/plot.7cb86dc2e3ef25b56bc93730b2620745a7f02b5715de87f94e61908f60e51690.js" integrity="sha256-fLhtwuPvJbVryTcwsmIHRafwK1cV3of5TmGQj2DlFpA=" crossorigin="anonymous"&gt;&lt;/script&gt;&#10;&#10;&#10;&lt;p&gt;Above 14.5 cents per kWh, my electricity alone costs more than Luna&amp;rsquo;s API bill.&#10;Below it, I save a few dollars per run and wait four weeks instead of a few hours.&lt;/p&gt;&#10;&lt;p&gt;Those four weeks matter more than the few dollars.&#10;An API takes hundreds of requests at once, so even one benchmark run can finish in an hour or two.&#10;My setup works on one conversation at a time: when I sent it eight requests at once, seven of them waited in line.&lt;/p&gt;&#10;&lt;figure class="plot-figure"&gt;&#10; &lt;div class="plot-container" role="img" aria-label="Wall-clock time for one benchmark run. The API times use Artificial Analysis&amp;rsquo;s measured time per task for Luna; the parallel case assumes 100 requests at once. Log scale." data-plot-module="/post/self-hosting-ai-is-not-cheaper/plots.js" data-plot-name="wallClock"&gt;&lt;/div&gt;&#10; &lt;figcaption&gt;Wall-clock time for one benchmark run. The API times use Artificial Analysis&amp;rsquo;s measured time per task for Luna; the parallel case assumes 100 requests at once. Log scale.&lt;/figcaption&gt;&#10; &lt;noscript&gt;This chart needs JavaScript.&lt;/noscript&gt;&#10;&lt;/figure&gt;&#10;&#10;&lt;script type="module" src="https://www.nijho.lt/js/plot.7cb86dc2e3ef25b56bc93730b2620745a7f02b5715de87f94e61908f60e51690.js" integrity="sha256-fLhtwuPvJbVryTcwsmIHRafwK1cV3of5TmGQj2DlFpA=" crossorigin="anonymous"&gt;&lt;/script&gt;&#10;&#10;&#10;&lt;p&gt;This is one more reason my coding agents run on APIs.&#10;When I &lt;a href="https://www.nijho.lt/post/parallel-agentic-coding/"&gt;run several agents in parallel&lt;/a&gt;, each of them should be as fast as if it were alone.&lt;/p&gt;&#10;&lt;h2 id="3-why-datacenters-are-so-much-better-at-this"&gt;3. Why datacenters are so much better at this &lt;a class="anchor" href="#3-why-datacenters-are-so-much-better-at-this" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;When my 3090s generate a token, they read all of the model&amp;rsquo;s weights from memory to produce one token for one conversation.&#10;Most of the chip&amp;rsquo;s compute sits idle while it waits for memory.&#10;A datacenter GPU reads the same weights once and produces a token for hundreds of conversations at the same time.&#10;This is called batching, and it is where most of the efficiency comes from.&lt;/p&gt;&#10;&lt;p&gt;Artificial Analysis measures this with &lt;a href="https://artificialanalysis.ai/hardware-inference-stack/datacenter" target="_blank" rel="noopener"&gt;AgentPerf&lt;/a&gt;, which replays real coding-agent sessions against datacenter hardware.&#10;It counts how many agents a system can serve while each one still gets at least 60 tokens per second.&lt;/p&gt;&#10;&lt;figure class="plot-figure"&gt;&#10; &lt;div class="plot-container" role="img" aria-label="Coding agents served per kW of accelerator power, each getting at least 60 tokens per second, from AgentPerf. The datacenter systems all run DeepSeek V4 Pro. The 3090 bar is my machine running Qwen3.8 27B, a much smaller model, with GPU power only, so the comparison favors my machine." data-plot-module="/post/self-hosting-ai-is-not-cheaper/plots.js" data-plot-name="agentsPerKw"&gt;&lt;/div&gt;&#10; &lt;figcaption&gt;Coding agents served per kW of accelerator power, each getting at least 60 tokens per second, from AgentPerf. The datacenter systems all run DeepSeek V4 Pro. The 3090 bar is my machine running Qwen3.8 27B, a much smaller model, with GPU power only, so the comparison favors my machine.&lt;/figcaption&gt;&#10; &lt;noscript&gt;This chart needs JavaScript.&lt;/noscript&gt;&#10;&lt;/figure&gt;&#10;&#10;&lt;script type="module" src="https://www.nijho.lt/js/plot.7cb86dc2e3ef25b56bc93730b2620745a7f02b5715de87f94e61908f60e51690.js" integrity="sha256-fLhtwuPvJbVryTcwsmIHRafwK1cV3of5TmGQj2DlFpA=" crossorigin="anonymous"&gt;&lt;/script&gt;&#10;&#10;&#10;&lt;p&gt;The rack of 36 GB300s serves 20 times more agents per kW than the 8-GPU H200 server.&#10;Part of that is newer chips, and part is software: NVIDIA tuned the B300 and GB300 setups itself, while Artificial Analysis configured the H200 one.&#10;The B300 and the GB300 share both the chip generation and the software.&#10;Most of the gap between those two bars comes from the rack itself: 36 GPUs on one fast interconnect keep more than a thousand agents in flight at once.&#10;My two 3090s serve one conversation at a time.&lt;/p&gt;&#10;&lt;h2 id="4-but-i-have-solar-panels"&gt;4. &amp;ldquo;But I have solar panels&amp;rdquo; &lt;a class="anchor" href="#4-but-i-have-solar-panels" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Solar power is not free.&#10;With net metering, every kWh my GPUs burn is a kWh that does not get credited at the retail price.&#10;Without net metering, it is a kWh I do not sell, unless the surplus would go to waste anyway, which is the free-power case below.&#10;And the GPUs also run at night.&lt;/p&gt;&#10;&lt;p&gt;But fine, say the electricity is free and the only cost is the hardware.&#10;I was lucky and bought my two 3090s for about $750 each,&lt;sup id="fnref:5"&gt;&lt;a href="#fn:5" class="footnote-ref" role="doc-noteref"&gt;5&lt;/a&gt;&lt;/sup&gt; and I write them off over three years.&#10;The GPUs only save money while they do work I would otherwise pay an API for, and while they sit idle they save nothing.&#10;So the question is how many hours a day they have to be busy before they pay for themselves.&lt;sup id="fnref:6"&gt;&lt;a href="#fn:6" class="footnote-ref" role="doc-noteref"&gt;6&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;&#10;&lt;p&gt;To make this fair to open models, imagine an open model that is exactly as efficient as Luna: the same quality from the same number of tokens, sold at Luna&amp;rsquo;s price.&lt;/p&gt;&#10;&lt;figure class="plot-figure"&gt;&#10; &lt;div class="plot-container" role="img" aria-label="How many hours a day my two $750 RTX 3090s have to be busy, every day for three years, before they cost less than a ZDR API. The host PC is not counted." data-plot-module="/post/self-hosting-ai-is-not-cheaper/plots.js" data-plot-name="breakEven"&gt;&lt;/div&gt;&#10; &lt;figcaption&gt;How many hours a day my two $750 RTX 3090s have to be busy, every day for three years, before they cost less than a ZDR API. The host PC is not counted.&lt;/figcaption&gt;&#10; &lt;noscript&gt;This chart needs JavaScript.&lt;/noscript&gt;&#10;&lt;/figure&gt;&#10;&#10;&lt;script type="module" src="https://www.nijho.lt/js/plot.7cb86dc2e3ef25b56bc93730b2620745a7f02b5715de87f94e61908f60e51690.js" integrity="sha256-fLhtwuPvJbVryTcwsmIHRafwK1cV3of5TmGQj2DlFpA=" crossorigin="anonymous"&gt;&lt;/script&gt;&#10;&#10;&#10;&lt;p&gt;With free power, the GPUs pay for themselves if they are busy 4 hours a day, every day for three years.&#10;At 18 cents per kWh, they need 8.5.&#10;That uses what I paid for the cards; at today&amp;rsquo;s price of about $1,500 each, the 8.5 hours become 14.&lt;/p&gt;&#10;&lt;p&gt;And API prices keep falling.&#10;GPT-6 Luna costs 58% less per output token than GPT-5.6 Luna did in July.&#10;If the price halves again, no amount of use pays off at 18 cents per kWh.&#10;If it halves twice, the API costs less than my electricity alone.&lt;/p&gt;&#10;&lt;h2 id="5-but-my-mac-dgx-spark-or-ai-max-does-save-money"&gt;5. &amp;ldquo;But my Mac, DGX Spark, or AI Max does save money&amp;rdquo; &lt;a class="anchor" href="#5-but-my-mac-dgx-spark-or-ai-max-does-save-money" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Several readers told me their machines do save them money: a MacBook Pro with an M5 Max, an NVIDIA DGX Spark, and a computer with AMD&amp;rsquo;s Ryzen AI Max+ 395.&#10;If you would have bought the machine anyway, that is true, for the reason in section 2: their electricity costs little compared with the API.&#10;Counting the hardware is a different story.&lt;/p&gt;&#10;&lt;p&gt;With 128 GB of memory, all three can hold a better model than my 3090s: Qwen3.8-Flash-Next, a sparse model with 180 billion parameters, of which only 6 billion work on each token.&#10;It scores 39.8, close to GPT-6 Luna on its highest setting (38.1), although at the Q2 a reader uses on their Mac it probably loses a few points.&lt;sup id="fnref:7"&gt;&lt;a href="#fn:7" class="footnote-ref" role="doc-noteref"&gt;7&lt;/a&gt;&lt;/sup&gt;&#10;So how many years would each machine have to run nonstop before it pays for itself?&lt;/p&gt;&#10;&lt;figure class="plot-figure"&gt;&#10; &lt;div class="plot-container" role="img" aria-label="Years of nonstop generation before each machine pays for itself, compared with the most efficient API model at a similar score. Each machine runs the best model it can hold." data-plot-module="/post/self-hosting-ai-is-not-cheaper/plots.js" data-plot-name="paybackYears"&gt;&lt;/div&gt;&#10; &lt;figcaption&gt;Years of nonstop generation before each machine pays for itself, compared with the most efficient API model at a similar score. Each machine runs the best model it can hold.&lt;/figcaption&gt;&#10; &lt;noscript&gt;This chart needs JavaScript.&lt;/noscript&gt;&#10;&lt;/figure&gt;&#10;&#10;&lt;script type="module" src="https://www.nijho.lt/js/plot.7cb86dc2e3ef25b56bc93730b2620745a7f02b5715de87f94e61908f60e51690.js" integrity="sha256-fLhtwuPvJbVryTcwsmIHRafwK1cV3of5TmGQj2DlFpA=" crossorigin="anonymous"&gt;&lt;/script&gt;&#10;&#10;&#10;&lt;p&gt;Even with free power, the 128 GB machines need six to ten years.&#10;The AMD machine draws only about 120 W, but it is slow too: one benchmark run of Qwen3.8 27B takes it four and a half months, and almost as much energy as my 3090s.&#10;The RTX PRO 6000 runs the same model about three times as fast but costs about twice as much, so it needs six to nine years.&#10;My 3090s pay off in under two years with free power, which is why the 3090 is still the value king, but at 18 cents per kWh they never do: the electricity for one run already costs more than Luna charges for it.&lt;/p&gt;&#10;&lt;p&gt;Ten years is a long time in AI.&#10;Better and more efficient open models will run on the same machines, but the frontier moves at the same pace, and API prices keep falling.&#10;Hardware gets more efficient too, in datacenters and at home, so a machine bought today also ends up competing with next year&amp;rsquo;s machines.&#10;These numbers only show how far apart the two sides start.&lt;/p&gt;&#10;&lt;h2 id="6-the-bar-keeps-moving"&gt;6. The bar keeps moving &lt;a class="anchor" href="#6-the-bar-keeps-moving" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Section 4&amp;rsquo;s eight and a half hours a day would be easy for me: I run agents for longer than that, just not on Qwen.&#10;When Claude Opus 4.6 came out in February, I was perfectly happy with it.&#10;I thought it was all I would ever need, and I could not have imagined how much better models would get in half a year.&#10;Qwen3.8 27B now scores about the same as Opus 4.6, and I would no longer accept it for coding.&#10;Until GPT-6 Astra came out at the start of September, my go-to was GPT-5.6 Sol, which scores ten points higher than Qwen.&#10;I then used Astra until Claude Opus 5.5 came out less than three weeks later.&#10;Now I don&amp;rsquo;t even accept what was considered the best model a month ago.&lt;/p&gt;&#10;&lt;figure class="plot-figure"&gt;&#10; &lt;div class="plot-container" role="img" aria-label="Intelligence Index of the best model available on each date, of the models I used as my go-to, and of Qwen&amp;rsquo;s 27B models, which fit on a single 3090. Scores use the xhigh reasoning setting where a model has it." data-plot-module="/post/self-hosting-ai-is-not-cheaper/plots.js" data-plot-name="frontierGap"&gt;&lt;/div&gt;&#10; &lt;figcaption&gt;Intelligence Index of the best model available on each date, of the models I used as my go-to, and of Qwen&amp;rsquo;s 27B models, which fit on a single 3090. Scores use the xhigh reasoning setting where a model has it.&lt;/figcaption&gt;&#10; &lt;noscript&gt;This chart needs JavaScript.&lt;/noscript&gt;&#10;&lt;/figure&gt;&#10;&#10;&lt;script type="module" src="https://www.nijho.lt/js/plot.7cb86dc2e3ef25b56bc93730b2620745a7f02b5715de87f94e61908f60e51690.js" integrity="sha256-fLhtwuPvJbVryTcwsmIHRafwK1cV3of5TmGQj2DlFpA=" crossorigin="anonymous"&gt;&lt;/script&gt;&#10;&#10;&#10;&lt;p&gt;The models that fit on my 3090s keep improving, but they stay about half a year behind the frontier, and my standard moves with the frontier.&lt;/p&gt;&#10;&lt;h2 id="7-a-small-company-has-it-worse"&gt;7. A small company has it worse &lt;a class="anchor" href="#7-a-small-company-has-it-worse" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;For me, this is a hobby, and a hobby is allowed to be inefficient.&#10;It gets worse once you try to use self-hosted models seriously, say for a team of ten developers.&lt;/p&gt;&#10;&lt;p&gt;If you stay fully self-hosted and want fast responses at peak, you have to buy hardware for the busiest hour of the year, not for the average one.&#10;The rest of the time, it sits idle.&lt;/p&gt;&#10;&lt;figure class="plot-figure"&gt;&#10; &lt;div class="plot-container" role="img" aria-label="An illustrative week of coding agents for a ten-person team. The hardware has to cover the busiest hour of the year, but a typical week uses only a fraction of it." data-plot-module="/post/self-hosting-ai-is-not-cheaper/plots.js" data-plot-name="peakLoad"&gt;&lt;/div&gt;&#10; &lt;figcaption&gt;An illustrative week of coding agents for a ten-person team. The hardware has to cover the busiest hour of the year, but a typical week uses only a fraction of it.&lt;/figcaption&gt;&#10; &lt;noscript&gt;This chart needs JavaScript.&lt;/noscript&gt;&#10;&lt;/figure&gt;&#10;&#10;&lt;script type="module" src="https://www.nijho.lt/js/plot.7cb86dc2e3ef25b56bc93730b2620745a7f02b5715de87f94e61908f60e51690.js" integrity="sha256-fLhtwuPvJbVryTcwsmIHRafwK1cV3of5TmGQj2DlFpA=" crossorigin="anonymous"&gt;&lt;/script&gt;&#10;&#10;&#10;&lt;p&gt;In this made-up but realistic week, the team uses 17% of what it paid for, so every token costs about six times more than it would at full load.&#10;You also need a spare GPU for when one dies, and someone who gets paged when it does.&#10;You could queue work or send the peaks to an API, but then you are paying for an API anyway.&lt;/p&gt;&#10;&lt;p&gt;An API provider has the opposite situation.&#10;It serves thousands of customers across every time zone, so its load curve is much flatter than yours.&#10;It fills the nights with discounted batch jobs (Luna&amp;rsquo;s batch tier is half price) and training runs.&#10;And more concurrent requests mean bigger batches, which is where the efficiency from section 3 comes from.&lt;/p&gt;&#10;&lt;h2 id="8-they-lose-money-on-inference-so-prices-will-go-up"&gt;8. &amp;ldquo;They lose money on inference, so prices will go up&amp;rdquo; &lt;a class="anchor" href="#8-they-lose-money-on-inference-so-prices-will-go-up" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Another argument I hear is that API prices are subsidized by investors and will go up once those investors want their money back.&#10;I don&amp;rsquo;t think that holds for inference.&lt;/p&gt;&#10;&lt;p&gt;AgentPerf also reports how many tokens each GPU serves.&#10;On the GB300 rack, one GPU serving DeepSeek V4 Pro handles about 4 million output tokens and 530 million input tokens per hour.&#10;Most of that input is conversation history that coding agents send again with every step.&#10;Providers keep it in a cache and charge less than 1% of the normal input price for it.&lt;/p&gt;&#10;&lt;p&gt;At &lt;a href="https://artificialanalysis.ai/models/deepseek-v4-pro-0424" target="_blank" rel="noopener"&gt;DeepSeek V4 Pro&amp;rsquo;s list prices&lt;/a&gt;, that GPU brings in at least $5.70 per hour, even if every input token is billed at the cache price.&#10;If 5% of the input misses the cache, it brings in about $17.&#10;Renting a B200, the closest GPU with a &lt;a href="https://artificialanalysis.ai/hardware-inference-stack/datacenter" target="_blank" rel="noopener"&gt;published rental price&lt;/a&gt;, costs $3.50 to $5.90 per hour from smaller cloud providers, and that price already includes their profit.&lt;/p&gt;&#10;&lt;p&gt;So the tokens pay for the hardware that serves them, even in the worst case.&#10;This leaves out costs like staff and training, so it does not tell you whether a lab makes money overall.&lt;/p&gt;&#10;&lt;p&gt;The money goes to training, research, free users, and flat-rate subscriptions for heavy users like me.&#10;When I wrote that I used &lt;a href="https://www.nijho.lt/post/agentic-coding/"&gt;$10,000 worth of API tokens for $200&lt;/a&gt;, that was list price, not what those tokens cost to serve.&lt;/p&gt;&#10;&lt;p&gt;And prices go down, not up.&#10;This is what the same level of intelligence has cost this year:&lt;/p&gt;&#10;&lt;figure class="plot-figure"&gt;&#10; &lt;div class="plot-container" role="img" aria-label="Output price per million tokens for models scoring between 30 and 35 on the Intelligence Index, by release date, as listed by Artificial Analysis. Log scale." data-plot-module="/post/self-hosting-ai-is-not-cheaper/plots.js" data-plot-name="priceDrop"&gt;&lt;/div&gt;&#10; &lt;figcaption&gt;Output price per million tokens for models scoring between 30 and 35 on the Intelligence Index, by release date, as listed by Artificial Analysis. Log scale.&lt;/figcaption&gt;&#10; &lt;noscript&gt;This chart needs JavaScript.&lt;/noscript&gt;&#10;&lt;/figure&gt;&#10;&#10;&lt;script type="module" src="https://www.nijho.lt/js/plot.7cb86dc2e3ef25b56bc93730b2620745a7f02b5715de87f94e61908f60e51690.js" integrity="sha256-fLhtwuPvJbVryTcwsmIHRafwK1cV3of5TmGQj2DlFpA=" crossorigin="anonymous"&gt;&lt;/script&gt;&#10;&#10;&#10;&lt;p&gt;From Claude Opus 4.6 in February to GPT-6 Luna in September, the output price for this level of intelligence dropped by a factor of 50.&#10;Labs are in a race to the bottom on price.&lt;/p&gt;&#10;&lt;h2 id="9-but-they-sell-your-data"&gt;9. &amp;ldquo;But they sell your data&amp;rdquo; &lt;a class="anchor" href="#9-but-they-sell-your-data" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Some people say APIs are cheap because the provider trains on your data or sells it.&#10;That is why I only used prices from providers that OpenRouter lists as &lt;a href="https://openrouter.ai/docs/features/zdr" target="_blank" rel="noopener"&gt;zero data retention&lt;/a&gt; (ZDR).&lt;/p&gt;&#10;&lt;div class="table-wrap"&gt;&#10;&lt;table&gt;&#10;&lt;thead&gt;&#10;&lt;tr&gt;&#10;&lt;th&gt;Model&lt;/th&gt;&#10;&lt;th&gt;Provider&lt;/th&gt;&#10;&lt;th&gt;ZDR&lt;/th&gt;&#10;&lt;th&gt;Output ($ per million tokens)&lt;/th&gt;&#10;&lt;/tr&gt;&#10;&lt;/thead&gt;&#10;&lt;tbody&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;GPT-6 Luna&lt;/td&gt;&#10;&lt;td&gt;OpenAI&lt;/td&gt;&#10;&lt;td&gt;no&lt;/td&gt;&#10;&lt;td&gt;0.50&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;GPT-6 Luna&lt;/td&gt;&#10;&lt;td&gt;Azure&lt;/td&gt;&#10;&lt;td&gt;yes&lt;/td&gt;&#10;&lt;td&gt;0.50&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;Qwen3.8 27B&lt;/td&gt;&#10;&lt;td&gt;AkashML (FP8)&lt;/td&gt;&#10;&lt;td&gt;yes&lt;/td&gt;&#10;&lt;td&gt;1.78&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;Qwen3.8 27B&lt;/td&gt;&#10;&lt;td&gt;DeepInfra (full precision)&lt;/td&gt;&#10;&lt;td&gt;yes&lt;/td&gt;&#10;&lt;td&gt;1.88&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;Qwen3.8 27B&lt;/td&gt;&#10;&lt;td&gt;Alibaba&lt;/td&gt;&#10;&lt;td&gt;no&lt;/td&gt;&#10;&lt;td&gt;2.55&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;Qwen3.8 27B&lt;/td&gt;&#10;&lt;td&gt;Cloudflare&lt;/td&gt;&#10;&lt;td&gt;no&lt;/td&gt;&#10;&lt;td&gt;3.20&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;Azure serves GPT-6 Luna with zero data retention at the same price as OpenAI.&#10;For Qwen3.8 27B, the cheapest endpoints are all ZDR, and the endpoints without ZDR cost more.&#10;The &lt;code&gt;:free&lt;/code&gt; tier is where you pay with your data.&lt;/p&gt;&#10;&lt;h2 id="10-where-self-hosting-does-win-the-same-model"&gt;10. Where self-hosting does win: the same model &lt;a class="anchor" href="#10-where-self-hosting-does-win-the-same-model" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;If you compare running Qwen3.8 27B yourself with paying for Qwen3.8 27B through an API, self-hosting wins.&lt;/p&gt;&#10;&lt;p&gt;DeepInfra, the cheapest full-precision ZDR provider, charges $619 for one benchmark run, while my electricity costs $83.&#10;That compares the full model with my quantized copy, so the gap overstates my advantage by whatever quantization costs in quality.&#10;My machine pays for itself if it is busy 2.4 hours a day, or 1.5 hours with free power (the first group in the chart in section 4).&lt;/p&gt;&#10;&lt;p&gt;Providers charge a lot for a dense 27B model, because every token runs through all 27 billion parameters.&#10;Sparse open models, which use only a small part of their weights for each token, can be much cheaper to serve.&#10;If you specifically need Qwen and use it heavily, self-hosting it pays off.&#10;But you don&amp;rsquo;t need Qwen to get answers of Qwen&amp;rsquo;s quality.&#10;Luna gives you those for $67, with zero data retention.&lt;/p&gt;&#10;&lt;h2 id="11-so-why-do-i-do-it"&gt;11. So why do I do it? &lt;a class="anchor" href="#11-so-why-do-i-do-it" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;First, it is fun.&#10;I like knowing how the whole stack works, from the drivers in &lt;a href="https://www.nijho.lt/post/llama-nixos/"&gt;my NixOS configuration&lt;/a&gt; to how the layers are split between my two GPUs.&lt;/p&gt;&#10;&lt;p&gt;Second, nobody can take it away.&#10;Artificial Analysis already lists GPT-5.6 Luna as deprecated, less than three months after its release.&#10;My Qwen weights will still be on my disk in ten years, and no company can change their license or &lt;a href="https://www.nijho.lt/post/truenas-to-nixos/"&gt;close their build system&lt;/a&gt; on me.&lt;/p&gt;&#10;&lt;p&gt;Third, some data should not leave my house.&#10;I would never send 200 GB of email, my messages, and my location history to an API, zero data retention or not.&#10;That is what &lt;a href="https://www.nijho.lt/post/local-ai-journey/"&gt;my local AI projects&lt;/a&gt; are for.&lt;/p&gt;&#10;&lt;p&gt;Those are good reasons.&#10;Saving money is not one of them.&lt;/p&gt;&#10;&lt;h2 id="appendix-all-assumptions"&gt;Appendix: all assumptions &lt;a class="anchor" href="#appendix-all-assumptions" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;details&gt;&#10; &lt;summary&gt;Show all assumptions, and which side each one favors&lt;/summary&gt;&#10; &lt;p&gt;&lt;strong&gt;Assumptions that favor self-hosting&lt;/strong&gt;&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;The host PC around the GPUs is not counted, and my 3090s count at the $750 each I paid, not the $1,500 they sell for today.&lt;/li&gt;&#10;&lt;li&gt;Quantized local copies get the score Artificial Analysis measured through an API, with no penalty for quantization.&lt;/li&gt;&#10;&lt;li&gt;The payback chart assumes each machine is busy 24 hours a day, with no idle time.&lt;/li&gt;&#10;&lt;li&gt;API prices stay where they are, although GPT-6 Luna&amp;rsquo;s output price is 58% lower than GPT-5.6 Luna&amp;rsquo;s, two and a half months earlier.&lt;/li&gt;&#10;&lt;li&gt;My time to set up and maintain the machine is not counted.&lt;/li&gt;&#10;&lt;li&gt;Every machine also gets a case with free electricity.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;&lt;strong&gt;Assumptions that favor the API&lt;/strong&gt;&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;The example electricity price is 18 cents per kWh, and some people pay less.&lt;/li&gt;&#10;&lt;li&gt;My machine serves one request at a time; batching several requests could raise its total throughput.&lt;/li&gt;&#10;&lt;li&gt;The hardware has no resale value at the end.&lt;/li&gt;&#10;&lt;li&gt;Waste heat that replaces heating in winter is not counted.&lt;/li&gt;&#10;&lt;li&gt;API outages, rate limits, and deprecated models are not counted.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;&lt;strong&gt;Inputs&lt;/strong&gt;&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Benchmark: the Artificial Analysis Intelligence Index on September 30, 2026, at the xhigh reasoning setting, or the highest setting where noted. Its token counts per model are the unit of work.&lt;/li&gt;&#10;&lt;li&gt;API prices: the cheapest zero-data-retention endpoints on OpenRouter, with cached input billed at the cache price.&lt;/li&gt;&#10;&lt;li&gt;My machine: two RTX 3090s capped at 270 W, running Qwen3.8 27B in vLLM with AutoRound INT4 and DFlash2. Measured: 107 tokens per second, 1,600 tokens per second of prefill, and 540 W for both GPUs. Estimated: about 700 W for the whole machine under load and 150 W idle.&lt;/li&gt;&#10;&lt;li&gt;Break-even: the cards are written off over three years; busy hours save what the API would charge minus the electricity, and idle hours cost the idle power.&lt;/li&gt;&#10;&lt;li&gt;Other machines: prices, power, and speeds as in the footnotes. The M5 Max and RTX PRO 6000 numbers come from readers, and the DGX Spark and AMD Ryzen AI Max+ 395 speeds are estimated from Artificial Analysis measurements.&lt;/li&gt;&#10;&lt;li&gt;Datacenter: AgentPerf measures accelerator power only, and the B200 rental price stands in for the GB300, which has no published rental price.&lt;/li&gt;&#10;&lt;li&gt;Small company: the peak-load week is made up.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&#10;&lt;/details&gt;&#10;&#10;&lt;div class="footnotes" role="doc-endnotes"&gt;&#10;&lt;hr&gt;&#10;&lt;ol&gt;&#10;&lt;li id="fn:1"&gt;&#10;&lt;p&gt;For each model, Artificial Analysis publishes how many input and output tokens the whole index took, and how many of the input tokens were read from a cache. I multiplied those by each provider&amp;rsquo;s prices, with cached input at the cache price. For Qwen that is 198 million output tokens and 820 million input tokens that miss the cache. For the bars at home, the same tokens divided by the machine&amp;rsquo;s speed give the run time, with the uncached input processed at the 1,600 tokens per second I measured on my 3090s and an estimated 2,000 on the RTX PRO 6000, and the run time times the machine&amp;rsquo;s power and the price per kWh gives the electricity.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li id="fn:2"&gt;&#10;&lt;p&gt;The RTX PRO 6000 has the same memory bandwidth as an RTX 5090, and at full precision every token reads three times as many bytes as in Q4_K_M. Scaling Artificial Analysis&amp;rsquo;s 5090 measurement gives about 48 tokens per second. I assume 600 W for the whole machine.&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li id="fn:3"&gt;&#10;&lt;p&gt;I measured the model as I normally run it: vLLM with an AutoRound INT4 quant and DFlash2 speculative decoding, split across both cards. Single 1,500-token answers came out at 105 to 109 tokens per second, and reading a 22,000-token prompt ran at 1,600 tokens per second. Sending 2, 4, or 8 requests at once did not raise the total above about 110 tokens per second. The two GPUs drew 540 W together, each at its 270 W power limit; the 700 W adds my estimate for the rest of the machine, since I could not measure the CPU. The setup follows the recipes from &lt;a href="https://github.com/noonghunna/club-3090" target="_blank" rel="noopener"&gt;club-3090&lt;/a&gt;, a community project that tunes LLM serving for RTX 3090s and publishes measured numbers, so I think it is about as fast as these cards get. Its benchmarks for this configuration at a similar power limit match mine: about 110 tokens per second for prose, and up to about 195 for code, where speculative decoding guesses more tokens right. Most of Qwen&amp;rsquo;s output on the benchmark is reasoning, which runs at the prose speed.&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li id="fn:4"&gt;&#10;&lt;p&gt;The stock limit is 350 W. My cards sit in a &lt;a href="https://www.nijho.lt/post/local-ai-journey/"&gt;normal desktop case with normal fans&lt;/a&gt;, and I worry that running both at full power for hours would shorten their life. The cap is &lt;a href="https://github.com/basnijholt/dotfiles/blob/e63a3f341ff36b7b57bf31361c2844e1d8b78e95/configs/nixos/hosts/pc/nvidia-undervolt.nix#L1-L7" target="_blank" rel="noopener"&gt;a few lines in my NixOS configuration&lt;/a&gt;, and measurements from Puget Systems and r/LocalLLaMA put a 3090 at about 95% of its speed at 270 W. That is 77% of the power for 95% of the speed, so the cap also lowers my electricity cost per token.&amp;#160;&lt;a href="#fnref:4" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li id="fn:5"&gt;&#10;&lt;p&gt;The 3090 is still the value king for VRAM per dollar, and $750 badly understates what mine are worth. That is what I paid more than a year ago; today a used 3090 sells for about $1,500. Even at that price it costs $62.50 per GB of VRAM, the same as an RTX 5090 at its $2,000 launch price, which is not what a 5090 sells for today. The right number for this calculation is what I could sell my cards for, and at $1,500 each the break-even points rise by about two thirds: at 18 cents per kWh, the same-model case moves from 2.4 to 4.0 hours per day, and the Luna-efficient case from 8.5 to 14.2 hours per day.&amp;#160;&lt;a href="#fnref:5" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li id="fn:6"&gt;&#10;&lt;p&gt;While the cards are busy, they save what the API would have charged for the same work, minus the electricity at 700 W. While they are idle, the machine still draws about 150 W, of which the two GPUs with the model loaded take 85 W. The break-even point is the number of busy hours per day at which those savings cover the price of the cards, written off over three years, plus the idle power.&amp;#160;&lt;a href="#fnref:6" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li id="fn:7"&gt;&#10;&lt;p&gt;The M5 Max numbers come from the reader: Qwen3.8-Flash-Next at Q2 (about 2.7 bits per weight), about 45 tokens per second, 1,250 tokens per second of prefill, and about 90 W, on a $7,000 laptop. The RTX PRO 6000 numbers come from another reader, &lt;a href="https://www.reddit.com/r/LocalLLaMA/comments/1ww2jsu/comment/pdhs3ea/" target="_blank" rel="noopener"&gt;u/TheAILegend&lt;/a&gt;, who measured Qwen3.8-Flash-Next at NVFP4 on theirs. For a single request, it stays at about 150 tokens per second from 4,000 to 200,000 tokens of context, with about 10,000 tokens per second of prefill and 266 W for the GPU plus about 160 W for the rest of the PC. With 32 short requests in parallel, the card reaches 590 tokens per second and pays off in about two years, which works for short batch jobs. At the long contexts of this benchmark, parallel requests are slower than one, so the chart uses the single-request speed. I use $15,000, the middle of the card&amp;rsquo;s price range. The DGX Spark and the AMD machine run Qwen3.8-Flash-Next at about 4.5 bits. For their speed, I took Artificial Analysis&amp;rsquo;s measurements of Qwen3.8 27B on each machine and doubled them, because the owner of the AMD machine finds Qwen3.8-Flash-Next about twice as fast as Qwen3.8 27B. That gives about 55 tokens per second on the DGX Spark ($6,950, about 150 W) and 47 on the AMD Ryzen AI Max+ 395 ($4,000, the price Artificial Analysis lists, about 120 W). All of them are compared with GPT-6 Luna on its highest setting, which runs the whole benchmark for $122. The 3090s run Qwen3.8 27B, as in the rest of this post, and are compared with Luna on xhigh. Artificial Analysis measured Qwen3.8 27B on the AMD machine at 23 tokens per second, so one benchmark run takes it 134 days and about 390 kWh, against about 460 kWh on my 3090s. Running Qwen3.8-Flash-Next on the 3090s with CPU expert offload, which club-3090 measured at 37 tokens per second, does not change their result: one run then takes 82 days, and at 18 cents per kWh its electricity costs more than Luna&amp;rsquo;s $122 unless the whole machine draws less than about 350 W.&amp;#160;&lt;a href="#fnref:7" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;/div&gt;&#10;</description></item><item><title>Frontier-level dictation on your iPhone keyboard, self-hosted</title><link>https://www.nijho.lt/post/diction-agent-cli-qwen/</link><pubDate>Tue, 22 Sep 2026 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/diction-agent-cli-qwen/</guid><description>&lt;p&gt;On a recent month-long trip visiting family in Europe, I did a lot of work from my phone, using &lt;a href="https://www.nijho.lt/post/agentic-mobile-workflow/"&gt;my mobile coding workflow&lt;/a&gt;.&#10;It still boggles my mind that real, productive work from a phone is possible now.&#10;Most of that work is talking to coding agents, and the built-in iOS dictation is garbage for that.&#10;I had an iOS Shortcut that sent recordings to &lt;a href="https://github.com/basnijholt/agent-cli" target="_blank" rel="noopener"&gt;Agent CLI&lt;/a&gt;, &lt;a href="https://www.nijho.lt/post/auto-install-extras/"&gt;my local AI toolbox&lt;/a&gt;, running at home instead.&#10;The problem was that the recording screen takes over the whole display, so I could not see the thing I was commenting on.&#10;The transcript then landed in my clipboard, and I had to paste it myself.&lt;/p&gt;&#10;&lt;p&gt;So I threw my principles overboard and installed &lt;a href="https://wisprflow.ai" target="_blank" rel="noopener"&gt;Wispr Flow&lt;/a&gt;, a voice keyboard for iOS.&#10;I loved it instantly.&#10;iOS does not let keyboards use the microphone, so the keyboard&amp;rsquo;s button hands off to the Wispr Flow app, which keeps recording in the background.&#10;I was less happy about that, since it meant a closed-source app always had the microphone open and sent my voice to their servers.&#10;I was on the free plan, so I was probably the product.&#10;I also hit the free weekly word limit very quickly, and started looking for an alternative.&lt;/p&gt;&#10;&lt;p&gt;That is how I found &lt;a href="https://apps.apple.com/app/id6759807364" target="_blank" rel="noopener"&gt;Diction&lt;/a&gt;, which has since become my favorite app when I&amp;rsquo;m away from my laptop.&#10;It works the same way, background recording included.&#10;The difference is that it has a self-hosted mode with an open-source gateway, so my voice goes to a machine I own instead of their servers.&lt;/p&gt;&#10;&lt;p&gt;I run the &lt;a href="https://github.com/DictionLabs/Diction" target="_blank" rel="noopener"&gt;gateway&lt;/a&gt; on my home machine in the U.S., reachable only over my private network.&#10;The gateway streams audio to Agent CLI, which runs Alibaba&amp;rsquo;s &lt;a href="https://huggingface.co/Qwen/Qwen3-ASR-1.7B-hf" target="_blank" rel="noopener"&gt;&lt;code&gt;Qwen/Qwen3-ASR-1.7B-hf&lt;/code&gt;&lt;/a&gt; on an RTX 3090 behind an OpenAI-compatible transcription endpoint.&#10;There is no LLM cleanup step, so Diction&amp;rsquo;s Writing Style and Tones features do not work with this setup.&#10;I don&amp;rsquo;t miss them: Qwen&amp;rsquo;s raw transcription is already good enough for me.&lt;/p&gt;&#10;&lt;h2 id="1-why-qwen"&gt;1. Why Qwen? &lt;a class="anchor" href="#1-why-qwen" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;For the longest time I used &lt;a href="https://github.com/SYSTRAN/faster-whisper" target="_blank" rel="noopener"&gt;Faster-Whisper&lt;/a&gt; with Whisper large-v3.&#10;Whisper is still the default choice for most people, but many newer models beat it.&#10;I tried some of them, including NVIDIA&amp;rsquo;s &lt;a href="https://huggingface.co/nvidia/parakeet-tdt-0.6b-v3" target="_blank" rel="noopener"&gt;Parakeet&lt;/a&gt;.&#10;Parakeet is fast, but it cannot take custom instructions.&#10;Those instructions are how Diction&amp;rsquo;s &lt;strong&gt;My Words&lt;/strong&gt; feature biases a transcription toward specialized names.&lt;/p&gt;&#10;&lt;p&gt;At the start of September, I looked at the &lt;a href="https://huggingface.co/spaces/hf-audio/open_asr_leaderboard" target="_blank" rel="noopener"&gt;Hugging Face Open ASR Leaderboard&lt;/a&gt;, which tests speech-to-text models on the same recordings and ranks them.&#10;The score is the word error rate (WER): the share of words a model gets wrong, counting words it swaps, drops, or makes up.&#10;Lower is better, and a WER of 4% means about one wrong word in every 25.&#10;The test recordings range from audiobooks and podcasts to meetings and earnings calls, so the average says more than any single clean benchmark.&lt;/p&gt;&#10;&lt;p&gt;These are the averages on the public English test sets as of September 19, 2026:&lt;/p&gt;&#10;&lt;div class="table-wrap"&gt;&#10;&lt;table&gt;&#10;&lt;thead&gt;&#10;&lt;tr&gt;&#10;&lt;th&gt;Model&lt;/th&gt;&#10;&lt;th&gt;WER&lt;/th&gt;&#10;&lt;th&gt;Can I run it at home?&lt;/th&gt;&#10;&lt;/tr&gt;&#10;&lt;/thead&gt;&#10;&lt;tbody&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;Zoom Scribe v2 Pro&lt;/td&gt;&#10;&lt;td&gt;3.6%&lt;/td&gt;&#10;&lt;td&gt;No, paid API (#1 overall)&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;ElevenLabs Scribe v2&lt;/td&gt;&#10;&lt;td&gt;4.0%&lt;/td&gt;&#10;&lt;td&gt;No, paid API&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;strong&gt;Qwen3-ASR 1.7B&lt;/strong&gt;&lt;/td&gt;&#10;&lt;td&gt;&lt;strong&gt;4.3%&lt;/strong&gt;&lt;/td&gt;&#10;&lt;td&gt;&lt;strong&gt;Yes, Apache-2.0&lt;/strong&gt;&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;AssemblyAI Universal-3.5 Pro&lt;/td&gt;&#10;&lt;td&gt;4.3%&lt;/td&gt;&#10;&lt;td&gt;No, paid API&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;NVIDIA Parakeet TDT 0.6B v3&lt;/td&gt;&#10;&lt;td&gt;4.9%&lt;/td&gt;&#10;&lt;td&gt;Yes&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;OpenAI Whisper large-v3&lt;/td&gt;&#10;&lt;td&gt;5.8%&lt;/td&gt;&#10;&lt;td&gt;Yes&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;I picked the best model on the board whose weights you can download: Qwen3-ASR 1.7B.&#10;It lands within a point of the best paid API, which is what I mean by frontier-level.&#10;Since I maintain Agent CLI, I told an agent to add a Qwen backend, and shortly afterward &lt;a href="https://github.com/basnijholt/agent-cli/pull/636" target="_blank" rel="noopener"&gt;it had landed&lt;/a&gt; and was running on my server.&lt;/p&gt;&#10;&lt;p&gt;Agent CLI&amp;rsquo;s transcription server loads a model on the first request and unloads it after an idle timeout, so the model only takes VRAM while I use it.&#10;The same process serves an OpenAI-compatible API, which the Diction gateway talks to, and the &lt;a href="https://www.home-assistant.io/integrations/wyoming/" target="_blank" rel="noopener"&gt;Wyoming protocol&lt;/a&gt; that Home Assistant uses for local voice.&lt;/p&gt;&#10;&lt;h2 id="2-the-recipe"&gt;2. The recipe &lt;a class="anchor" href="#2-the-recipe" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;You need an NVIDIA GPU with the &lt;a href="https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html" target="_blank" rel="noopener"&gt;NVIDIA Container Toolkit&lt;/a&gt;, Docker Compose 2.30 or newer, and a private route from your iPhone to the server.&#10;I use Headscale, but Tailscale or plain WireGuard works just as well.&#10;Do not expose the gateway to the public internet.&lt;/p&gt;&#10;&lt;h3 id="write-the-compose-file"&gt;Write the Compose file &lt;a class="anchor" href="#write-the-compose-file" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;Save the following as &lt;code&gt;compose.yaml&lt;/code&gt; in an empty directory.&#10;Agent CLI&amp;rsquo;s CI publishes a CUDA image with the Transformers backend and Qwen as the default model, so there is nothing to build.&#10;The file points Diction&amp;rsquo;s gateway at Agent CLI&amp;rsquo;s transcription port.&#10;&lt;code&gt;WHISPER_TTL&lt;/code&gt; raises the idle timeout from the default five minutes to a day, so dictation rarely waits for a reload:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;services&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;asr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;ghcr.io/basnijholt/agent-cli-whisper:0.112.0-cuda-transformers&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;WHISPER_TTL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;86400&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;volumes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;model-cache:/home/whisper/.cache&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;gpus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;all&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;restart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;unless-stopped&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;gateway&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;dictionlabs/gateway:v14.0&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;depends_on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;asr&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;CUSTOM_BACKEND_URL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;http://asr:10301&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;CUSTOM_BACKEND_MODEL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Qwen/Qwen3-ASR-1.7B-hf&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;CUSTOM_BACKEND_CANONICAL_ID&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Qwen/Qwen3-ASR-1.7B-hf&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;CUSTOM_BACKEND_NEEDS_WAV&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;true&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;ports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="s2"&gt;&amp;#34;8080:8080&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;restart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;unless-stopped&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;volumes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;model-cache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="start-it-and-connect-the-app"&gt;Start it and connect the app &lt;a class="anchor" href="#start-it-and-connect-the-app" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;Start both services:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker compose up -d&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The first transcription downloads Qwen into the &lt;code&gt;model-cache&lt;/code&gt; volume, so that request can take a few minutes.&#10;&lt;code&gt;docker compose logs -f&lt;/code&gt; shows the progress.&lt;/p&gt;&#10;&lt;p&gt;On the iPhone, go to &lt;strong&gt;Settings → General → Keyboard → Keyboards → Add New Keyboard → Diction&lt;/strong&gt;.&#10;Tap Diction in the keyboard list, enable &lt;strong&gt;Allow Full Access&lt;/strong&gt;, and grant microphone access when prompted.&#10;Then open Diction, go to &lt;strong&gt;Preferences → Mode → Self-Hosted&lt;/strong&gt;, enter &lt;code&gt;http://&amp;lt;private-server-ip&amp;gt;:8080&lt;/code&gt;, and tap &lt;strong&gt;Test connection&lt;/strong&gt;.&lt;/p&gt;&#10;&lt;aside class="callout callout-note"&gt;&lt;svg class="ico" viewBox="0 0 256 256" aria-hidden="true"&gt;&lt;path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm16-40a8,8,0,0,1-8,8,16,16,0,0,1-16-16V128a8,8,0,0,1,0-16,16,16,0,0,1,16,16v40A8,8,0,0,1,144,176ZM112,84a12,12,0,1,1,12,12A12,12,0,0,1,112,84Z"/&gt;&lt;/svg&gt;&lt;div&gt;&lt;strong&gt;A small upstream detour:&lt;/strong&gt; Diction&amp;rsquo;s &lt;strong&gt;My Words&lt;/strong&gt; feature was not reaching the ASR backend during streaming, so I opened &lt;a href="https://github.com/DictionLabs/Diction/pull/20" target="_blank" rel="noopener"&gt;Diction PR #20&lt;/a&gt; to forward those words as the transcription prompt.&#10;With the fix, package names such as &lt;code&gt;PipeFunc&lt;/code&gt; and &lt;code&gt;MindRoom&lt;/code&gt; come back with the correct spelling and capitalization.&#10;It shipped in gateway v13.0, which is why the Compose file needs at least that version.&lt;/div&gt;&lt;/aside&gt;&#10;&lt;p&gt;On my machine, Qwen3-ASR 1.7B uses about 4.7 GiB of GPU memory and roughly 3 GiB of system memory while loaded.&lt;/p&gt;&#10;&lt;h2 id="3-the-bar-test"&gt;3. The bar test &lt;a class="anchor" href="#3-the-bar-test" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;A day later, I met a friend in a bar in the Netherlands and wanted to let him try it.&lt;/p&gt;&#10;&lt;p&gt;He was already on my tailnet for some of my other self-hosted services, so his phone only needed an ACL change to reach the gateway.&#10;I opened the terminal app on my phone, connected to an agent, and dictated that change.&#10;In less than a minute, Diction was connected and working on his phone.&#10;His speech crossed the Atlantic, was transcribed by Qwen on my home GPU, and appeared back in the app with barely any delay.&lt;/p&gt;&#10;&lt;p&gt;That moment sold the whole setup to me.&#10;I was in a bar without a laptop, and I used dictation to grant access to the dictation service.&lt;/p&gt;&#10;</description></item><item><title>SOTA RAG &amp; Memory without the database: Files, Git, and simple folders</title><link>https://www.nijho.lt/post/file-based-rag-memory/</link><pubDate>Wed, 10 Dec 2025 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/file-based-rag-memory/</guid><description>&lt;details class="toc" open&gt;&lt;summary&gt;Table of contents&lt;/summary&gt;&lt;nav id="TableOfContents"&gt;&#10; &lt;ul&gt;&#10; &lt;li&gt;&lt;a href="#1-the-amnesia-problem"&gt;1. The amnesia problem&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#2-learning-from-failure-ai-journal"&gt;2. Learning from failure: AI Journal&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#3-how-i-studied-sota-clone-read-re-implement"&gt;3. How I studied SOTA: clone, read, re-implement&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#4-the-dependency-problem-why-onnx-over-pytorch"&gt;4. The dependency problem: why ONNX over PyTorch&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#5-rag-just-let-me-grep-it-kind-of"&gt;5. RAG: just let me grep it (kind of)&lt;/a&gt;&#10; &lt;ul&gt;&#10; &lt;li&gt;&lt;a href="#the-architecture-openai-compatibility"&gt;The architecture: OpenAI compatibility&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#two-stage-retrieval-the-sota-approach"&gt;Two-stage retrieval: the SOTA approach&lt;/a&gt;&lt;/li&gt;&#10; &lt;/ul&gt;&#10; &lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#6-memory-files-over-databases"&gt;6. Memory: files over databases&lt;/a&gt;&#10; &lt;ul&gt;&#10; &lt;li&gt;&lt;a href="#git-integration-versioning-memory-"&gt;Git integration: versioning memory 🕰️&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#the-reconciliation-loop"&gt;The reconciliation loop&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#the-tiny-detail-that-broke-everything"&gt;The tiny detail that broke everything&lt;/a&gt;&lt;/li&gt;&#10; &lt;/ul&gt;&#10; &lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#7-why-building-this-was-necessary"&gt;7. Why building this was necessary&lt;/a&gt;&#10; &lt;ul&gt;&#10; &lt;li&gt;&lt;a href="#privacy"&gt;Privacy&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#integration"&gt;Integration&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#dependencies"&gt;Dependencies&lt;/a&gt;&lt;/li&gt;&#10; &lt;/ul&gt;&#10; &lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#8-try-it-yourself"&gt;8. Try it yourself&lt;/a&gt;&#10; &lt;ul&gt;&#10; &lt;li&gt;&lt;a href="#try-memory"&gt;Try Memory&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#try-rag"&gt;Try RAG&lt;/a&gt;&lt;/li&gt;&#10; &lt;/ul&gt;&#10; &lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#9-what-i-learned"&gt;9. What I learned&lt;/a&gt;&lt;/li&gt;&#10; &lt;/ul&gt;&#10;&lt;/nav&gt;&lt;/details&gt;&#10;&#10;&lt;h2 id="1-the-amnesia-problem"&gt;1. The amnesia problem &lt;a class="anchor" href="#1-the-amnesia-problem" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;If you&amp;rsquo;ve read my posts on &lt;a href="https://www.nijho.lt/post/agentic-coding/"&gt;agentic coding&lt;/a&gt; or my &lt;a href="https://www.nijho.lt/post/local-ai-journey/"&gt;local AI journey&lt;/a&gt;, you know I am all-in on using AI to control my computer and write software.&#10;But one critical piece was missing: &lt;strong&gt;Context&lt;/strong&gt;.&lt;/p&gt;&#10;&lt;p&gt;LLMs are powerful but stateless.&#10;Every time I start a new chat, the model forgets who I am, what I&amp;rsquo;m working on, and that I prefer &lt;a href="https://www.nijho.lt/post/functional-python/"&gt;functional Python code&lt;/a&gt; over object-oriented styles.&#10;Worse, it also forgets the thing I just told it five minutes ago in another window.&lt;/p&gt;&#10;&lt;p&gt;The standard solution to this is &lt;a href="https://en.wikipedia.org/wiki/Retrieval-augmented_generation" target="_blank" rel="noopener"&gt;RAG (Retrieval-Augmented Generation)&lt;/a&gt; and &lt;a href="https://arxiv.org/abs/2310.08560" target="_blank" rel="noopener"&gt;Memory&lt;/a&gt;.&#10;But when I looked at the available tools, I hit a wall.&lt;/p&gt;&#10;&lt;p&gt;Most solutions fell into two buckets:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;&lt;strong&gt;SaaS / Web UIs:&lt;/strong&gt; Upload your files to a cloud service or use a specific web chat interface that locks you in.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Developer Libraries:&lt;/strong&gt; &amp;ldquo;Just use LangChain/LlamaIndex!&amp;rdquo; they say. Great, now I have to write a Python script every time I want to ask a question about a PDF?&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;I didn&amp;rsquo;t want a library. I didn&amp;rsquo;t want a web UI.&#10;&lt;strong&gt;I wanted a folder.&lt;/strong&gt;&#10;I wanted to drop a file into &lt;code&gt;~/my-docs&lt;/code&gt;, and have my AI instantly know about it.&#10;And I wanted my AI&amp;rsquo;s &amp;ldquo;memory&amp;rdquo; to be a file I can edit, not a hidden vector in a database I can&amp;rsquo;t inspect.&lt;/p&gt;&#10;&lt;p&gt;So I decided to build my own file-based solution.&lt;/p&gt;&#10;&lt;aside class="callout callout-note"&gt;&lt;svg class="ico" viewBox="0 0 256 256" aria-hidden="true"&gt;&lt;path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm16-40a8,8,0,0,1-8,8,16,16,0,0,1-16-16V128a8,8,0,0,1,0-16,16,16,0,0,1,16,16v40A8,8,0,0,1,144,176ZM112,84a12,12,0,1,1,12,12A12,12,0,0,1,112,84Z"/&gt;&lt;/svg&gt;&lt;div&gt;&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; I built &lt;a href="https://github.com/basnijholt/agent-cli" target="_blank" rel="noopener"&gt;&lt;code&gt;agent-cli&lt;/code&gt;&lt;/a&gt; features that combine two ideas:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;&lt;strong&gt;File-based storage:&lt;/strong&gt; Documents in a folder, memories as Markdown files with Git versioning.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;OpenAI-compatible proxy:&lt;/strong&gt; Works with &lt;em&gt;any&lt;/em&gt; OpenAI-compatible tool—Cursor, Cline, Open WebUI, LibreChat, Lobe Chat, your terminal—without lock-in.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;Tools like Open WebUI have built-in RAG, but that forces you to use only Open WebUI.&#10;My proxy means I can switch tools whenever I want, and they all share the same memory and document index.&lt;/p&gt;&#10;&lt;/div&gt;&lt;/aside&gt;&#10;&lt;h2 id="2-learning-from-failure-ai-journal"&gt;2. Learning from failure: AI Journal &lt;a class="anchor" href="#2-learning-from-failure-ai-journal" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;This isn&amp;rsquo;t my first attempt at building a memory system.&lt;/p&gt;&#10;&lt;p&gt;A few months ago, I built &lt;a href="https://github.com/basnijholt/aijournal" target="_blank" rel="noopener"&gt;&lt;strong&gt;AI Journal&lt;/strong&gt;&lt;/a&gt;—an ambitious local-first journaling app where an AI would read your entries and build a &amp;ldquo;living self-model&amp;rdquo; of who you are.&#10;It had sophisticated features: a structured &amp;ldquo;persona core,&amp;rdquo; typed &amp;ldquo;claim atoms&amp;rdquo; with evidence links, four organizational layers (L1→L4), recency-aware scoring with decay functions, and Git-based time travel to &amp;ldquo;ask questions of your younger self.&amp;rdquo;&lt;/p&gt;&#10;&lt;p&gt;The architecture seemed solid.&#10;The problem was simpler: &lt;strong&gt;local models aren&amp;rsquo;t good enough yet.&lt;/strong&gt;&lt;/p&gt;&#10;&lt;p&gt;I designed the whole thing to run entirely on Ollama with my dual RTX 3090s.&#10;But even with a 48GB of VRAM, the models couldn&amp;rsquo;t reliably extract structured claims, handle the multi-step reconciliation, or maintain coherence across long conversations.&#10;Whether the design was right, I still don&amp;rsquo;t know—but the execution definitely hit the wall of current local LLM capabilities.&lt;/p&gt;&#10;&lt;p&gt;The lesson: I tried to build the entire system at once without validating the core concepts and individual ideas first.&#10;This time, I took a different approach: &lt;strong&gt;tackle it problem by problem.&lt;/strong&gt;&#10;Instead of designing a complete system upfront, I would validate each piece—chunking, retrieval, re-ranking, memory reconciliation—before combining them.&#10;The experience and UX would be my own, but the algorithms would be adapted from established projects.&lt;/p&gt;&#10;&lt;h2 id="3-how-i-studied-sota-clone-read-re-implement"&gt;3. How I studied SOTA: clone, read, re-implement &lt;a class="anchor" href="#3-how-i-studied-sota-clone-read-re-implement" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;My approach was more methodical this time.&#10;I cloned five major frameworks into the same workspace as &lt;code&gt;agent-cli&lt;/code&gt;:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;&lt;a href="https://github.com/run-llama/llama_index" target="_blank" rel="noopener"&gt;LlamaIndex&lt;/a&gt;&lt;/strong&gt;: The OG RAG framework&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;&lt;a href="https://github.com/langchain-ai/langchain" target="_blank" rel="noopener"&gt;LangChain&lt;/a&gt;&lt;/strong&gt;: The kitchen-sink approach&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;&lt;a href="https://github.com/letta-ai/letta" target="_blank" rel="noopener"&gt;Letta&lt;/a&gt;&lt;/strong&gt; (formerly MemGPT): Stateful agents with long-term memory&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;&lt;a href="https://github.com/mem0ai/mem0" target="_blank" rel="noopener"&gt;Mem0&lt;/a&gt;&lt;/strong&gt;: Focused specifically on memory extraction&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;&lt;a href="https://github.com/pydantic/pydantic-ai" target="_blank" rel="noopener"&gt;PydanticAI&lt;/a&gt;&lt;/strong&gt;: Clean, typed agent framework&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;I didn&amp;rsquo;t read through all this code myself—that would take months.&#10;Instead, I used AI coding agents to search the codebases and write summaries of the technical implementation details:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;Gemini CLI&lt;/strong&gt; (Gemini 3.0 Pro)&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Claude Code&lt;/strong&gt; (Opus 4.5)&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Codex CLI&lt;/strong&gt; (GPT-5.1-Codex-Max)&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;I fed those summaries into &lt;strong&gt;ChatGPT 5 Pro&lt;/strong&gt; together with my high level plan of what I wanted to create, and it created an implementation plan.&#10;Then I took that plan back to the coding agents for implementation.&#10;I personally found Opus 4.5 the best coding model, but GPT‑5.1 Codex Max was the best at reviewing the code and actually identifying critical bugs.&lt;/p&gt;&#10;&lt;p&gt;For validation, I used &lt;a href="https://github.com/basnijholt/clip-files" target="_blank" rel="noopener"&gt;&lt;code&gt;clip-files&lt;/code&gt;&lt;/a&gt; to copy the agent-cli source code plus the architecture documents (like &lt;a href="https://github.com/basnijholt/agent-cli/blob/main/docs/architecture/memory.md" target="_blank" rel="noopener"&gt;memory.md&lt;/a&gt; and &lt;a href="https://github.com/basnijholt/agent-cli/blob/main/docs/architecture/rag.md" target="_blank" rel="noopener"&gt;rag.md&lt;/a&gt;) into ChatGPT 5 Pro to validate the approach.&lt;/p&gt;&#10;&lt;p&gt;The AI did the searching and implementing; I did the understanding and decision-making.&lt;/p&gt;&#10;&lt;aside class="callout callout-note"&gt;&lt;svg class="ico" viewBox="0 0 256 256" aria-hidden="true"&gt;&lt;path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm16-40a8,8,0,0,1-8,8,16,16,0,0,1-16-16V128a8,8,0,0,1,0-16,16,16,0,0,1,16,16v40A8,8,0,0,1,144,176ZM112,84a12,12,0,1,1,12,12A12,12,0,0,1,112,84Z"/&gt;&lt;/svg&gt;&lt;div&gt;&lt;strong&gt;The key insight:&lt;/strong&gt; Existing frameworks are incredibly over-engineered for my use case, but the &lt;em&gt;core algorithms&lt;/em&gt; are gold.&#10;My job was to extract the essence and re-implement it cleanly.&lt;/div&gt;&lt;/aside&gt;&#10;&lt;h2 id="4-the-dependency-problem-why-onnx-over-pytorch"&gt;4. The dependency problem: why ONNX over PyTorch &lt;a class="anchor" href="#4-the-dependency-problem-why-onnx-over-pytorch" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;One thing became immediately clear when going through these frameworks: &lt;strong&gt;they&amp;rsquo;re massive&lt;/strong&gt;.&lt;/p&gt;&#10;&lt;p&gt;Installing LlamaIndex with all its dependencies pulls in PyTorch.&#10;PyTorch alone is &lt;strong&gt;2-3GB&lt;/strong&gt;.&#10;Add transformers, sentence-transformers, and the kitchen sink, and you&amp;rsquo;re looking at an &lt;strong&gt;8GB+ environment&lt;/strong&gt; just to ask questions about some PDFs.&lt;/p&gt;&#10;&lt;p&gt;This felt excessive for a CLI tool.&lt;/p&gt;&#10;&lt;p&gt;One of my core design goals was &lt;strong&gt;minimal dependencies&lt;/strong&gt;.&#10;I wanted &lt;code&gt;agent-cli&lt;/code&gt; to install in seconds, not minutes.&#10;I wanted it to work on my Mac, my NixOS server, and my friend&amp;rsquo;s laptop without downloading half the internet.&lt;/p&gt;&#10;&lt;p&gt;The solution: &lt;strong&gt;ONNX Runtime&lt;/strong&gt;.&lt;/p&gt;&#10;&lt;p&gt;ONNX Runtime is ~200MB and runs inference on basically anything—CPU, CUDA, even Apple Silicon.&#10;For embeddings, I use &lt;a href="https://developers.googleblog.com/introducing-embeddinggemma/" target="_blank" rel="noopener"&gt;&lt;code&gt;embeddinggemma&lt;/code&gt;&lt;/a&gt; running locally via &lt;a href="https://www.nijho.lt/post/llama-nixos/"&gt;llama.cpp&lt;/a&gt;.&#10;For re-ranking, I use a cross-encoder exported to ONNX (&lt;code&gt;Xenova/ms-marco-MiniLM-L-6-v2&lt;/code&gt;).&lt;/p&gt;&#10;&lt;p&gt;The result: &lt;code&gt;pip install agent-cli[rag]&lt;/code&gt; takes about 30 seconds and adds ~300MB.&#10;The key insight: embeddings and re-ranking are cheap operations.&#10;You don&amp;rsquo;t need a GPU for them—CPU is perfectly fine.&#10;PyTorch with CUDA support can easily reach 2-3GB+; ONNX Runtime is ~200MB and runs anywhere.&#10;Since I&amp;rsquo;m not training models, just running inference on small encoders, ONNX is the obvious choice.&lt;/p&gt;&#10;&lt;h2 id="5-rag-just-let-me-grep-it-kind-of"&gt;5. RAG: just let me grep it (kind of) &lt;a class="anchor" href="#5-rag-just-let-me-grep-it-kind-of" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;My requirement for RAG was simple: &lt;strong&gt;Files on disk are the source of truth.&lt;/strong&gt;&lt;/p&gt;&#10;&lt;p&gt;I didn&amp;rsquo;t want to manage an ingestion pipeline or trigger APIs.&#10;I implemented a &lt;a href="https://github.com/basnijholt/agent-cli/blob/main/docs/architecture/rag.md" target="_blank" rel="noopener"&gt;RAG proxy&lt;/a&gt; that uses OS-level file watchers (&lt;code&gt;watchfiles&lt;/code&gt;).&#10;Here is the workflow:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;I drop a PDF, Markdown, or Python file into &lt;code&gt;~/rag_docs&lt;/code&gt;.&lt;/li&gt;&#10;&lt;li&gt;The system detects the &lt;code&gt;Create&lt;/code&gt; event.&lt;/li&gt;&#10;&lt;li&gt;It chunks the text and embeds it into a local ChromaDB instance.&lt;/li&gt;&#10;&lt;li&gt;It&amp;rsquo;s immediately searchable.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;When moving or deleting files, the system updates the index accordingly.&lt;/p&gt;&#10;&lt;h3 id="the-architecture-openai-compatibility"&gt;The architecture: OpenAI compatibility &lt;a class="anchor" href="#the-architecture-openai-compatibility" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;The &amp;ldquo;secret sauce&amp;rdquo; isn&amp;rsquo;t the retrieval itself; it&amp;rsquo;s how I expose it.&#10;Instead of building a custom chat app, I built an &lt;strong&gt;OpenAI-compatible proxy&lt;/strong&gt;.&lt;/p&gt;&#10;&lt;div class="mermaid"&gt;flowchart LR&#10; Client["Client&lt;br/&gt;(Cursor, Open WebUI)"] --&gt; Retrieve&#10;&#10; subgraph Proxy ["RAG Proxy"]&#10; direction TB&#10; Retrieve --&gt; Augment["Augment &amp; Forward"]&#10; Chroma[("ChromaDB")] -.-&gt; Retrieve&#10; Watcher["File Watcher"] --&gt; Chroma&#10; end&#10;&#10; Augment --&gt; Upstream["Upstream LLM"]&lt;/div&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Point any OpenAI-compatible tool at your local proxy&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;export&lt;/span&gt; &lt;span class="nv"&gt;OPENAI_BASE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://localhost:8000/v1&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That&amp;rsquo;s it. Any tool that supports custom OpenAI endpoints—Cursor, Cline, Open WebUI, LibreChat—instantly gains the ability to &amp;ldquo;read&amp;rdquo; my document folder.&lt;/p&gt;&#10;&lt;h3 id="two-stage-retrieval-the-sota-approach"&gt;Two-stage retrieval: the SOTA approach &lt;a class="anchor" href="#two-stage-retrieval-the-sota-approach" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;I looked at LlamaIndex here.&#10;Standard &amp;ldquo;semantic search&amp;rdquo; (using just embeddings/bi-encoders) is often disappointing.&#10;It finds text that &lt;em&gt;looks&lt;/em&gt; similar but isn&amp;rsquo;t actually relevant.&lt;/p&gt;&#10;&lt;p&gt;A common pattern in modern RAG systems is &lt;strong&gt;Two-Stage Retrieval&lt;/strong&gt;.&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;&lt;strong&gt;Fast Stage:&lt;/strong&gt; Retrieve 3x candidates using standard embeddings (&lt;code&gt;text-embedding-3-small&lt;/code&gt; or local models like &lt;a href="https://developers.googleblog.com/introducing-embeddinggemma/" target="_blank" rel="noopener"&gt;&lt;code&gt;embeddinggemma&lt;/code&gt;&lt;/a&gt;).&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Smart Stage:&lt;/strong&gt; Use a &lt;strong&gt;Cross-Encoder&lt;/strong&gt; to re-rank the results.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;A cross-encoder looks at the query and the document &lt;em&gt;together&lt;/em&gt; and outputs a relevance score.&#10;It&amp;rsquo;s computationally heavier, but since I&amp;rsquo;m running this locally on my RTX 3090 (or even CPU via ONNX), the 50ms latency penalty is worth it for results that are actually useful.&lt;/p&gt;&#10;&lt;p&gt;This isn&amp;rsquo;t a new idea, but implementing it cleanly with ONNX avoided the huge dependency tax.&lt;/p&gt;&#10;&lt;h2 id="6-memory-files-over-databases"&gt;6. Memory: files over databases &lt;a class="anchor" href="#6-memory-files-over-databases" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;This is where my approach diverges most from standard tools like Mem0.&#10;Most memory systems treat memories as database rows.&#10;If the AI remembers &amp;ldquo;User likes pizza,&amp;rdquo; that&amp;rsquo;s a vector in a DB.&#10;If it hallucinates and records &amp;ldquo;User hates pizza,&amp;rdquo; good luck fixing it.&#10;You need to write a script to query the DB and delete the row.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;I prefer data I can inspect.&lt;/strong&gt;&lt;/p&gt;&#10;&lt;p&gt;My memory system stores every fact as a &lt;strong&gt;Markdown file&lt;/strong&gt; with YAML front matter.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-markdown" data-lang="markdown"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nn"&gt;---&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;550e8400-e29b-41d4-a716-446655440000&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;memory&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;2025-12-15T10:00:00Z&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;conversation_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;default&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nn"&gt;---&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;The user prefers using &amp;#39;uv&amp;#39; for Python package management.&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="git-integration-versioning-memory-"&gt;Git integration: versioning memory 🕰️ &lt;a class="anchor" href="#git-integration-versioning-memory-" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;Because memories are just files, I added a feature that I haven&amp;rsquo;t seen anywhere else: &lt;strong&gt;Automatic Git Versioning&lt;/strong&gt;.&lt;/p&gt;&#10;&lt;p&gt;Every time the AI learns a new fact, updates an old one, or deletes a contradiction, the system automatically commits the change to a local git repository.&lt;/p&gt;&#10;&lt;p&gt;This leads to incredible capabilities:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;&lt;strong&gt;Inspectability:&lt;/strong&gt; I can &lt;code&gt;git log&lt;/code&gt; and see exactly &lt;em&gt;when&lt;/em&gt; the AI learned something.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Correction:&lt;/strong&gt; If the AI learns something wrong, I open the markdown file in VS Code, edit it, and save.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Reversion:&lt;/strong&gt; Agent hallucinated wildly yesterday? &lt;code&gt;git reset --hard HEAD~1&lt;/code&gt;.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h3 id="the-reconciliation-loop"&gt;The reconciliation loop &lt;a class="anchor" href="#the-reconciliation-loop" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;The logic for storing memories isn&amp;rsquo;t just &amp;ldquo;append to file.&amp;rdquo;&#10;Digging into Mem0 (and Letta) showed me that memory systems need active &lt;strong&gt;reconciliation&lt;/strong&gt;.&#10;When I say &amp;ldquo;Actually, I switched to nix instead of uv,&amp;rdquo; the system:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;&lt;strong&gt;Retrieves&lt;/strong&gt; existing memories about package managers.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Decides&lt;/strong&gt; (using a lightweight LLM) whether to &lt;code&gt;ADD&lt;/code&gt;, &lt;code&gt;UPDATE&lt;/code&gt;, or &lt;code&gt;DELETE&lt;/code&gt;.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Executes&lt;/strong&gt; the file operation (moving the old fact to a &lt;code&gt;deleted/&lt;/code&gt; folder for audit trails).&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;This keeps the memory bank clean and contradictory-free, unlike simple vector stores that just accumulate conflicting junk over time.&lt;/p&gt;&#10;&lt;h3 id="the-tiny-detail-that-broke-everything"&gt;The tiny detail that broke everything &lt;a class="anchor" href="#the-tiny-detail-that-broke-everything" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;After implementing the memory system—mostly modeled after Mem0—I noticed it didn&amp;rsquo;t work as well.&#10;After a lot of debugging, I found a tiny difference: I was asking the model for structured data via a tool call, whereas Mem0 uses freeform JSON that the LLM returns directly.&lt;/p&gt;&#10;&lt;p&gt;Since I use PydanticAI, fixing this was a one-line change:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;model_cfg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;system_prompt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;UPDATE_MEMORY_PROMPT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;output_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;MemoryDecision&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;output_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;PromptedOutput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;MemoryDecision&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt; &lt;span class="c1"&gt;# JSON mode instead of tool calls&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;retries&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This is exactly why reading the source code matters—not just the algorithms, but the tiny details that make them work.&lt;/p&gt;&#10;&lt;h2 id="7-why-building-this-was-necessary"&gt;7. Why building this was necessary &lt;a class="anchor" href="#7-why-building-this-was-necessary" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;You might ask: &amp;ldquo;Bas, why write thousands of lines of code for this? Why not just use ChatGPT&amp;rsquo;s memory or install LangChain?&amp;rdquo;&lt;/p&gt;&#10;&lt;p&gt;Three reasons: &lt;strong&gt;Privacy&lt;/strong&gt;, &lt;strong&gt;Integration&lt;/strong&gt;, and &lt;strong&gt;Dependencies&lt;/strong&gt;.&lt;/p&gt;&#10;&lt;h3 id="privacy"&gt;Privacy &lt;a class="anchor" href="#privacy" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;I want to feed my AI my financial records, my health data, and my private journals.&#10;I am not comfortable sending that to OpenAI&amp;rsquo;s long-term storage or some VC-backed startup&amp;rsquo;s vector cloud.&#10;With &lt;code&gt;agent-cli&lt;/code&gt;, the vectors live in &lt;code&gt;~/.cache/agent-cli/chroma&lt;/code&gt;, the files live in &lt;code&gt;~/documents&lt;/code&gt;, and the only thing leaving my machine is the specific context needed for a specific query (if I use a cloud model) or nothing at all (if I use Ollama/&lt;a href="https://www.nijho.lt/post/llama-nixos/"&gt;llama.cpp&lt;/a&gt;).&lt;/p&gt;&#10;&lt;h3 id="integration"&gt;Integration &lt;a class="anchor" href="#integration" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;By building this as an API proxy, I solved the &amp;ldquo;fragmented tools&amp;rdquo; problem.&#10;Any tool that supports custom OpenAI-compatible endpoints works out of the box:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;&lt;code&gt;agent-cli chat&lt;/code&gt;&lt;/strong&gt; in the terminal&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Cursor&lt;/strong&gt; or &lt;strong&gt;Cline&lt;/strong&gt; for agentic coding&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;&lt;a href="https://openwebui.com/" target="_blank" rel="noopener"&gt;Open WebUI&lt;/a&gt;&lt;/strong&gt;, &lt;strong&gt;&lt;a href="https://www.librechat.ai/" target="_blank" rel="noopener"&gt;LibreChat&lt;/a&gt;&lt;/strong&gt;, or &lt;strong&gt;&lt;a href="https://lobechat.com/" target="_blank" rel="noopener"&gt;Lobe Chat&lt;/a&gt;&lt;/strong&gt; in the browser&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Because my system speaks &amp;ldquo;OpenAI,&amp;rdquo; all of these tools share the &lt;em&gt;same&lt;/em&gt; memory and the &lt;em&gt;same&lt;/em&gt; document index.&#10;If I tell the terminal agent &amp;ldquo;I&amp;rsquo;m working on Project X,&amp;rdquo; and then switch to Cursor, Cursor knows about Project X.&#10;I can try a new chat UI tomorrow without losing anything—just point it to &lt;code&gt;localhost:8000&lt;/code&gt;.&lt;/p&gt;&#10;&lt;h3 id="dependencies"&gt;Dependencies &lt;a class="anchor" href="#dependencies" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;To me, it just doesn&amp;rsquo;t feel right that asking questions about a PDF requires downloading 8GB of PyTorch.&#10;By choosing ONNX Runtime over the PyTorch ecosystem, I kept the entire install lightweight and fast.&#10;This matters when you want to run the system on a cheap VPS or your friend&amp;rsquo;s laptop.&lt;/p&gt;&#10;&lt;h2 id="8-try-it-yourself"&gt;8. Try it yourself &lt;a class="anchor" href="#8-try-it-yourself" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;The entire system is open-source and available in &lt;a href="https://github.com/basnijholt/agent-cli" target="_blank" rel="noopener"&gt;&lt;code&gt;agent-cli&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;h3 id="try-memory"&gt;Try Memory &lt;a class="anchor" href="#try-memory" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;Get an LLM that remembers you using &lt;a href="https://ollama.com" target="_blank" rel="noopener"&gt;Ollama&lt;/a&gt;.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 1. Pull models and start proxy&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ollama pull embeddinggemma:300m &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; ollama pull gpt-oss:20b&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;uvx -p 3.13 --from &lt;span class="s2"&gt;&amp;#34;agent-cli[memory]&amp;#34;&lt;/span&gt; agent-cli memory proxy &lt;span class="se"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --memory-path ./my-memories &lt;span class="se"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --openai-base-url http://localhost:11434/v1 &lt;span class="se"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --embedding-model embeddinggemma:300m&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 2. Chat from the terminal (env vars point to proxy)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;export&lt;/span&gt; &lt;span class="nv"&gt;OPENAI_BASE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://localhost:8100/v1 &lt;span class="nv"&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;dummy&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Tell it something about yourself&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;uvx openai api chat.completions.create -m gpt-oss:20b -g user &lt;span class="s2"&gt;&amp;#34;My name is Alice and I love hiking&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Later, ask if it remembers&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;uvx openai api chat.completions.create -m gpt-oss:20b -g user &lt;span class="s2"&gt;&amp;#34;What&amp;#39;s my name?&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="try-rag"&gt;Try RAG &lt;a class="anchor" href="#try-rag" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;Chat with your documents.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 1. Start the RAG proxy (runs in foreground)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;uvx -p 3.13 --from &lt;span class="s2"&gt;&amp;#34;agent-cli[rag]&amp;#34;&lt;/span&gt; agent-cli rag-proxy &lt;span class="se"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --docs-folder ./my-docs &lt;span class="se"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --openai-base-url http://localhost:11434/v1 &lt;span class="se"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --embedding-model embeddinggemma:300m&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 2. Chat with your docs&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;OPENAI_BASE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://localhost:8000/v1 &lt;span class="nv"&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;dummy &lt;span class="se"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; uvx openai api chat.completions.create &lt;span class="se"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -m gpt-oss:20b &lt;span class="se"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -g user &lt;span class="s2"&gt;&amp;#34;What does my documentation say about X?&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="9-what-i-learned"&gt;9. What I learned &lt;a class="anchor" href="#9-what-i-learned" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;This journey reinforced a few beliefs:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;&lt;strong&gt;Validate components first.&lt;/strong&gt; AI Journal failed not because the ideas were wrong, but because I tried to build a complete system before validating individual components. This time I tackled problems one at a time—chunking, retrieval, re-ranking, memory reconciliation—and only combined them once each piece worked.&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;&lt;strong&gt;Study implementations, not just papers.&lt;/strong&gt; Reading about two-stage retrieval is one thing. Reading how LlamaIndex actually implements it—the edge cases, the defaults, the performance tradeoffs—is far more valuable. Clone the repos. Read (or make AI read) the code.&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;&lt;strong&gt;Dependencies matter.&lt;/strong&gt; A 30-second install versus a 10-minute download changes whether someone actually tries your tool. I am happy with the ONNX Runtime over PyTorch choice.&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;&lt;strong&gt;Files are the ultimate API.&lt;/strong&gt; By stripping away the complex databases and proprietary interfaces, I ended up with a system that is robust, version-controllable, and surprisingly simple to reason about.&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;&lt;strong&gt;Use the best model for each job.&lt;/strong&gt; I used 4 different AI models during development, each with different strengths. Gemini 3.0 Pro, Opus 4.5, GPT-5.1-Codex Max. I used all of them during the implementation phase, but each one has its own strengths. Fore example, Opus 4.5 was the best coding model, but GPT-5.1 Codex Max was the best at reviewing the code and actually identifying critical bugs (use the built-in &lt;code&gt;/review&lt;/code&gt; feature).&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;/ol&gt;&#10;</description></item><item><title>Switching from Ollama to llama-swap + llama.cpp on NixOS: the power user's choice 🦙</title><link>https://www.nijho.lt/post/llama-nixos/</link><pubDate>Sat, 29 Nov 2025 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/llama-nixos/</guid><description>&lt;details class="toc" open&gt;&lt;summary&gt;Table of contents&lt;/summary&gt;&lt;nav id="TableOfContents"&gt;&#10; &lt;ul&gt;&#10; &lt;li&gt;&lt;a href="#the-draft-i-never-published"&gt;The draft I never published&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#the-original-trigger-broken-models"&gt;The original trigger: broken models&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#the-catalyst-dual-gpus-and-control"&gt;The catalyst: dual GPUs and control&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#api-compatibility-and-the-switch"&gt;API compatibility and the &amp;ldquo;switch&amp;rdquo;&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#my-nixos-setup"&gt;My NixOS setup&lt;/a&gt;&#10; &lt;ul&gt;&#10; &lt;li&gt;&lt;a href="#compiler-flags-matter"&gt;Compiler flags matter&lt;/a&gt;&lt;/li&gt;&#10; &lt;/ul&gt;&#10; &lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#what-i-gained-and-lost"&gt;What I gained (and lost)&lt;/a&gt;&#10; &lt;ul&gt;&#10; &lt;li&gt;&lt;a href="#advantages-of-llama-swap--llamacpp"&gt;Advantages of llama-swap + llama.cpp&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#the-trade-off"&gt;The trade-off&lt;/a&gt;&lt;/li&gt;&#10; &lt;/ul&gt;&#10; &lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#conclusion-maturing-as-an-ai-hobbyist"&gt;Conclusion: maturing as an AI hobbyist&lt;/a&gt;&lt;/li&gt;&#10; &lt;/ul&gt;&#10;&lt;/nav&gt;&lt;/details&gt;&#10;&#10;&lt;h2 id="the-draft-i-never-published"&gt;The draft I never published &lt;a class="anchor" href="#the-draft-i-never-published" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;I started writing this blog post four months ago.&#10;Back then, I was frustrated because a specific model I wanted to try—&lt;code&gt;gpt-oss-20b&lt;/code&gt;—was completely broken in &lt;a href="https://ollama.com/" target="_blank" rel="noopener"&gt;Ollama&lt;/a&gt;.&#10;I went down the rabbit hole, set up &lt;a href="https://github.com/mostlygeek/llama-swap" target="_blank" rel="noopener"&gt;&lt;code&gt;llama-swap&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://github.com/ggml-org/llama.cpp" target="_blank" rel="noopener"&gt;&lt;code&gt;llama.cpp&lt;/code&gt;&lt;/a&gt;, got the model working perfectly, wrote half this post&amp;hellip; and then I stopped.&lt;/p&gt;&#10;&lt;p&gt;Why? &lt;strong&gt;Laziness.&lt;/strong&gt;&#10;Once the novelty of that specific model wore off, I drifted back to Ollama.&#10;The real convenience of Ollama was never the &lt;code&gt;ollama run&lt;/code&gt; command—I almost exclusively interact with my models via the API using my own tools like &lt;a href="https://github.com/basnijholt/agent-cli" target="_blank" rel="noopener"&gt;agent-cli&lt;/a&gt;.&#10;The convenience was &lt;code&gt;ollama pull&lt;/code&gt;.&#10;Being able to grab a model with one command and have it &amp;ldquo;just work&amp;rdquo; was great.&lt;/p&gt;&#10;&lt;p&gt;But as a &lt;a href="https://www.nijho.lt/post/proxmox-to-nixos/"&gt;NixOS&lt;/a&gt; user, this imperative convenience is actually a flaw.&#10;I want my system to be declarative.&#10;I want my model configuration committed to git, not hiding in a local database.&#10;So I let convenience win for a while, but it always felt wrong.&lt;/p&gt;&#10;&lt;p&gt;But last month, that changed.&#10;I upgraded my hobbyist AI rig by adding a &lt;strong&gt;second RTX 3090&lt;/strong&gt;.&#10;Suddenly, I had 48GB of VRAM and a whole new set of problems—and possibilities.&#10;I wanted to run massive models (like &lt;code&gt;gpt-oss-120b&lt;/code&gt; or &lt;code&gt;Qwen-3-VL-32B&lt;/code&gt; with a huge context window).&#10;I needed to balance layers between two GPUs.&#10;I needed to spill specific parts of the model into system RAM without crashing.&lt;/p&gt;&#10;&lt;p&gt;When you start pushing hardware to its limits, &amp;ldquo;magic&amp;rdquo; abstractions like Ollama stop being helpful and start getting in the way.&#10;I realized that &lt;code&gt;llama.cpp&lt;/code&gt; isn&amp;rsquo;t just a backend; it is &lt;em&gt;the&lt;/em&gt; place where development happens.&#10;If you want to use split-mode inference, granular CPU offloading, or the latest quantization tricks immediately, there is no alternative.&lt;/p&gt;&#10;&lt;p&gt;So, I dusted off this draft, updated my Nix configuration, and I&amp;rsquo;ve been running &lt;code&gt;llama-swap&lt;/code&gt; full-time for a month.&#10;I&amp;rsquo;m finally ready to vouch for it.&lt;/p&gt;&#10;&lt;h2 id="the-original-trigger-broken-models"&gt;The original trigger: broken models &lt;a class="anchor" href="#the-original-trigger-broken-models" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Four months ago, my frustration stemmed from compatibility.&#10;I was trying to run &lt;code&gt;gpt-oss-20b&lt;/code&gt;, a model that required specific chat templates and GGUF handling that &lt;a href="https://ollama.com/" target="_blank" rel="noopener"&gt;Ollama&lt;/a&gt; simply broke.&lt;/p&gt;&#10;&lt;p&gt;The maintainer of &lt;a href="https://github.com/ggml-org/llama.cpp" target="_blank" rel="noopener"&gt;&lt;code&gt;llama.cpp&lt;/code&gt;&lt;/a&gt;, Georgi Gerganov, &lt;a href="https://github.com/ollama/ollama/issues/11714#issuecomment-3172893576" target="_blank" rel="noopener"&gt;explained it perfectly&lt;/a&gt; regarding the gpt-oss debacle:&#10;Ollama forked the ggml inference engine to rush out &amp;ldquo;day-1 support&amp;rdquo; for gpt-oss without coordinating with upstream.&#10;The result? Their implementation was not only incompatible with standard GGUF files but also significantly slower and unoptimized.&lt;/p&gt;&#10;&lt;p&gt;Benchmarks at the time showed Ollama had significant performance overhead—often 20-30% slower inference speeds compared to running &lt;code&gt;llama.cpp&lt;/code&gt; directly.&lt;/p&gt;&#10;&lt;p&gt;This pattern means Ollama will always be behind &lt;code&gt;llama.cpp&lt;/code&gt;.&#10;Meanwhile, I&amp;rsquo;ve had &lt;a href="https://github.com/ollama/ollama/pull/11249" target="_blank" rel="noopener"&gt;a pull request&lt;/a&gt; sitting unreviewed for over a month that fixes their broken OpenAI API.&#10;Compare that to &lt;code&gt;llama.cpp&lt;/code&gt;: I submitted &lt;a href="https://github.com/ggml-org/llama.cpp/pull/15295" target="_blank" rel="noopener"&gt;a PR there&lt;/a&gt; and it was merged in less than an hour.&#10;If you want the latest features &lt;em&gt;now&lt;/em&gt; (and a responsive community), you go to the source.&lt;/p&gt;&#10;&lt;h2 id="the-catalyst-dual-gpus-and-control"&gt;The catalyst: dual GPUs and control &lt;a class="anchor" href="#the-catalyst-dual-gpus-and-control" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;With a single GPU, you usually either fit the model or you don&amp;rsquo;t.&#10;With two GPUs (and 64GB of system RAM), things get interesting.&#10;You enter the territory of &lt;strong&gt;heterogeneous inference&lt;/strong&gt;.&lt;/p&gt;&#10;&lt;p&gt;Ollama tries to handle splitting automatically, but I found myself fighting it.&#10;I wanted to say: &lt;em&gt;&amp;ldquo;Put 40 layers on GPU 0, 40 layers on GPU 1, and keep the KV cache here.&amp;rdquo;&lt;/em&gt;&#10;Or: &lt;em&gt;&amp;ldquo;This model is slightly too big for VRAM; I want to offload exactly the last 10% to system RAM.&amp;rdquo;&lt;/em&gt;&lt;/p&gt;&#10;&lt;p&gt;With &lt;code&gt;llama.cpp&lt;/code&gt; directly, this is just a command line flag.&#10;With &lt;code&gt;llama-swap&lt;/code&gt;, I can define this behavior per-model in a config file:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;&amp;#34;gpt-oss:120b&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="sd"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; ${pkgs.llama-cpp}/bin/llama-server&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; -hf ggml-org/gpt-oss-120b-GGUF&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; --port ${PORT}&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; --ctx-size 65536&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; --split-mode layer&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; --tensor-split 3,1.3 # Tuned balance between my GPUs&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; --n-cpu-moe 15 # Offload 15 experts to CPU RAM&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; --threads 8&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; --chat-template-kwargs &amp;#39;{&amp;#34;reasoning_effort&amp;#34;: &amp;#34;high&amp;#34;}&amp;#39;&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This level of granularity turned my rig from a &amp;ldquo;black box&amp;rdquo; into a tunable instrument.&#10;The &lt;code&gt;--tensor-split&lt;/code&gt; and &lt;code&gt;--n-cpu-moe&lt;/code&gt; flags allow me to run a 120B parameter model that technically shouldn&amp;rsquo;t fit, by carefully balancing the load across both GPUs and system RAM.&lt;/p&gt;&#10;&lt;h2 id="api-compatibility-and-the-switch"&gt;API compatibility and the &amp;ldquo;switch&amp;rdquo; &lt;a class="anchor" href="#api-compatibility-and-the-switch" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;One of the reasons I hesitated to switch was the fear of breaking my existing workflows.&#10;I use &lt;a href="https://github.com/basnijholt/agent-cli" target="_blank" rel="noopener"&gt;agent-cli&lt;/a&gt; and other custom Python scripts daily.&#10;But it turned out to be a non-issue.&#10;Both Ollama and &lt;code&gt;llama-swap&lt;/code&gt; (wrapping &lt;code&gt;llama-server&lt;/code&gt;) expose an OpenAI-compatible API.&lt;/p&gt;&#10;&lt;p&gt;Once I adopted the pattern of setting a custom &lt;code&gt;OPENAI_BASE_URL&lt;/code&gt; in my projects, the backend became irrelevant.&#10;My tools don&amp;rsquo;t care if they are talking to &lt;code&gt;api.openai.com&lt;/code&gt;, a local Ollama instance, or &lt;code&gt;llama-swap&lt;/code&gt;.&#10;They just send JSON and get JSON back.&#10;This standardization meant I could swap the entire inference engine underneath my applications without changing a single line of their code.&lt;/p&gt;&#10;&lt;h2 id="my-nixos-setup"&gt;My NixOS setup &lt;a class="anchor" href="#my-nixos-setup" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Since I manage my entire system with Nix, setting up &lt;code&gt;llama-swap&lt;/code&gt; and pinning &lt;code&gt;llama-cpp&lt;/code&gt; to the absolute bleeding edge was surprisingly elegant.&#10;I even wrote an &lt;a href="https://github.com/basnijholt/dotfiles/blob/51c7af46e62a7d13b4ff497380c8b58c05ed81c8/configs/nixos/scripts/update_overrides.py" target="_blank" rel="noopener"&gt;auto-update script&lt;/a&gt; to ensure I&amp;rsquo;m always on the latest commit.&lt;/p&gt;&#10;&lt;p&gt;This is where my &lt;a href="https://www.nijho.lt/post/nixos-cache/"&gt;NixOS build cache&lt;/a&gt; shines.&#10;Unlike Ollama, which releases every few weeks, &lt;code&gt;llama.cpp&lt;/code&gt; can have multiple releases &lt;em&gt;a day&lt;/em&gt;.&#10;My cache server runs this update script automatically, builds the new binaries, and serves them to my workstation.&#10;I get the absolute latest performance improvements without ever compiling source code locally.&lt;/p&gt;&#10;&lt;h3 id="compiler-flags-matter"&gt;Compiler flags matter &lt;a class="anchor" href="#compiler-flags-matter" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;One critical lesson I learned: default builds matter.&#10;Initially, I was getting a measly &lt;strong&gt;8 tokens/sec&lt;/strong&gt; on &lt;code&gt;gpt-oss:120b&lt;/code&gt;.&#10;It turned out the default &lt;code&gt;llama.cpp&lt;/code&gt; package wasn&amp;rsquo;t enabling BLAS or native CPU optimizations, crippling the layers I offloaded to system RAM.&lt;/p&gt;&#10;&lt;p&gt;By overriding the package to enable &lt;code&gt;blasSupport&lt;/code&gt; and passing &lt;code&gt;-DGGML_NATIVE=ON&lt;/code&gt; (as seen below), performance jumped to &lt;strong&gt;50 tokens/sec&lt;/strong&gt;.&#10;This is another reason why I prefer managing this via Nix: I can enforce these compile-time flags declaratively.&lt;/p&gt;&#10;&lt;p&gt;Here&amp;rsquo;s my complete configuration (see &lt;a href="https://github.com/basnijholt/dotfiles/blob/51c7af46e62a7d13b4ff497380c8b58c05ed81c8/configs/nixos/hosts/pc/package-overrides.nix#L28-L76" target="_blank" rel="noopener"&gt;&lt;code&gt;package-overrides.nix&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://github.com/basnijholt/dotfiles/blob/51c7af46e62a7d13b4ff497380c8b58c05ed81c8/configs/nixos/hosts/pc/ai.nix#L360-L380" target="_blank" rel="noopener"&gt;&lt;code&gt;ai.nix&lt;/code&gt;&lt;/a&gt;):&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-nix" data-lang="nix"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Override llama-cpp to latest version with CUDA support&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;llama-cpp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;llama-cpp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;override&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;cudaSupport&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;rocmSupport&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;metalSupport&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Enable BLAS for optimized CPU layer performance (OpenBLAS)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;blasSupport&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;overrideAttrs&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;oldAttrs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;rec&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;7205&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fetchFromGitHub&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;owner&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;ggml-org&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;repo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;llama.cpp&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;tag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;b&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;sha256-1CcYbc8RWAPVz8hoxKEmbAgQesC1oGFZ3fhfuU5vmOc=&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;leaveDotGit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;postFetch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&amp;#39;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; git -C &amp;#34;$out&amp;#34; rev-parse --short HEAD &amp;gt; $out/COMMIT&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; find &amp;#34;$out&amp;#34; -name .git -print0 | xargs -0 rm -rf&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; &amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Enable native CPU optimizations (AVX, AVX2, etc.)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;cmakeFlags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;oldAttrs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cmakeFlags&lt;/span&gt; &lt;span class="n"&gt;or&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="o"&gt;++&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;-DGGML_NATIVE=ON&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;];&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Disable Nix&amp;#39;s march=native stripping&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;preConfigure&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&amp;#39;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; export NIX_ENFORCE_NO_NATIVE=0&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="n"&gt;oldAttrs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;preConfigure&lt;/span&gt; &lt;span class="n"&gt;or&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s1"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; &amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;});&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# llama-swap from GitHub releases&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;llama-swap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;runCommand&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;llama-swap&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&amp;#39;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; mkdir -p $out/bin&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; tar -xzf &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;pkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fetchurl&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;https://github.com/mostlygeek/llama-swap/releases/download/v175/llama-swap_175_linux_amd64.tar.gz&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;sha256-zeyVz0ldMxV4HKK+u5TtAozfRI6IJmeBo92IJTgkGrQ=&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s1"&gt; -C $out/bin&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; chmod +x $out/bin/llama-swap&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Configure llama-swap as a systemd service&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;systemd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;llama-swap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;llama-swap - OpenAI compatible proxy with automatic model swapping&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;after&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;network.target&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;];&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;wantedBy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;multi-user.target&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;];&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;serviceConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;Type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;simple&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;User&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;basnijholt&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;Group&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;users&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Point to your declarative config file&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;ExecStart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="n"&gt;pkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;llama-swap&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/bin/llama-swap --config /etc/llama-swap/config.yaml --listen 0.0.0.0:9292 --watch-config&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;Restart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;always&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;RestartSec&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Environment for CUDA support&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;Environment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;PATH=/run/current-system/sw/bin&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;LD_LIBRARY_PATH=/run/opengl-driver/lib:/run/opengl-driver-32/lib&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;];&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;};&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="what-i-gained-and-lost"&gt;What I gained (and lost) &lt;a class="anchor" href="#what-i-gained-and-lost" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;h3 id="advantages-of-llama-swap--llamacpp"&gt;Advantages of llama-swap + llama.cpp &lt;a class="anchor" href="#advantages-of-llama-swap--llamacpp" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;Heterogeneous compute&lt;/strong&gt;: Perfectly balance loads between GPUs and CPU RAM.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Bleeding edge&lt;/strong&gt;: I use features (like new quantization types) the day they land in &lt;code&gt;llama.cpp&lt;/code&gt;.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Zero idle VRAM&lt;/strong&gt;: Models unload completely when not in use.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Transparent operation&lt;/strong&gt;: No hidden prompts or &amp;ldquo;helpful&amp;rdquo; formatting that breaks complex tasks.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="the-trade-off"&gt;The trade-off &lt;a class="anchor" href="#the-trade-off" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;The main trade-off is &amp;ldquo;laziness.&amp;rdquo;&#10;You can&amp;rsquo;t just &lt;code&gt;ollama pull new-model&lt;/code&gt; and have it appear.&#10;You have to find the GGUF on HuggingFace, decide on the quantization (Q4_K_M? Q6_K?), and add 5 lines of YAML to your config.&lt;/p&gt;&#10;&lt;p&gt;But honestly? That&amp;rsquo;s a feature, not a bug.&#10;It forces you to understand &lt;em&gt;what&lt;/em&gt; you are running.&#10;It stops you from running a quantized model that is too stupid for your task just because it was the default.&#10;And for a NixOS user, putting that configuration into code instead of a hidden database is exactly how it should be.&lt;/p&gt;&#10;&lt;h2 id="conclusion-maturing-as-an-ai-hobbyist"&gt;Conclusion: maturing as an AI hobbyist &lt;a class="anchor" href="#conclusion-maturing-as-an-ai-hobbyist" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;My switch back to &lt;code&gt;llama-swap&lt;/code&gt; wasn&amp;rsquo;t about fixing a bug; it was about outgrowing a tool.&#10;Ollama is the &amp;ldquo;starter bike&amp;rdquo; of local AI—fantastic for getting rolling.&#10;But when you add a second GPU, start caring about tokens per second, and want to maximize every gigabyte of your 48GB VRAM buffer, you need a manual transmission.&lt;/p&gt;&#10;&lt;p&gt;For anyone else building a hobbyist multi-GPU rig: stop fighting the abstraction.&#10;Go to the source. The control is worth it.&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;p&gt;&lt;em&gt;Check out my &lt;a href="https://github.com/basnijholt/dotfiles/tree/51c7af46e62a7d13b4ff497380c8b58c05ed81c8/configs/nixos" target="_blank" rel="noopener"&gt;NixOS configuration&lt;/a&gt; for the complete setup!&lt;/em&gt;&lt;/p&gt;&#10;</description></item><item><title>Using gpt-oss:20b in OpenCode with a larger context and high reasoning</title><link>https://www.nijho.lt/post/ollama-opencode/</link><pubDate>Thu, 13 Nov 2025 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/ollama-opencode/</guid><description>&lt;p&gt;I have hundreds of private journal entries stored locally, many with incorrect date formats.&#10;The correct date is in the filename, so I wanted to use an LLM to batch update them.&#10;However, I do not want to share my &lt;strong&gt;private journal&lt;/strong&gt; with any third-party service.&#10;Instead, I used &lt;a href="https://ollama.com/" target="_blank" rel="noopener"&gt;Ollama&lt;/a&gt; with &lt;code&gt;gpt-oss:20b&lt;/code&gt; and &lt;a href="https://opencode.ai/" target="_blank" rel="noopener"&gt;OpenCode&lt;/a&gt; to do this all locally.&lt;/p&gt;&#10;&lt;p&gt;The naive solution of just using &lt;code&gt;gpt-oss:20b&lt;/code&gt; with Ollama&amp;rsquo;s defaults doesn&amp;rsquo;t work (e.g., tool calls don&amp;rsquo;t work).&#10;The main issues is that Ollama sets the context window to only &lt;strong&gt;4096 tokens&lt;/strong&gt; and medium reasoning by default.&#10;It took me too long to figure out how to get &lt;code&gt;gpt-oss:20b&lt;/code&gt; working well in OpenCode (mostly because the OpenAI compatible API of Ollama doesn&amp;rsquo;t allow setting the context window), so I’m writing the steps down (partly for my future self).&lt;/p&gt;&#10;&lt;p&gt;In the end I could simply run the following command to batch update my journal entries:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;for&lt;/span&gt; file in ./*.md&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; opencode -m ollama/gpt-oss-20b-high-32k run &lt;span class="se"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;Use the date in the filename \&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;file&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\&amp;#34; and correct the &amp;#39;date:&amp;#39; entry in the YAML front matter of this file&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;done&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;aside class="callout callout-note"&gt;&lt;svg class="ico" viewBox="0 0 256 256" aria-hidden="true"&gt;&lt;path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm16-40a8,8,0,0,1-8,8,16,16,0,0,1-16-16V128a8,8,0,0,1,0-16,16,16,0,0,1,16,16v40A8,8,0,0,1,144,176ZM112,84a12,12,0,1,1,12,12A12,12,0,0,1,112,84Z"/&gt;&lt;/svg&gt;&lt;div&gt;Yes, of course I could have scripted this without an LLM, but where’s the fun in that?&#10;Also I might want to do more complex updates in the future on less structured data.&lt;/div&gt;&lt;/aside&gt;&#10;&lt;p&gt;This post describes how to:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Create a &lt;strong&gt;long-context&lt;/strong&gt; variant of &lt;code&gt;gpt-oss:20b&lt;/code&gt; in Ollama.&lt;/li&gt;&#10;&lt;li&gt;Configure &lt;strong&gt;OpenCode&lt;/strong&gt; to use that model with &lt;strong&gt;high reasoning&lt;/strong&gt; enabled by default.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="1-create-a-long-context-model-in-ollama"&gt;1. Create a long-context model in Ollama &lt;a class="anchor" href="#1-create-a-long-context-model-in-ollama" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Start an interactive session with &lt;code&gt;gpt-oss:20b&lt;/code&gt;:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ollama run gpt-oss:20b&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;In the prompt:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&amp;gt;&amp;gt;&amp;gt; /set parameter num_ctx 32768&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&amp;gt;&amp;gt;&amp;gt; /save gpt-oss-20b-32k&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&amp;gt;&amp;gt;&amp;gt; /bye&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Sets the context window to &lt;strong&gt;32k tokens&lt;/strong&gt; (&lt;code&gt;num_ctx 32768&lt;/code&gt;).&lt;/li&gt;&#10;&lt;li&gt;Saves a new model/tag named &lt;strong&gt;&lt;code&gt;gpt-oss-20b-32k&lt;/code&gt;&lt;/strong&gt; with that parameter stored.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Adjust &lt;code&gt;num_ctx&lt;/code&gt; as needed for your GPU (e.g. &lt;code&gt;16384&lt;/code&gt;, &lt;code&gt;65536&lt;/code&gt;, etc.).&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="2-configure-opencode-to-use-high-reasoning"&gt;2. Configure OpenCode to use high reasoning &lt;a class="anchor" href="#2-configure-opencode-to-use-high-reasoning" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;OpenCode talks to Ollama via the OpenAI-compatible API. In:&lt;/p&gt;&#10;&lt;p&gt;&lt;code&gt;~/.config/opencode/opencode.json&lt;/code&gt;&lt;/p&gt;&#10;&lt;p&gt;add a provider configuration similar to:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-jsonc" data-lang="jsonc"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;$schema&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;https://opencode.ai/config.json&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;provider&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;ollama&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;npm&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;@ai-sdk/openai-compatible&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;name&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Ollama (local)&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;options&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;baseURL&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;http://localhost:11434/v1&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;apiKey&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;ollama&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;},&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;models&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;gpt-oss-20b-high-32k&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;id&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;gpt-oss-20b-32k&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;options&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;extraBody&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;think&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;high&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;},&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;model&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;ollama/gpt-oss-20b-high-32k&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Notes:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;code&gt;&amp;quot;id&amp;quot;: &amp;quot;gpt-oss-20b-32k&amp;quot;&lt;/code&gt; refers to the custom Ollama model with the larger context window.&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;&amp;quot;think&amp;quot;: &amp;quot;high&amp;quot;&lt;/code&gt; enables &lt;strong&gt;high reasoning&lt;/strong&gt; for &lt;code&gt;gpt-oss:20b&lt;/code&gt; on each request.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="3-select-the-model-in-opencode"&gt;3. Select the model in OpenCode &lt;a class="anchor" href="#3-select-the-model-in-opencode" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Run OpenCode:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;opencode&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Inside OpenCode:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&amp;gt; /models&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then select:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ollama/gpt-oss-20b-high-32k&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;From this point, OpenCode uses &lt;code&gt;gpt-oss:20b&lt;/code&gt; with:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;A &lt;strong&gt;larger context window&lt;/strong&gt; (configured in Ollama), and&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;High reasoning&lt;/strong&gt; enabled by default (via &lt;code&gt;think: &amp;quot;high&amp;quot;&lt;/code&gt;).&lt;/li&gt;&#10;&lt;/ul&gt;&#10;</description></item><item><title>Coding from my phone 📱</title><link>https://www.nijho.lt/post/agentic-mobile-workflow/</link><pubDate>Mon, 20 Oct 2025 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/agentic-mobile-workflow/</guid><description>&lt;p&gt;I am dictating this post at ten kilometers altitude on a flight home from Mexico, Blink Shell open on my iPhone, while an agentic assistant on my &lt;a href="https://nixos.org/" target="_blank" rel="noopener"&gt;NixOS&lt;/a&gt; machine writes an initial draft for this post.&#10;Admittedly, I’m a little addicted to agentic coding: when an idea pops up, I want to jump on it right away—even if that means doing it from a cramped seat in the sky.&#10;This is the workflow I’ve arrived at after trying many alternatives, it extends &lt;a href="https://www.nijho.lt/post/agentic-coding/"&gt;my agentic coding write-up&lt;/a&gt; and the self-hosted AI obsession in &lt;a href="https://www.nijho.lt/post/local-ai-journey/"&gt;my local AI journey&lt;/a&gt;.&#10;It&amp;rsquo;s my backup plan for those moments when a computer simply isn&amp;rsquo;t nearby—the rest of the time I&amp;rsquo;m still at a keyboard like any other developer.&#10;This is in no way a replacement for a proper computer/laptop workflow but gets the job done when needed.&#10;It&amp;rsquo;s a personal, mostly open-source stack built around &lt;a href="https://github.com/basnijholt/agent-cli" target="_blank" rel="noopener"&gt;&lt;code&gt;agent-cli&lt;/code&gt;&lt;/a&gt;—my local-first voice layer.&#10;Here&amp;rsquo;s how it works: I speak into my phone, the audio goes to &lt;a href="https://github.com/SYSTRAN/faster-whisper-server" target="_blank" rel="noopener"&gt;FasterWhisper&lt;/a&gt; for transcription, &lt;a href="https://ollama.com/" target="_blank" rel="noopener"&gt;Ollama&lt;/a&gt; cleans up the text, and it lands on my clipboard ready to paste.&#10;My iOS Shortcut handles this through a small HTTP interface that agent-cli exposes.&#10;The coding agent itself still uses the best proprietary model I can access.&lt;/p&gt;&#10;&lt;aside class="callout callout-note"&gt;&lt;svg class="ico" viewBox="0 0 256 256" aria-hidden="true"&gt;&lt;path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm16-40a8,8,0,0,1-8,8,16,16,0,0,1-16-16V128a8,8,0,0,1,0-16,16,16,0,0,1,16,16v40A8,8,0,0,1,144,176ZM112,84a12,12,0,1,1,12,12A12,12,0,0,1,112,84Z"/&gt;&lt;/svg&gt;&lt;div&gt;Meta: I&amp;rsquo;m writing about agentic coding on mobile from my actual phone.&#10;It&amp;rsquo;s not the most efficient way to write, but it gets the job done — during a 6.5‑hour flight I dictated and iterated this entire post.&#10;Each iteration was a commit: dictate changes, review the diff, refine, commit, repeat.&#10;See all &lt;a href="https://github.com/basnijholt/nijho.lt/pull/40" target="_blank" rel="noopener"&gt;50+ commits in PR #40&lt;/a&gt; for the full iteration trail.&lt;/div&gt;&lt;/aside&gt;&#10;&lt;aside class="callout callout-note"&gt;&lt;svg class="ico" viewBox="0 0 256 256" aria-hidden="true"&gt;&lt;path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm16-40a8,8,0,0,1-8,8,16,16,0,0,1-16-16V128a8,8,0,0,1,0-16,16,16,0,0,1,16,16v40A8,8,0,0,1,144,176ZM112,84a12,12,0,1,1,12,12A12,12,0,0,1,112,84Z"/&gt;&lt;/svg&gt;&lt;div&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; iPhone → WireGuard → Blink+Mosh → Zellij.&#10;I dictate via a Shortcut → FasterWhisper (transcribe) → Ollama (polish) → clipboard, then paste into Codex CLI using OpenAI’s &lt;code&gt;gpt-5-codex-high&lt;/code&gt;.&#10;Everything is local except the model.&lt;/div&gt;&lt;/aside&gt;&#10;&lt;details class="toc" open&gt;&lt;summary&gt;Table of contents&lt;/summary&gt;&lt;nav id="TableOfContents"&gt;&#10; &lt;ul&gt;&#10; &lt;li&gt;&lt;a href="#1-why-phone-coding-works-now"&gt;1. Why Phone Coding Works Now&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#2-constraints-and-trade-offs"&gt;2. Constraints and Trade-offs&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#3-layer-1-wireguard-from-the-router"&gt;3. Layer 1: WireGuard From the Router&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#4-layer-2-blink-shell--mosh-for-durable-sessions"&gt;4. Layer 2: Blink Shell + Mosh for Durable Sessions&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#5-layer-3-zellij-layouts"&gt;5. Layer 3: Zellij Layouts&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#6-layer-4-agent-cli-server"&gt;6. Layer 4: &lt;code&gt;agent-cli&lt;/code&gt; Server&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#7-layer-5-the-ios-shortcut-pipeline"&gt;7. Layer 5: The iOS Shortcut Pipeline&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#8-workflow-in-practice"&gt;8. Workflow in Practice&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#9-conclusion-and-further-reading"&gt;9. Conclusion and Further Reading&lt;/a&gt;&lt;/li&gt;&#10; &lt;/ul&gt;&#10;&lt;/nav&gt;&lt;/details&gt;&#10;&#10;&lt;h2 id="1-why-phone-coding-works-now"&gt;1. Why Phone Coding Works Now &lt;a class="anchor" href="#1-why-phone-coding-works-now" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;For years I used &lt;a href="https://ish.app/" target="_blank" rel="noopener"&gt;&lt;strong&gt;iSH&lt;/strong&gt;&lt;/a&gt; (a full Alpine Linux emulator) with SSH to hop into servers from my phone, but dropped connections were a constant frustration (Mosh fixes this, more on that later).&#10;Because coding on a phone keyboard is terrible, I kept it to tiny configuration tweaks or one-off fixes of a few characters.&#10;Agentic tools changed that: with a CLI coding agent, I don’t need to type the code—I describe the change, review the patch, and run it.&#10;That made meaningful work on the phone possible for the first time, for those moments when a computer isn’t around.&lt;/p&gt;&#10;&lt;p&gt;I tried &lt;strong&gt;VS Code in the browser&lt;/strong&gt;, bounced between &lt;strong&gt;iSH&lt;/strong&gt; and &lt;strong&gt;Terminus&lt;/strong&gt; for SSH, and even lived inside a handful of in-browser terminal clients.&#10;I also spent time with mobile companions like &lt;a href="https://happy.engineering/" target="_blank" rel="noopener"&gt;&lt;strong&gt;Happy&lt;/strong&gt;&lt;/a&gt; and &lt;a href="https://www.ycombinator.com/companies/omnara" target="_blank" rel="noopener"&gt;&lt;strong&gt;Omnara&lt;/strong&gt;&lt;/a&gt;, both designed to mirror Claude Code sessions on the phone, but they still felt like extra steps between me and my actual terminal.&#10;That friction pushed me toward a phone‑ready, self‑hosted workflow that still gives me raw SSH access to my own machine when the computer is out of reach.&lt;/p&gt;&#10;&lt;p&gt;This post is based on the way I develop software today.&#10;Your mileage may vary, but if you also care about privacy, open tooling, and reproducible environments, I think there are useful pieces here.&lt;/p&gt;&#10;&lt;h2 id="2-constraints-and-trade-offs"&gt;2. Constraints and Trade-offs &lt;a class="anchor" href="#2-constraints-and-trade-offs" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;This setup grew organically over time. What emerged are less strict requirements and more happy accidents that turned into core benefits:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;Single trust boundary:&lt;/strong&gt; Only the model provider (OpenAI) sees code context; audio and automation stay local, and I avoid any additional third‑party relays.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Resilient sessions:&lt;/strong&gt; Connections should survive sleep and spotty networks.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Voice-friendly:&lt;/strong&gt; Dictation should be accurate enough that I can trust it.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Reproducible config:&lt;/strong&gt; The entire stack must live in my &lt;a href="https://www.nijho.lt/post/dotfiles/"&gt;dotfiles&lt;/a&gt; and &lt;a href="https://github.com/basnijholt/dotfiles/tree/8f6bf0b7219195a46a3e010d3538e1e449634db7/configs/nixos" target="_blank" rel="noopener"&gt;NixOS configuration&lt;/a&gt;.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;To give you a sense of what I tried, here is the short comparison that convinced me to roll my own:&lt;/p&gt;&#10;&lt;div class="table-wrap"&gt;&#10;&lt;table&gt;&#10;&lt;thead&gt;&#10;&lt;tr&gt;&#10;&lt;th&gt;Approach&lt;/th&gt;&#10;&lt;th&gt;Pros&lt;/th&gt;&#10;&lt;th&gt;Why I moved on&lt;/th&gt;&#10;&lt;/tr&gt;&#10;&lt;/thead&gt;&#10;&lt;tbody&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;VS Code in the browser&lt;/td&gt;&#10;&lt;td&gt;Familiar editor UI&lt;/td&gt;&#10;&lt;td&gt;Needs a steady connection and still lives outside my dotfiles comfort zone&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;iSH / Terminus SSH&lt;/td&gt;&#10;&lt;td&gt;Works without extra infrastructure&lt;/td&gt;&#10;&lt;td&gt;Laggy, no Mosh, and awkward keybindings&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;In-browser terminals&lt;/td&gt;&#10;&lt;td&gt;Instant access from anywhere&lt;/td&gt;&#10;&lt;td&gt;Poor copy/paste ergonomics and flaky mobile keyboards&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;a href="https://apps.apple.com/us/app/happy-codex-claude-code-app/id6748571505" target="_blank" rel="noopener"&gt;Happy&lt;/a&gt; (Claude Code companion)&lt;/td&gt;&#10;&lt;td&gt;Push notifications, encrypted mobile UI for Claude Code&lt;/td&gt;&#10;&lt;td&gt;Requires wrapping every session with a separate CLI and still abstracts away my shell, and was quite buggy for me&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;a href="https://omnara.com" target="_blank" rel="noopener"&gt;Omnara&lt;/a&gt; (agent command center)&lt;/td&gt;&#10;&lt;td&gt;Centralizes Claude Code/Codex sessions with terminal replay&lt;/td&gt;&#10;&lt;td&gt;Proxies via their servers; I already trust OpenAI for the model and don&amp;rsquo;t want another third‑party handling my code&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;The stack below gives me the resilience of Mosh, the ergonomics of Zellij, and full control over the AI layer.&lt;/p&gt;&#10;&lt;h2 id="3-layer-1-wireguard-from-the-router"&gt;3. Layer 1: WireGuard From the Router &lt;a class="anchor" href="#3-layer-1-wireguard-from-the-router" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;&lt;a href="https://www.wireguard.com/" target="_blank" rel="noopener"&gt;WireGuard&lt;/a&gt; is a modern VPN that lets me securely access my home network from anywhere.&#10;I terminate WireGuard on my router so every device in the house (and on the road) can dial home with the same config.&#10;(If you want something easier to set up without opening ports, &lt;a href="https://tailscale.com/" target="_blank" rel="noopener"&gt;Tailscale&lt;/a&gt; is a more user-friendly option.)&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;Server:&lt;/strong&gt; ASUS XT8 router with WireGuard enabled via the router UI.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Client:&lt;/strong&gt; The WireGuard iOS app with &lt;code&gt;On-Demand&lt;/code&gt; rules so the tunnel flips on whenever I am off trusted Wi-Fi.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;DNS:&lt;/strong&gt; All mobile sessions resolve through my home DNS, so &lt;code&gt;git.nijho.lt&lt;/code&gt; and internal services resolve instantly.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;This gives Blink a local-LAN address for my desktop &lt;code&gt;nixos&lt;/code&gt;.&lt;/p&gt;&#10;&lt;h2 id="4-layer-2-blink-shell--mosh-for-durable-sessions"&gt;4. Layer 2: Blink Shell + Mosh for Durable Sessions &lt;a class="anchor" href="#4-layer-2-blink-shell--mosh-for-durable-sessions" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;What is &lt;a href="https://mosh.org/" target="_blank" rel="noopener"&gt;Mosh&lt;/a&gt;?&#10;It&amp;rsquo;s like SSH, but it stays connected when the network changes or the phone sleeps, and it feels faster on bad connections.&lt;/p&gt;&#10;&lt;p&gt;&lt;a href="https://blink.sh/" target="_blank" rel="noopener"&gt;Blink Shell&lt;/a&gt; is my daily driver on iOS because it pairs beautifully with Mosh and has great keyboard ergonomics.&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;I launch sessions using &lt;code&gt;mosh bas@nixos -- zellij attach --create phone&lt;/code&gt; (&lt;a href="https://zellij.dev/" target="_blank" rel="noopener"&gt;Zellij&lt;/a&gt; is my terminal multiplexer—more on that in the next section).&lt;/li&gt;&#10;&lt;li&gt;Mosh smooths over spotty LTE and keeps my session alive when the phone sleeps.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Mosh keeps the session responsive across flaky networks and sleep.&lt;/p&gt;&#10;&lt;h2 id="5-layer-3-zellij-layouts"&gt;5. Layer 3: Zellij Layouts &lt;a class="anchor" href="#5-layer-3-zellij-layouts" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;I use &lt;strong&gt;Zellij&lt;/strong&gt; as my terminal multiplexer for mobile work.&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;I stick to the defaults and use a couple of predefined layouts (e.g., a &amp;ldquo;phone&amp;rdquo; layout with editor + shell panes).&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Ergonomics matter even more on a glass keyboard, so I lean on a few shell helpers:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;&lt;a href="https://github.com/ajeetdsouza/zoxide" target="_blank" rel="noopener"&gt;&lt;code&gt;zoxide&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt; means I can jump between repos with &lt;code&gt;z foo&lt;/code&gt; instead of pecking long paths.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Single-character aliases&lt;/strong&gt; like &lt;code&gt;p&lt;/code&gt; for &lt;code&gt;pytest&lt;/code&gt; and &lt;code&gt;gs&lt;/code&gt; for &lt;code&gt;git status&lt;/code&gt; live in &lt;a href="https://github.com/basnijholt/dotfiles/blob/8f6bf0b7219195a46a3e010d3538e1e449634db7/configs/shell/10_aliases.sh" target="_blank" rel="noopener"&gt;&lt;code&gt;configs/shell/10_aliases.sh&lt;/code&gt;&lt;/a&gt;, keeping command entry to a minimum.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Autocompletion plugins&lt;/strong&gt;—&lt;a href="https://github.com/zsh-users/zsh-autosuggestions" target="_blank" rel="noopener"&gt;&lt;code&gt;zsh-autosuggestions&lt;/code&gt;&lt;/a&gt; and &lt;code&gt;zsh-syntax-highlighting&lt;/code&gt;—are wired up in &lt;a href="https://github.com/basnijholt/dotfiles/blob/8f6bf0b7219195a46a3e010d3538e1e449634db7/configs/shell/70_zsh_plugins.sh" target="_blank" rel="noopener"&gt;&lt;code&gt;configs/shell/70_zsh_plugins.sh&lt;/code&gt;&lt;/a&gt;, so I get inline hints and colour cues while typing.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;I break these tricks down in more depth in &lt;a href="https://www.nijho.lt/post/terminal-ninja/"&gt;Terminal Ninja&lt;/a&gt;, explain how I sync them with my &lt;a href="https://www.nijho.lt/post/dotfiles/"&gt;Dotfiles&lt;/a&gt;, and even package binaries like &lt;code&gt;zoxide&lt;/code&gt; with &lt;a href="https://www.nijho.lt/post/dotbins/"&gt;Dotbins&lt;/a&gt;.&lt;/p&gt;&#10;&lt;h2 id="6-layer-4-agent-cli-server"&gt;6. Layer 4: &lt;code&gt;agent-cli&lt;/code&gt; Server &lt;a class="anchor" href="#6-layer-4-agent-cli-server" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;&lt;a href="https://github.com/basnijholt/agent-cli" target="_blank" rel="noopener"&gt;&lt;code&gt;agent-cli&lt;/code&gt;&lt;/a&gt; runs as a small server on my NixOS machine.&#10;In this workflow I only use &lt;code&gt;transcribe&lt;/code&gt; from iOS Shortcuts: the phone records audio, the server transcribes and cleans it up, and I paste the text.&#10;(Agent-CLI has other features like &lt;code&gt;autocorrect&lt;/code&gt; and &lt;code&gt;chat&lt;/code&gt;, but I don&amp;rsquo;t use them for this workflow.)&#10;On this machine I run a long‑lived &lt;code&gt;agent-cli&lt;/code&gt; user service (systemd) so Shortcuts can POST audio to it and get cleaned text back—ready to paste into Codex CLI.&#10;The service is defined in my dotfiles—see: &lt;a href="https://github.com/basnijholt/dotfiles/blob/8f6bf0b7219195a46a3e010d3538e1e449634db7/configs/nixos/modules/user.nix#L29-L40" target="_blank" rel="noopener"&gt;&lt;code&gt;configs/nixos/modules/user.nix&lt;/code&gt; (agent-cli service)&lt;/a&gt;.&lt;/p&gt;&#10;&lt;p&gt;For more background on why I built it and how I use it in practice, see &lt;a href="https://www.nijho.lt/post/local-ai-journey/"&gt;Local AI Journey&lt;/a&gt; (§3) and &lt;a href="https://www.nijho.lt/post/agentic-coding/"&gt;Agentic Coding&lt;/a&gt;.&lt;/p&gt;&#10;&lt;p&gt;The models and services run on the same box:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;FasterWhisper&lt;/strong&gt; via &lt;a href="https://github.com/SYSTRAN/faster-whisper" target="_blank" rel="noopener"&gt;&lt;code&gt;faster-whisper-server&lt;/code&gt;&lt;/a&gt; for high‑accuracy streaming transcription.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Ollama&lt;/strong&gt; for on‑device rewrite/cleanup before sending prompts to the coding agent.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;My iOS Shortcut dictation workflow is slower than Apple&amp;rsquo;s built-in dictation, but the accuracy is much better—and that matters more.&lt;/p&gt;&#10;&lt;p&gt;For the actual coding agent, I use a &lt;a href="https://github.com/just-every/code" target="_blank" rel="noopener"&gt;fork&lt;/a&gt; of Codex CLI with OpenAI’s &lt;code&gt;gpt-5-codex-high&lt;/code&gt; model (here no open-source solution matches the frontier).&lt;/p&gt;&#10;&lt;h2 id="7-layer-5-the-ios-shortcut-pipeline"&gt;7. Layer 5: The iOS Shortcut Pipeline &lt;a class="anchor" href="#7-layer-5-the-ios-shortcut-pipeline" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;My iPhone&amp;rsquo;s Action Button runs a Shortcut that records audio and sends my voice to agent-cli on my NixOS machine for transcription and cleanup.&lt;/p&gt;&#10;&lt;p&gt;For the full recipe, see the iOS Shortcut Guide: &lt;a href="https://github.com/basnijholt/agent-cli/blob/main/iOS_Shortcut_Guide.md" target="_blank" rel="noopener"&gt;agent-cli/iOS_Shortcut_Guide.md&lt;/a&gt;.&lt;/p&gt;&#10;&lt;p&gt;In short: I press the Action Button, the Shortcut records a snippet, sends it to &lt;code&gt;agent-cli&lt;/code&gt;, and copies the cleaned text to my clipboard so I can paste it into Codex CLI.&lt;/p&gt;&#10;&lt;p&gt;The whole loop finishes fast enough that I can capture intent by voice and paste it into Codex CLI without typing.&lt;/p&gt;&#10;&lt;aside class="callout callout-note"&gt;&lt;svg class="ico" viewBox="0 0 256 256" aria-hidden="true"&gt;&lt;path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm16-40a8,8,0,0,1-8,8,16,16,0,0,1-16-16V128a8,8,0,0,1,0-16,16,16,0,0,1,16,16v40A8,8,0,0,1,144,176ZM112,84a12,12,0,1,1,12,12A12,12,0,0,1,112,84Z"/&gt;&lt;/svg&gt;&lt;div&gt;&lt;strong&gt;Dictation quality:&lt;/strong&gt; In my experience, the built‑in iOS dictation is absolute garbage compared to Whisper/FasterWhisper.&#10;Accuracy and punctuation are much better with the FasterWhisper server running at home; it’s a bit slower, but that trade‑off is worth it for clean, usable text.&lt;/div&gt;&lt;/aside&gt;&#10;&lt;h2 id="8-workflow-in-practice"&gt;8. Workflow in Practice &lt;a class="anchor" href="#8-workflow-in-practice" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Here&amp;rsquo;s the typical flow:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;WireGuard auto-connects when I&amp;rsquo;m off home WiFi&lt;/li&gt;&#10;&lt;li&gt;Open Blink—my Mosh session is still alive from last time, Zellij panes intact&lt;/li&gt;&#10;&lt;li&gt;Dictate the change, paste into Codex CLI, review the agent&amp;rsquo;s response&lt;/li&gt;&#10;&lt;li&gt;Check diffs in an adjacent Zellij pane or different Blink tab with separate Mosh session&lt;/li&gt;&#10;&lt;li&gt;The agent commits and pushes to a branch, then opens a PR via &lt;code&gt;gh&lt;/code&gt; for me to review and merge&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;On the phone, I aim for the smallest practical edits and initial implementations.&#10;Even on personal repos, I still open a PR—sometimes prompting alone gets me to a merge‑ready result.&#10;For open‑source with real users, I finish on the computer with a careful review and any final edits.&lt;/p&gt;&#10;&lt;h2 id="9-conclusion-and-further-reading"&gt;9. Conclusion and Further Reading &lt;a class="anchor" href="#9-conclusion-and-further-reading" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;This phone setup extends my &lt;a href="https://www.nijho.lt/post/agentic-coding/"&gt;Agentic Coding&lt;/a&gt; workflow to mobile.&#10;I connect to the same Zellij session on &lt;code&gt;nixos&lt;/code&gt;, so when an idea hits, I pick up exactly where I left off—same environment, same context, no setup.&#10;Everything runs on my hardware (voice processing and automation) except the coding model itself.&#10;It’s the most effective mobile workflow I’ve had so far.&#10;It’s mostly open‑source not out of dogma, but because those tools are the best options for my needs.&#10;The one exception is the coding model: there’s no true open equivalent right now, and it makes no sense to buy 20× H100s just to self‑host a frontier model even if such a high quality model would be available open-source.&lt;/p&gt;&#10;&lt;p&gt;If you&amp;rsquo;re curious to dive deeper, here are a few related posts:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;a href="https://www.nijho.lt/post/agentic-coding/"&gt;Agentic Coding&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://www.nijho.lt/post/dotfiles/"&gt;Dotfiles: A Practical, Cross-Platform Terminal Setup&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://www.nijho.lt/post/local-ai-journey/"&gt;Local AI Journey: RTX 3090 Edition&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://www.nijho.lt/post/terminal-ninja/"&gt;Terminal Ninja&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://www.nijho.lt/post/using-llms/"&gt;Using LLMs Effectively&lt;/a&gt; (already outdated at this point!)&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;I&amp;rsquo;d love to hear what self-hosted tricks you&amp;rsquo;re using for mobile development—reach out if you adapt this stack or build something wild on top of it.&lt;/p&gt;&#10;</description></item><item><title>How a 3090 turned into two local‑first AI projects 🎮</title><link>https://www.nijho.lt/post/local-ai-journey/</link><pubDate>Mon, 14 Jul 2025 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/local-ai-journey/</guid><description>&lt;details class="toc" open&gt;&lt;summary&gt;Table of contents&lt;/summary&gt;&lt;nav id="TableOfContents"&gt;&#10; &lt;ul&gt;&#10; &lt;li&gt;&lt;a href="#1-introduction-the-unplayed-games"&gt;1. Introduction: The Unplayed Games&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#2-the-rocky-road-to-a-working-setup"&gt;2. The Rocky Road to a Working Setup&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#3-the-first-victory-agent-cli-"&gt;3. The First Victory: &lt;code&gt;agent-cli&lt;/code&gt; 🐍&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#4-the-grand-ambition-aibrain-"&gt;4. The Grand Ambition: &lt;code&gt;AIBrain&lt;/code&gt; 🧠&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#5-conclusion-the-journey-continues"&gt;5. Conclusion: The Journey Continues&lt;/a&gt;&lt;/li&gt;&#10; &lt;/ul&gt;&#10;&lt;/nav&gt;&lt;/details&gt;&#10;&#10;&lt;h2 id="1-introduction-the-unplayed-games"&gt;1. Introduction: The Unplayed Games &lt;a class="anchor" href="#1-introduction-the-unplayed-games" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;I&amp;rsquo;ve wanted to dabble with local AI for a long time, mainly because of the possibilities it unlocks.&#10;Local AI allows me to use all of my data—financial records, personal messages, location history, even transcribing everything I say—without the privacy concerns of sending it to a cloud provider.&#10;This level of deep, personal data analysis is only something I&amp;rsquo;d be comfortable with on my own hardware.&lt;/p&gt;&#10;&lt;p&gt;However, I couldn&amp;rsquo;t justify buying a powerful GPU just for an imagined AI project that I might never really start.&#10;Then it hit me: I could also use it for gaming (duh).&#10;Because I really enjoyed &lt;em&gt;The Last of Us&lt;/em&gt; series, I thought, “&lt;em&gt;Okay, why not play &lt;strong&gt;The Last of Us&lt;/strong&gt; on PC?&lt;/em&gt;”&#10;This was the push I needed.&#10;So, a couple of weeks ago, I convinced myself to buy someone&amp;rsquo;s old gaming machine.&#10;After many deep research sessions with ChatGPT about the best bang-for-the-buck consumer-grade GPU, I landed on an NVIDIA 3090.&#10;I found an (old) beast of a machine, and my initial idea was set.&lt;/p&gt;&#10;&lt;details&gt;&#10; &lt;summary&gt;Click here to see the full system specs ($1350 for all this!)&lt;/summary&gt;&#10; &lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;GPU:&lt;/strong&gt; ASUS ROG STRIX RTX 3090 OC (24GB)&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;CPU:&lt;/strong&gt; AMD Ryzen 9 3900X (12-Core)&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Motherboard:&lt;/strong&gt; ASUS ROG Crosshair VIII Hero&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Power Supply:&lt;/strong&gt; Be Quiet! 1200W Dark Power Pro&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;RAM:&lt;/strong&gt; 32GB G.Skill Trident Z Royal DDR4&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Storage:&lt;/strong&gt; 2TB Intel M.2 NVMe SSD&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Case:&lt;/strong&gt; Corsair 4000D Airflow&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;CPU Cooler:&lt;/strong&gt; Thermalright Peerless Assassin 120&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&#10;&lt;/details&gt;&#10;&#10;&lt;p&gt;Fast forward four weeks.&#10;I haven&amp;rsquo;t played a single minute, but my machine is now indexing 200 GB of email archives and facing a serious performance bottleneck I&amp;rsquo;ll get to later.&#10;That &amp;ldquo;gaming&amp;rdquo; machine has been humming away, but not rendering virtual worlds.&#10;It&amp;rsquo;s been training, transcribing, and thinking.&lt;/p&gt;&#10;&lt;p&gt;I&amp;rsquo;m writing this post simply because I am so enthusiastic about local AI.&#10;I&amp;rsquo;ve found that local models are surprisingly powerful for specific use cases.&#10;Of course, they aren&amp;rsquo;t going to replace Gemini or Claude for coding, but for many other applications, they work incredibly well.&#10;Lately, I&amp;rsquo;ve been so obsessed with this that I&amp;rsquo;m sleeping significantly less and spending all my free hours working on it.&lt;/p&gt;&#10;&lt;aside class="callout callout-note"&gt;&lt;svg class="ico" viewBox="0 0 256 256" aria-hidden="true"&gt;&lt;path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm16-40a8,8,0,0,1-8,8,16,16,0,0,1-16-16V128a8,8,0,0,1,0-16,16,16,0,0,1,16,16v40A8,8,0,0,1,144,176ZM112,84a12,12,0,1,1,12,12A12,12,0,0,1,112,84Z"/&gt;&lt;/svg&gt;&lt;div&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; This post is the story of how a quest for gaming turned into a deep dive into local, private AI.&#10;I&amp;rsquo;ll share the two open-source Python packages that came out of it, &lt;a href="https://github.com/basnijholt/agent-cli" target="_blank" rel="noopener"&gt;&lt;code&gt;agent-cli&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://github.com/basnijholt/aibrain" target="_blank" rel="noopener"&gt;&lt;code&gt;AIBrain&lt;/code&gt;&lt;/a&gt;, and the lessons I learned along the way.&lt;/div&gt;&lt;/aside&gt;&#10;&lt;h2 id="2-the-rocky-road-to-a-working-setup"&gt;2. The Rocky Road to a Working Setup &lt;a class="anchor" href="#2-the-rocky-road-to-a-working-setup" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Before I could even get to the fun AI stuff, I had to wrestle with the machine itself.&#10;I started with Pop!_OS, which is supposed to be a great out of the box solution that &amp;ldquo;just works&amp;rdquo;.&#10;However, it&amp;rsquo;s been a long time since I used Linux on the desktop; my experience is almost exclusively with servers.&#10;I quickly installed some of the wrong NVIDIA drivers and ended up debugging stuff in Grub.&#10;I got frustrated with the different commands I had to run to set up everything and realized that reproducing this system was going to be a massive pain.&#10;Since I&amp;rsquo;ve grown to love Nix on my Mac with Nix-Darwin, I decided to switch the whole system to &lt;strong&gt;NixOS&lt;/strong&gt;.&#10;This has been an amazing decision.&#10;Even though there are many pain points with Nix, I think it is well worth it.&#10;For those interested, you can see the complete setup, including all the local AI services, in my &lt;a href="https://github.com/basnijholt/dotfiles/blob/841cd5fb94b65556e7a2cc7d8ea618f86b50bf1d/configs/nixos/configuration.nix" target="_blank" rel="noopener"&gt;NixOS configuration on GitHub&lt;/a&gt;.&lt;/p&gt;&#10;&lt;h2 id="3-the-first-victory-agent-cli-"&gt;3. The First Victory: &lt;code&gt;agent-cli&lt;/code&gt; 🐍 &lt;a class="anchor" href="#3-the-first-victory-agent-cli-" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;With a stable system, I quickly got started with a few scripts.&#10;These soon evolved into my first real project of this adventure: &lt;a href="https://github.com/basnijholt/agent-cli" target="_blank" rel="noopener"&gt;&lt;code&gt;agent-cli&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;p&gt;&lt;code&gt;agent-cli&lt;/code&gt; is a collection of &lt;strong&gt;local-first&lt;/strong&gt;, AI-powered command-line agents that run entirely on your machine.&#10;The core philosophy is privacy; your data never leaves your computer.&#10;It&amp;rsquo;s designed for seamless integration with system-wide hotkeys.&#10;I&amp;rsquo;m now using it constantly.&lt;/p&gt;&#10;&lt;p&gt;Instead of typing, I now dictate almost everything.&#10;A quick hotkey, and &lt;code&gt;agent-cli transcribe&lt;/code&gt; turns my speech into text.&#10;Another hotkey, and &lt;code&gt;agent-cli autocorrect&lt;/code&gt; cleans it up using a local LLM running on &lt;strong&gt;Ollama&lt;/strong&gt;.&#10;It has genuinely streamlined my workflow.&#10;For example, I can pipe the tools together, like correcting text from my clipboard and then having it spoken aloud: &lt;code&gt;agent-cli autocorrect | agent-cli speak&lt;/code&gt;.&lt;/p&gt;&#10;&lt;aside class="callout callout-note"&gt;&lt;svg class="ico" viewBox="0 0 256 256" aria-hidden="true"&gt;&lt;path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm16-40a8,8,0,0,1-8,8,16,16,0,0,1-16-16V128a8,8,0,0,1,0-16,16,16,0,0,1,16,16v40A8,8,0,0,1,144,176ZM112,84a12,12,0,1,1,12,12A12,12,0,0,1,112,84Z"/&gt;&lt;/svg&gt;&lt;div&gt;&lt;p&gt;&lt;strong&gt;⚡ Surprising Speed: Local vs. Cloud&lt;/strong&gt;&lt;/p&gt;&#10;&lt;p&gt;One of my biggest surprises was the raw speed of local models.&#10;While I added an OpenAI provider to &lt;code&gt;agent-cli&lt;/code&gt; for those without local hardware (someone I know wanted to try it), I consistently find that local execution is faster than waiting on a cloud API.&#10;For example, a local transcription with Whisper + Ollama lands in my clipboard in well under a second—often beating the network round-trip to a remote service.&lt;/p&gt;&#10;&lt;/div&gt;&lt;/aside&gt;&#10;&lt;p&gt;The toolkit includes:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;code&gt;autocorrect&lt;/code&gt;: Fixes grammar and spelling.&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;transcribe&lt;/code&gt;: Uses a local Whisper model for speech-to-text.&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;speak&lt;/code&gt;: Converts text to speech with a local TTS engine.&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;voice-edit&lt;/code&gt;: A voice-powered clipboard assistant.&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;assistant&lt;/code&gt;: A hands-free voice assistant using a wake word.&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;chat&lt;/code&gt;: A conversational AI with tool-calling capabilities.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;It&amp;rsquo;s been a fantastic success in my personal workflow, and it&amp;rsquo;s all open-source for you to try.&#10;Contributions and ideas welcome!&lt;/p&gt;&#10;&lt;p&gt;Once I trusted my toolchain, the obvious next step was to feed &lt;em&gt;all&lt;/em&gt; my data into it.&lt;/p&gt;&#10;&lt;h2 id="4-the-grand-ambition-aibrain-"&gt;4. The Grand Ambition: &lt;code&gt;AIBrain&lt;/code&gt; 🧠 &lt;a class="anchor" href="#4-the-grand-ambition-aibrain-" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;My second package, &lt;a href="https://github.com/basnijholt/aibrain" target="_blank" rel="noopener"&gt;&lt;code&gt;AIBrain&lt;/code&gt;&lt;/a&gt;, is a much larger endeavor.&#10;The vision is to create a &amp;ldquo;life-OS&amp;rdquo;—a private, local AI that processes all my personal data (emails, calendar, files, photos, messages, browser history, location data) to generate summaries and answer questions, eventually allowing for queries like:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;aibrain ask &lt;span class="s2"&gt;&amp;#34;When did I get into landscape photography?&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;aibrain ask &lt;span class="s2"&gt;&amp;#34;How many countries did I visit between 2015-2025?&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This project led me to explore the landscape of agentic frameworks.&#10;I started with &lt;strong&gt;CrewAI&lt;/strong&gt;, but I quickly found that despite the hype and funding, it had some painful problems (specifically &lt;a href="https://github.com/crewAIInc/crewAI/issues/3031" target="_blank" rel="noopener"&gt;this issue&lt;/a&gt; where things just fail without any helpful error messages).&#10;I decided to switch to &lt;strong&gt;LangGraph&lt;/strong&gt;, which felt more robust.&#10;I also chose to not use &lt;strong&gt;PydanticAI&lt;/strong&gt; (which I used in &lt;code&gt;agent-cli&lt;/code&gt;) because its &lt;strong&gt;Ollama&lt;/strong&gt; support is just a wrapper around the OpenAI-compatible API, which doesn&amp;rsquo;t expose all the useful options (I even opened &lt;a href="https://github.com/ollama/ollama/pull/11249/" target="_blank" rel="noopener"&gt;a PR&lt;/a&gt; in Ollama to make its OpenAI compatible API more feature-complete, but it has not been merged yet).&lt;/p&gt;&#10;&lt;p&gt;The core infrastructure of &lt;code&gt;AIBrain&lt;/code&gt; is ready.&#10;It can index emails, calendar events, and files in various formats (PDF, Excel, PowerPoint, images).&#10;However, I&amp;rsquo;ve hit a significant challenge.&lt;/p&gt;&#10;&lt;aside class="callout callout-warning"&gt;&lt;svg class="ico" viewBox="0 0 256 256" aria-hidden="true"&gt;&lt;path d="M236.8,188.09,149.35,36.22h0a24.76,24.76,0,0,0-42.7,0L19.2,188.09a23.51,23.51,0,0,0,0,23.72A24.35,24.35,0,0,0,40.55,224h174.9a24.35,24.35,0,0,0,21.33-12.19A23.51,23.51,0,0,0,236.8,188.09ZM222.93,203.8a8.5,8.5,0,0,1-7.48,4.2H40.55a8.5,8.5,0,0,1-7.48-4.2,7.59,7.59,0,0,1,0-7.72L120.52,44.21a8.75,8.75,0,0,1,15,0l87.45,151.87A7.59,7.59,0,0,1,222.93,203.8ZM120,144V104a8,8,0,0,1,16,0v40a8,8,0,0,1-16,0Zm20,36a12,12,0,1,1-12-12A12,12,0,0,1,140,180Z"/&gt;&lt;/svg&gt;&lt;div&gt;&lt;p&gt;&lt;strong&gt;🔥 The Performance Bottleneck&lt;/strong&gt;&lt;/p&gt;&#10;&lt;p&gt;Even with a 12-core CPU and 24GB GPU, processing just a few days’ worth of email made the fans scream for half an hour.&#10;Processing hundreds of gigabytes of personal data is a massive undertaking, and it&amp;rsquo;s clear that my current approach isn&amp;rsquo;t scalable enough.&lt;/p&gt;&#10;&lt;/div&gt;&lt;/aside&gt;&#10;&lt;h2 id="5-conclusion-the-journey-continues"&gt;5. Conclusion: The Journey Continues &lt;a class="anchor" href="#5-conclusion-the-journey-continues" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;This past month has been a whirlwind.&#10;I set out to play games and ended up with two open-source AI packages and a much deeper appreciation for the power sitting in a consumer-grade GPU.&#10;It&amp;rsquo;s incredible what you can achieve with local hardware today.&lt;/p&gt;&#10;&lt;p&gt;&lt;code&gt;agent-cli&lt;/code&gt; is a finished, polished tool that I use daily.&#10;&lt;code&gt;AIBrain&lt;/code&gt; is a much bigger dream, and I&amp;rsquo;m still figuring out how to solve the performance puzzle to make it truly practical.&#10;Next up: smarter chunking with LangGraph, vector-DB swap-outs, and GPU scheduling tricks.&lt;/p&gt;&#10;&lt;p&gt;This experience has reinforced my passion for local-first software and open-source development.&#10;The future of personal, private AI is being built right now, not just in large corporate labs, but in the homes of hobbyists tinkering with their gaming rigs.&lt;/p&gt;&#10;&lt;p&gt;⭐ If this resonates, star the repos &amp;amp; drop issues/questions.&lt;/p&gt;&#10;&lt;p&gt;And who knows, maybe I&amp;rsquo;ll even find the time to play &amp;lsquo;The Last of Us&amp;rsquo;.&lt;/p&gt;&#10;</description></item></channel></rss>