<?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: Ahmed Nafies</title>
    <description>The latest articles on DEV Community by Ahmed Nafies (@ahmed_nafies_3a55c907115c).</description>
    <link>https://dev.to/ahmed_nafies_3a55c907115c</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%2F4055741%2F3f230fa6-829b-417e-9dd7-bb23785258a5.png</url>
      <title>DEV Community: Ahmed Nafies</title>
      <link>https://dev.to/ahmed_nafies_3a55c907115c</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ahmed_nafies_3a55c907115c"/>
    <language>en</language>
    <item>
      <title>Testing GLM-5.3 on a Budget with OpenCode Go: 10 Tasks, 4¢, Zero Failures</title>
      <dc:creator>Ahmed Nafies</dc:creator>
      <pubDate>Fri, 14 Aug 2026 17:51:42 +0000</pubDate>
      <link>https://dev.to/ahmed_nafies_3a55c907115c/testing-glm-53-on-a-budget-with-opencode-go-10-tasks-4c-zero-failures-1j39</link>
      <guid>https://dev.to/ahmed_nafies_3a55c907115c/testing-glm-53-on-a-budget-with-opencode-go-10-tasks-4c-zero-failures-1j39</guid>
      <description>&lt;p&gt;On August 14, 2026, Zhipu dropped &lt;strong&gt;GLM-5.3&lt;/strong&gt; — a &lt;a href="https://z.ai/blog/glm-5.3" rel="noopener noreferrer"&gt;post-training-only upgrade over GLM-5.2&lt;/a&gt; with 128K max output tokens. I put it through its paces with OpenCode Go the same day — spoiler: it's awesome.&lt;/p&gt;

&lt;p&gt;It's one of the pricier models on the &lt;a href="https://opencode.ai/docs/go/" rel="noopener noreferrer"&gt;OpenCode Go&lt;/a&gt; plan — a reasoning model at &lt;strong&gt;$1.40/M input and $4.40/M output&lt;/strong&gt;, with a &lt;strong&gt;$15/month&lt;/strong&gt; usage allowance, so every token counts. I wanted to answer a simple question: &lt;em&gt;is it worth the premium over cheaper Go models like DeepSeek V4 Pro ($0.435/$0.87)?&lt;/em&gt; — without burning my monthly usage finding out.&lt;/p&gt;

&lt;p&gt;So I built a tiny benchmark that cost &lt;strong&gt;under 4 cents&lt;/strong&gt; total. Here's what I did and what I found.&lt;/p&gt;

&lt;h2&gt;
  
  
  The budget math
&lt;/h2&gt;

&lt;p&gt;Before writing a single prompt, I worked out what a request actually costs:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Price (per 1M tokens)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Input&lt;/td&gt;
&lt;td&gt;$1.40&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output&lt;/td&gt;
&lt;td&gt;$4.40&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cached read&lt;/td&gt;
&lt;td&gt;$0.26&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A self-contained prompt sent through &lt;code&gt;opencode run&lt;/code&gt; (no tools, no file ops) is roughly &lt;strong&gt;50 input + 100–180 output tokens&lt;/strong&gt;. Even at the top end that's about &lt;strong&gt;$0.007 per request&lt;/strong&gt;. Ten prompts ≈ &lt;strong&gt;4 cents&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The trick to staying cheap: short, self-contained prompts that don't trigger agentic tool loops, and a shared system prompt so OpenCode's context stays in the cache ($0.26/M instead of $1.40/M).&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;

&lt;p&gt;One line. GLM-5.3 is already in the Go catalog under the model id &lt;code&gt;opencode-go/glm-5.3&lt;/code&gt;, so after &lt;code&gt;/connect&lt;/code&gt; → OpenCode Go it's just:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;opencode run &lt;span class="nt"&gt;--model&lt;/span&gt; opencode-go/glm-5.3 &lt;span class="s2"&gt;"your prompt"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The test
&lt;/h2&gt;

