<?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>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>
    <item>
      <title>I Built a Morning Briefing Bot in 50 Lines of Python — Here's Why I Check Telegram Before Email Now</title>
      <dc:creator>Sam Hartley</dc:creator>
      <pubDate>Wed, 01 Jul 2026 08:05:34 +0000</pubDate>
      <link>https://dev.to/samhartley_dev/i-built-a-morning-briefing-bot-in-50-lines-of-python-heres-why-i-check-telegram-before-email-now-3fhh</link>
      <guid>https://dev.to/samhartley_dev/i-built-a-morning-briefing-bot-in-50-lines-of-python-heres-why-i-check-telegram-before-email-now-3fhh</guid>
      <description>&lt;h1&gt;
  
  
  I Built a Morning Briefing Bot in 50 Lines of Python — Here's Why I Check Telegram Before Email Now
&lt;/h1&gt;

&lt;p&gt;For years, my morning routine was the same: open laptop, check email, get distracted by Slack, remember I needed to check the weather, forget what I was doing, and 20 minutes later realize I hadn't started actual work yet.&lt;/p&gt;

&lt;p&gt;Sound familiar?&lt;/p&gt;

&lt;p&gt;Three months ago, I built a dead-simple Telegram bot that aggregates everything I actually care about into one message. It fires at 8 AM every day. I read it in 30 seconds. Then I start working.&lt;/p&gt;

&lt;p&gt;No apps to switch between. No rabbit holes. Just one message with the stuff that matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  What It Actually Sends
&lt;/h2&gt;

&lt;p&gt;Every morning at 8 AM, my phone buzzes with something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;📅 Morning Brief — Wednesday, Jul 1

Today: Team standup at 10:00, Dentist at 14:30
Tomorrow: Deploy to prod (set reminder!)

🌤️ Weather: 28°C, sunny — no rain expected

💻 Systems: Mac Mini ✅ | GPU Server ✅ | Ubuntu Box ✅
All green. Uptime: 47 days.

📝 Yesterday's notes: "Refactored auth module, tests passing"

⚠️ One thing: GitHub issue #142 still open — "fix API rate limiting"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. No graphs. No dashboards. No "click here to see more." Just the facts I need to plan my day.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Telegram?
&lt;/h2&gt;

&lt;p&gt;I already use Telegram for my devops dashboard (wrote about that &lt;a href="https://dev.to/samhartley_dev/i-use-telegram-as-my-devops-dashboard-no-web-ui-no-vpn-just-works-4abc"&gt;here&lt;/a&gt;). Adding a morning briefing was a natural extension.&lt;/p&gt;

&lt;p&gt;But honestly? The real reason is &lt;strong&gt;friction reduction.&lt;/strong&gt; My phone's notification shade is a graveyard of app alerts I ignore. Telegram is one of the few apps I actually open. Putting my briefing there means I actually read it.&lt;/p&gt;

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

&lt;p&gt;Here's the entire stack:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cron job&lt;/strong&gt; on my Mac Mini — triggers at 8:00 AM daily&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;50-line Python script&lt;/strong&gt; — gathers data from 4 sources&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Telegram Bot API&lt;/strong&gt; — sends the formatted message&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's it. No web framework. No database. No message queue. Just a script that runs, collects, formats, and sends.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Script
&lt;/h2&gt;

&lt;p&gt;Here's the core of it (simplified, but functional):&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;#!/usr/bin/env python3
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&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;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timedelta&lt;/span&gt;

