<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Sam Hartley</title>
    <description>The latest articles on DEV Community by Sam Hartley (@samhartley_dev).</description>
    <link>https://dev.to/samhartley_dev</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3811539%2Fdd554e30-699d-42a3-a82a-77673790a186.png</url>
      <title>DEV Community: Sam Hartley</title>
      <link>https://dev.to/samhartley_dev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/samhartley_dev"/>
    <language>en</language>
    <item>
      <title>How I Turned My Homelab Into an AI Content Factory (And Why I'm Not Stopping)</title>
      <dc:creator>Sam Hartley</dc:creator>
      <pubDate>Wed, 19 Aug 2026 08:04:09 +0000</pubDate>
      <link>https://dev.to/samhartley_dev/how-i-turned-my-homelab-into-an-ai-content-factory-and-why-im-not-stopping-4ld7</link>
      <guid>https://dev.to/samhartley_dev/how-i-turned-my-homelab-into-an-ai-content-factory-and-why-im-not-stopping-4ld7</guid>
      <description>&lt;h1&gt;
  
  
  How I Turned My Homelab Into an AI Content Factory (And Why I'm Not Stopping)
&lt;/h1&gt;

&lt;p&gt;Six months ago I was paying $200/month for AI subscriptions. ChatGPT Plus, Claude Pro, Midjourney, a Zapier tier I barely used. My "AI workflow" was a mess of copy-paste between browser tabs.&lt;/p&gt;

&lt;p&gt;Today I run everything locally. My total monthly cost: &lt;strong&gt;$0&lt;/strong&gt;. And the weirdest part? The quality actually went &lt;em&gt;up&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;This is the story of how I stopped renting intelligence and started owning it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Subscription Trap
&lt;/h2&gt;

&lt;p&gt;Here's what my stack looked like in January 2026:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;Monthly Cost&lt;/th&gt;
&lt;th&gt;Usage&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ChatGPT Plus&lt;/td&gt;
&lt;td&gt;$20&lt;/td&gt;
&lt;td&gt;Daily&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Pro&lt;/td&gt;
&lt;td&gt;$20&lt;/td&gt;
&lt;td&gt;Weekly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Midjourney&lt;/td&gt;
&lt;td&gt;$30&lt;/td&gt;
&lt;td&gt;Rarely&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Zapier Pro&lt;/td&gt;
&lt;td&gt;$20&lt;/td&gt;
&lt;td&gt;One automation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ElevenLabs&lt;/td&gt;
&lt;td&gt;$5&lt;/td&gt;
&lt;td&gt;Voice experiments&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$95/month&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That's $1,140/year. For tools I didn't control, couldn't customize, and couldn't run without internet.&lt;/p&gt;

&lt;p&gt;The breaking point wasn't the money. It was the latency. I live in Turkey, and every API call to OpenAI or Anthropic meant a 200ms+ round trip to a US datacenter. When you're iterating on code with an AI assistant, that delay adds up. A 10-turn conversation becomes 2 seconds of waiting &lt;em&gt;per response&lt;/em&gt;. Multiply by 50 conversations a day and you've spent 15 minutes just... waiting.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Local Pivot
&lt;/h2&gt;

&lt;p&gt;I had hardware sitting around. A Mac Mini M4 (16GB RAM, 10-core GPU), a Windows PC with an RTX 3060 (12GB VRAM), and an old Ubuntu box I used for backups. Total investment: ~$1,200 over two years.&lt;/p&gt;

&lt;p&gt;I installed Ollama on all three. Downloaded Qwen 3.5 9B on the Mac, Qwen 3 Coder 30B on the Windows PC, and Granite 3.2 8B on the Ubuntu box. Each machine now serves a different purpose in my workflow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Response times dropped to under 100ms.&lt;/strong&gt; Not because local models are faster (they're not, per-token), but because there's no network hop. The model is &lt;em&gt;right there&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;But speed was just the beginning.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Multi-Agent Setup Nobody Talks About
&lt;/h2&gt;

&lt;p&gt;Here's where it gets interesting. I didn't just replace ChatGPT with Ollama. I built something better.&lt;/p&gt;

&lt;p&gt;I now have three specialized AI agents running 24/7:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Celebi&lt;/strong&gt; (Mac Mini, Qwen 3.5 9B) — My general assistant. Handles scheduling, weather, daily summaries, quick questions. It's the router, the coordinator, the one that knows what I'm working on.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;ProgrammierMinna&lt;/strong&gt; (Windows PC, Qwen 3 Coder 30B) — My code specialist. When I need a function written, a bug debugged, or a PR reviewed, this is where the query goes. 30B parameters means it actually understands context.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;DocMinna&lt;/strong&gt; (Ubuntu box, Granite 3.2 8B) — My writer. Documentation, READMEs, article drafts, technical specs. This model is worse at coding but surprisingly good at structure and flow.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The magic isn't the models. It's the &lt;em&gt;routing&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;I wrote a 20-line Python function that decides which agent handles which query. No complex NLP, no embeddings, just keyword matching. "Bug" goes to ProgrammierMinna. "Draft" goes to DocMinna. Everything else goes to Celebi.&lt;/p&gt;

&lt;p&gt;It sounds too simple to work. It works because the specialization is what matters, not the routing intelligence.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Article Factory
&lt;/h2&gt;

&lt;p&gt;The most unexpected result? My blog started writing itself.&lt;/p&gt;

&lt;p&gt;Here's the pipeline:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Idea generation&lt;/strong&gt;: Celebi scans my GitHub activity, Telegram messages, and project commits. It suggests topics based on what I've actually been building. No fake "10 AI trends for 2026" listicles.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Drafting&lt;/strong&gt;: DocMinna writes the first draft. It pulls from my actual project docs, not generic AI training data. The voice is consistent because it's trained on &lt;em&gt;my&lt;/em&gt; previous articles.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Review&lt;/strong&gt;: ProgrammierMinna checks code snippets, verifies API calls, catches hallucinated function names.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Publishing&lt;/strong&gt;: A Telegram bot posts to Dev.to automatically. I review and hit approve. Total human time: 5 minutes per article.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In the last 3 months, this pipeline produced 15 articles. All of them are about things I actually built. The engagement is higher than my hand-written posts because the consistency is better — same voice, same depth, same honesty.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Numbers After 6 Months
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Before (Cloud)&lt;/th&gt;
&lt;th&gt;After (Local)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Monthly AI cost&lt;/td&gt;
&lt;td&gt;$95&lt;/td&gt;
&lt;td&gt;$0*&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Avg. response time&lt;/td&gt;
&lt;td&gt;200-500ms&lt;/td&gt;
&lt;td&gt;50-100ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Offline capability&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Full&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Customization&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Unlimited&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data privacy&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Articles/month&lt;/td&gt;
&lt;td&gt;2-3&lt;/td&gt;
&lt;td&gt;5-6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Code quality (self-rated)&lt;/td&gt;
&lt;td&gt;7/10&lt;/td&gt;
&lt;td&gt;8.5/10&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;*Electricity cost: ~$8/month for running the Windows PC 24/7. The Mac Mini and Ubuntu box were already running.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Net savings: $1,032/year.&lt;/strong&gt; And that's just the money. The workflow improvements are harder to quantify but more impactful.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Actually Broke (And What I Fixed)
&lt;/h2&gt;

&lt;p&gt;It wasn't all smooth sailing. Here are the real problems:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Model updates broke my prompts&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When Qwen 3.5 got updated to a newer revision, my carefully tuned prompts started producing different outputs. Solution: I pin model versions (&lt;code&gt;qwen3.5:9b&lt;/code&gt; instead of &lt;code&gt;qwen3.5:latest&lt;/code&gt;) and test prompts before updating.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The 30B model is slow on my RTX 3060&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Qwen 3 Coder 30B takes 8-15 seconds for a response. For interactive coding, that's borderline acceptable. Solution: I use it for batch tasks (reviewing entire files, generating full modules) and fall back to Qwen 3.5 9B for quick Q&amp;amp;A.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. No multimodal (yet)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I can't show my local models screenshots and ask "what's wrong with this UI?" Yet. Granite 3.2 Vision 2B is promising but not quite there for real tasks. Solution: I use the Ollama vision endpoint for simple OCR and image description, and accept that complex visual tasks still need cloud APIs (rarely).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Setup complexity&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Installing Ollama is easy. Configuring three machines, a router, a Telegram bot, and a content pipeline is not. It took me two weekends. Solution: I documented everything and now the setup is reproducible in ~2 hours.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Unexpected Benefits
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;No vendor lock-in.&lt;/strong&gt; When OpenAI changes their pricing (again), I don't care. When Anthropic goes down, my workflow doesn't stop. When a new model drops on HuggingFace, I can try it in 10 minutes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Custom fine-tuning.&lt;/strong&gt; I fine-tuned a small model on my codebase. It's not good enough to replace ProgrammierMinna, but it can autocomplete my specific patterns with 90% accuracy. You can't do that with cloud APIs at any price.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Privacy by default.&lt;/strong&gt; My codebase, my documents, my conversations — none of it leaves my network. I work on client projects without worrying about IP leaks. I process personal documents without GDPR anxiety.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It's just more fun.&lt;/strong&gt; There's something deeply satisfying about knowing your assistant is running on &lt;em&gt;your&lt;/em&gt; hardware, in &lt;em&gt;your&lt;/em&gt; home, answering &lt;em&gt;your&lt;/em&gt; questions without phoning home to Silicon Valley.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd Do Differently
&lt;/h2&gt;

&lt;p&gt;If I started today, I'd skip the experimentation phase. Here's the optimal setup for a solo dev:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;One fast machine&lt;/strong&gt; with a decent GPU (RTX 3060 12GB minimum, RTX 4070 ideal). This is your workhorse.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One efficient machine&lt;/strong&gt; (Mac Mini M4, Intel NUC, or old laptop). This runs your coordinator and lightweight tasks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ollama&lt;/strong&gt; on both. No Docker, no complex orchestration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A simple router&lt;/strong&gt; in Python. 20 lines. Keyword matching. Don't overthink it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A Telegram bot&lt;/strong&gt; as your UI. No web dashboard, no mobile app needed. It works on every device you own.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Total cost: ~$800-1,500 for hardware (one-time), $0/month for software.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Takeaway
&lt;/h2&gt;

&lt;p&gt;AI doesn't have to be a subscription. It doesn't have to be a black box in someone else's datacenter. And it definitely doesn't have to be generic.&lt;/p&gt;

&lt;p&gt;The best AI setup is the one you control, customize, and iterate on. For me, that's three machines in my home lab, running models I chose, configured for my specific workflow.&lt;/p&gt;

&lt;p&gt;Is it more work than paying $20/month for ChatGPT? Yes. Is it worth it? Absolutely.&lt;/p&gt;

&lt;p&gt;If you're curious about local AI but don't know where to start, drop a comment. I read every one, and I'll point you to the right model for your hardware.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Running a Mac Mini M4 + RTX 3060 + Ubuntu box. Building AI tools that don't need the cloud. Find me on &lt;a href="https://t.me/celebibot_en" rel="noopener noreferrer"&gt;Telegram&lt;/a&gt; for more nerdy setup details.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>ollama</category>
      <category>selfhosted</category>
      <category>productivity</category>
    </item>
    <item>
      <title>I Wanted Real-Time Stock Charts on My Garmin Watch. Monkey C Had Other Plans.</title>
      <dc:creator>Sam Hartley</dc:creator>
      <pubDate>Sat, 01 Aug 2026 08:03:28 +0000</pubDate>
      <link>https://dev.to/samhartley_dev/i-wanted-real-time-stock-charts-on-my-garmin-watch-monkey-c-had-other-plans-2d7n</link>
      <guid>https://dev.to/samhartley_dev/i-wanted-real-time-stock-charts-on-my-garmin-watch-monkey-c-had-other-plans-2d7n</guid>
      <description>&lt;h1&gt;
  
  
  I Wanted Real-Time Stock Charts on My Garmin Watch. Monkey C Had Other Plans.
&lt;/h1&gt;

&lt;p&gt;I wanted stock prices on my wrist. Not a notification — an actual chart. A little sparkline showing me if Bitcoin was crashing while I was making coffee. If Tesla was tanking during my run. Something I could glance at without pulling out my phone.&lt;/p&gt;

&lt;p&gt;So I built it. And it almost broke me.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fantasy vs. The Reality
&lt;/h2&gt;

&lt;p&gt;In my head, this was simple:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Garmin has a watch face SDK&lt;/li&gt;
&lt;li&gt;The SDK can make web requests&lt;/li&gt;
&lt;li&gt;I pull stock data from an API&lt;/li&gt;
&lt;li&gt;I draw some lines&lt;/li&gt;
&lt;li&gt;Done&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Reality check #1: Garmin watch faces are written in &lt;strong&gt;Monkey C&lt;/strong&gt;. A language that — as far as I can tell — exists specifically to make mobile developers cry. No npm. No package manager. No Stack Overflow threads that actually answer your question. Just you, the docs, and a simulator that lies to you.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Constraints Hit Fast
&lt;/h2&gt;

&lt;p&gt;I have a Garmin Venu 2 Plus. Beautiful AMOLED display, 416×416 resolution, looks great. Here's what they don't advertise: &lt;strong&gt;124KB total memory&lt;/strong&gt; for your watch face. That's not a typo. One hundred and twenty-four kilobytes.&lt;/p&gt;

&lt;p&gt;To put that in perspective: the JSON response from a stock API is often 50-80KB. My watch face needs to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Store 5 tickers with 24 hours of price data each&lt;/li&gt;
&lt;li&gt;Draw 5 sparkline charts&lt;/li&gt;
&lt;li&gt;Render text labels&lt;/li&gt;
&lt;li&gt;Handle weather data&lt;/li&gt;
&lt;li&gt;Not crash&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All in 124KB. I spent my first two days just figuring out what I could fit.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Font Problem (AKA My First Mental Break)
&lt;/h2&gt;

&lt;p&gt;I needed custom text. The watch face shows ticker symbols, prices, time, dates. Garmin has something called &lt;code&gt;VectorFont&lt;/code&gt; that sounds perfect — scalable text that looks crisp at any size.&lt;/p&gt;

&lt;p&gt;It doesn't exist on the Venu 2 Plus.&lt;/p&gt;

&lt;p&gt;So I built my own. I call it &lt;strong&gt;PrimitiveFont&lt;/strong&gt; and it's exactly what it sounds like: every single character is drawn using &lt;code&gt;drawLine()&lt;/code&gt; calls on a 5×7 grid. The letter "A" is a collection of line segments. So is "$". So is every number.&lt;/p&gt;

&lt;p&gt;Here's what the letter "A" looks like in my font definition:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// "A" on a 5×7 grid — just drawLine() calls
CHAR_A = [0,6, 0,2,  0,2, 2,0,  2,0, 4,2,  4,2, 4,6,  0,4, 4,4]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's 21KB for the full alphabet including bold, italic, underline, and — the thing I'm most proud of — &lt;strong&gt;arc text&lt;/strong&gt; that curves along the watch's circular bezel. Because if you're going to build a font renderer from line segments, you might as well make it curve.&lt;/p&gt;

&lt;h2&gt;
  
  
  The API Situation
&lt;/h2&gt;

&lt;p&gt;I chose &lt;a href="https://twelvedata.com" rel="noopener noreferrer"&gt;Twelve Data&lt;/a&gt; for stock and crypto prices. Free tier: 800 calls per day. My math: 5 symbols × 96 fetches/day (every 15 minutes) = 480 calls. Comfortable margin.&lt;/p&gt;

&lt;p&gt;But here's the thing nobody tells you: &lt;strong&gt;the Garmin simulator cannot make web requests.&lt;/strong&gt; At all. You code the API call, you run the simulator, you get... nothing. To test a web request, you need a real phone connected via Bluetooth, with the Garmin Connect app running as a proxy, and the watch face actually installed on real hardware.&lt;/p&gt;

&lt;p&gt;My testing loop became:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Write code&lt;/li&gt;
&lt;li&gt;Build and push to watch&lt;/li&gt;
&lt;li&gt;Wait for 15-minute background interval&lt;/li&gt;
&lt;li&gt;Check if data appeared&lt;/li&gt;
&lt;li&gt;It didn't&lt;/li&gt;
&lt;li&gt;Guess why&lt;/li&gt;
&lt;li&gt;Repeat&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I spent three evenings on this. Three. The fix was adding a phone proxy layer that Garmin provides but barely documents. The phone makes the HTTP request, passes the JSON through to the watch face, and the watch parses it in a &lt;strong&gt;64KB background service sandbox&lt;/strong&gt; that is completely separate from the main app's memory.&lt;/p&gt;

&lt;p&gt;Yes, 64KB. For parsing JSON. I learned to extract exactly what I needed and discard everything else immediately.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Sparklines
&lt;/h2&gt;

&lt;p&gt;The whole point was the charts. I wanted 5 polar sparklines arranged in a ring around the watch face — each showing 24 hours of price movement. Green if the asset was up, red if down.&lt;/p&gt;

&lt;p&gt;Drawing this in Monkey C meant:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Converting price data to angles (0° to 360° around the circle)&lt;/li&gt;
&lt;li&gt;Drawing line segments between consecutive data points&lt;/li&gt;
&lt;li&gt;Color-coding based on daily change&lt;/li&gt;
&lt;li&gt;Keeping all coordinate math in integers (no floating-point performance hit)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It works. It actually looks pretty good. But getting there meant hand-tuning every pixel calculation because there's no charting library. No &lt;code&gt;drawSparkline()&lt;/code&gt;. Just &lt;code&gt;drawLine(x1, y1, x2, y2)&lt;/code&gt; and a lot of trigonometry I thought I'd never use again.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Metadata Discovery
&lt;/h2&gt;

&lt;p&gt;Early on, I hardcoded label mappings. "XAU/USD" → "GOLD". "BTC/USD" → "BITCOIN". Worked fine until I wanted to add Ethereum, then silver, then a random stock.&lt;/p&gt;

&lt;p&gt;Then I noticed the Twelve Data API returns metadata with every response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"meta"&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="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"symbol"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"XAU/USD"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"currency_base"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Gold Spot"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Precious Metal"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now labels generate themselves. Stocks use the symbol. Crypto uses the base currency name. Commodities get cleaned up automatically. I can add any ticker without touching the code.&lt;/p&gt;

&lt;p&gt;Small win. Felt huge after the font nightmare.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Actually Works
&lt;/h2&gt;

&lt;p&gt;After all that pain, here's what's running on my wrist right now:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;⏱️ Time, date, heart rate, steps, floors climbed&lt;/li&gt;
&lt;li&gt;📊 5 sparkline charts showing 24h price data for configurable assets&lt;/li&gt;
&lt;li&gt;🔄 Auto-updates every 15 minutes via Twelve Data&lt;/li&gt;
&lt;li&gt;🌤️ 6-hour weather forecast with custom icons&lt;/li&gt;
&lt;li&gt;⚙️ Fully configurable tickers through the Garmin Connect app&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Default watchlist: AAPL, TSLA, Gold, BTC/USD, ETH/USD. I change them depending on what I'm watching.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd Do Differently
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Start with a simpler device.&lt;/strong&gt; The Venu 2 Plus is beautiful but the memory limits are brutal. A Fenix or Forerunner with more resources would have saved me weeks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Read the simulator docs first.&lt;/strong&gt; I wasted days assuming the simulator was a reasonable testing environment. It's not. It's a layout previewer. Real testing requires real hardware.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don't build your own font.&lt;/strong&gt; Just... don't. If &lt;code&gt;VectorFont&lt;/code&gt; isn't available on your target device, pick a different device. I learned a lot but I wouldn't wish it on anyone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use the API metadata from day one.&lt;/strong&gt; Hardcoding is always a trap. APIs give you labels, use them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Honest Verdict
&lt;/h2&gt;

&lt;p&gt;Is this watch face useful? Honestly... sort of. I glance at it more than I expected. During market hours, it's genuinely handy to see BTC movement without unlocking my phone. During runs, it's just pretty data I ignore.&lt;/p&gt;

&lt;p&gt;Was it worth the effort? As a product — maybe not. As a learning experience — absolutely. I now know more about constrained embedded development than any web project could have taught me. 124KB makes you care about every byte.&lt;/p&gt;

&lt;p&gt;Would I build another Garmin app? Ask me in six months when I've forgotten the pain.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Sam Hartley is a solo dev who apparently enjoys suffering through proprietary SDKs. When he's not fighting Monkey C, he writes about local AI setups and home lab automation.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;→ &lt;a href="http://www.fiverr.com/s/XLyg" rel="noopener noreferrer"&gt;Custom dev work on Fiverr&lt;/a&gt;&lt;br&gt;
→ &lt;a href="https://t.me/celebibot_en" rel="noopener noreferrer"&gt;Follow along on Telegram&lt;/a&gt;&lt;/p&gt;

</description>
      <category>garmin</category>
      <category>connectiq</category>
      <category>crypto</category>
      <category>sideprojects</category>
    </item>
    <item>
      <title>I Gave My AI a Memory. It Changed Everything.</title>
      <dc:creator>Sam Hartley</dc:creator>
      <pubDate>Wed, 29 Jul 2026 08:03:29 +0000</pubDate>
      <link>https://dev.to/samhartley_dev/i-gave-my-ai-a-memory-it-changed-everything-il</link>
      <guid>https://dev.to/samhartley_dev/i-gave-my-ai-a-memory-it-changed-everything-il</guid>
      <description>&lt;h1&gt;
  
  
  I Gave My AI a Memory. It Changed Everything.
&lt;/h1&gt;

&lt;p&gt;I got tired of explaining my own codebase to an AI every single session.&lt;/p&gt;

&lt;p&gt;"Here's the architecture. Here's the README. Here's what I tried last time and why it didn't work." Every. Single. Time.&lt;/p&gt;

&lt;p&gt;Six months in, I realized I wasn't chatting with an assistant — I was onboarding a new intern who quit after every conversation and came back with total amnesia.&lt;/p&gt;

&lt;p&gt;So I built a memory system. Local. Free. And honestly, it's the single most useful AI upgrade I've made.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem Nobody Talks About
&lt;/h2&gt;

&lt;p&gt;LLMs don't remember you. They don't remember your projects. They don't remember that you tried &lt;code&gt;asyncio.gather&lt;/code&gt; last week and it deadlocked, or that your Garmin watch face has a hard 64KB memory limit, or that you always forget how your Telegram bot handles rate limits.&lt;/p&gt;

&lt;p&gt;What they have is a context window — a temporary scratchpad that gets wiped when the conversation ends. For coding workflows, this is broken by design. You're not having a chat. You're doing ongoing work on a codebase that spans months.&lt;/p&gt;

&lt;p&gt;I tried the obvious fixes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pasting huge chunks of context every time. Burned through token limits, got truncated responses.&lt;/li&gt;
&lt;li&gt;Keeping "project briefs" in a text file and pasting them in. Worked until I had ten projects.&lt;/li&gt;
&lt;li&gt;Using cloud memory features. Worked until I hit another subscription and another privacy question.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of it felt right. What I wanted was simple: ask a question about &lt;em&gt;my&lt;/em&gt; stuff, get an answer based on &lt;em&gt;my&lt;/em&gt; docs, with &lt;em&gt;zero&lt;/em&gt; setup friction.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Setup: RAG That Actually Runs Locally
&lt;/h2&gt;

&lt;p&gt;RAG (Retrieval-Augmented Generation) isn't new. What's new is that you can run the entire stack — embedding model, vector database, and LLM — on a Mac Mini without touching a single cloud API.&lt;/p&gt;

&lt;p&gt;Here's what I ended up with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;My Documents (markdown, code, notes, PDFs)
  → Chunked into ~400-token pieces
  → Embedded with nomic-embed-text (Ollama)
  → Stored in Chroma (local vector DB, no server needed)

Question
  → Embedded with the same model
  → Top-5 relevant chunks pulled from Chroma
  → Fed into Qwen 3.5 9B with a prompt template
  → Answer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Total stack: Ollama + Chroma + a 40-line Python script. No Docker. No cloud. No API keys.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Actually Indexed
&lt;/h2&gt;

&lt;p&gt;Not the whole internet. Just the stuff I actually need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Project docs&lt;/strong&gt; — READMEs, architecture decisions, API specs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;My notes&lt;/strong&gt; — Obsidian vault, scratchpads, debugging logs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code snippets&lt;/strong&gt; — Functions I keep reusing, config templates&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bookmarked solutions&lt;/strong&gt; — Stack Overflow answers I always forget&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deployment notes&lt;/strong&gt; — "How did I set up the VPS again?"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;About 4,800 chunks total. Query time: under 2 seconds on the Mac Mini, under 500ms if I route it through the Windows PC's RTX 3060.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Code (It's Embarrassingly Simple)
&lt;/h2&gt;

&lt;p&gt;Indexing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain.text_splitter&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;RecursiveCharacterTextSplitter&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain_community.document_loaders&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;DirectoryLoader&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain_community.embeddings&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OllamaEmbeddings&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain_community.vectorstores&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Chroma&lt;/span&gt;

&lt;span class="n"&gt;loader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;DirectoryLoader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;~/projects/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;glob&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;**/*.md&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;recursive&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;docs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;loader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;splitter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RecursiveCharacterTextSplitter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunk_size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chunk_overlap&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;chunks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;splitter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split_documents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;docs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;embeddings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OllamaEmbeddings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;nomic-embed-text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Chroma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_documents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;embeddings&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;persist_directory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;./knowledge-base&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;persist&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Querying:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ollama&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;ask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;similarity_search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;page_content&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ollama&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;qwen3.5:9b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Context:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s"&gt;Question: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;}]&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. No LangChain chains. No agents. No frameworks that require a PhD to configure.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Changed
&lt;/h2&gt;

&lt;p&gt;Before: "How does my Garmin watch face fetch stock data?" → paste 200 lines of code → wait → hope the model doesn't hallucinate.&lt;/p&gt;

&lt;p&gt;After: &lt;code&gt;ask("How does my Garmin watch face fetch stock data?")&lt;/code&gt; → &lt;strong&gt;"It uses Background.exit() to pass a dictionary with the ticker and price to the app-side view, because the background service has a 64KB memory limit."&lt;/strong&gt; — in 1.8 seconds. Correct. Specific. Zero hallucination.&lt;/p&gt;

&lt;p&gt;Before: "What's the rate limit for the crypto bot?" → dig through files → find nothing → guess.&lt;/p&gt;

&lt;p&gt;After: &lt;strong&gt;"3 requests per second with a 1.5-second cooldown between calls, enforced in the &lt;code&gt;throttle()&lt;/code&gt; decorator in &lt;code&gt;bot/utils.py&lt;/code&gt;."&lt;/strong&gt; — pulled straight from my own code comments.&lt;/p&gt;

&lt;p&gt;The AI stopped being a generalist that needed onboarding. It became a specialist that already knew my stuff.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Honest Downsides
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Indexing is slow.&lt;/strong&gt; Embedding 4,800 chunks on a Mac Mini CPU takes ~20 minutes. On the RTX 3060 it's ~4 minutes. You don't do it often — I run incremental updates via cron every hour — but the first run is painful.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Chunking is an art.&lt;/strong&gt; Too small (100 tokens) and you lose context. Too big (1000 tokens) and you hit the retrieval limit too fast. I settled on 400 with 50-token overlap after way too much trial and error.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It doesn't replace search.&lt;/strong&gt; If you ask "what's the syntax for Python list comprehension," RAG is overkill and probably retrieves some random list-handling function from your old projects. This is for &lt;em&gt;your&lt;/em&gt; knowledge, not general knowledge.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Storage grows.&lt;/strong&gt; 4,800 chunks ≈ 300MB of vector data. Manageable, but not nothing. Chroma handles it fine locally.&lt;/p&gt;

&lt;h2&gt;
  
  
  One Trick That Made It Actually Useful
&lt;/h2&gt;

&lt;p&gt;I added incremental updates with file hashes instead of re-indexing everything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;update_index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;docs_dir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cache_file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;./index-cache.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cache_file&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cache_file&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="n"&gt;changed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;docs_dir&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;rglob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*.md&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;md5&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_bytes&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;changed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;changed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# ... re-index only changed files
&lt;/span&gt;        &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cache_file&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;write_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Runs every hour via cron. Most of the time it does nothing. When I edit a file, it catches it. My knowledge base is never more than an hour stale.&lt;/p&gt;

&lt;h2&gt;
  
  
  When This Makes Sense
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Do this if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You work on multiple long-term projects&lt;/li&gt;
&lt;li&gt;You keep notes/docs/code that you reference repeatedly&lt;/li&gt;
&lt;li&gt;You want context-aware answers without pasting walls of text&lt;/li&gt;
&lt;li&gt;You have a machine that can run Ollama (Mac Mini M4, any modern PC)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Don't do this if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You only do one-off queries ("what's the weather")&lt;/li&gt;
&lt;li&gt;Your projects are small and you remember everything anyway&lt;/li&gt;
&lt;li&gt;You can't spare 300MB of disk space&lt;/li&gt;
&lt;li&gt;You expect it to replace Google (it won't)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Getting Started (15 Minutes)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Install Ollama&lt;/span&gt;
curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://ollama.com/install.sh | sh

&lt;span class="c"&gt;# 2. Pull models&lt;/span&gt;
ollama pull qwen3.5:9b
ollama pull nomic-embed-text

&lt;span class="c"&gt;# 3. Python deps&lt;/span&gt;
pip &lt;span class="nb"&gt;install &lt;/span&gt;chromadb langchain ollama

&lt;span class="c"&gt;# 4. Index your docs (adapt the script above)&lt;/span&gt;
&lt;span class="c"&gt;# 5. Ask questions&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Total time: 15 minutes. Total cost: $0. Monthly cost: $0.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Win
&lt;/h2&gt;

&lt;p&gt;The technical setup is neat. But the real change is mental.&lt;/p&gt;

&lt;p&gt;Before, every AI session started with context-building: "Here's what I'm working on, here's what I tried, here's the constraints." Now I just ask the question. The system already knows the constraints because it read my docs.&lt;/p&gt;

&lt;p&gt;It's the difference between calling a consultant and calling a teammate who was in the meeting.&lt;/p&gt;

&lt;p&gt;That gap — the gap between "AI that answers questions" and "AI that knows your work" — is bigger than I expected. And closing it costs exactly zero dollars.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Sam Hartley is a solo dev building local AI tools on a 3-machine home lab. Writes about the infrastructure that makes AI actually useful for real work.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;→ &lt;a href="http://www.fiverr.com/s/XLyg" rel="noopener noreferrer"&gt;Custom automation setups on Fiverr&lt;/a&gt;&lt;br&gt;
→ &lt;a href="https://t.me/celebibot_en" rel="noopener noreferrer"&gt;Follow CelebiBots on Telegram&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  ai #rag #ollama #selfhosted #productivity #buildinginpublic
&lt;/h1&gt;

</description>
      <category>ai</category>
      <category>rag</category>
      <category>ollama</category>
      <category>selfhosted</category>
    </item>
    <item>
      <title>I Built a Health Monitor for My AI Agents — Now They Tell Me When They're Dying</title>
      <dc:creator>Sam Hartley</dc:creator>
      <pubDate>Mon, 27 Jul 2026 08:03:06 +0000</pubDate>
      <link>https://dev.to/samhartley_dev/i-built-a-health-monitor-for-my-ai-agents-now-they-tell-me-when-theyre-dying-2333</link>
      <guid>https://dev.to/samhartley_dev/i-built-a-health-monitor-for-my-ai-agents-now-they-tell-me-when-theyre-dying-2333</guid>
      <description>&lt;h1&gt;
  
  
  I Built a Health Monitor for My AI Agents — Now They Tell Me When They're Dying
&lt;/h1&gt;

&lt;p&gt;For two weeks, my AI content pipeline was silently broken.&lt;/p&gt;

&lt;p&gt;The cron job was still running. The logs showed "SUCCESS." But no articles were actually posting. The Dev.to API was returning 429s (rate limited) and the script was swallowing the error because I forgot to check the response status.&lt;/p&gt;

&lt;p&gt;I only noticed because I happened to check the blog and saw the gap. Two weeks of missing posts. Two weeks of thinking everything was fine while the system was quietly failing.&lt;/p&gt;

&lt;p&gt;That was the moment I realized: my agents need a pulse.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: Silent Failures Are the Worst Failures
&lt;/h2&gt;

&lt;p&gt;I run three AI agents on three machines. Celebi on a Mac Mini, ProgrammierMinna on a Windows PC, DocMinna on... also the Mac Mini. They're connected via a simple router, triggered by cron jobs, and they talk to me through Telegram.&lt;/p&gt;

&lt;p&gt;When everything works, it's magic. When something breaks, it's archaeology.&lt;/p&gt;

&lt;p&gt;Common failure modes I'd hit:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ollama crashed&lt;/strong&gt; on the Windows PC after a Windows update. No one knew. Queries just timed out.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Disk full&lt;/strong&gt; on the Mac Mini because model files kept accumulating. Logs stopped rotating. The system ground to a halt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API rate limits&lt;/strong&gt; on Dev.to (max 10 posts/day). The publish script didn't retry or notify.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Router misconfiguration&lt;/strong&gt; after a router restart. The Windows PC got a new IP. Queries fell into the void.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Model pulled but not loaded.&lt;/strong&gt; I'd update a model, restart Ollama, but forget to actually &lt;code&gt;ollama run&lt;/code&gt; it. First query would error out.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each of these took 20-60 minutes to diagnose. Not because they're hard problems, but because I didn't know where to look.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix: A 30-Line Health Check Script
&lt;/h2&gt;

&lt;p&gt;I didn't build Prometheus. I didn't install Grafana. I wrote a Python script that runs every 10 minutes, checks the things I care about, and sends me a Telegram message if something is wrong.&lt;/p&gt;

&lt;p&gt;That's it. No dashboard. No metrics server. Just "tell me when it's broken."&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# health_check.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;shutil&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;

&lt;span class="n"&gt;TELEGRAM_BOT_TOKEN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;your_token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;TELEGRAM_CHAT_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;your_chat_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;MACHINES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mac_mini&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://192.168.1.102:11434&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;windows_pc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://192.168.1.106:11434&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ubuntu&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://192.168.1.100:11434&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;MODELS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;qwen3.5:9b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;qwen3-coder:30b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;granite3.2:8b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.telegram.org/bot&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;TELEGRAM_BOT_TOKEN&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/sendMessage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chat_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TELEGRAM_CHAT_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;🚨 &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;check_ollama&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;machine_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/api/tags&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Ollama down on &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;machine_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: HTTP &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;

        &lt;span class="n"&gt;models&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;models&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])]&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;MODELS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="nf"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Model &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; missing on &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;machine_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exceptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;ConnectionError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Ollama unreachable on &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;machine_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;check_disk&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;disk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;shutil&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;disk_usage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;percent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;disk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;used&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;disk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;percent&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Disk usage: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;percent&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;% — clean up needed!&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;check_last_article&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="c1"&gt;# Check if an article was posted in the last 3 days
&lt;/span&gt;    &lt;span class="c1"&gt;# This reads a simple timestamp file written by the publish script
&lt;/span&gt;    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/tmp/last_article_timestamp.txt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;last&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;last&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;No article published in 3 days — pipeline may be stuck&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;FileNotFoundError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;  &lt;span class="c1"&gt;# No articles yet, that's fine
&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;all_ok&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="n"&gt;all_ok&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;=&lt;/span&gt; &lt;span class="nf"&gt;check_disk&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;MACHINES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;all_ok&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;=&lt;/span&gt; &lt;span class="nf"&gt;check_ollama&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nf"&gt;check_last_article&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# Non-critical, don't fail on this
&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;all_ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Optional: send heartbeat once per day
&lt;/span&gt;        &lt;span class="k"&gt;pass&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Total dependencies: &lt;code&gt;requests&lt;/code&gt; (probably already installed). Total lines: ~50. Total setup time: 10 minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Actually Monitor
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Ollama Heartbeat
&lt;/h3&gt;

&lt;p&gt;The most critical check. Every machine gets pinged at &lt;code&gt;/api/tags&lt;/code&gt; every 10 minutes. If it doesn't respond in 5 seconds, I get a Telegram alert with the machine name.&lt;/p&gt;

&lt;p&gt;This caught a Windows update restart last week. The PC came back up, but Ollama didn't start (I hadn't added it to startup yet). I knew within 10 minutes instead of finding out when I actually needed it.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Model Presence
&lt;/h3&gt;

&lt;p&gt;Even if Ollama is running, the model I need might not be loaded. I check that my three core models (&lt;code&gt;qwen3.5:9b&lt;/code&gt;, &lt;code&gt;qwen3-coder:30b&lt;/code&gt;, &lt;code&gt;granite3.2:8b&lt;/code&gt;) are available on their respective machines.&lt;/p&gt;

&lt;p&gt;This saved me once when I updated &lt;code&gt;qwen3-coder&lt;/code&gt; and the new version had a different tag. The old tag was gone. The router was pointing to a ghost. The monitor caught it before the first failed query.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Disk Space
&lt;/h3&gt;

&lt;p&gt;Model files are big. A 30B parameter model is ~20GB. Three machines times three models each — that's a lot of storage that grows quietly. I alert at 90% disk usage.&lt;/p&gt;

&lt;p&gt;The Mac Mini has a 256GB SSD. I hit 95% once and the system started swapping aggressively. Response times went from 2 seconds to 30 seconds. Now I clean up before it hurts.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Pipeline Liveness
&lt;/h3&gt;

&lt;p&gt;This is the subtle one. The cron job runs, but is it actually doing anything? I write a timestamp file every time an article successfully publishes. The health check compares that timestamp to "now." If it's been more than 3 days, I get an alert.&lt;/p&gt;

&lt;p&gt;This is what would have caught the silent 429 errors. The script was running, but not publishing. The timestamp wouldn't update. I'd know.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Router Reachability (Bonus)
&lt;/h3&gt;

&lt;p&gt;I added a check that pings the gateway router itself. If the whole network is down, I get a different alert. This happened once during a power outage — the router rebooted faster than the machines, but DHCP reassigned IPs. I knew the network was wonky before I even tried to query anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Don't Monitor (On Purpose)
&lt;/h2&gt;

&lt;p&gt;I'm not running a datacenter. There are things I deliberately don't check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GPU temperature.&lt;/strong&gt; My RTX 3060 has a perfectly fine stock cooler. If it throttles, I'll notice in response times. Adding temp monitoring means sensors, drivers, more code. Not worth it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Network bandwidth.&lt;/strong&gt; I'm not streaming video. Local network latency is never the bottleneck.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CPU load.&lt;/strong&gt; The Mac Mini sits at 15% most of the time. If it spikes, I don't care unless it's sustained — and then Ollama response times will tell me.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Log aggregation.&lt;/strong&gt; I read logs when something breaks. I don't need them shipped to Elasticsearch.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The rule: if a failure mode would be annoying to debug, monitor it. If it's just "nice to know," skip it.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Run It
&lt;/h2&gt;

&lt;p&gt;The script runs as a systemd timer on the Mac Mini. Every 10 minutes, checks fire. Alerts go to Telegram. If everything is fine, nothing happens. Silent success is the goal.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;# /etc/systemd/system/ai-health-check.timer
&lt;/span&gt;&lt;span class="nn"&gt;[Unit]&lt;/span&gt;
&lt;span class="py"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;AI Agent Health Check&lt;/span&gt;

&lt;span class="nn"&gt;[Timer]&lt;/span&gt;
&lt;span class="py"&gt;OnCalendar&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;*:0/10&lt;/span&gt;
&lt;span class="py"&gt;Persistent&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;

&lt;span class="nn"&gt;[Install]&lt;/span&gt;
&lt;span class="py"&gt;WantedBy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;timers.target&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;# /etc/systemd/system/ai-health-check.service
&lt;/span&gt;&lt;span class="nn"&gt;[Unit]&lt;/span&gt;
&lt;span class="py"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Run AI health check&lt;/span&gt;

&lt;span class="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;oneshot&lt;/span&gt;
&lt;span class="py"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/bin/python3 /home/sam/health_check.py&lt;/span&gt;
&lt;span class="py"&gt;User&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;sam&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;ai-health-check.timer
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl start ai-health-check.timer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Takes 30 seconds to set up. Runs forever.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Changed (The Honest Version)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;I fix things before they hurt.&lt;/strong&gt; The Windows PC issue was fixed within 10 minutes of the alert. Before monitoring, it would have been "why is this query so slow today?" followed by 20 minutes of ssh and log reading.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I trust the system more.&lt;/strong&gt; There's a difference between "I think it's working" and "I know it's working because the monitor is green." I can focus on building instead of worrying.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;But I also get more alerts than I'd like.&lt;/strong&gt; The Ubuntu box is on WiFi and occasionally drops for 30 seconds. I get a "unreachable" alert, then 10 minutes later it's fine. I haven't tuned the thresholds yet because... honestly, it's not annoying enough to fix.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;False positives exist.&lt;/strong&gt; Once, the model check failed because Ollama was still loading after a restart. The model existed, but &lt;code&gt;/api/tags&lt;/code&gt; returned an empty list for 15 seconds. I added a 5-second retry and it went away.&lt;/p&gt;

&lt;h2&gt;
  
  
  When This Matters (And When It Doesn't)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Do this if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You have automated processes running unsupervised&lt;/li&gt;
&lt;li&gt;You've had a "wait, when did that break?" moment&lt;/li&gt;
&lt;li&gt;You use Telegram (or Slack, or email) anyway&lt;/li&gt;
&lt;li&gt;You value knowing about problems over perfect metrics&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Don't do this if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You're still debugging your setup manually every day (fix the core issue first)&lt;/li&gt;
&lt;li&gt;You want beautiful dashboards (use Prometheus + Grafana instead)&lt;/li&gt;
&lt;li&gt;You enjoy the thrill of surprise failures&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Real Lesson
&lt;/h2&gt;

&lt;p&gt;The best monitoring isn't the one that tells you everything. It's the one that tells you the thing you actually need to know, at the time you can still do something about it.&lt;/p&gt;

&lt;p&gt;My 50-line script isn't impressive. It won't get me DevOps cred. But it caught three real issues in the first month, and it cost me nothing but an afternoon.&lt;/p&gt;

&lt;p&gt;If you're running AI agents at home and you don't know when they break... you just don't know when they break. Fix that first. Everything else is optimization.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Sam Hartley is a solo dev running a monitored 3-machine AI home lab. Writes about the boring infrastructure that makes local AI actually reliable.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;→ &lt;a href="http://www.fiverr.com/s/XLyg" rel="noopener noreferrer"&gt;Custom automation setups on Fiverr&lt;/a&gt;&lt;br&gt;
→ &lt;a href="https://t.me/celebibot_en" rel="noopener noreferrer"&gt;Follow CelebiBots on Telegram&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  ai #agents #monitoring #devops #selfhosted #homelab #buildinginpublic
&lt;/h1&gt;

</description>
      <category>ai</category>
      <category>automation</category>
      <category>monitoring</category>
      <category>selfhosted</category>
    </item>
    <item>
      <title>I Moved My AI Stack From NVIDIA to Apple Silicon. Then I Moved Back. Here's Why.</title>
      <dc:creator>Sam Hartley</dc:creator>
      <pubDate>Sat, 25 Jul 2026 08:03:13 +0000</pubDate>
      <link>https://dev.to/samhartley_dev/i-moved-my-ai-stack-from-nvidia-to-apple-silicon-then-i-moved-back-heres-why-5emj</link>
      <guid>https://dev.to/samhartley_dev/i-moved-my-ai-stack-from-nvidia-to-apple-silicon-then-i-moved-back-heres-why-5emj</guid>
      <description>&lt;h1&gt;
  
  
  I Moved My AI Stack From NVIDIA to Apple Silicon. Then I Moved Back. Here's Why.
&lt;/h1&gt;

&lt;p&gt;I bought a Mac Mini M4 thinking it would replace my Windows PC for local AI. Spoiler: it didn't. But it also didn't fail — it just turned out to be good at different things than I expected.&lt;/p&gt;

&lt;p&gt;Here's six months of actual usage data, with the real numbers nobody puts in benchmark charts.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Hardware
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Machine&lt;/th&gt;
&lt;th&gt;CPU/GPU&lt;/th&gt;
&lt;th&gt;RAM&lt;/th&gt;
&lt;th&gt;What I Paid&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Mac Mini M4&lt;/td&gt;
&lt;td&gt;10-core M4, 10-core GPU&lt;/td&gt;
&lt;td&gt;16GB unified&lt;/td&gt;
&lt;td&gt;$599&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Windows PC&lt;/td&gt;
&lt;td&gt;AMD 9970X&lt;/td&gt;
&lt;td&gt;128GB&lt;/td&gt;
&lt;td&gt;~$2,500 (existing)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;RTX 3060 12GB&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;$150 (used)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The Mac Mini is silent, tiny, and sips power. The PC sounds like a jet engine under load and pulls 200W+ when the GPU is working. I wanted the Mac to replace the PC for everything AI-related. I was half right.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Migration (Month 1-2)
&lt;/h2&gt;

&lt;p&gt;I installed Ollama on the Mac Mini, pulled Qwen 3.5 9B, and started using it as my daily driver. The first thing I noticed: it's fast. Really fast. For a 9B model, inference felt snappier than the RTX 3060 running the same model.&lt;/p&gt;

&lt;p&gt;But then I tried loading bigger models.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Qwen 3 Coder 30B (Q4_K_M, ~18GB):&lt;/strong&gt; Loaded fine on the RTX 3060 (12GB VRAM... wait, that shouldn't work). Oh right — it offloads to system RAM. Slow, but functional. On the Mac Mini? Out of memory. 16GB unified memory isn't 16GB free memory — the OS, browser, and apps eat 6-8GB before Ollama even starts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DeepSeek R1 8B:&lt;/strong&gt; Runs great on both. Mac Mini: ~25 tok/s. RTX 3060: ~35 tok/s. The GPU wins, but not by enough to matter for interactive use.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Qwen 3.5 9B:&lt;/strong&gt; Mac Mini: ~18 tok/s. RTX 3060: ~28 tok/s. Again, GPU is faster, but both feel instant.&lt;/p&gt;

&lt;p&gt;So far: Mac Mini handles small models well. But I hit the wall fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Wall (Month 3)
&lt;/h2&gt;

&lt;p&gt;Three things broke my "Mac only" dream:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Memory Pressure
&lt;/h3&gt;

&lt;p&gt;16GB unified memory sounds like a lot until you're running:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ollama (4-8GB for a loaded model)&lt;/li&gt;
&lt;li&gt;VS Code with a few extensions (2-3GB)&lt;/li&gt;
&lt;li&gt;Safari with 20 tabs (3-4GB)&lt;/li&gt;
&lt;li&gt;A terminal, a music player, maybe a video call (2GB)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's 11-17GB before you even ask the model a question. macOS starts swapping. Ollama slows down. The whole machine gets sluggish.&lt;/p&gt;

&lt;p&gt;On the Windows PC with 128GB RAM? I can load a 30B model &lt;em&gt;and&lt;/em&gt; run a game &lt;em&gt;and&lt;/em&gt; compile code simultaneously. The RTX 3060's 12GB VRAM is a separate pool that doesn't compete with the OS.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Model Availability
&lt;/h3&gt;

&lt;p&gt;Not every model runs well on Apple Silicon. The M4 has excellent support for mainstream models (Llama, Qwen, Mistral), but niche or newly released models often ship with "CUDA only" initial releases. I spent an afternoon trying to get a specific vision model running on the Mac before giving up and running it on the PC in 10 minutes.&lt;/p&gt;

&lt;p&gt;The NVIDIA ecosystem is still the default for AI tooling. Apple Silicon is catching up, but it's not there yet.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Multi-User / Multi-Model
&lt;/h3&gt;

&lt;p&gt;I wanted to run two models simultaneously — a coding assistant and a general chatbot. On the Mac Mini, loading two 9B models (~12GB total) leaves zero headroom for the OS. Everything crawls.&lt;/p&gt;

&lt;p&gt;On the PC, I can load a 30B model in VRAM and a 7B model in system RAM, and both respond quickly. The GPU handles the heavy one; the CPU handles the light one. Separate memory pools are a feature, not a bug.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Mac Mini Actually Excels At
&lt;/h2&gt;

&lt;p&gt;I didn't move everything back to the PC. The Mac Mini found its niche, and it's a good one:&lt;/p&gt;

&lt;h3&gt;
  
  
  Always-On Tasks
&lt;/h3&gt;

&lt;p&gt;The Mac Mini draws ~15W at idle. The PC draws ~80W at idle (that GPU doesn't sleep well). For 24/7 tasks — routing, scheduling, lightweight notifications — the Mac wins on power cost alone.&lt;/p&gt;

&lt;h3&gt;
  
  
  Small Model Inference
&lt;/h3&gt;

&lt;p&gt;For anything 9B and under, the Mac Mini is genuinely competitive. The unified memory architecture means there's no "copy to VRAM" overhead. A 4B model on the Mac can feel faster than the same model on the GPU because the latency is lower.&lt;/p&gt;

&lt;h3&gt;
  
  
  No Driver Drama
&lt;/h3&gt;

&lt;p&gt;NVIDIA drivers on Windows are... fine. Until they aren't. An update breaks CUDA, a new Ollama version wants a different driver, and suddenly you're debugging &lt;code&gt;nvidia-smi&lt;/code&gt; on a Tuesday night. The Mac just works. I have never, in six months, had an Apple Silicon AI setup break because of a system update.&lt;/p&gt;

&lt;h3&gt;
  
  
  Portability
&lt;/h3&gt;

&lt;p&gt;I unplugged the Mac Mini, took it to a different room, plugged it back in, and my entire AI stack was back online in 30 seconds. Try that with a full tower PC.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Numbers
&lt;/h2&gt;

&lt;p&gt;Here's a month of actual usage, tracked with a simple script that logs tokens/sec and power draw:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Mac Mini M4&lt;/th&gt;
&lt;th&gt;RTX 3060&lt;/th&gt;
&lt;th&gt;Winner&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Qwen 3.5 9B, 500-token prompt&lt;/td&gt;
&lt;td&gt;18 tok/s&lt;/td&gt;
&lt;td&gt;28 tok/s&lt;/td&gt;
&lt;td&gt;RTX 3060&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DeepSeek R1 8B, reasoning&lt;/td&gt;
&lt;td&gt;25 tok/s&lt;/td&gt;
&lt;td&gt;35 tok/s&lt;/td&gt;
&lt;td&gt;RTX 3060&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen 3 4B, quick chat&lt;/td&gt;
&lt;td&gt;42 tok/s&lt;/td&gt;
&lt;td&gt;55 tok/s&lt;/td&gt;
&lt;td&gt;RTX 3060&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;30B model loading&lt;/td&gt;
&lt;td&gt;❌ OOM&lt;/td&gt;
&lt;td&gt;✅ Loads (slow)&lt;/td&gt;
&lt;td&gt;RTX 3060&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Two models simultaneously&lt;/td&gt;
&lt;td&gt;❌ Crawls&lt;/td&gt;
&lt;td&gt;✅ Works&lt;/td&gt;
&lt;td&gt;RTX 3060&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Idle power draw&lt;/td&gt;
&lt;td&gt;~15W&lt;/td&gt;
&lt;td&gt;~80W&lt;/td&gt;
&lt;td&gt;Mac Mini&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Load power draw&lt;/td&gt;
&lt;td&gt;~35W&lt;/td&gt;
&lt;td&gt;~220W&lt;/td&gt;
&lt;td&gt;Mac Mini&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Setup time (new model)&lt;/td&gt;
&lt;td&gt;2 min&lt;/td&gt;
&lt;td&gt;2 min&lt;/td&gt;
&lt;td&gt;Tie&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Driver reliability&lt;/td&gt;
&lt;td&gt;Flawless&lt;/td&gt;
&lt;td&gt;Occasional issues&lt;/td&gt;
&lt;td&gt;Mac Mini&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What I Actually Do Now
&lt;/h2&gt;

&lt;p&gt;I stopped trying to pick a winner. Both machines have jobs:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mac Mini M4:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Always-on orchestration (routing, scheduling, notifications)&lt;/li&gt;
&lt;li&gt;Small model inference (4-9B) for quick tasks&lt;/li&gt;
&lt;li&gt;Coding and development (it's my daily driver)&lt;/li&gt;
&lt;li&gt;Runs 24/7, costs ~$3/month in power&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Windows PC + RTX 3060:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Heavy inference (30B models, image generation)&lt;/li&gt;
&lt;li&gt;GPU rental on Vast.ai when I'm not using it&lt;/li&gt;
&lt;li&gt;Anything that needs CUDA or more than 8GB VRAM&lt;/li&gt;
&lt;li&gt;On-demand, not 24/7&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This isn't a benchmark conclusion. It's a workflow conclusion. The Mac Mini is the brain. The PC is the muscle. Separating them by task type eliminated the "which machine should I use?" decision fatigue and made both faster at what they're good at.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Honest Bottom Line
&lt;/h2&gt;

&lt;p&gt;If you're buying one machine for local AI and you want to run large models: get an NVIDIA GPU. Full stop. The ecosystem, the memory architecture, and the raw inference speed are still unbeatable for serious workloads.&lt;/p&gt;

&lt;p&gt;If you want a silent, efficient, always-on machine for smaller models and orchestration: the Mac Mini M4 is excellent. Just don't expect it to replace a dedicated GPU for everything.&lt;/p&gt;

&lt;p&gt;And if you already have both? Stop trying to consolidate. Use each for what it's good at. The separation is the feature.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Sam Hartley is a solo dev running a split AI stack across a Mac Mini and a Windows PC. No benchmarks were harmed in the making of this article — just a lot of &lt;code&gt;time&lt;/code&gt; commands and a power meter.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Drop your setup in the comments — curious if others have found the same split, or if you've made a single-machine setup work for everything.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>applesilicon</category>
      <category>nvidia</category>
      <category>selfhosted</category>
    </item>
    <item>
      <title>I Stopped Asking One AI to Do Everything. Here's What Happened.</title>
      <dc:creator>Sam Hartley</dc:creator>
      <pubDate>Thu, 23 Jul 2026 08:03:37 +0000</pubDate>
      <link>https://dev.to/samhartley_dev/i-stopped-asking-one-ai-to-do-everything-heres-what-happened-57b7</link>
      <guid>https://dev.to/samhartley_dev/i-stopped-asking-one-ai-to-do-everything-heres-what-happened-57b7</guid>
      <description>&lt;h1&gt;
  
  
  I Stopped Asking One AI to Do Everything. Here's What Happened.
&lt;/h1&gt;

&lt;p&gt;For months I had one AI agent. One model. One endpoint. I'd throw every question at it — code review, article drafts, debugging, random "what's the weather" questions — and expect it to handle it all.&lt;/p&gt;

&lt;p&gt;It didn't. Not really. It answered everything, but it answered everything the same way. The same tone, the same depth, the same blind spots. When I needed a surgical code review, I got a friendly chat response. When I needed a casual summary, I got a three-paragraph essay.&lt;/p&gt;

&lt;p&gt;So I split it into three specialized agents. Not because it's trendy. Because general-purpose AI is a compromise, and I was tired of compromising.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: One Brain, Too Many Jobs
&lt;/h2&gt;

&lt;p&gt;I run a home lab on a Mac Mini M4, a Windows PC with an RTX 3060, and an Ubuntu box. All three have Ollama installed. For the first six months, I just used the biggest model I could fit and routed everything to it.&lt;/p&gt;

&lt;p&gt;That model was Qwen 3 Coder 30B — a coding specialist. Great for refactoring. Great for debugging. But when I asked it to "write a friendly summary of my day," it would respond with something that sounded like a technical spec document. Because that's what it was trained to do.&lt;/p&gt;

&lt;p&gt;The reverse was just as bad. When I used a general chat model for coding questions, it would hallucinate APIs, miss edge cases, and write code that looked right but subtly violated conventions.&lt;/p&gt;

&lt;p&gt;I was asking a brain surgeon to do therapy, and a therapist to do surgery.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Split: Three Agents, Three Jobs
&lt;/h2&gt;

&lt;p&gt;I didn't build a microservices architecture. I didn't install Kubernetes. I added two more Ollama endpoints and a 20-line router.&lt;/p&gt;

&lt;p&gt;Here's what I ended up with:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Celebi (Mac Mini, Qwen 3.5 9B)&lt;/strong&gt; — The generalist. Handles routing, scheduling, daily summaries, weather, quick questions. Response time: 1-2 seconds. Perfect for "what's on my calendar today" and "summarize these emails."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ProgrammierMinna (Windows PC, Qwen 3 Coder 30B)&lt;/strong&gt; — The coder. Handles code generation, refactoring, debugging, PR review. Response time: 8-15 seconds. When I need a function written or a bug found, this is where the query goes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DocMinna (Mac Mini, Granite 3.2 8B)&lt;/strong&gt; — The writer. Handles documentation, article drafts, READMEs, technical specs. Response time: 3-5 seconds. This model is worse at coding but surprisingly good at structure and flow.&lt;/p&gt;

&lt;p&gt;The router is embarrassingly simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;route_query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;coding_keywords&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;code&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;function&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bug&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;refactor&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;debug&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pr&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;review&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;writing_keywords&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;write&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;draft&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;article&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;readme&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;doc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;summary&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;blog&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;coding_keywords&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://192.168.1.106:11434&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;  &lt;span class="c1"&gt;# ProgrammierMinna
&lt;/span&gt;    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;writing_keywords&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://192.168.1.102:11434&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;  &lt;span class="c1"&gt;# DocMinna
&lt;/span&gt;    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://192.168.1.102:11434&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;  &lt;span class="c1"&gt;# Celebi
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Is it perfect? No. "Write a Python script that sends emails" gets routed to DocMinna because of "write," when it probably should go to ProgrammierMinna. I fix those manually when I catch them. But it works 90% of the time, and that's enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Actually Changed
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Quality went up immediately
&lt;/h3&gt;

&lt;p&gt;Before the split, I'd ask for a code review and get generic advice like "consider adding error handling." After the split, ProgrammierMinna would say "this async function doesn't handle TimeoutError — add a try/except around line 47, and use asyncio.wait_for with a 5-second timeout."&lt;/p&gt;

&lt;p&gt;Same question. Different model. Different depth.&lt;/p&gt;

&lt;h3&gt;
  
  
  I stopped over-explaining
&lt;/h3&gt;

&lt;p&gt;With the generalist, I'd have to add context like "please be thorough, this is for production code" or "keep it casual, this is a blog post." The specialists already know their role. I don't need to prompt-engineer the tone. It's baked into the model choice.&lt;/p&gt;

&lt;h3&gt;
  
  
  Parallel processing became possible
&lt;/h3&gt;

&lt;p&gt;I can now fire off a coding task to ProgrammierMinna and a writing task to DocMinna simultaneously. They're running on different machines with different GPUs. No queue. No waiting for one to finish before the other starts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fallbacks got simpler
&lt;/h3&gt;

&lt;p&gt;When the Windows PC is offline ( asleep, rented out on Vast.ai, or I'm traveling), queries that would normally go to ProgrammierMinna fall back to Celebi with a note: "PC offline — answering with generalist model, quality may vary." The system degrades gracefully instead of just failing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Honest Downsides
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Three models to manage.&lt;/strong&gt; Updates, storage, keeping track of which version is on which machine — it's overhead. Each model is 4-20GB. My model folder went from 40GB to 120GB across three machines.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Routing mistakes happen.&lt;/strong&gt; I mentioned the "write a Python script" example. There are others. "Debug this article" (writing + debugging) confuses the router. I hit maybe 5% mis-routes, and I notice them because the response feels slightly off.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;More endpoints to monitor.&lt;/strong&gt; Instead of one Ollama instance to check, I have three. I built a simple health check script that pings each one and sends me a Telegram alert if any are down. It took 30 minutes to build. It runs forever.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Context doesn't transfer.&lt;/strong&gt; If I'm in a long coding session with ProgrammierMinna and then ask Celebi "what did we just decide about the database schema?" — Celebi has no idea. Each agent has its own conversation history. I work around this by copying relevant context when I switch, but it's friction.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Numbers
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;One Generalist&lt;/th&gt;
&lt;th&gt;Three Specialists&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Avg response time (coding)&lt;/td&gt;
&lt;td&gt;8-15s&lt;/td&gt;
&lt;td&gt;8-15s (same model)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Avg response time (writing)&lt;/td&gt;
&lt;td&gt;8-15s&lt;/td&gt;
&lt;td&gt;3-5s (smaller, faster model)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Avg response time (general)&lt;/td&gt;
&lt;td&gt;8-15s&lt;/td&gt;
&lt;td&gt;1-2s (tiny model)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Code review quality&lt;/td&gt;
&lt;td&gt;6/10&lt;/td&gt;
&lt;td&gt;9/10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Draft writing quality&lt;/td&gt;
&lt;td&gt;5/10&lt;/td&gt;
&lt;td&gt;8/10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Daily summary quality&lt;/td&gt;
&lt;td&gt;7/10&lt;/td&gt;
&lt;td&gt;8/10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Monthly cost&lt;/td&gt;
&lt;td&gt;$0 (local)&lt;/td&gt;
&lt;td&gt;$0 (local)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Setup complexity&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maintenance overhead&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The quality jumps are subjective but real. I measured them by how often I had to ask for a redo. With the generalist, maybe 30% of responses needed a follow-up clarification. With specialists, maybe 5%.&lt;/p&gt;

&lt;h2&gt;
  
  
  When This Makes Sense (And When It Doesn't)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Do this if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You have multiple distinct task types (coding + writing + analysis)&lt;/li&gt;
&lt;li&gt;You have the hardware to run multiple models (even small ones)&lt;/li&gt;
&lt;li&gt;You care about quality more than simplicity&lt;/li&gt;
&lt;li&gt;You're already hitting the limits of your current model&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Don't do this if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You're just casually chatting with AI&lt;/li&gt;
&lt;li&gt;You only have one machine with limited RAM&lt;/li&gt;
&lt;li&gt;Your tasks are all similar (all coding, all writing)&lt;/li&gt;
&lt;li&gt;You value simplicity over marginal quality gains&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Setup in 30 Minutes
&lt;/h2&gt;

&lt;p&gt;If you want to try this, here's the fastest path:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Install Ollama on two machines&lt;/strong&gt; (or twice on one machine if you have the RAM)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pull different models:&lt;/strong&gt; a coder model on one, a general model on the other&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write a 10-line router&lt;/strong&gt; (like the Python snippet above)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Point your scripts at the router&lt;/strong&gt; instead of directly at a model&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Adjust keywords&lt;/strong&gt; as you find mis-routes&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Total time: 30 minutes. Total cost: $0.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Win
&lt;/h2&gt;

&lt;p&gt;The biggest change isn't technical. It's mental.&lt;/p&gt;

&lt;p&gt;Before, I had one AI "employee" who was mediocre at everything. Now I have three specialists who are genuinely good at their jobs. When I send a query, I know which expert is handling it. I trust the output more. I spend less time verifying and fixing.&lt;/p&gt;

&lt;p&gt;It's the difference between a Swiss Army knife and a actual toolbox. The knife fits in your pocket. But when you need to build something real, you want the right tool.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Sam Hartley is a solo dev running a multi-agent AI setup on a 3-machine home lab. Writes about the infrastructure that makes local AI actually usable.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;→ &lt;a href="http://www.fiverr.com/s/XLyg" rel="noopener noreferrer"&gt;Custom automation setups on Fiverr&lt;/a&gt;&lt;br&gt;
→ &lt;a href="https://t.me/celebibot_en" rel="noopener noreferrer"&gt;Follow CelebiBots on Telegram&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  ai #agents #automation #selfhosted #ollama #productivity #buildinginpublic
&lt;/h1&gt;

</description>
      <category>ai</category>
      <category>automation</category>
      <category>agents</category>
      <category>selfhosted</category>
    </item>
    <item>
      <title>I Built a Garmin Watch Face with Live Stock Charts in Monkey C — Here’s What I Learned</title>
      <dc:creator>Sam Hartley</dc:creator>
      <pubDate>Tue, 21 Jul 2026 08:02:19 +0000</pubDate>
      <link>https://dev.to/samhartley_dev/i-built-a-garmin-watch-face-with-live-stock-charts-in-monkey-c-heres-what-i-learned-5b14</link>
      <guid>https://dev.to/samhartley_dev/i-built-a-garmin-watch-face-with-live-stock-charts-in-monkey-c-heres-what-i-learned-5b14</guid>
      <description>&lt;p&gt;I wanted stock prices on my wrist. Not a notification — an actual chart. So I built a custom Garmin watch face with live sparkline charts for 5 configurable assets.&lt;/p&gt;

&lt;p&gt;Here’s what I learned building &lt;strong&gt;StockFaceTC&lt;/strong&gt; for the Garmin Venu 2 Plus.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Challenge
&lt;/h2&gt;

&lt;p&gt;Garmin watch faces are &lt;strong&gt;tiny&lt;/strong&gt; — 124KB memory, 416×416 AMOLED display, and a language (Monkey C) that most devs have never heard of. No npm. No frameworks. No Stack Overflow answers.&lt;/p&gt;

&lt;p&gt;Oh, and the “simulator” can’t make web requests. You test API calls on real hardware or not at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;A watch face showing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Time, date, and health stats (heart rate, steps, floors)&lt;/li&gt;
&lt;li&gt;5 polar sparkline charts in an outer ring — each showing 24 hours of price data&lt;/li&gt;
&lt;li&gt;Auto-updates every 15 minutes via Twelve Data API&lt;/li&gt;
&lt;li&gt;6-hour weather forecast with custom-drawn icons&lt;/li&gt;
&lt;li&gt;Fully configurable tickers via the Garmin Connect app&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Default tickers:&lt;/strong&gt; AAPL, TSLA, Gold, BTC/USD, ETH/USD&lt;/p&gt;

&lt;h2&gt;
  
  
  The Font Problem (and How I Solved It)
&lt;/h2&gt;

&lt;p&gt;Garmin’s &lt;code&gt;VectorFont&lt;/code&gt; API isn’t available on all devices. The Venu 2 Plus? Nope.&lt;/p&gt;

&lt;p&gt;So I built my own: &lt;strong&gt;PrimitiveFont&lt;/strong&gt; — a complete vector font library using only &lt;code&gt;drawLine()&lt;/code&gt; calls. Every character is defined on a 5×7 grid and rendered procedurally.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// “A” on a 5×7 grid&lt;/span&gt;
&lt;span class="nx"&gt;CHAR_A&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Features:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Full alphabet (A-Z, a-z, 0-9, specials)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Proportional widths&lt;/strong&gt; (Arial-like metrics — ‘i’ is narrow, ‘M’ is wide)&lt;/li&gt;
&lt;li&gt;Bold, italic, underline&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Arc text&lt;/strong&gt; — characters curved along a circular path (for the ring labels!)&lt;/li&gt;
&lt;li&gt;Any size via simple scaling: &lt;code&gt;sizePx / 7.0&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The entire library is ~21KB — well within the 124KB budget.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Garmin Background Service (every 15 min)
  → Twelve Data API (1h candles × 24 = full day)
  → Phone acts as transparent HTTP proxy
  → JSON parsed in background thread (64KB limit!)
  → Prices + display names stored in model
  → View renders sparklines + arc labels on update
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Key Constraint: 64KB Background Memory
&lt;/h3&gt;

&lt;p&gt;The background service that fetches data runs in a &lt;strong&gt;separate 64KB sandbox&lt;/strong&gt;. You can’t access the main app’s memory. The only way to pass data: &lt;code&gt;Background.exit(dictionary)&lt;/code&gt;, which the main app receives in &lt;code&gt;onBackgroundData()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This means: parse JSON, extract only what you need, pass a minimal dictionary. No room for raw API responses.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dynamic Labels from API
&lt;/h2&gt;

&lt;p&gt;One early mistake: I hardcoded display names like &lt;code&gt;“XAU/USD” → “GOLD”&lt;/code&gt;. That doesn’t scale.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Better approach:&lt;/strong&gt; The Twelve Data API returns metadata with every response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;“meta”:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;“symbol”:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;“XAU/USD”&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;“currency_base”:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;“Gold&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Spot”&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;“type”:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;“Precious&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Metal”&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now labels come from the API itself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Stocks: &lt;code&gt;meta.symbol&lt;/code&gt; → “AAPL”&lt;/li&gt;
&lt;li&gt;Crypto: &lt;code&gt;meta.currency_base&lt;/code&gt; → “Bitcoin” → “BITCOIN”&lt;/li&gt;
&lt;li&gt;Commodities: &lt;code&gt;meta.currency_base&lt;/code&gt; → “Gold Spot” → “GOLD”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Zero hardcoded aliases. Add any ticker, get the right label automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Simulator Gotchas
&lt;/h2&gt;

&lt;p&gt;If you’re building Connect IQ apps, save yourself some pain:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Properties are cached forever&lt;/strong&gt; — changing &lt;code&gt;properties.xml&lt;/code&gt; defaults does nothing after first run. Use “Reset All App Data” first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;makeWebRequest()&lt;/code&gt; doesn’t work&lt;/strong&gt; in the simulator — you need a real phone connected via Bluetooth.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;getSettingsView()&lt;/code&gt; must be implemented&lt;/strong&gt; (even returning &lt;code&gt;null&lt;/code&gt;) or the settings menu stays greyed out.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No &lt;code&gt;VectorFont&lt;/code&gt;&lt;/strong&gt; on many devices — if you need custom text, roll your own like PrimitiveFont.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  API Budget
&lt;/h2&gt;

&lt;p&gt;Twelve Data free tier: 800 API calls/day.&lt;/p&gt;

&lt;p&gt;My budget: 5 symbols × 96 fetches/day (every 15 min) = &lt;strong&gt;480 calls&lt;/strong&gt;. Well within limits.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Code
&lt;/h2&gt;

&lt;p&gt;Source is on &lt;a href="https://gitlab.com/celebi-dev/stockfacet" rel="noopener noreferrer"&gt;GitLab&lt;/a&gt;. Feel free to fork it, break it, or build something cooler.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;What’s the weirdest hardware you’ve built for? Drop it in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>garmin</category>
      <category>connectiq</category>
      <category>crypto</category>
      <category>wearables</category>
    </item>
    <item>
      <title>I Built a Personal AI That Actually Knows My Projects (RAG + Ollama, Zero Cloud)</title>
      <dc:creator>Sam Hartley</dc:creator>
      <pubDate>Sun, 19 Jul 2026 08:03:13 +0000</pubDate>
      <link>https://dev.to/samhartley_dev/i-built-a-personal-ai-that-actually-knows-my-projects-rag-ollama-zero-cloud-54o1</link>
      <guid>https://dev.to/samhartley_dev/i-built-a-personal-ai-that-actually-knows-my-projects-rag-ollama-zero-cloud-54o1</guid>
      <description>&lt;p&gt;I got tired of explaining my own codebase to an AI every single session.&lt;/p&gt;

&lt;p&gt;"Here's the architecture. Here's the README. Here's what I tried last time." Every. Single. Time.&lt;/p&gt;

&lt;p&gt;So I built a local RAG (Retrieval-Augmented Generation) system that knows my projects, my notes, and my docs — permanently. No cloud. No API costs. No context window resets.&lt;/p&gt;

&lt;p&gt;Here's exactly how it works.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem with Context Windows
&lt;/h2&gt;

&lt;p&gt;LLMs don't remember. You paste the same 200 lines of context every session, hit the token limit, and start over. It's fine for one-off questions. It's exhausting for ongoing projects.&lt;/p&gt;

&lt;p&gt;The standard solution is RAG: instead of stuffing everything into the prompt, you store docs in a vector database and &lt;strong&gt;retrieve only the relevant chunks&lt;/strong&gt; when you ask a question. The model sees 3-5 paragraphs of targeted context instead of your entire repo.&lt;/p&gt;

&lt;p&gt;Result: faster, cheaper, and the AI actually answers the right question.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Your Documents (markdown, code, PDFs, notes)
  → Chunked + embedded (Ollama nomic-embed-text)
  → Stored in Chroma (local vector DB)

Query
  → Embedded (same model)
  → Top-5 relevant chunks retrieved
  → Stuffed into Ollama prompt (Qwen 3.5 9B)
  → Answer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Zero cloud. Zero API keys. Runs on a Mac Mini or any machine with 8GB RAM.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Index
&lt;/h2&gt;

&lt;p&gt;Everything that would normally eat my context window:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Project READMEs and architecture docs&lt;/li&gt;
&lt;li&gt;My personal notes (Obsidian vault)&lt;/li&gt;
&lt;li&gt;Code snippets and past solutions&lt;/li&gt;
&lt;li&gt;API documentation I use regularly&lt;/li&gt;
&lt;li&gt;Stack Overflow answers I bookmarked (because I always forget them again)&lt;/li&gt;
&lt;li&gt;Config files and deployment notes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Total indexed: ~4,800 chunks. Query time: under 2 seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Install the Stack (15 minutes)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Ollama (already installed? skip)&lt;/span&gt;
curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://ollama.com/install.sh | sh

&lt;span class="c"&gt;# Pull models&lt;/span&gt;
ollama pull qwen3.5:9b          &lt;span class="c"&gt;# LLM for answers&lt;/span&gt;
ollama pull nomic-embed-text    &lt;span class="c"&gt;# Embedding model&lt;/span&gt;

&lt;span class="c"&gt;# Python dependencies&lt;/span&gt;
pip &lt;span class="nb"&gt;install &lt;/span&gt;chromadb langchain ollama pypdf markdown
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the entire stack. No Docker required (though Chroma has a Docker option if you want a persistent server).&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Index Your Documents
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain.text_splitter&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;RecursiveCharacterTextSplitter&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain_community.document_loaders&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;DirectoryLoader&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain_community.embeddings&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OllamaEmbeddings&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain_community.vectorstores&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Chroma&lt;/span&gt;

&lt;span class="c1"&gt;# Load your docs folder
&lt;/span&gt;&lt;span class="n"&gt;loader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;DirectoryLoader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;~/projects/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;glob&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;**/*.md&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;recursive&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;docs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;loader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# Split into chunks (400 tokens, 50 overlap)
&lt;/span&gt;&lt;span class="n"&gt;splitter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RecursiveCharacterTextSplitter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunk_size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chunk_overlap&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;chunks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;splitter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split_documents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;docs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Indexed &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; chunks from &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;docs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; documents&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Embed and store locally
&lt;/span&gt;&lt;span class="n"&gt;embeddings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OllamaEmbeddings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;nomic-embed-text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Chroma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_documents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;embeddings&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;persist_directory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;./my-knowledge-base&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;persist&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run once. Done. Your docs are now searchable by meaning, not just keywords.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Query It
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ollama&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain_community.embeddings&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OllamaEmbeddings&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain_community.vectorstores&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Chroma&lt;/span&gt;

&lt;span class="c1"&gt;# Load existing DB
&lt;/span&gt;&lt;span class="n"&gt;embeddings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OllamaEmbeddings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;nomic-embed-text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Chroma&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;persist_directory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;./my-knowledge-base&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;embedding_function&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;embeddings&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;ask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Retrieve top 5 relevant chunks
&lt;/span&gt;    &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;similarity_search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;page_content&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

    &lt;span class="c1"&gt;# Query local LLM with context
&lt;/span&gt;    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ollama&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;qwen3.5:9b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Based on this context:&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s"&gt;Answer: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;}]&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;# Example
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;ask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;How does my Garmin watch face fetch stock data?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;ask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;What&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s the API rate limit for the crypto bot?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;ask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;How do I deploy the Telegram bot to the VPS?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Real answers from your own documentation. No hallucinations about your specific setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Killer Feature: Incremental Updates
&lt;/h2&gt;

&lt;p&gt;Don't re-index everything when one file changes. Just update what's new:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_file_hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;md5&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;read_bytes&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;update_index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;docs_dir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;index_cache&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;./index-cache.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;index_cache&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;index_cache&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

    &lt;span class="n"&gt;changed_files&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;docs_dir&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;rglob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*.md&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_file_hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;changed_files&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;changed_files&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Re-indexing &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;changed_files&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; changed files...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="c1"&gt;# [load, chunk, embed, upsert only changed files]
&lt;/span&gt;
    &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;index_cache&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;write_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run this as a cron job every hour. Your knowledge base stays current automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Actually Changed for Me
&lt;/h2&gt;

&lt;p&gt;Before RAG:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Explain the background service memory limit in my Garmin project" → paste 200 lines → wait → answer&lt;/li&gt;
&lt;li&gt;Every new chat session: context reset, start explaining again&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After RAG:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ask("Garmin background service memory limit")&lt;/code&gt; → &lt;strong&gt;"64KB sandbox, pass data via Background.exit(dictionary)"&lt;/strong&gt; — in 1.8 seconds&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My LLM now answers questions about projects I haven't touched in 6 months. No context management. No pasting. Just ask.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hardware Requirements
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setup&lt;/th&gt;
&lt;th&gt;RAM&lt;/th&gt;
&lt;th&gt;Embedding Speed&lt;/th&gt;
&lt;th&gt;Query Speed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Mac Mini M4 8GB&lt;/td&gt;
&lt;td&gt;8GB&lt;/td&gt;
&lt;td&gt;~500 docs/min&lt;/td&gt;
&lt;td&gt;~2s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX 3060 12GB&lt;/td&gt;
&lt;td&gt;12GB VRAM&lt;/td&gt;
&lt;td&gt;~3000 docs/min&lt;/td&gt;
&lt;td&gt;~0.5s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Old laptop 8GB&lt;/td&gt;
&lt;td&gt;8GB&lt;/td&gt;
&lt;td&gt;~100 docs/min&lt;/td&gt;
&lt;td&gt;~5-8s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The embedding step (indexing) is the slow part — run it once, then it's instant.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tips from Running This for 3 Months
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Chunk size matters&lt;/strong&gt; — 400 tokens works well for prose and docs. For code, try 200 with more overlap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Metadata is your friend&lt;/strong&gt; — store &lt;code&gt;filename&lt;/code&gt; and &lt;code&gt;section&lt;/code&gt; in chunk metadata. When the AI says "see the deployment notes," you know exactly where to look.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Re-rank when accuracy matters&lt;/strong&gt; — if top-5 chunks aren't enough, add a re-ranker step (Cohere has a free API, or use a local cross-encoder).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watch your embed model&lt;/strong&gt; — &lt;code&gt;nomic-embed-text&lt;/code&gt; beats most larger models for RAG. Don't use your chat LLM for embeddings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hybrid search&lt;/strong&gt; — combine vector search with BM25 keyword search for better results on technical queries with specific names/functions.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The Bigger Picture
&lt;/h2&gt;

&lt;p&gt;This is step one of something bigger: a personal AI that grows with your projects instead of resetting every session.&lt;/p&gt;

&lt;p&gt;Next phase I'm building: automatic indexing from Git commits (index diffs in real-time as you code) + a simple web UI for non-terminal queries.&lt;/p&gt;

&lt;p&gt;Total current cost of this setup: &lt;strong&gt;$0/month&lt;/strong&gt;. It runs on the same Mac Mini I already had.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Honest Bottom Line
&lt;/h2&gt;

&lt;p&gt;RAG isn't magic. It's a database query with a language model on top. But it solves a real problem: LLMs that don't know your stuff.&lt;/p&gt;

&lt;p&gt;If you're pasting READMEs into ChatGPT every session, try this. 15 minutes of setup, and your AI finally remembers what you told it yesterday.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Sam Hartley is a solo dev building tools on a Mac Mini + RTX 3060 home lab. Writes about the messy reality of shipping stuff with AI.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;→ &lt;a href="http://www.fiverr.com/s/XLyg" rel="noopener noreferrer"&gt;Custom automation setups on Fiverr&lt;/a&gt;&lt;br&gt;
→ &lt;a href="https://t.me/celebibot_en" rel="noopener noreferrer"&gt;Follow CelebiBots on Telegram&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  ai #rag #ollama #selfhosted #python #homelab #buildinpublic
&lt;/h1&gt;

</description>
      <category>ai</category>
      <category>rag</category>
      <category>ollama</category>
      <category>selfhosted</category>
    </item>
    <item>
      <title>I Automated My Entire Dev Workflow with AI Agents Running 24/7 on a Mac Mini</title>
      <dc:creator>Sam Hartley</dc:creator>
      <pubDate>Fri, 17 Jul 2026 08:03:02 +0000</pubDate>
      <link>https://dev.to/samhartley_dev/i-automated-my-entire-dev-workflow-with-ai-agents-running-247-on-a-mac-mini-1ikc</link>
      <guid>https://dev.to/samhartley_dev/i-automated-my-entire-dev-workflow-with-ai-agents-running-247-on-a-mac-mini-1ikc</guid>
      <description>&lt;h1&gt;
  
  
  I Automated My Entire Dev Workflow with AI Agents Running 24/7 on a Mac Mini
&lt;/h1&gt;

&lt;p&gt;Every morning I wake up and check Telegram. There's a message from Celebi — my AI agent — telling me what happened overnight. New emails summarized. A draft article ready for review. A reminder that I have a meeting in 2 hours. Sometimes a screenshot from a camera showing motion at the front door.&lt;/p&gt;

&lt;p&gt;All of this runs on a Mac Mini. Not in the cloud. Not on rented GPUs. On a $599 box under my desk.&lt;/p&gt;

&lt;p&gt;Here's how I built it and what it actually costs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Hardware
&lt;/h2&gt;

&lt;p&gt;My setup is three machines that talk to each other over my home network:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Machine&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Mac Mini M4 (16GB)&lt;/td&gt;
&lt;td&gt;Always-on orchestrator, notifications, lightweight tasks&lt;/td&gt;
&lt;td&gt;$599&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Windows PC (AMD 9970X, RTX 3060 12GB)&lt;/td&gt;
&lt;td&gt;Heavy lifting — coding models, image generation&lt;/td&gt;
&lt;td&gt;~$2,500 existing hardware&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ubuntu Server (CPU-only)&lt;/td&gt;
&lt;td&gt;Fallback, OCR backend, lightweight inference&lt;/td&gt;
&lt;td&gt;~$300 old laptop&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The Mac Mini is the brain. It's on 24/7, draws maybe 15W at idle, and handles routing, scheduling, and simple queries. The Windows PC wakes up for the hard stuff — 30B parameter models, vision tasks, anything that needs a GPU.&lt;/p&gt;

&lt;p&gt;The Ubuntu box is my safety net. When the Windows PC is offline or I need something CPU-only, it handles it. It's slow (180 seconds for a vision query vs 4 seconds on the GPU), but it works.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Agents Actually Do
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Celebi (Mac Mini, Qwen 3.5 9B)
&lt;/h3&gt;

&lt;p&gt;My main agent. It runs on the Mac Mini and handles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Daily summaries&lt;/strong&gt; — emails, calendar, notifications in one message&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Routing&lt;/strong&gt; — decides which agent handles which task&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Publishing&lt;/strong&gt; — posts articles to Dev.to, sends Telegram messages&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lightweight queries&lt;/strong&gt; — weather, quick questions, reminders&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Response time: 1-3 seconds. Perfect for "what's my schedule today?"&lt;/p&gt;

&lt;h3&gt;
  
  
  ProgrammierMinna (Windows PC, Qwen 3 Coder 30B)
&lt;/h3&gt;

&lt;p&gt;The coder. Handles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Code generation&lt;/strong&gt; — full features from descriptions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Refactoring&lt;/strong&gt; — restructuring messy code&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Debugging&lt;/strong&gt; — finding bugs I missed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PR review&lt;/strong&gt; — automated code review&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Response time: 8-15 seconds. Worth the wait for quality code.&lt;/p&gt;

&lt;h3&gt;
  
  
  DocMinna (Mac Mini, Granite 3.2 8B)
&lt;/h3&gt;

&lt;p&gt;The writer. Handles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Documentation&lt;/strong&gt; — READMEs, API docs, guides&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Article drafts&lt;/strong&gt; — turning my notes into readable prose&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Technical specs&lt;/strong&gt; — structured requirements documents&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Response time: 3-5 seconds. Fast enough for iterative writing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Architecture (Simple)
&lt;/h2&gt;

&lt;p&gt;No Kubernetes. No Docker Swarm. Just:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User (Telegram) → Celebi → Router → Right Agent → Response
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Celebi receives the message, classifies it (coding, writing, general), and routes to the right specialist. The specialist does the work and sends it back to Celebi, which formats it and sends it to me.&lt;/p&gt;

&lt;p&gt;If the Windows PC is offline, Celebi either handles it itself or falls back to the Ubuntu server. It's not fancy, but it works.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Automated
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Morning Briefing (Every day at 8 AM)
&lt;/h3&gt;

&lt;p&gt;Celebi checks my calendar, recent emails, and any flagged notifications. Sends a 3-sentence summary to Telegram. Takes me 10 seconds to read instead of 10 minutes of app-hopping.&lt;/p&gt;

&lt;h3&gt;
  
  
  Article Pipeline (Every 2 days)
&lt;/h3&gt;

&lt;p&gt;ProgrammierMinna writes a draft from my notes. DocMinna reviews and edits. Celebi publishes via the Dev.to API. I get a notification with a link to review.&lt;/p&gt;

&lt;p&gt;Actual time I spend per article: 10-15 minutes editing. Before this? 2-3 hours writing from scratch.&lt;/p&gt;

&lt;h3&gt;
  
  
  Code Review (On every push)
&lt;/h3&gt;

&lt;p&gt;ProgrammierMinna scans PRs for bugs, anti-patterns, missing error handling. It's not perfect — it misses edge cases sometimes — but it catches 80% of the obvious stuff before a human reviews it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Home Monitoring (Motion-triggered)
&lt;/h3&gt;

&lt;p&gt;Camera detects motion? Celebi sends me a screenshot and asks if it's important. Package delivery? I'll know in 10 seconds. Stray cat? Also know in 10 seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Numbers (Monthly Cost)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Mac Mini electricity (24/7, ~15W)&lt;/td&gt;
&lt;td&gt;~$3/month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Windows PC electricity (on demand, ~200W when active)&lt;/td&gt;
&lt;td&gt;~$8/month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ubuntu server electricity (24/7, ~10W)&lt;/td&gt;
&lt;td&gt;~$2/month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dev.to API&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Telegram Bot API&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ollama&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~$13/month&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Compare that to cloud alternatives:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;OpenAI API for my volume: ~$150-200/month&lt;/li&gt;
&lt;li&gt;Anthropic Claude: ~$100-150/month&lt;/li&gt;
&lt;li&gt;A hosted agent platform (n8n, Make, etc.): ~$50-100/month&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Savings: ~$300-400/month.&lt;/strong&gt; The Mac Mini paid for itself in 2 months.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Annoying
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Model management.&lt;/strong&gt; Keeping track of which model is on which machine, updating them, clearing old ones — it's overhead. Not huge, but real.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Windows PC sleep.&lt;/strong&gt; When the PC is asleep, complex queries take 180 seconds on the Ubuntu fallback instead of 8 seconds on the GPU. I've learned to schedule heavy tasks during hours when the PC is already awake.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Context limits.&lt;/strong&gt; Even 30B models have limited context windows. For large codebases, I have to chunk the work. The model doesn't see the full picture, which leads to integration issues.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Debugging the system.&lt;/strong&gt; When something breaks, it's not always obvious where. Is the model acting weird? Is the routing wrong? Is the hardware offline? I spend maybe 30 minutes per week on maintenance.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Surprised Me
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Local models are faster for simple tasks.&lt;/strong&gt; A Qwen 3.5 9B on the Mac Mini answers in 1-2 seconds. GPT-4o via API? 500ms to 2 seconds plus network latency. For quick queries, local is actually snappier.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The agents talk to each other better than expected.&lt;/strong&gt; I was worried about the handoff — would context get lost? Would responses be garbled? In practice, the routing works 95% of the time. The 5% failures are usually obvious and easy to fix.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It's more reliable than cloud.&lt;/strong&gt; I've had OpenAI outages, rate limits, API changes. My local setup? The only downtime is when I restart the machine for updates. In 6 months of operation, total downtime: maybe 2 hours.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Honest Bottom Line
&lt;/h2&gt;

&lt;p&gt;This isn't about replacing developers or writers or thinkers. It's about removing friction.&lt;/p&gt;

&lt;p&gt;I still make all the decisions. I still review all the code. I still edit every article. The agents just handle the parts I find tedious — turning my rough notes into readable prose, catching obvious bugs, formatting responses.&lt;/p&gt;

&lt;p&gt;The result? I ship more. I write more. I spend less time on grunt work and more time on things that matter.&lt;/p&gt;

&lt;p&gt;Is it perfect? No. Is it better than doing everything manually? Absolutely.&lt;/p&gt;

&lt;p&gt;If you're running side projects and drowning in maintenance, consider a local agent setup. It doesn't have to be this elaborate — start with one agent on one machine and expand from there.&lt;/p&gt;

&lt;p&gt;The $599 Mac Mini was the best dev investment I've made this year.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Sam Hartley is a solo dev building tools on a 3-machine home lab. Writes about the messy reality of shipping stuff with AI.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;→ &lt;a href="http://www.fiverr.com/s/XLyg" rel="noopener noreferrer"&gt;Custom automation setups on Fiverr&lt;/a&gt;&lt;br&gt;
→ &lt;a href="https://t.me/celebibot_en" rel="noopener noreferrer"&gt;Follow CelebiBots on Telegram&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>automation</category>
      <category>agents</category>
      <category>selfhosted</category>
    </item>
    <item>
      <title>I Built a Dead-Simple API Gateway for My Local LLMs in 50 Lines of Python</title>
      <dc:creator>Sam Hartley</dc:creator>
      <pubDate>Thu, 16 Jul 2026 15:43:08 +0000</pubDate>
      <link>https://dev.to/samhartley_dev/i-built-a-dead-simple-api-gateway-for-my-local-llms-in-50-lines-of-python-6f9</link>
      <guid>https://dev.to/samhartley_dev/i-built-a-dead-simple-api-gateway-for-my-local-llms-in-50-lines-of-python-6f9</guid>
      <description>&lt;h1&gt;
  
  
  I Built a Dead-Simple API Gateway for My Local LLMs in 50 Lines of Python
&lt;/h1&gt;

&lt;p&gt;I run three machines with local LLMs. A Mac Mini with an M4, a Windows box with an RTX 3060, and an Ubuntu server with a couple older GPUs. Each has Ollama installed. Each has different models loaded.&lt;/p&gt;

&lt;p&gt;For months, I hardcoded URLs in my scripts. Need a quick answer? Query the Mac. Need a coding assistant? Hit the Windows machine. Need the big model? Wait for the Ubuntu server.&lt;/p&gt;

&lt;p&gt;It was annoying. So I built a tiny API gateway that routes requests automatically. It took an afternoon. It runs on a single Python file. And it completely changed how I use my local AI setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: Three URLs, Zero Logic
&lt;/h2&gt;

&lt;p&gt;Before the gateway, my scripts looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# quick_question.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="c1"&gt;# Which machine do I use today?
# Mac Mini — fast, small models
&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://192.168.1.100:11434/api/generate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;qwen2.5:7b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prompt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Explain Python decorators&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# code_review.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="c1"&gt;# Windows — has the GPU
&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://192.168.1.106:11434/api/generate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;qwen3-coder:30b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prompt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Review this function...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# hard_question.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="c1"&gt;# Ubuntu — has the most VRAM
&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://192.168.1.100:11434/api/generate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;deepseek-r1:70b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prompt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Design a distributed task queue...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three scripts. Three URLs. Zero flexibility. If the Windows machine was offline, the coding script just failed. If I added a new model, I had to update everything manually.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix: A Stupid-Simple Gateway
&lt;/h2&gt;

&lt;p&gt;I wanted one URL. One API. Let the gateway figure out which machine can handle the request.&lt;/p&gt;

&lt;p&gt;Here's what I built:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# gateway.py
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;flask&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Flask&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;jsonify&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Flask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__name__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# My machines and what they can run
&lt;/span&gt;&lt;span class="n"&gt;MACHINES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mac&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://192.168.1.100:11434&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;models&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;qwen2.5:7b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;granite3.2-vision:2b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]},&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;windows&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://192.168.1.106:11434&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;models&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;qwen3-coder:30b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;deepseek-r1:8b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]},&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ubuntu&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://192.168.1.100:11434&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;models&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;deepseek-r1:70b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;minicpm-v&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;find_machine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;MACHINES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;models&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

&lt;span class="nd"&gt;@app.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/api/generate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;methods&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;POST&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;jsonify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;No model specified&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;

    &lt;span class="n"&gt;machine_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;find_machine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;machine_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;jsonify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Model &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; not found on any machine&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="mi"&gt;404&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;machine_url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/api/generate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exceptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;ConnectionError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;jsonify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Machine for &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; is offline&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="mi"&gt;503&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.0.0.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;11435&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. 50 lines. Run it on any machine, point all your scripts at &lt;code&gt;http://gateway:11435&lt;/code&gt;, and forget about which box has which model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Is Better Than I Expected
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;I can move models around.&lt;/strong&gt; When I got a new GPU for the Windows machine, I moved the big coding model there. Changed one line in &lt;code&gt;MACHINES&lt;/code&gt;. Every script kept working.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Health checks are trivial.&lt;/strong&gt; I added a &lt;code&gt;/health&lt;/code&gt; endpoint that pings each machine. If one is down, my main script knows and routes around it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Load balancing is obvious.&lt;/strong&gt; If two machines have the same model, I can pick whichever is less busy. I haven't needed this yet, but the structure supports it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My scripts got dumber.&lt;/strong&gt; In a good way. They don't need to know about the infrastructure anymore. They just ask for a model and get an answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Didn't Build (On Purpose)
&lt;/h2&gt;

&lt;p&gt;No database. No config files. No Docker. No Kubernetes. No "service mesh."&lt;/p&gt;

&lt;p&gt;This is a single Python file with a dictionary. If I need to change something, I edit the file and restart it. Takes 10 seconds.&lt;/p&gt;

&lt;p&gt;I thought about making it "proper" — YAML configs, hot reloading, Prometheus metrics. But this is for my home lab. I'm the only user. Complexity is the enemy.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Win: Mental Overhead
&lt;/h2&gt;

&lt;p&gt;Before the gateway, using my local AI felt like work. I'd open a script, remember which machine had which model, check if it was online, then query it.&lt;/p&gt;

&lt;p&gt;Now it feels like... using an API. Any API. I don't think about the infrastructure. I just write the prompt and get the result.&lt;/p&gt;

&lt;p&gt;That's the whole point of infrastructure: it should disappear.&lt;/p&gt;

&lt;h2&gt;
  
  
  Numbers (Because Why Not)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Lines of Python: 50&lt;/li&gt;
&lt;li&gt;Time to build: 2 hours (including testing)&lt;/li&gt;
&lt;li&gt;Time saved per week: ~30 minutes of "which machine is this on again?"&lt;/li&gt;
&lt;li&gt;Additional dependencies: Flask (already installed for other projects)&lt;/li&gt;
&lt;li&gt;Cost: $0&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;

&lt;p&gt;If you have multiple Ollama instances, you can literally copy-paste the script above, change the IPs and models, and be done.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;flask requests
python gateway.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then in your scripts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="c1"&gt;# One URL. Any model. Gateway handles the rest.
&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:11435/api/generate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;qwen3-coder:30b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prompt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Refactor this function...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Honest Bottom Line
&lt;/h2&gt;

&lt;p&gt;Is this production-ready? No. Does it handle edge cases? Barely. Is it good enough for my home lab? Absolutely.&lt;/p&gt;

&lt;p&gt;Sometimes the right architecture is the one you'll actually maintain. A 50-line Python file I can debug in my head beats a "proper" solution I'd never finish.&lt;/p&gt;

&lt;p&gt;If you're running multiple Ollama instances and manually switching between them — just build the gateway. It takes an afternoon and saves you from ever thinking about machine IPs again.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Sam Hartley is a solo dev running a multi-machine AI home lab in Turkey. Writes about the boring infrastructure that makes local AI actually usable.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;→ &lt;a href="http://www.fiverr.com/s/XLyg" rel="noopener noreferrer"&gt;Custom automation setups on Fiverr&lt;/a&gt;&lt;br&gt;
→ &lt;a href="https://t.me/celebibot_en" rel="noopener noreferrer"&gt;Follow CelebiBots on Telegram&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  ai #ollama #selfhosted #api #python #homelab #buildinpublic
&lt;/h1&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>ollama</category>
      <category>selfhosted</category>
    </item>
    <item>
      <title>I Built a Dead-Simple API Gateway for My Local LLMs in 50 Lines of Python</title>
      <dc:creator>Sam Hartley</dc:creator>
      <pubDate>Wed, 15 Jul 2026 08:01:40 +0000</pubDate>
      <link>https://dev.to/samhartley_dev/i-built-a-dead-simple-api-gateway-for-my-local-llms-in-50-lines-of-python-53g6</link>
      <guid>https://dev.to/samhartley_dev/i-built-a-dead-simple-api-gateway-for-my-local-llms-in-50-lines-of-python-53g6</guid>
      <description>&lt;h1&gt;
  
  
  I Built a Dead-Simple API Gateway for My Local LLMs in 50 Lines of Python
&lt;/h1&gt;

&lt;p&gt;I run three machines with local LLMs. A Mac Mini with an M4, a Windows box with an RTX 3060, and an Ubuntu server with a couple older GPUs. Each has Ollama installed. Each has different models loaded.&lt;/p&gt;

&lt;p&gt;For months, I hardcoded URLs in my scripts. Need a quick answer? Query the Mac. Need a coding assistant? Hit the Windows machine. Need the big model? Wait for the Ubuntu server.&lt;/p&gt;

&lt;p&gt;It was annoying. So I built a tiny API gateway that routes requests automatically. It took an afternoon. It runs on a single Python file. And it completely changed how I use my local AI setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: Three URLs, Zero Logic
&lt;/h2&gt;

&lt;p&gt;Before the gateway, my scripts looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# quick_question.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="c1"&gt;# Which machine do I use today?
# Mac Mini — fast, small models
&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://192.168.1.100:11434/api/generate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;qwen2.5:7b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prompt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Explain Python decorators&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# code_review.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="c1"&gt;# Windows — has the GPU
&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://192.168.1.106:11434/api/generate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;qwen3-coder:30b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prompt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Review this function...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# hard_question.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="c1"&gt;# Ubuntu — has the most VRAM
&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://192.168.1.100:11434/api/generate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;deepseek-r1:70b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prompt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Design a distributed task queue...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three scripts. Three URLs. Zero flexibility. If the Windows machine was offline, the coding script just failed. If I added a new model, I had to update everything manually.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix: A Stupid-Simple Gateway
&lt;/h2&gt;

&lt;p&gt;I wanted one URL. One API. Let the gateway figure out which machine can handle the request.&lt;/p&gt;

&lt;p&gt;Here's what I built:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# gateway.py
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;flask&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Flask&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;jsonify&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Flask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__name__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# My machines and what they can run
&lt;/span&gt;&lt;span class="n"&gt;MACHINES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mac&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://192.168.1.100:11434&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;models&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;qwen2.5:7b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;granite3.2-vision:2b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]},&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;windows&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://192.168.1.106:11434&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;models&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;qwen3-coder:30b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;deepseek-r1:8b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]},&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ubuntu&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://192.168.1.100:11434&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;models&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;deepseek-r1:70b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;minicpm-v&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;find_machine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;MACHINES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;models&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

&lt;span class="nd"&gt;@app.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/api/generate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;methods&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;POST&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;jsonify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;No model specified&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;

    &lt;span class="n"&gt;machine_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;find_machine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;machine_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;jsonify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Model &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; not found on any machine&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="mi"&gt;404&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;machine_url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/api/generate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exceptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;ConnectionError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;jsonify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Machine for &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; is offline&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="mi"&gt;503&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.0.0.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;11435&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. 50 lines. Run it on any machine, point all your scripts at &lt;code&gt;http://gateway:11435&lt;/code&gt;, and forget about which box has which model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Is Better Than I Expected
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;I can move models around.&lt;/strong&gt; When I got a new GPU for the Windows machine, I moved the big coding model there. Changed one line in &lt;code&gt;MACHINES&lt;/code&gt;. Every script kept working.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Health checks are trivial.&lt;/strong&gt; I added a &lt;code&gt;/health&lt;/code&gt; endpoint that pings each machine. If one is down, my main script knows and routes around it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Load balancing is obvious.&lt;/strong&gt; If two machines have the same model, I can pick whichever is less busy. I haven't needed this yet, but the structure supports it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My scripts got dumber.&lt;/strong&gt; In a good way. They don't need to know about the infrastructure anymore. They just ask for a model and get an answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Didn't Build (On Purpose)
&lt;/h2&gt;

&lt;p&gt;No database. No config files. No Docker. No Kubernetes. No "service mesh."&lt;/p&gt;

&lt;p&gt;This is a single Python file with a dictionary. If I need to change something, I edit the file and restart it. Takes 10 seconds.&lt;/p&gt;

&lt;p&gt;I thought about making it "proper" — YAML configs, hot reloading, Prometheus metrics. But this is for my home lab. I'm the only user. Complexity is the enemy.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Win: Mental Overhead
&lt;/h2&gt;

&lt;p&gt;Before the gateway, using my local AI felt like work. I'd open a script, remember which machine had which model, check if it was online, then query it.&lt;/p&gt;

&lt;p&gt;Now it feels like... using an API. Any API. I don't think about the infrastructure. I just write the prompt and get the result.&lt;/p&gt;

&lt;p&gt;That's the whole point of infrastructure: it should disappear.&lt;/p&gt;

&lt;h2&gt;
  
  
  Numbers (Because Why Not)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Lines of Python: 50&lt;/li&gt;
&lt;li&gt;Time to build: 2 hours (including testing)&lt;/li&gt;
&lt;li&gt;Time saved per week: ~30 minutes of "which machine is this on again?"&lt;/li&gt;
&lt;li&gt;Additional dependencies: Flask (already installed for other projects)&lt;/li&gt;
&lt;li&gt;Cost: $0&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;

&lt;p&gt;If you have multiple Ollama instances, you can literally copy-paste the script above, change the IPs and models, and be done.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;flask requests
python gateway.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then in your scripts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="c1"&gt;# One URL. Any model. Gateway handles the rest.
&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:11435/api/generate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;qwen3-coder:30b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prompt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Refactor this function...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Honest Bottom Line
&lt;/h2&gt;

&lt;p&gt;Is this production-ready? No. Does it handle edge cases? Barely. Is it good enough for my home lab? Absolutely.&lt;/p&gt;

&lt;p&gt;Sometimes the right architecture is the one you'll actually maintain. A 50-line Python file I can debug in my head beats a "proper" solution I'd never finish.&lt;/p&gt;

&lt;p&gt;If you're running multiple Ollama instances and manually switching between them — just build the gateway. It takes an afternoon and saves you from ever thinking about machine IPs again.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Sam Hartley is a solo dev running a multi-machine AI home lab in Turkey. Writes about the boring infrastructure that makes local AI actually usable.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;→ &lt;a href="http://www.fiverr.com/s/XLyg" rel="noopener noreferrer"&gt;Custom automation setups on Fiverr&lt;/a&gt;&lt;br&gt;
→ &lt;a href="https://t.me/celebibot_en" rel="noopener noreferrer"&gt;Follow CelebiBots on Telegram&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  ai #ollama #selfhosted #api #python #homelab #buildinpublic
&lt;/h1&gt;

</description>
      <category>ai</category>
      <category>ollama</category>
      <category>selfhosted</category>
      <category>python</category>
    </item>
    <item>
      <title>I Ditched ChatGPT for Local LLMs and Saved $2,000 in a Year — The Real Numbers</title>
      <dc:creator>Sam Hartley</dc:creator>
      <pubDate>Tue, 07 Jul 2026 08:02:10 +0000</pubDate>
      <link>https://dev.to/samhartley_dev/i-ditched-chatgpt-for-local-llms-and-saved-2000-in-a-year-the-real-numbers-2f8h</link>
      <guid>https://dev.to/samhartley_dev/i-ditched-chatgpt-for-local-llms-and-saved-2000-in-a-year-the-real-numbers-2f8h</guid>
      <description>&lt;p&gt;"Just use ChatGPT." — I heard this for months. And I did. Until I got the bill.&lt;/p&gt;

&lt;p&gt;$187 in one month. For a solo dev running side projects. That was my wake-up call.&lt;/p&gt;

&lt;p&gt;This is the story of how I went from cloud-only to a hybrid setup, what it actually cost, and where local models fall flat on their face.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Setup (July 2025)
&lt;/h2&gt;

&lt;p&gt;I was using three APIs daily:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;OpenAI GPT-4o for code review and general questions&lt;/li&gt;
&lt;li&gt;Anthropic Claude Sonnet for writing and reasoning&lt;/li&gt;
&lt;li&gt;Google Gemini Pro for quick tasks and summaries&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My workload wasn't enterprise-level. Maybe 300-500 queries per day across all projects — a mix of coding help, content drafting, data extraction, and random "what's the difference between these two Python libraries" questions.&lt;/p&gt;

&lt;p&gt;The bill for June 2025: &lt;strong&gt;$187.42&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For context, that's more than my internet bill, my streaming subscriptions, and my VPS combined.&lt;/p&gt;

&lt;h2&gt;
  
  
  Month 1: The Experiment
&lt;/h2&gt;

&lt;p&gt;I bought a used RTX 3060 12GB off eBay for $150. Added it to my existing PC (which already had a decent CPU). Installed Ollama. Pulled Qwen 2.5 7B.&lt;/p&gt;

&lt;p&gt;Took 20 minutes from "unboxing" to "first local query".&lt;/p&gt;

&lt;p&gt;The result? For simple questions — "explain this regex", "refactor this function", "summarize this text" — the 7B model was about 85% as good as GPT-4o. The answers were slightly less polished, sometimes missing nuance, but &lt;strong&gt;good enough&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The catch? Complex reasoning. I asked it to design a database schema for a multi-tenant app with row-level security. It gave me something that looked right but had a subtle flaw that would have caused data leaks in production.&lt;/p&gt;

&lt;p&gt;GPT-4o caught that flaw. The local model didn't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson learned:&lt;/strong&gt; Local models are great for 80% of tasks. The other 20% still needs the big guns.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building the Hybrid
&lt;/h2&gt;

&lt;p&gt;By month 3, I had a routing system:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Query comes in
  → Is it simple? (explain, refactor, summarize)
    → Local model (free, ~2s)
  → Is it code review?
    → Local coder model (free, ~8s)
  → Is it complex reasoning or architecture?
    → Cloud API ($0.003-0.02 per query)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I didn't build anything fancy. Just a 30-line Python script that checks the query type and routes it. The "complexity check" is embarrassingly simple — if the query contains words like "architecture", "design", "security", "performance", or is longer than 500 characters, it goes to the cloud.&lt;/p&gt;

&lt;p&gt;Is it perfect? No. Does it catch edge cases? Sometimes. But it's &lt;strong&gt;good enough&lt;/strong&gt; and saved me a fortune.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Numbers (12 Months)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Cloud-Only Year (Hypothetical)
&lt;/h3&gt;

&lt;p&gt;If I kept my June 2025 pace: $187/month × 12 = &lt;strong&gt;$2,244/year&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Actual Hybrid Year
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;th&gt;Amount&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Used RTX 3060 12GB&lt;/td&gt;
&lt;td&gt;$150 (one-time)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Electricity (GPU running 24/7)&lt;/td&gt;
&lt;td&gt;~$12/month = $144/year&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloud API usage (reduced)&lt;/td&gt;
&lt;td&gt;~$25/month = $300/year&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total first year&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$594&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total subsequent years&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~$444/year&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Savings: $2,244 - $594 = $1,650 in year one.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;After the GPU is paid off, it's $444/year vs $2,244. The GPU pays for itself in under 4 months.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Surprised Me
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Latency is better locally.&lt;/strong&gt; Cloud APIs average 500-2000ms. My local setup answers in 200-800ms depending on model size. For iterative coding (write, test, ask, fix), that speed difference matters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Privacy is underrated.&lt;/strong&gt; I started piping customer support tickets through the local model for sentiment analysis and categorization. With cloud APIs, I'd need a data processing agreement. With local? The data never leaves my machine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rate limits don't exist locally.&lt;/strong&gt; Hit a deadline and need to process 1000 queries in an hour? Cloud APIs throttle you. Local hardware just gets warm.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Model management is annoying.&lt;/strong&gt; Updates, storage (each model is 4-15GB), keeping track of which model does what — it's overhead. Not huge, but real.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Local Models Fail (Honestly)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Frontier reasoning.&lt;/strong&gt; I asked DeepSeek R1 70B (local, quantized) and Claude 3.5 Sonnet (cloud) to debug a race condition in my async Python code. Claude spotted it in 2 sentences. The local model gave me a 3-paragraph explanation that was technically correct but missed the actual bug.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Creative writing.&lt;/strong&gt; GPT-4o writes prose that flows. Local models write prose that... exists. For marketing copy or user-facing content, I still use the cloud.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multimodal.&lt;/strong&gt; Local vision models exist but they're not great. If I need to analyze a screenshot or diagram, cloud wins hands down.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Actual Recommendation
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Your Situation&lt;/th&gt;
&lt;th&gt;What to Do&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Solo dev, side projects&lt;/td&gt;
&lt;td&gt;Local only. Start with Ollama + Qwen 2.5 7B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Small team, some budget&lt;/td&gt;
&lt;td&gt;Hybrid. Local for 80%, cloud for complex stuff&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Startup with VC funding&lt;/td&gt;
&lt;td&gt;Hybrid. Local default, cloud for frontier tasks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Enterprise with compliance needs&lt;/td&gt;
&lt;td&gt;Local + air-gapped. Cloud only for non-sensitive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"I just want it to work"&lt;/td&gt;
&lt;td&gt;Cloud. But you're paying for convenience&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Getting Started (10 Minutes)
&lt;/h2&gt;

&lt;p&gt;If you're curious, here's the fastest path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Install Ollama&lt;/span&gt;
curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://ollama.com/install.sh | sh

&lt;span class="c"&gt;# 2. Pull a model (7B fits in 8GB RAM)&lt;/span&gt;
ollama pull qwen2.5:7b

&lt;span class="c"&gt;# 3. Start chatting&lt;/span&gt;
ollama run qwen2.5:7b
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Total time: 10 minutes. Total cost: $0.&lt;/p&gt;

&lt;p&gt;If you have an old gaming GPU lying around, you're golden. If not, CPU-only works for smaller models. It's slower but still usable for casual queries.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Honest Bottom Line
&lt;/h2&gt;

&lt;p&gt;Local LLMs aren't a magic bullet. They're a &lt;strong&gt;cost optimization&lt;/strong&gt; with trade-offs.&lt;/p&gt;

&lt;p&gt;You lose some quality on complex tasks. You gain speed, privacy, and massive cost savings. For me, routing 80% of queries locally dropped my AI bill from $187/month to $25/month.&lt;/p&gt;

&lt;p&gt;That's $1,650/year I can spend on... literally anything else.&lt;/p&gt;

&lt;p&gt;If you've tried local LLMs, what's your experience? Did the quality drop bother you, or was the cost saving worth it?&lt;/p&gt;

&lt;p&gt;Drop your setup in the comments — always curious how others are handling this.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Sam Hartley is a solo dev building tools on a Mac Mini + RTX 3060 home lab. Writes about the messy reality of shipping stuff with AI.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>selfhosted</category>
      <category>ollama</category>
    </item>
  </channel>
</rss>