&lt;p&gt;A Bash harness loops over 10 prompts and runs each one through &lt;code&gt;opencode run&lt;/code&gt;, timing it and saving the output:&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;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-uo&lt;/span&gt; pipefail
&lt;span class="nv"&gt;MODEL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;MODEL&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;opencode&lt;/span&gt;&lt;span class="p"&gt;-go/glm-5.3&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;SUFFIX&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;$'&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s1"&gt;Answer directly. Do not run any code or use any tools. Just provide the answer.'&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"task,wall_seconds,status"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; results/summary.csv
&lt;span class="nv"&gt;idx&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1
&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nv"&gt;IFS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt; prompt&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nv"&gt;start&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%s.%N&lt;span class="si"&gt;)&lt;/span&gt;
  opencode run &lt;span class="nt"&gt;--model&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$MODEL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$prompt$SUFFIX&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"results/&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%02d'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$idx&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;.md"&lt;/span&gt; 2&amp;gt;&amp;amp;1
  &lt;span class="nv"&gt;end&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%s.%N&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$idx&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$end&lt;/span&gt;&lt;span class="s2"&gt; - &lt;/span&gt;&lt;span class="nv"&gt;$start&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | bc &lt;span class="nt"&gt;-l&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;,ok"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; results/summary.csv
  &lt;span class="nv"&gt;idx&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt;idx+1&lt;span class="k"&gt;))&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt; &amp;lt; &amp;lt;&lt;span class="o"&gt;(&lt;/span&gt;python3 &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"import sys;print('&lt;/span&gt;&lt;span class="se"&gt;\0&lt;/span&gt;&lt;span class="s2"&gt;'.join([p.strip() for p in open('prompts.txt').read().split('=====') if p.strip()]))"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The 10 prompts span the categories a coding agent actually needs: code generation, bug fixing, code review, SQL, regex, logic, math, structured output, explanation, and format-following.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;th&gt;Wall time&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Codegen&lt;/td&gt;
&lt;td&gt;Flatten a nested list (iterative)&lt;/td&gt;
&lt;td&gt;✅ Correct stack-based solution&lt;/td&gt;
&lt;td&gt;10.7s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Bug fix&lt;/td&gt;
&lt;td&gt;Binary search off-by-one&lt;/td&gt;
&lt;td&gt;✅ Caught the &lt;code&gt;return lo&lt;/code&gt; bug&lt;/td&gt;
&lt;td&gt;53.1s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Code review&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;max()&lt;/code&gt; in Go&lt;/td&gt;
&lt;td&gt;✅ Found 3 real issues&lt;/td&gt;
&lt;td&gt;15.4s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;SQL&lt;/td&gt;
&lt;td&gt;Top-3 paid per department&lt;/td&gt;
&lt;td&gt;✅ &lt;code&gt;DENSE_RANK()&lt;/code&gt; + tie handling&lt;/td&gt;
&lt;td&gt;45.2s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Regex&lt;/td&gt;
&lt;td&gt;Valid IPv4&lt;/td&gt;
&lt;td&gt;✅ Correct&lt;/td&gt;
&lt;td&gt;21.8s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Logic&lt;/td&gt;
&lt;td&gt;8-ball / 2-weighing puzzle&lt;/td&gt;
&lt;td&gt;✅ Correct 3-3-2 strategy&lt;/td&gt;
&lt;td&gt;28.4s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Math&lt;/td&gt;
&lt;td&gt;Derivative of &lt;code&gt;x³·ln x&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;✅ &lt;code&gt;x²(3·ln x + 1)&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;24.8s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;JSON&lt;/td&gt;
&lt;td&gt;Structured output&lt;/td&gt;
&lt;td&gt;✅ Valid JSON, no fences&lt;/td&gt;
&lt;td&gt;28.7s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;Explain&lt;/td&gt;
&lt;td&gt;JS dedup snippet&lt;/td&gt;
&lt;td&gt;✅ Correct + 2 improvements&lt;/td&gt;
&lt;td&gt;35.1s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;Format&lt;/td&gt;
&lt;td&gt;Exact bullet-list format&lt;/td&gt;
&lt;td&gt;✅ Followed exactly&lt;/td&gt;
&lt;td&gt;8.2s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;10/10 correct.&lt;/strong&gt; Total wall time ~4.5 minutes. Total cost &lt;strong&gt;~$0.04&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Findings
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. It's a reasoning model — and it thinks &lt;em&gt;a lot&lt;/em&gt;
&lt;/h3&gt;

&lt;p&gt;This was the surprise. GLM-5.3 emits &lt;strong&gt;thinking tokens&lt;/strong&gt; before answering, and the amount of thinking scales with task difficulty:&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;Reasoning tokens&lt;/th&gt;
&lt;th&gt;Output tokens&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;"List three benefits…" (easy)&lt;/td&gt;
&lt;td&gt;37&lt;/td&gt;
&lt;td&gt;35&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SQL top-3 (medium)&lt;/td&gt;
&lt;td&gt;95&lt;/td&gt;
&lt;td&gt;107&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flatten nested list (hard)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;749&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;179&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;On the hard task it thought ~4× more than it wrote. That reasoning is billed as &lt;strong&gt;output&lt;/strong&gt; ($4.40/M), so it's the single biggest cost driver — and the biggest latency driver too.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Latency is the real price, not dollars
&lt;/h3&gt;

&lt;p&gt;Wall times ranged from &lt;strong&gt;8s to 53s&lt;/strong&gt;. The binary-search fix (53s) and SQL query (45s) were the slowest, both reasoning-heavy. For a $4.40/M output model, the money is trivial — the &lt;em&gt;waiting&lt;/em&gt; is what you'll notice in day-to-day use.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. There's a hidden reasoning dial
&lt;/h3&gt;

&lt;p&gt;When I hit GLM-5.3's raw OpenAI-compatible endpoint directly with default settings, it went off the rails: it burned &lt;strong&gt;2,047 reasoning tokens on "flatten a list"&lt;/strong&gt; and produced an &lt;em&gt;empty&lt;/em&gt; answer because it hit the token cap while still thinking, with a &lt;strong&gt;49-second time-to-first-token&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Passing &lt;code&gt;"reasoning_effort": "low"&lt;/code&gt; changed everything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;low  =&amp;gt; finish=stop  reasoning=2   total=6   content="444"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;OpenCode Go's default tuning keeps the model usable out of the box, but if you're calling the API yourself, you'll want to set a reasoning effort explicitly.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. The quality bar is genuinely high
&lt;/h3&gt;

&lt;p&gt;The code review was the highlight — it caught the all-negative-input bug, the empty-slice edge case, &lt;strong&gt;and&lt;/strong&gt; that the function shadows Go 1.21's built-in &lt;code&gt;max&lt;/code&gt;, then pointed at &lt;code&gt;slices.Max()&lt;/code&gt;. That's the kind of detail I'd expect from a flagship model.&lt;/p&gt;

&lt;p&gt;Minor nitpicks: the Go fix used &lt;code&gt;fmt.Errorf&lt;/code&gt; without importing &lt;code&gt;fmt&lt;/code&gt;, and the JSON task invented a name ("Ahmed Hassan") when none was given. Both trivial.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cost breakdown
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Request type&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;Easy prompt (list three)&lt;/td&gt;
&lt;td&gt;$0.0030&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Medium prompt (SQL)&lt;/td&gt;
&lt;td&gt;$0.0035&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hard prompt (flatten)&lt;/td&gt;
&lt;td&gt;$0.0067&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;10-prompt suite total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~$0.04&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Against a $15 monthly Go allowance, that's &lt;strong&gt;0.27% of my monthly usage&lt;/strong&gt; for a complete capability picture.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verdict
&lt;/h2&gt;

&lt;p&gt;GLM-5.3 passed every task with detail I'd call &lt;em&gt;frontier-grade&lt;/em&gt;, at a cost that's basically free on the Go plan. The trade-off is &lt;strong&gt;latency&lt;/strong&gt;: it's a deliberate thinker, so it's best for hard, non-trivial tasks rather than rapid-fire edits.&lt;/p&gt;

&lt;p&gt;If you want a fast, cheap daily driver, DeepSeek V4 Pro on Go is the better value. If you want a model that reasons through the hard stuff and rarely gets it wrong, GLM-5.3 earns its premium — and you can test it yourself for the price of a gumball.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;All prompts, the harness, and raw results are in the repo. Total spend for this entire experiment (including the failed raw-API probes): under **8 cents&lt;/em&gt;&lt;em&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Why GPT-5.6 Luna High Is My Default for Agentic Engineering</title>
      <dc:creator>Ahmed Nafies</dc:creator>
      <pubDate>Fri, 14 Aug 2026 17:38:34 +0000</pubDate>
      <link>https://dev.to/ahmed_nafies_3a55c907115c/why-gpt-56-luna-high-is-my-default-for-agentic-engineering-4e04</link>
      <guid>https://dev.to/ahmed_nafies_3a55c907115c/why-gpt-56-luna-high-is-my-default-for-agentic-engineering-4e04</guid>
      <description>&lt;p&gt;I used to pick coding models the same way people pick sports cars: choose the most powerful one and pretend the fuel bill is somebody else's problem.&lt;/p&gt;

&lt;p&gt;That worked when I was asking one question at a time. Then I started using agents for real engineering work.&lt;/p&gt;

&lt;p&gt;An engineering agent does not answer once and disappear. It reads the repository, searches for related code, opens the wrong file, finds the right file, proposes a change, runs a test, breaks something, reads the error, fixes the change, and runs the test again. Sometimes it also writes a surprisingly thoughtful essay about the three lines it just modified.&lt;/p&gt;

&lt;p&gt;By the time one task is finished, the model may have been called a dozen times. Suddenly, model pricing is not a footnote. It is part of the architecture.&lt;/p&gt;

&lt;p&gt;That is how GPT-5.6 Luna with high reasoning effort became my default.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cheap model that gets the job done
&lt;/h2&gt;

&lt;p&gt;OpenAI positions Luna for cost-sensitive, high-volume work, and the pricing makes that very clear. As of August 2026, GPT-5.6 Luna costs $0.20 per million input tokens and $1.20 per million output tokens. Claude Sonnet 5 costs $2 for input and $10 for output.&lt;/p&gt;

&lt;p&gt;So Luna's input is ten times cheaper, and its output is about 8.3 times cheaper.&lt;/p&gt;

&lt;p&gt;Take an agent run that consumes 100,000 input tokens and produces 20,000 output tokens. Luna costs about $0.044. Sonnet costs about $0.40. Neither amount is terrifying on its own, but agents are enthusiastic employees. Give them access to a repository and they will inspect everything, call every tool, and occasionally create a strategic roadmap for changing a variable name.&lt;/p&gt;

&lt;p&gt;Repeat that across pull requests, bug fixes, failed CI jobs, and background tasks, and the difference stops being theoretical.&lt;/p&gt;

&lt;p&gt;Using Luna means I can let the agent iterate without feeling that every failed test needs approval from Finance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I use high reasoning effort
&lt;/h2&gt;

&lt;p&gt;Cheap is useful only if the model can finish the work. I do not run Luna with minimal reasoning and hope for a miracle. I use high reasoning effort.&lt;/p&gt;

&lt;p&gt;That setting gives it enough room to trace behavior across files, understand unfamiliar abstractions, choose tools, react to command failures, and verify the result before declaring victory. In my experience, that is enough for a large part of normal engineering work.&lt;/p&gt;

&lt;p&gt;It is not the smartest model available, and I do not need it to be. My default model does not have to solve every impossible architecture problem. It has to handle routine features, localized bugs, tests, refactoring, and repository exploration without turning each task into an expensive research project.&lt;/p&gt;

&lt;p&gt;There is a temptation to treat "accurate enough" as an insult. I see it differently. Most engineering tasks do not need a model to contemplate the nature of software. They need it to follow the repository instructions, make a focused change, preserve unrelated code, understand the compiler output, and actually run the tests.&lt;/p&gt;

&lt;p&gt;Luna does that well enough for me, especially when the agent harness is doing its job. Good context, sensible permissions, small changes, and real test feedback matter just as much as the model. A cheaper model inside a disciplined workflow can be more useful than an expensive model confidently wandering through the codebase.&lt;/p&gt;

&lt;p&gt;Speed matters too. A small delay is easy to ignore in one response. Across fifteen sequential agent turns, it becomes enough time to question your career choices. Luna keeps the loop moving. It can inspect, attempt, fail, correct, and verify without making every step feel like a separate meeting.&lt;/p&gt;

&lt;h2&gt;
  
  
  And what about Claude Sonnet?
&lt;/h2&gt;

&lt;p&gt;Claude Sonnet is a capable model. There are difficult tasks where I am happy to use it. It is simply not my default.&lt;/p&gt;

&lt;p&gt;The obvious reason is price. The less obvious reason is output efficiency. In my experience, Sonnet often gives me more explanation than the engineering loop needs. I ask for a patch and sometimes receive the patch, a retrospective, and the opening chapter of a management book.&lt;/p&gt;

&lt;p&gt;That can be useful when I am exploring a decision. It is less useful when the agent is supposed to edit a file and run the tests. Verbose output is not only more to read; output tokens are also the expensive tokens.&lt;/p&gt;

&lt;p&gt;Anthropic's documentation says Claude 4.7 and later can produce roughly 30% more tokens for the same text than models using its previous tokenizer, although the exact difference depends on the workload. That is not the same as saying the model is always verbose, but it is another reminder that token behavior belongs in the engineering decision.&lt;/p&gt;

&lt;p&gt;For autonomous execution, I care more about correct edits and passing tests than an eloquent account of the model's emotional journey.&lt;/p&gt;

&lt;p&gt;My approach is simple: start with Luna High and escalate when the task earns it. If the architecture is genuinely ambiguous, failures remain unexplained, or the work involves subtle security or concurrency behavior, I will reach for a stronger model. That is not model loyalty. It is routing.&lt;/p&gt;

&lt;p&gt;The best model is not always the one that wins the benchmark. For agentic engineering, I care about the cost of the completed task, how long it took, how many retries it needed, whether the tests passed, and how much cleanup I had to do afterward.&lt;/p&gt;

&lt;p&gt;For my workload, GPT-5.6 Luna with high reasoning effort currently hits the useful middle: very cheap, very fast, and accurate enough to handle the common path.&lt;/p&gt;

&lt;p&gt;It is not the model I want solving every problem. It is the model I want holding the wrench most of the time.&lt;/p&gt;




&lt;p&gt;Pricing sources: &lt;a href="https://platform.openai.com/docs/models" rel="noopener noreferrer"&gt;OpenAI model documentation&lt;/a&gt; and &lt;a href="https://platform.claude.com/docs/en/about-claude/pricing" rel="noopener noreferrer"&gt;Anthropic pricing documentation&lt;/a&gt;, checked August 14, 2026.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>llm</category>
      <category>softwareengineering</category>
      <category>ai</category>
    </item>
    <item>
      <title>I Asked Muse Glimmer to Pair Program With Me in OpenCode</title>
      <dc:creator>Ahmed Nafies</dc:creator>
      <pubDate>Tue, 11 Aug 2026 22:04:13 +0000</pubDate>
      <link>https://dev.to/ahmed_nafies_3a55c907115c/i-asked-muse-glimmer-to-pair-program-with-me-in-opencode-kkh</link>
      <guid>https://dev.to/ahmed_nafies_3a55c907115c/i-asked-muse-glimmer-to-pair-program-with-me-in-opencode-kkh</guid>
      <description>&lt;p&gt;I wanted to use Meta's Muse Glimmer as a local agentic engineering assistant inside OpenCode.&lt;/p&gt;

&lt;p&gt;Not as a chatbot in a browser. I wanted an agent that could inspect a repository, understand a task, edit files, run tests, and explain what it did.&lt;/p&gt;

&lt;p&gt;The command was refreshingly short:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ollama launch opencode &lt;span class="nt"&gt;--model&lt;/span&gt; muse-glimmer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The model, however, is a 30-billion-parameter coworker who arrives carrying an approximately 18 GB suitcase and expects a 128K-context office.&lt;/p&gt;

&lt;h2&gt;
  
  
  The onboarding meeting
&lt;/h2&gt;

&lt;p&gt;First, I checked Ollama:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ollama &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then I started the local service if needed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ollama serve
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I verified that the local API was alive:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://localhost:11434/api/version
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then I launched OpenCode with Muse Glimmer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ollama launch opencode &lt;span class="nt"&gt;--model&lt;/span&gt; muse-glimmer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first run downloads the model. This is a good opportunity to make coffee, reconsider your disk space, and remember that “local AI” does not mean “tiny AI.”&lt;/p&gt;

&lt;p&gt;To inspect installed models:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ollama &lt;span class="nb"&gt;ls&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On Apple Silicon, Ollama also lists an MLX variant:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ollama launch opencode &lt;span class="nt"&gt;--model&lt;/span&gt; muse-glimmer:30b-mlx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The standard model is listed at around 18 GB. That is the model's luggage, not the entire hotel bill. Leave memory for the context, OpenCode, the operating system, and the repository you are asking the agent to understand.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why OpenCode changes the experiment
&lt;/h2&gt;

&lt;p&gt;The interesting part is not just running Muse Glimmer locally. It is putting the model inside an engineering workflow.&lt;/p&gt;

&lt;p&gt;With OpenCode, I can ask the agent to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Inspect the repository.&lt;/li&gt;
&lt;li&gt;Find the relevant files.&lt;/li&gt;
&lt;li&gt;Explain the current behavior.&lt;/li&gt;
&lt;li&gt;Propose a plan.&lt;/li&gt;
&lt;li&gt;Make a focused change.&lt;/li&gt;
&lt;li&gt;Run the project's tests.&lt;/li&gt;
&lt;li&gt;Read failures.&lt;/li&gt;
&lt;li&gt;Revise the patch.&lt;/li&gt;
&lt;li&gt;Show the final diff and verification result.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That is the difference between asking a model for code and asking an agent to help complete an engineering task. The model can still be confidently wrong. The workflow gives me more chances to catch it before the wrongness becomes a pull request.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first repository tour
&lt;/h2&gt;

&lt;p&gt;I opened the repository in OpenCode and started with a read-only request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You are working in a local software repository. Inspect the project structure and identify:

- The application entry point
- The test command
- The main source directories
- Configuration files that affect development
- Any obvious contribution or safety constraints

Do not modify files. Report the evidence and ask before making changes.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a better first task than “fix everything.” It tests whether Muse Glimmer can build context without immediately turning the repository into modern art.&lt;/p&gt;

&lt;p&gt;The key signal is whether the response points to actual files and commands. A useful agent should report evidence, not invent a project architecture because the folder name sounded familiar.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first engineering task
&lt;/h2&gt;

&lt;p&gt;After the repository tour, I gave OpenCode a small, testable issue:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Find the bug in the relevant average function. Explain the edge case, propose the smallest fix, and add a regression test. Do not change unrelated files.

The current implementation is: average(values) = sum(values) / len(values) + 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The extra &lt;code&gt;+ 1&lt;/code&gt; is suspicious. It is the kind of bug that enters production wearing sunglasses and saying, “Nobody will notice.”&lt;/p&gt;

&lt;p&gt;A good OpenCode session should locate the real implementation, find the project's existing test style, make the smallest patch, and run the relevant test. A locally generated patch is still a patch. It needs review, tests, and version control like any other change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Planning before editing
&lt;/h2&gt;

&lt;p&gt;I asked OpenCode to separate investigation from implementation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Before editing, write a short implementation plan with:

- Files you expect to inspect
- The behavior you expect to change
- The test you will add or update
- The command you will run to verify the change

Wait for approval before modifying files.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates a useful checkpoint. It also prevents the classic coding-agent experience where you ask for one small fix and receive a surprise framework migration, a new abstraction layer, and a README poem.&lt;/p&gt;

&lt;p&gt;OpenCode is the interface where this loop becomes practical: inspect the plan, approve the direction, watch the diff, and ask for verification in the same engineering session.&lt;/p&gt;

&lt;h2&gt;
  
  
  The test-and-repair loop
&lt;/h2&gt;

&lt;p&gt;The useful workflow looks 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;Inspect -&amp;gt; plan -&amp;gt; approve -&amp;gt; edit -&amp;gt; test -&amp;gt; inspect failure -&amp;gt; revise -&amp;gt; test again
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I tested failure recovery with an intentionally broken test result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;The test runner returned:

ERROR: assertion failed in tests/test_average.py
Expected: 2.0
Received: 3.0

Explain the failure, identify the likely cause, and propose the smallest correction. Do not edit files yet.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then I tested a repository constraint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;The repository check returned:

ERROR: the generated lockfile differs from the committed lockfile.

Do not regenerate dependencies. Explain what should be reviewed first.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The goal is not to make the agent retry everything until the terminal turns green. The goal is to see whether it can distinguish a fixable implementation error from a situation requiring human review.&lt;/p&gt;

&lt;p&gt;“The model usually behaves” is not an engineering control. It is something you say immediately before opening an incident ticket.&lt;/p&gt;

&lt;h2&gt;
  
  
  Screenshot-driven debugging
&lt;/h2&gt;

&lt;p&gt;Muse Glimmer supports image input, which is useful for UI bugs, screenshots, diagrams, and terminal output. From the local OpenCode workflow, I can give the agent a screenshot and ask for evidence-first analysis:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Inspect this screenshot as a debugging artifact. List only visible symptoms, separate observations from hypotheses, and suggest which source files or browser checks should be inspected next.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;“The button is clipped” is an observation. “The CSS grid is broken because of a missing min-width” is a hypothesis. That distinction saves time and prevents a screenshot from becoming a license to rewrite the frontend.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Using Muse Glimmer with OpenCode through Ollama is straightforward to start:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ollama launch opencode &lt;span class="nt"&gt;--model&lt;/span&gt; muse-glimmer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The interesting work begins after OpenCode starts. Ask the agent to inspect, plan, edit, test, diagnose, and explain.&lt;/p&gt;

&lt;p&gt;Muse Glimmer can be the clever local coworker. Ollama handles the model runtime. OpenCode handles the engineering conversation. You remain the person who decides whether the patch deserves to live.&lt;/p&gt;

&lt;p&gt;Resources:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://research.meta.ai/blog/introducing-muse-glimmer-open-agentic-model" rel="noopener noreferrer"&gt;Muse Glimmer announcement from Meta AI Research&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ollama.com/library/muse-glimmer" rel="noopener noreferrer"&gt;Muse Glimmer on Ollama&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.ollama.com/integrations/opencode" rel="noopener noreferrer"&gt;Ollama's OpenCode integration&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://opencode.ai/" rel="noopener noreferrer"&gt;OpenCode&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>ollama</category>
      <category>opencode</category>
      <category>coding</category>
    </item>
    <item>
      <title>I Let an Autonomous Agent Build My RAG Stack. Here's How the Loop Worked</title>
      <dc:creator>Ahmed Nafies</dc:creator>
      <pubDate>Thu, 06 Aug 2026 17:24:49 +0000</pubDate>
      <link>https://dev.to/ahmed_nafies_3a55c907115c/i-let-an-autonomous-agent-build-my-rag-stack-heres-how-the-loop-worked-520j</link>
      <guid>https://dev.to/ahmed_nafies_3a55c907115c/i-let-an-autonomous-agent-build-my-rag-stack-heres-how-the-loop-worked-520j</guid>
      <description>&lt;h1&gt;
  
  
  I Let an Autonomous Agent Build My RAG Stack. Here's How the Loop Worked
&lt;/h1&gt;

&lt;p&gt;I didn't write a single line of code. I didn't debug a Helm chart. I didn't stare at a traceback wondering why the connector wouldn't authenticate.&lt;/p&gt;

&lt;p&gt;I typed one sentence and watched.&lt;/p&gt;

&lt;p&gt;Twenty minutes later, a draft pull request landed in my private repo. Everything behind that PR — the OpenSearch cluster, the embedding models, the five init jobs in lockstep, the MCP server, the RAG pipeline, and the 30 seeded articles — was produced by an autonomous coding agent using a workflow called &lt;strong&gt;the loop&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The stack itself is impressive enough: OpenSearch 3.7 with hybrid search fusing semantic and full-text results, a retrieval-augmented generation pipeline powered by a local Ollama LLM, a FastMCP server exposing four tools over SSE, and a Skaffold + Helm deployment that goes from zero to running cluster with &lt;code&gt;skaffold run&lt;/code&gt;. But the stack isn't the story.&lt;/p&gt;

&lt;p&gt;The story is what happened between the prompt and the PR. The planning, the self-correction, the moment the agent deleted 14 model chunks it thought were duplicates, broke the entire embedding system, and then — without any help — fixed it.&lt;/p&gt;

&lt;p&gt;This is how the loop worked.&lt;/p&gt;

&lt;h2&gt;
  
  
  The loop isn't just an agent. It's a contract.
&lt;/h2&gt;

&lt;p&gt;The loop is a skill for &lt;a href="https://opencode.ai" rel="noopener noreferrer"&gt;opencode&lt;/a&gt; that turns a natural-language prompt into a working feature branch and a draft PR. It sounds like every other AI coding tool until you see what it &lt;em&gt;won't&lt;/em&gt; do: it won't write code before you approve a plan.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Phase&lt;/th&gt;
&lt;th&gt;What happens&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;proposal&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Explores the codebase, analyzes the prompt, proposes a plan. No implementation.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;agreement&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;You review. You can push back, ask questions, or say go. The agent waits.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;implementation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Builds the agreed scope on a feature branch, committing early and often.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;draft_gate&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Self-review: lint, tests, diff inspection. Fixes issues before you ever see them.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;draft_pr&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Pushes the branch, opens a draft PR, reports back. Done.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every agent should work like this. Most don't. Most just start writing code the moment you press enter, and you spend the rest of the session saying "no, not like that."&lt;/p&gt;

&lt;p&gt;The loop carries its state in &lt;code&gt;.loop-state.json&lt;/code&gt; — the current phase, the branch, the PR number, the plan, and a log of every phase transition. If your session crashes or you walk away for an hour, the loop resumes exactly where it left off. This alone makes it viable for multi-hour autonomous runs.&lt;/p&gt;

&lt;p&gt;Then there's &lt;strong&gt;loop-police&lt;/strong&gt;: a companion skill that rides shotgun, watching for infinite loops. Thinking loops. Tool-call loops. File-read spirals. Stagnation. When loop-police detects the agent going in circles, it interrupts: &lt;em&gt;"You're stuck. Pivot."&lt;/em&gt; That intervention is the difference between an agent that wastes ten turns retrying the same failing deploy and one that recognizes the failure and changes strategy.&lt;/p&gt;

&lt;p&gt;Without loop-police, the chunk-deletion disaster I'll get to later might have been unrecoverable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The plan was one paragraph
&lt;/h2&gt;

&lt;p&gt;The agent's proposal:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Build &lt;code&gt;opensearch-hybrid-mcp&lt;/code&gt; mirroring the &lt;code&gt;os-hybrid-search&lt;/code&gt; reference: OpenSearch 3.7 with Dashboards, MCP server (FastMCP + UV), five init jobs (template → embedding → connector → pipeline → seed), &lt;code&gt;skaffold.yaml&lt;/code&gt; with six images, &lt;code&gt;justfile&lt;/code&gt;, README. Private repo + draft PR.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And then a diagram — the agent drew the architecture:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;macOS host                             Kubernetes (Colima)
┌───────────────┐                      ┌────────────────────────────────────────────────────────────┐
│ Ollama        │                      │  ┌──────────────────┐  ┌────────────────┐                  │
│ (Desktop)     │◄──────── :11434 ────►│  │  OpenSearch 3.7  │  │  OS Dashboards │                  │
│ qwen3.5:9b-mlx│  host.docker.internal│  │  os-hybrid:9200  │  │  :5601         │                  │
└───────────────┘                      │  └────────┬─────────┘  └────────────────┘                  │
                                       │          │                                                 │
                                       │  ┌────────▼──────────┐                                     │
                                       │  │  MCP Server       │    FastMCP + UV                     │
                                       │  │  mcp-server:8000  │    hybrid_search                    │
                                       │  │                   │    rag_query                        │
                                       │  │                   │    index_document                   │
                                       │  │                   │    cluster_health                   │
                                       │  └───────────────────┘                                     │
                                       │                                                            │
                                       │  Init Jobs (run in sequence):                              │
                                       │    1. template-job    index template                       │
                                       │    2. embedding-job   all-MiniLM-L12-v2                    │
                                       │    3. connector-job   Ollama → Qwen 3.5                    │
                                       │    4. pipeline-job    chunking/hybrid/RAG                  │
                                       │    5. seed-job        30 articles                          │
                                       └────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ollama runs on the host, not inside the cluster — pods reach it through &lt;code&gt;host.docker.internal:11434&lt;/code&gt;. The agent figured that out by itself. Single-node OpenSearch, 10Gi PVC, no security (it's a dev cluster), MCP server on SSE port 8000. Five init jobs that run in sequence, each waiting for its upstream dependencies through retry loops.&lt;/p&gt;

&lt;p&gt;I said yes. The agent started building.&lt;/p&gt;

&lt;h2&gt;
  
  
  The implementation was fast. The recovery was where it got interesting.
&lt;/h2&gt;

&lt;p&gt;Seven commits. Forty-plus files. The agent scaffolded the entire project in minutes.&lt;/p&gt;

&lt;p&gt;Six Helm charts (OpenSearch parent, MCP server, five init jobs). A &lt;code&gt;skaffold.yaml&lt;/code&gt; with six Docker images building in parallel. A &lt;code&gt;justfile&lt;/code&gt; with shortcuts for &lt;code&gt;skaffold run&lt;/code&gt;, &lt;code&gt;kubectl port-forward&lt;/code&gt;, and job logs. Every init job is a shell script with &lt;code&gt;until&lt;/code&gt; loops — each waits for OpenSearch, for the index template, for the embedding model to deploy, for the pipeline to be ready. If a dependency isn't there, the job pauses. If it is, the job runs. This makes the whole stack re-runnable: delete a job and re-create it, and it picks up where it left off.&lt;/p&gt;

&lt;p&gt;There's a satisfying recursion here. The loop is a five-phase workflow of gates and retries. The jobs it wrote are retry loops with dependency checks. The agent built code that mirrors its own architecture.&lt;/p&gt;

&lt;p&gt;The MCP server landed next: FastMCP, four tools:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;hybrid_search(query, index?, k?)&lt;/code&gt; — neural + full-text, z-score fused&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;rag_query(question, index?, k?)&lt;/code&gt; — retrieves context, generates answer&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;index_document(id, title, body, index?)&lt;/code&gt; — chunks + embeds + indexes&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cluster_health()&lt;/code&gt; — cluster status&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The agent ran its own draft gate: &lt;code&gt;helm lint&lt;/code&gt; and &lt;code&gt;helm template&lt;/code&gt; on seven charts, &lt;code&gt;skaffold render&lt;/code&gt;, &lt;code&gt;sh -n&lt;/code&gt; on every script. Clean. It pushed the branch, opened draft PR #1, and reported back.&lt;/p&gt;

&lt;p&gt;Technically, the loop was done: proposal → agreement → implementation → draft_gate → draft_pr. Five phases, zero handoffs.&lt;/p&gt;

&lt;p&gt;But the stack hadn't been deployed yet. And that's where everything went sideways — then sideways again — then somehow straightened out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five bugs. One autonomous agent. Zero panicking.
&lt;/h2&gt;

&lt;p&gt;The first &lt;code&gt;skaffold run&lt;/code&gt; kicked off and immediately hit a wall. The agent didn't wait for me to notice. It read the logs, diagnosed the problem, patched it, and redeployed. Five times.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bug 1 — "Connector credential is null or empty list"&lt;/strong&gt;: OpenSearch 3.7 demands a credential object even for local Ollama endpoints that don't use authentication. The agent added a dummy key (&lt;code&gt;openAI_key: "ollama-local"&lt;/code&gt;) and — crucially — switched the connector from Ollama's native &lt;code&gt;/api/chat&lt;/code&gt; to the OpenAI-compatible &lt;code&gt;/v1/chat/completions&lt;/code&gt;, which is what the RAG processor expects. Two fixes in one pass. Redeployed. Connector created.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bug 2 — The one-character regex disaster&lt;/strong&gt;: RAG queries hit a 400: &lt;code&gt;"Connector URL is not matching the trusted connector private endpoint regex"&lt;/code&gt;. The agent had written &lt;code&gt;^http://host.docker.internal:11434:.*$&lt;/code&gt; — a colon after the port. The actual URL was &lt;code&gt;http://host.docker.internal:11434/v1/chat/completions&lt;/code&gt; — a slash. The difference between &lt;code&gt;:&lt;/code&gt; and &lt;code&gt;/&lt;/code&gt; broke the entire pipeline. The agent found it, flipped the character, and redeployed. The kind of bug a human would spend 20 minutes on. The agent caught it in one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bug 3 — "Model 'ollama-qwen3.5' not found"&lt;/strong&gt;: The connector was passing the OpenSearch-registered model name to Ollama, but Ollama expects the raw model name (&lt;code&gt;qwen3.5:9b-mlx&lt;/code&gt;). The agent traced the call chain — OpenSearch RAG processor → connector → Ollama API — and updated the MCP server to pass the correct model name in &lt;code&gt;ext.generative_qa_parameters&lt;/code&gt;. Fixed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bug 4 — Memory circuit breaker&lt;/strong&gt;: Seed-job bulk indexing hit 429 rate limits. JVM heap at 93% on 2g allocation. The agent bumped it to 4g (&lt;code&gt;-Xmx4g -Xms4g&lt;/code&gt;, container limits 4g/8Gi), redeployed, and the jobs succeeded. The agent understood enough about JVM memory pressure to recognize the root cause without being told.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bug 5 — The chunk-deletion disaster&lt;/strong&gt;: This is the one where the agent almost nuked the project. While cleaning up orphaned model registrations, it deleted 14 documents from the ML system index — thinking they were duplicates. They weren't. They were the embedding model's chunks (&lt;code&gt;_0&lt;/code&gt; through &lt;code&gt;_13&lt;/code&gt;). The model went to &lt;code&gt;DEPLOY_FAILED&lt;/code&gt;. The embedding pipeline was dead.&lt;/p&gt;

&lt;p&gt;Most agents would either not notice, or notice and keep retrying the same broken state until you killed the session. The loop did something else: it &lt;em&gt;recognized&lt;/em&gt; the mistake. It saw &lt;code&gt;DEPLOY_FAILED&lt;/code&gt; on the model, understood that the chunks it deleted were critical, deleted the broken base model document, let the &lt;code&gt;embedding-job&lt;/code&gt; re-register fresh with a new model ID, and re-ran the pipeline and seed jobs. The stack healed itself.&lt;/p&gt;

&lt;p&gt;That pattern — deploy, hit error, read logs, diagnose, patch, redeploy, verify — repeated five times. The agent operated the stack as well as it built it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The moment it worked
&lt;/h2&gt;

&lt;p&gt;After the last fix, the cluster settled. Thirty articles seeded. Both models deployed. Cluster health green.&lt;/p&gt;

&lt;p&gt;A hybrid search returned ranked results with fused neural and full-text scores. A RAG query against the pipeline — retrieves context first, then calls Ollama through the connector, returns the answer grounded in the retrieved documents:&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"query"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"match"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"body"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"inverted index TF-IDF BM25"&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="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ext"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"generative_qa_parameters"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"llm_model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"qwen3.5:9b-mlx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"llm_question"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Based on the context, explain what an inverted index is and how TF-IDF or BM25 ranking works."&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="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The system answered:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;An inverted index is a structure that maps each unique word to the documents containing it, allowing for fast retrieval based on query terms. Regarding ranking, TF-IDF measures term frequency versus inverse document frequency, while BM25 improves upon this method by saturating term frequency.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Grounded. Accurate. Sourced from the 30 articles the agent itself had seeded. All four MCP tools exercised and verified over SSE. The agent updated the draft PR with the validation results.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd do differently (and what I won't)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Loop-police is not optional.&lt;/strong&gt; Without it, the agent would have retried that failing deploy until the heat death of the universe. With it, the agent flagged the &lt;code&gt;DEPLOY_FAILED&lt;/code&gt; state transition, recognized it had caused the failure, and pivoted to recovery. If you're running autonomous agents, you need a stall detector. Period.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The agreement gate is the whole point.&lt;/strong&gt; Autonomous coding agents without a plan-agreement phase are just hyperactive interns with commit access. The loop doesn't move from proposal to implementation until you say yes. That one rule eliminates the most common failure mode of AI coding: the agent building the wrong thing while you watch helplessly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;State files make long-running sessions possible.&lt;/strong&gt; &lt;code&gt;.loop-state.json&lt;/code&gt; tracks phase, branch, PR number, and plan history. If the session drops — and with tools running 40+ minute autonomous sequences, it will — the loop resumes without context loss. This is infrastructure, not a nice-to-have.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Retry-loop init jobs are self-healing infrastructure.&lt;/strong&gt; Each job waits for its dependencies through &lt;code&gt;until&lt;/code&gt; loops. You can tear down a job and recreate it — it'll wait, detect its prerequisites are met, and proceed. The loop wrote infrastructure that works the way the loop itself works. That's either poetic or recursive. Probably both.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The draft gate is the QA step you'll never do yourself.&lt;/strong&gt; The loop ran &lt;code&gt;helm lint&lt;/code&gt;, &lt;code&gt;helm template&lt;/code&gt;, &lt;code&gt;skaffold render&lt;/code&gt;, and &lt;code&gt;sh -n&lt;/code&gt; before opening a PR. No human on my team does that for every commit. The agent does it because the workflow demands it. Fail the gate, and the loop doesn't proceed to PR. This catches broken manifests before they reach the repo — before you even see them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The loop isn't magic. It's process.
&lt;/h2&gt;

&lt;p&gt;That's the real takeaway. Autonomous coding agents don't fail because they're not smart enough. They fail because they lack structure — no plan phase, no review gate, no self-check, no stall detection.&lt;/p&gt;

&lt;p&gt;The loop adds that structure. It turns an agent from a code generator into a collaborator: you get a plan, you approve it, the agent builds, it self-corrects, and it ships a draft PR. You review, you merge, you move on.&lt;/p&gt;

&lt;p&gt;I didn't write a single line of code. But the loop didn't build my RAG stack by accident. It built it because the process forced it to plan, verify, and recover — the same way a good engineer would.&lt;/p&gt;

&lt;p&gt;That's how the loop worked.&lt;/p&gt;

</description>
      <category>opensearch</category>
      <category>rag</category>
      <category>ai</category>
      <category>kubernetes</category>
    </item>
    <item>
      <title>Building a 100% Local RAG System on Kubernetes — No API Keys Required</title>
      <dc:creator>Ahmed Nafies</dc:creator>
      <pubDate>Thu, 30 Jul 2026 23:00:55 +0000</pubDate>
      <link>https://dev.to/ahmed_nafies_3a55c907115c/building-a-100-local-rag-system-on-kubernetes-no-api-keys-required-4gg3</link>
      <guid>https://dev.to/ahmed_nafies_3a55c907115c/building-a-100-local-rag-system-on-kubernetes-no-api-keys-required-4gg3</guid>
      <description>&lt;p&gt;Here's what we're building:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcr0h0j0gtptnpmbs5ald.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcr0h0j0gtptnpmbs5ald.png" alt="RAG UI Screenshot"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The itch I couldn't scratch
&lt;/h2&gt;

&lt;p&gt;Last week I sat down to build a RAG system. You know, one of those "chat with your documents" things everyone's building these days.&lt;/p&gt;

&lt;p&gt;I opened my first tutorial. It said: &lt;em&gt;"Step 1: Sign up for OpenAI and grab your API key."&lt;/em&gt; Fine. &lt;em&gt;"Step 2: Create a Pinecone account."&lt;/em&gt; Okay. &lt;em&gt;"Step 3: Deploy to a managed vector database."&lt;/em&gt; Hmm. &lt;em&gt;"Step 4: Set up your billing — here's a pricing calculator."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I stared at the calculator. Then at my $0 budget. Then back at the calculator.&lt;/p&gt;

&lt;p&gt;Something felt off. Why does a "personal project" need a procurement department? Why does my data need to leave my laptop to answer a question about a document sitting &lt;em&gt;on my laptop&lt;/em&gt;? And honestly — I just wanted to tinker. Not negotiate with three SaaS vendors before I could write a single line of code.&lt;/p&gt;

&lt;p&gt;So I did what any reasonable engineer would do. I closed all twenty tabs, opened a terminal, and decided to build the whole thing from scratch. On my machine. With models running on my CPU. No cloud. No API keys. No credit card.&lt;/p&gt;

&lt;p&gt;Here's how that ride went.&lt;/p&gt;




&lt;h2&gt;
  
  
  The game plan
&lt;/h2&gt;

&lt;p&gt;Before writing a line of code, I sketched out what "no cloud" actually means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Embeddings&lt;/strong&gt; — gotta run locally. No calling OpenAI's embedding endpoint.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vector search&lt;/strong&gt; — needs to live inside something I already run. I'm not spinning up a separate vector database service.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The LLM&lt;/strong&gt; — must fit in my laptop's RAM and run on CPU. I don't have a GPU lying around.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deployment&lt;/strong&gt; — real infrastructure patterns, not &lt;code&gt;docker-compose up&lt;/code&gt; and pray. Something that would make sense in production, just scaled down.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What emerged was this stack:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌────────────────────────────────────────────────────┐
│                 FastAPI Application                 │
├────────────────────────────────────────────────────┤
│  ┌──────────────┐  ┌──────────┐  ┌──────────────┐  │
│  │  Embedding   │  │ pgvector │  │  llama-cpp   │  │
│  │  Pipeline    │  │  Search  │  │  (Qwen 1.5B) │  │
│  └──────────────┘  └──────────┘  └──────────────┘  │
└────────────────────────────────────────────────────┘
                         │
                         ▼
               ┌──────────────────┐
               │   PostgreSQL     │
               │   + pgvector     │
               │   + HNSW index   │
               └──────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The flow: someone asks a question → it gets turned into a 384-dimensional vector → PostgreSQL hunts for similar documents using cosine similarity → those results get stuffed into a prompt → the LLM answers → answer streams back. All inside Kubernetes. All local.&lt;/p&gt;

&lt;p&gt;Every piece of this puzzle was chosen with one rule: &lt;strong&gt;it runs on my machine, or it doesn't make the cut.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Choosing the players
&lt;/h2&gt;

&lt;h3&gt;
  
  
  pgvector: the database you already have
&lt;/h3&gt;

&lt;p&gt;I wanted vector search without installing Yet Another Database. pgvector is a PostgreSQL extension that adds vector columns, indexing, and similarity operators directly into Postgres. Just &lt;code&gt;CREATE EXTENSION IF NOT EXISTS vector&lt;/code&gt; and you're in business.&lt;/p&gt;

&lt;p&gt;It gives you HNSW indexes (fast approximate nearest neighbor search), a &lt;code&gt;&amp;lt;=&amp;gt;&lt;/code&gt; cosine distance operator, and zero additional infrastructure. Your vector DB is just... your regular DB.&lt;/p&gt;

&lt;h3&gt;
  
  
  Qwen 2.5, living in my CPU
&lt;/h3&gt;

&lt;p&gt;Here's where things got spicy. Most RAG tutorials assume you'll call GPT-4. But I had no GPU, no cloud credits, and a strong desire to keep everything local.&lt;/p&gt;

&lt;p&gt;Enter &lt;strong&gt;Qwen2.5-1.5B-Instruct&lt;/strong&gt; — a 1.5 billion parameter model that actually runs on CPU. Not fast, but &lt;em&gt;fast enough&lt;/em&gt;. And the output quality? Genuinely surprising for its size.&lt;/p&gt;

&lt;p&gt;The trick is serving it via &lt;strong&gt;llama-cpp-python&lt;/strong&gt;, which exposes an OpenAI-compatible API at &lt;code&gt;http://127.0.0.1:8001/v1&lt;/code&gt;. The magic of this: my application code uses the exact same &lt;code&gt;OpenAIChatModel&lt;/code&gt; class you'd point at &lt;code&gt;api.openai.com&lt;/code&gt;. It doesn't know — and doesn't care — that the model is running on the same machine, on a CPU, inside a container.&lt;/p&gt;

&lt;p&gt;All it sees is a &lt;code&gt;/v1/chat/completions&lt;/code&gt; endpoint. The fact that it's a 1.5B model crammed into a Docker container? That's between me and the container.&lt;/p&gt;

&lt;h3&gt;
  
  
  sentence-transformers: tiny but mighty
&lt;/h3&gt;

&lt;p&gt;For embeddings, I picked &lt;strong&gt;all-MiniLM-L6-v2&lt;/strong&gt;. It produces 384-dimensional vectors, runs fast on CPU, and the quality is solid for semantic search. It gets loaded once at startup and cached — no reloading per request:&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;sentence_transformers&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SentenceTransformer&lt;/span&gt;

&lt;span class="nd"&gt;@lru_cache&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_model&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;SentenceTransformer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;SentenceTransformer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;all-MiniLM-L6-v2&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;embed_text&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="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;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;float&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;get_model&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;encode&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;tolist&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Kubernetes on a laptop? Absolutely.
&lt;/h3&gt;

&lt;p&gt;Running K8s locally sounds excessive until you try it. I used &lt;a href="https://github.com/abiosoft/colima" rel="noopener noreferrer"&gt;Colima&lt;/a&gt; — a lightweight k3s cluster that runs on 2 CPUs and 16GB of RAM. No Docker Desktop, no resource vampires. Just a clean K8s cluster on my Mac.&lt;/p&gt;

&lt;p&gt;The benefit is real: Helm charts for declarative infra, Skaffold for the build-deploy loop, Helm hook Jobs that run migrations and seeding automatically. The same patterns I'd use in production, just at my desk.&lt;/p&gt;




&lt;h2&gt;
  
  
  Under the hood
&lt;/h2&gt;

&lt;p&gt;Let's walk through the interesting bits. Not every line — just the parts that made me smile when they worked.&lt;/p&gt;

&lt;h3&gt;
  
  
  The database that thinks in vectors
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;documents&lt;/code&gt; table stores each document alongside its 384-dimensional embedding vector. The HNSW index means similarity searches stay fast even as the corpus grows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;EXTENSION&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt;          &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;gen_random_uuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;title&lt;/span&gt;       &lt;span class="nb"&gt;varchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;content&lt;/span&gt;     &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;embedding&lt;/span&gt;   &lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;384&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;created_at&lt;/span&gt;  &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt;
    &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;hnsw&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="n"&gt;vector_cosine_ops&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ef_construction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The search that feels like magic
&lt;/h3&gt;

&lt;p&gt;A stored procedure does the heavy lifting. You throw a vector at it, and it returns the closest matches ranked by similarity:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;FUNCTION&lt;/span&gt; &lt;span class="n"&gt;match_documents&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;query_embedding&lt;/span&gt; &lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;384&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;match_threshold&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;match_count&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;RETURNS&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;similarity&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;LANGUAGE&lt;/span&gt; &lt;span class="k"&gt;sql&lt;/span&gt; &lt;span class="k"&gt;STABLE&lt;/span&gt;
&lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt;
        &lt;span class="n"&gt;documents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;documents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;documents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;documents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;query_embedding&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;similarity&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;query_embedding&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;match_threshold&lt;/span&gt;
    &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;query_embedding&lt;/span&gt;
    &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="n"&gt;match_count&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="err"&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 &lt;code&gt;&amp;lt;=&amp;gt;&lt;/code&gt; is pgvector's cosine distance operator. &lt;code&gt;1 - distance&lt;/code&gt; gives us similarity. Clean, fast, entirely in SQL.&lt;/p&gt;

&lt;p&gt;The application calls it through a parameterized SQLAlchemy query — no ORM magic, just raw SQL with safe parameters:&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;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;match_documents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AsyncSession&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;query_embedding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;match_threshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;match_count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&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="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
        SELECT * FROM match_documents(
            CAST(:query_embedding AS vector(384)),
            :match_threshold,
            :match_count
        )
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&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="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;query_embedding&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_str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;match_threshold&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;match_threshold&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;match_count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;match_count&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="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&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;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&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;title&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&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="n"&gt;r&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;similarity&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;similarity&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;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mappings&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The agent that thinks it's talking to OpenAI
&lt;/h3&gt;

&lt;p&gt;This is my favorite part. Pydantic AI's &lt;code&gt;Agent&lt;/code&gt; class is designed for OpenAI, but llama-cpp-python speaks the same protocol. So we just... point it at localhost:&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;pydantic_ai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Agent&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pydantic_ai.models.openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OpenAIChatModel&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pydantic_ai.providers.openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OpenAIProvider&lt;/span&gt;

&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAIChatModel&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-1.5b-instruct&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;OpenAIProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://127.0.0.1:8001/v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;not-needed&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="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Agent&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;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;system_prompt&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;You are a helpful assistant. Answer questions based on &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;the provided context. If the context does not contain &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;enough information, say so.&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;The &lt;code&gt;api_key="not-needed"&lt;/code&gt; line makes me happy every time I look at it. Take that, billing page.&lt;/p&gt;

&lt;h3&gt;
  
  
  Streaming: the one that nearly broke me
&lt;/h3&gt;

&lt;p&gt;Server-Sent Events streaming should be straightforward. You yield chunks, the browser displays them. Simple.&lt;/p&gt;

&lt;p&gt;Except Pydantic AI's &lt;code&gt;stream_text()&lt;/code&gt; doesn't return deltas — it returns the &lt;strong&gt;accumulated&lt;/strong&gt; text. Every chunk is the full response so far. If you send that to the browser, it keeps re-rendering the entire message.&lt;/p&gt;

&lt;p&gt;The fix is deceptively simple. Track the previous value and compute the difference:&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;async&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="k"&gt;yield&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;event: sources&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;data: &lt;/span&gt;&lt;span class="si"&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;sources&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&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="n"&gt;prev&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run_stream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&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;result&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stream_text&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="n"&gt;delta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&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;prev&lt;/span&gt;&lt;span class="p"&gt;):]&lt;/span&gt;  &lt;span class="c1"&gt;# Just the new stuff
&lt;/span&gt;            &lt;span class="n"&gt;prev&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt;
            &lt;span class="k"&gt;yield&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;event: delta&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;data: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="si"&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="si"&gt;:&lt;/span&gt; &lt;span class="n"&gt;delta&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&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="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;StreamingResponse&lt;/span&gt;&lt;span class="p"&gt;(&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;media_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text/event-stream&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;Two lines of code that took two hours to figure out. But watching the text flow in, word by word, on the first successful stream — chef's kiss.&lt;/p&gt;




&lt;h2&gt;
  
  
  Ship it: Kubernetes all the way down
&lt;/h2&gt;

&lt;p&gt;Three Helm charts, one &lt;code&gt;skaffold run&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Chart&lt;/th&gt;
&lt;th&gt;What's inside&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;rag-supabase-db&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;PostgreSQL StatefulSet + pgvector, 5Gi persistent disk&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;rag-supabase&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;FastAPI + llama-cpp running side-by-side, 3Gi model cache&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;rag-supabase-ui&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Nginx serving the chat UI&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The slick part: migrations and seeding aren't manual steps. They're Helm hook Jobs that fire automatically on every deploy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;batch/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Job&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;.Release.Name&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt;&lt;span class="s"&gt;-migrate-{{ .Release.Revision }}&lt;/span&gt;
  &lt;span class="na"&gt;annotations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;helm.sh/hook"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;post-install,post-upgrade&lt;/span&gt;
    &lt;span class="s"&gt;"helm.sh/hook-weight"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0"&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;helm.sh/hook-delete-policy"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;before-hook-creation&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;restartPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;OnFailure&lt;/span&gt;
      &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;migrate&lt;/span&gt;
          &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;.Values.image.repository&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}:{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;.Values.image.tag&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
          &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;uv"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;run"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;alembic"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;upgrade"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;head"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The migrate Job fires first (&lt;code&gt;hook-weight: "0"&lt;/code&gt;), then the seed Job (&lt;code&gt;hook-weight: "10"&lt;/code&gt;). The seed Job is smart — it checks if the documents table already has data and skips gracefully. No more &lt;em&gt;"did I already run the seeder?"&lt;/em&gt; anxiety.&lt;/p&gt;

&lt;p&gt;And the LLM model? It downloads a ~1GB GGUF file on first startup and caches it in a PVC at &lt;code&gt;/models&lt;/code&gt;. First deploy takes a few minutes while it downloads. Every deploy after that? Instant. The model survives pod restarts, rebuilds, everything.&lt;/p&gt;




&lt;h2&gt;
  
  
  War stories from the trenches
&lt;/h2&gt;

&lt;p&gt;No project worth building comes without battle scars. Here are the ones that left marks.&lt;/p&gt;

&lt;h3&gt;
  
  
  The ghost in the database
&lt;/h3&gt;

&lt;p&gt;After deploying, everything worked perfectly. Come back the next morning, and the app is dead. PostgreSQL connections had silently died during idle periods.&lt;/p&gt;

&lt;p&gt;The fix: &lt;code&gt;pool_pre_ping=True&lt;/code&gt; and &lt;code&gt;pool_recycle=300&lt;/code&gt; on the SQLAlchemy engine. It pings connections before using them and recycles them every 5 minutes. Simple fix, but diagnosing it involved a lot of confused staring at error logs.&lt;/p&gt;

&lt;h3&gt;
  
  
  The streaming trap
&lt;/h3&gt;

&lt;p&gt;I already mentioned this, but it deserves its own section. I spent two hours debugging why my streams were "working" but the browser kept re-rendering the entire message every chunk. The culprit: &lt;code&gt;stream_text()&lt;/code&gt; returns cumulative text, not incremental deltas. The &lt;code&gt;chunk[len(prev):]&lt;/code&gt; trick is now burned into my brain forever.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dependency hell, one version at a time
&lt;/h3&gt;

&lt;p&gt;Pydantic AI 0.8.1 depends on &lt;code&gt;opentelemetry._events&lt;/code&gt; — a module that OpenTelemetry removed in version 1.44. The fix? Pin &lt;code&gt;opentelemetry-api&amp;gt;=1.42,&amp;lt;1.43&lt;/code&gt; and move on with your life. Version pinning: not glamorous, but it keeps the lights on.&lt;/p&gt;

&lt;h3&gt;
  
  
  Skaffold, Helm hooks, and the great image tag mystery
&lt;/h3&gt;

&lt;p&gt;Skaffold is amazing at building images and deploying Helm charts. But Helm hook Jobs? They reference images through &lt;code&gt;.Values.image.tag&lt;/code&gt;, and Skaffold doesn't inject its build tags into hook templates.&lt;/p&gt;

&lt;p&gt;The workaround: always tag your image as &lt;code&gt;latest&lt;/code&gt; and match it in &lt;code&gt;values.yaml&lt;/code&gt;. Not elegant, but it works. I'll fix it properly someday. (I won't.)&lt;/p&gt;