&lt;span class="c1"&gt;# Config
&lt;/span&gt;&lt;span class="n"&gt;BOT_TOKEN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;BRIEFING_BOT_TOKEN&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;CHAT_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;MY_TELEGRAM_CHAT_ID&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;# Gather data
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_calendar&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="c1"&gt;# I export from Apple Calendar to a local ICS file nightly
&lt;/span&gt;    &lt;span class="c1"&gt;# This reads today's events from it
&lt;/span&gt;    &lt;span class="c1"&gt;# ... (parsing logic here) ...
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Team standup at 10:00&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;Dentist at 14:30&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;get_weather&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="c1"&gt;# wttr.in — free, no API key needed
&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="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;https://wttr.in/Sakarya?format=%C+%t&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;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_system_status&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="c1"&gt;# Quick ping to my other machines
&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;192.168.1.105&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;GPU Server&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;192.168.1.106&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 Box&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;192.168.1.100&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;status&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;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ip&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;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;system&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;ping -c 1 -W 2 &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ip&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &amp;gt; /dev/null 2&amp;gt;&amp;amp;1&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;status&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="o"&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;else&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="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_github_issues&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="c1"&gt;# Check my main repo for open issues labeled "urgent"
&lt;/span&gt;    &lt;span class="c1"&gt;# ... (GitHub API call) ...
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#142: Fix API rate limiting&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;# Build and send message
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;send_briefing&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;calendar&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_calendar&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;weather&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_weather&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;systems&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_system_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;issues&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_github_issues&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;today&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;strftime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;%A, %b %d&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;lines&lt;/span&gt; &lt;span class="o"&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;📅 Morning Brief — &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;today&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="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;Today:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&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;event&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;calendar&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;calendar&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  • Nothing scheduled 🎉&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="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;🌤️ Weather: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;weather&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="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;💻 Systems:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&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;name&lt;/span&gt;&lt;span class="si"&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;status&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&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;status&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;systems&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="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;if&lt;/span&gt; &lt;span class="n"&gt;issues&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;lines&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;⚠️ Open issues:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&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;issue&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;issue&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;issues&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;

    &lt;span class="n"&gt;message&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&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;lines&lt;/span&gt;&lt;span class="p"&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="s"&gt;https://api.telegram.org/bot&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;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="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;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="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;parse_mode&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;HTML&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;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="nf"&gt;send_briefing&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The real version has error handling and a few more data sources (like yesterday's git commit summary), but this is the gist. 50 lines. One cron entry. Done.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Cron Job
&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;# crontab -e&lt;/span&gt;
0 8 &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; /usr/bin/python3 /Users/sam/scripts/morning_briefing.py &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; /tmp/briefing.log 2&amp;gt;&amp;amp;1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's literally the entire scheduling infrastructure.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Before:&lt;/strong&gt; I'd check 4-5 apps every morning. Sometimes I'd miss something. Sometimes I'd get distracted. Average time to "actually working": 20-30 minutes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;After:&lt;/strong&gt; One notification. 30 seconds to read. I know what's happening today, whether my systems are healthy, and if there's anything urgent. Then I close Telegram and start work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Average time to "actually working": 2 minutes.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The weird part? I feel less anxious. I used to have this low-grade worry that I was forgetting something. Now I know the bot checks for me. If there was a problem, I'd know.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Downsides (Because Nothing's Perfect)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;ICS parsing is brittle.&lt;/strong&gt; Apple Calendar exports aren't always clean. I had to add a nightly script that sanitizes the ICS file before the morning run.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Weather API can be flaky.&lt;/strong&gt; wttr.in is great until it isn't. I added a fallback to a local weather file that updates every hour.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No interactivity.&lt;/strong&gt; It's a one-way push. If I want details ("what's in that GitHub issue?"), I have to go look. I tried adding reply buttons, but honestly, it added complexity I didn't need. The goal was speed, not interactivity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Edge cases suck.&lt;/strong&gt; Daylight saving time shift? The cron fired at 7 AM for a week before I noticed. Public holidays? The bot doesn't know. I had to add a manual "skip" flag for vacation days.&lt;/p&gt;

&lt;h2&gt;
  
  
  Should You Build This?
&lt;/h2&gt;

&lt;p&gt;If you check more than 3 apps every morning: probably yes.&lt;/p&gt;

&lt;p&gt;You don't need my exact setup. Use whatever you have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Discord webhook&lt;/strong&gt; instead of Telegram?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A simple Bash script&lt;/strong&gt; instead of Python?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;iOS Shortcuts&lt;/strong&gt; instead of cron?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The pattern matters more than the stack: &lt;strong&gt;one automated message, curated by you, delivered where you already look.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Start small. My first version just sent the weather and today's calendar. Everything else came later. The 50-line script grew to 150 lines over three months — but it started as a weekend experiment.&lt;/p&gt;

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