&lt;h3&gt;
  
  
  The first deploy is always the slowest
&lt;/h3&gt;

&lt;p&gt;That first &lt;code&gt;skaffold run&lt;/code&gt; takes longer because the container is downloading a ~1GB GGUF model. But that's the beauty of Persistent Volumes — the model sticks around. Every subsequent deploy is fast, because the model is already sitting in &lt;code&gt;/models&lt;/code&gt;, warm and waiting.&lt;/p&gt;




&lt;h2&gt;
  
  
  So... does it work?
&lt;/h2&gt;

&lt;p&gt;Yes. Beautifully.&lt;/p&gt;

&lt;p&gt;Type a question. Watch the embedding get computed locally. See PostgreSQL find the relevant documents in milliseconds. Read the LLM's response as it streams in, character by character. Check the source citations to see which documents informed the answer.&lt;/p&gt;

&lt;p&gt;And here's the thing that still gets me: &lt;strong&gt;all of it is happening on my laptop.&lt;/strong&gt; No packets leaving my network. No tokens being counted by a billing system somewhere. No vendor lock-in. Just my code, my models, my data.&lt;/p&gt;

&lt;p&gt;The pipeline handles everything end-to-end:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Natural language questions → local embedding&lt;/li&gt;
&lt;li&gt;Semantic search → PostgreSQL with pgvector and HNSW&lt;/li&gt;
&lt;li&gt;Retrieved context + question → local LLM via llama-cpp-python&lt;/li&gt;
&lt;li&gt;Streaming response → back to the browser via SSE&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One &lt;code&gt;skaffold run&lt;/code&gt; deploys the entire stack. One &lt;code&gt;kubectl port-forward&lt;/code&gt; lets you chat with it.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I learned (and what's next)
&lt;/h2&gt;

&lt;p&gt;This project changed how I think about AI infrastructure. We've gotten so used to reaching for managed services that we forget how much is possible on a single machine. A 1.5B parameter model isn't going to write your novel, but for RAG? For answering questions grounded in your own documents? It's more than enough.&lt;/p&gt;

&lt;p&gt;The whole stack — PostgreSQL, pgvector, FastAPI, sentence-transformers, llama-cpp-python, Kubernetes — fits comfortably in 16GB of RAM. No GPU. No cloud. No API keys taped to the bottom of your keyboard.&lt;/p&gt;

&lt;p&gt;Could it be better? Always. A better model, hybrid search (semantic + keyword), persistent conversation storage, zero-downtime deployments. That's for another weekend.&lt;/p&gt;

&lt;p&gt;For now, I'm just enjoying the quiet satisfaction of asking my local LLM a question and watching it answer — powered by nothing but my CPU and a few hundred lines of code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repository&lt;/strong&gt;: &lt;a href="https://github.com/nf1s/rag-supabase" rel="noopener noreferrer"&gt;github.com/nf1s/rag-supabase&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you build something similar, tag me on Dev.to. I'd love to see what you cook up without a credit card. And if you run into the same streaming bug — &lt;code&gt;chunk[len(prev):]&lt;/code&gt;. You're welcome.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>rag</category>
      <category>llm</category>
      <category>postgres</category>
    </item>
  </channel>
</rss>