&lt;p&gt;I'm experimenting with two additions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Weekly summary on Sundays&lt;/strong&gt; — "This week you shipped 12 commits, closed 3 issues, and your GPU earned $47."&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Context-aware alerts&lt;/strong&gt; — If I have a meeting in 15 minutes and haven't checked the briefing, send a nudge. But only for external meetings (not "standup" — I won't forget that).&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The goal isn't to build a product. It's to build a personal utility that removes friction from my day.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I write about running AI locally, building weird automation, and occasionally making money from side projects. If this was useful, drop a comment with your morning routine — I'm always looking for ideas to steal.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>telegram</category>
      <category>automation</category>
      <category>productivity</category>
    </item>
    <item>
      <title>I Rented Out My GPU for Passive Income — Here's What Happened After My First Week</title>
      <dc:creator>Sam Hartley</dc:creator>
      <pubDate>Sat, 27 Jun 2026 08:05:35 +0000</pubDate>
      <link>https://dev.to/samhartley_dev/i-rented-out-my-gpu-for-passive-income-heres-what-happened-after-my-first-week-1mbh</link>
      <guid>https://dev.to/samhartley_dev/i-rented-out-my-gpu-for-passive-income-heres-what-happened-after-my-first-week-1mbh</guid>
      <description>&lt;p&gt;I had an RTX 3060 sitting on a shelf.&lt;/p&gt;

&lt;p&gt;Not broken. Not old. Just... not doing anything. My Windows PC runs models when I need them, but most of the time it's idle. The fans spin, the power draw ticks along, and that 12GB of VRAM just sits there.&lt;/p&gt;

&lt;p&gt;A week ago I connected it to &lt;a href="https://vast.ai" rel="noopener noreferrer"&gt;Vast.ai&lt;/a&gt; — a GPU marketplace where people rent compute time. No code required. You install a daemon, set a price, and wait for someone to rent your machine.&lt;/p&gt;

&lt;p&gt;Here's what actually happened.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why I Didn't Just Mine Crypto
&lt;/h2&gt;

&lt;p&gt;First thing people ask: "Why not just mine?"&lt;/p&gt;

&lt;p&gt;Short answer: it's 2026, the margins are brutal, and I didn't want to deal with it. GPU compute rental is different — you're renting raw processing power, and the demand right now is AI inference and training. People building LLMs, running diffusion models, doing batch jobs.&lt;/p&gt;

&lt;p&gt;The upside: no mining pool setup, no daily coin price anxiety, no special software. Your machine runs Docker containers, gets paid per second of use, you get a payout.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Setup (Genuinely About 90 Minutes)
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Created a Vast.ai account&lt;/li&gt;
&lt;li&gt;Installed the host daemon on Windows (it's a one-click installer)&lt;/li&gt;
&lt;li&gt;Set my RTX 3060 12GB at &lt;strong&gt;$0.15/hour&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Went to bed&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's it. No configuration rabbit holes, no drivers to hunt down. The daemon manages everything — spinning up containers, cleaning up after renters, reporting uptime.&lt;/p&gt;

&lt;p&gt;I set the minimum rental duration to 1 hour so I wouldn't get hit with a dozen 5-minute jobs.&lt;/p&gt;




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

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Day&lt;/th&gt;
&lt;th&gt;Hours Rented&lt;/th&gt;
&lt;th&gt;Earnings&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Day 1&lt;/td&gt;
&lt;td&gt;3.2h&lt;/td&gt;
&lt;td&gt;$0.48&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Day 2&lt;/td&gt;
&lt;td&gt;11.5h&lt;/td&gt;
&lt;td&gt;$1.73&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Day 3&lt;/td&gt;
&lt;td&gt;0h&lt;/td&gt;
&lt;td&gt;$0.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Day 4&lt;/td&gt;
&lt;td&gt;16.8h&lt;/td&gt;
&lt;td&gt;$2.52&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Day 5&lt;/td&gt;
&lt;td&gt;9.1h&lt;/td&gt;
&lt;td&gt;$1.37&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Day 6&lt;/td&gt;
&lt;td&gt;22.0h&lt;/td&gt;
&lt;td&gt;$3.30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Day 7&lt;/td&gt;
&lt;td&gt;14.4h&lt;/td&gt;
&lt;td&gt;$2.16&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Week 1 total: ~$11.56&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Annualized naively? About $600/year. Which would be great except day 3 was $0 and utilization is inconsistent.&lt;/p&gt;

&lt;p&gt;A more realistic steady-state: &lt;strong&gt;$50–130/month&lt;/strong&gt; depending on demand.&lt;/p&gt;




&lt;h2&gt;
  
  
  What People Actually Rent It For
&lt;/h2&gt;

&lt;p&gt;Vast.ai shows you the jobs (anonymized). Mine has been used for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Running &lt;code&gt;vllm&lt;/code&gt; inference servers (Mistral, Qwen, LLaMA variants)&lt;/li&gt;
&lt;li&gt;Stable Diffusion batch jobs&lt;/li&gt;
&lt;li&gt;Some kind of PyTorch training run that lasted 8 hours&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The 3060 with 12GB VRAM is actually sweet for inference — fits most 7B–13B models at 4-bit quantization without breaking a sweat. It's not the fastest card, but it's affordable to rent, which means demand is there.&lt;/p&gt;




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

&lt;p&gt;&lt;strong&gt;You can't use your GPU while it's rented.&lt;/strong&gt; Sounds obvious, but the practical implication: if you need your machine for local inference and someone's rented it, tough luck. I started routing heavy tasks to my Mac Mini during rental periods.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Electricity.&lt;/strong&gt; My RTX 3060 at load pulls about 150W. At Turkish electricity rates, that's roughly $8–15/month in power at typical utilization. So the net is lower than the gross numbers above.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It's genuinely passive but not predictable.&lt;/strong&gt; Day 3 was $0. Day 6 was near-full utilization. There's no way to forecast demand.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Payouts have a minimum.&lt;/strong&gt; Vast.ai pays out once you hit a threshold. Nothing to worry about, just something to know going in.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I'm Going to Try Next
&lt;/h2&gt;

&lt;p&gt;The obvious play is adding more GPUs. I have a few more in storage — an RTX 3080 and some older 3060s. If I rack those up, the math gets interesting:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;GPU&lt;/th&gt;
&lt;th&gt;Rate&lt;/th&gt;
&lt;th&gt;Monthly (50% util)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;RTX 3060 (current)&lt;/td&gt;
&lt;td&gt;$0.15/h&lt;/td&gt;
&lt;td&gt;~$54&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX 3080 10GB&lt;/td&gt;
&lt;td&gt;$0.20/h&lt;/td&gt;
&lt;td&gt;~$72&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2x RTX 3070 8GB&lt;/td&gt;
&lt;td&gt;$0.16/h&lt;/td&gt;
&lt;td&gt;~$115&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That's ~$240/month without doing anything after setup. At 70% utilization: ~$340.&lt;/p&gt;

&lt;p&gt;The real work is physical — pulling GPUs from storage, getting them into a rig, managing thermals. But the software side is almost zero maintenance.&lt;/p&gt;




&lt;h2&gt;
  
  
  Should You Try This?
&lt;/h2&gt;

&lt;p&gt;If you have a spare GPU collecting dust: &lt;strong&gt;yes, probably.&lt;/strong&gt; The setup is low friction, the risk is near-zero (worst case, you uninstall the daemon and move on), and even modest earnings beat $0.&lt;/p&gt;

&lt;p&gt;If you're thinking about buying a GPU specifically for this: &lt;strong&gt;do the math carefully.&lt;/strong&gt; At current rates, an RTX 3060 costs ~$300–350 used. Payback period at $50/month is 6–7 months, which is fine — but don't expect to fund your retirement from a single card.&lt;/p&gt;

&lt;p&gt;The real value for me isn't the income (yet). It's that I now have a system running, I understand the demand patterns, and I know the path to scale looks viable.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Setup Summary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Platform: &lt;a href="https://vast.ai" rel="noopener noreferrer"&gt;Vast.ai&lt;/a&gt; (there's also RunPod if you want alternatives)&lt;/li&gt;
&lt;li&gt;Time to set up: ~90 minutes&lt;/li&gt;
&lt;li&gt;Technical skill required: Know how to install software on Windows&lt;/li&gt;
&lt;li&gt;Ongoing maintenance: Almost none&lt;/li&gt;
&lt;li&gt;Realistic earnings: $50–130/month per mid-tier GPU&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Happy to answer questions if you try this and run into something weird. The daemon is pretty solid but there's always an edge case or two.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I write about running AI locally, automation side projects, and occasionally making money from hardware that would otherwise just collect dust. If any of this is useful, feel free to follow.&lt;/em&gt;&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
