<?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: Anup Sharma</title>
    <description>The latest articles on DEV Community by Anup Sharma (@anup_sharma_86fa94612fe3c).</description>
    <link>https://dev.to/anup_sharma_86fa94612fe3c</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%2F3947205%2F0f0dcd14-4a48-4d26-b9e2-7a9102608e05.png</url>
      <title>DEV Community: Anup Sharma</title>
      <link>https://dev.to/anup_sharma_86fa94612fe3c</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/anup_sharma_86fa94612fe3c"/>
    <language>en</language>
    <item>
      <title>What Word Break Leetcode Problem Taught Me About Debugging Order</title>
      <dc:creator>Anup Sharma</dc:creator>
      <pubDate>Mon, 06 Jul 2026 12:35:36 +0000</pubDate>
      <link>https://dev.to/anup_sharma_86fa94612fe3c/what-word-break-leetcode-problem-taught-me-about-debugging-order-2n19</link>
      <guid>https://dev.to/anup_sharma_86fa94612fe3c/what-word-break-leetcode-problem-taught-me-about-debugging-order-2n19</guid>
      <description>&lt;p&gt;I recently worked through the classic &lt;strong&gt;Word Break&lt;/strong&gt; problem in an interview. My approach was solid from the start — recursion with memoization, a &lt;code&gt;breakable&lt;/code&gt; helper that tests every prefix and recurses on the rest. The logic was right. What slowed me down was everything &lt;em&gt;around&lt;/em&gt; the logic.&lt;/p&gt;

&lt;p&gt;Here's the solution I landed on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Solution&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="nl"&gt;public:&lt;/span&gt;
    &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;wordBreak&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;wordDict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;unordered_set&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wordDict&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;begin&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;wordDict&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
        &lt;span class="n"&gt;unordered_map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;size_t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;memo&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;breakable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;memo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// missed: breakable was a free function defined below -&amp;gt; "not declared in this scope"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;private&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;// missed: had int here, compared against s.length() (size_t) -&amp;gt; sign-compare warnings&lt;/span&gt;
    &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;breakable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;unordered_set&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                   &lt;span class="n"&gt;unordered_map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;size_t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;memo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;starting&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;starting&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;memo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;starting&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;memo&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;starting&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;starting&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&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;string&lt;/span&gt; &lt;span class="n"&gt;word&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;substr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;starting&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;starting&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// missed: shadowed an outer `word`, and had substr(starting, e) instead of e - starting&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;word&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;breakable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;memo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;memo&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;starting&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// missed: wrote == instead of =, so success was never cached&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;true&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;memo&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;starting&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// missed: this line, so failures were never cached and memoization broke down&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;false&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The real lesson
&lt;/h2&gt;

&lt;p&gt;Most of what tripped me up was &lt;strong&gt;syntax and scope&lt;/strong&gt; — a function declared in the wrong place, signed/unsigned mismatches, a shadowed variable, &lt;code&gt;==&lt;/code&gt; where I meant &lt;code&gt;=&lt;/code&gt;. None of these were about the algorithm. But because I spent my time chasing them, I had less room to focus on the one thing that actually matters in this problem: the &lt;strong&gt;logical&lt;/strong&gt; correctness of the memoization.&lt;/p&gt;

&lt;p&gt;The takeaway I'm keeping: get the syntax and scoping clean early, so the debugging budget goes toward logic, not typos. That's the difference between a working solution and a working solution you can reason about under pressure.&lt;/p&gt;

</description>
      <category>beginners</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>Adding Observability to My AI Homelab</title>
      <dc:creator>Anup Sharma</dc:creator>
      <pubDate>Fri, 03 Jul 2026 16:46:42 +0000</pubDate>
      <link>https://dev.to/anup_sharma_86fa94612fe3c/adding-observability-to-my-ai-homelab-3dbj</link>
      <guid>https://dev.to/anup_sharma_86fa94612fe3c/adding-observability-to-my-ai-homelab-3dbj</guid>
      <description>&lt;p&gt;&lt;em&gt;Part 4 of the Homelab AI Series — &lt;a href="https://dev.to/anup_sharma_86fa94612fe3c/i-built-an-ai-that-decides-which-ai-to-talk-to-running-247-from-my-living-room-211p"&gt;Part 1&lt;/a&gt; | &lt;a href="https://dev.to/anup_sharma_86fa94612fe3c/i-traced-personal-agents-source-code-inside-was-pi-and-it-dreams-at-3-am-o0f"&gt;Part 2&lt;/a&gt; | &lt;a href="https://dev.to/anup_sharma_86fa94612fe3c/giving-agentgateway-a-semantic-brain-with-vllm-semantic-router-inside-my-homelab-542f"&gt;Part 3&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Let me set the scene.&lt;/p&gt;

&lt;p&gt;My personal AI agent — is running its nightly cron jobs. Calendar summaries. Email digests. Task prioritization. It's been doing this silently for three weeks since I integrated the vLLM Semantic Router in Part 3.&lt;/p&gt;

&lt;p&gt;And I have absolutely no idea if it's working.&lt;/p&gt;

&lt;p&gt;Not because it's broken. Because I have &lt;em&gt;no visibility into it at all.&lt;/em&gt; The Mac Mini sits in my living room, green light blinking quietly, processing requests — and I have zero idea whether the routing is actually working, whether my API bills are exploding, or whether the local Ollama model is grinding through prompts that should have gone to Gemini.&lt;/p&gt;

&lt;p&gt;I was flying completely blind.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Plan That Never Happened
&lt;/h2&gt;

&lt;p&gt;After Part 3, my original observability roadmap was ambitious. I was going to deploy the full "Big Tech" monitoring stack:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Prometheus&lt;/strong&gt; to scrape AgentGateway's &lt;code&gt;/metrics&lt;/code&gt; endpoint&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Jaeger&lt;/strong&gt; for distributed tracing via OpenTelemetry&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grafana&lt;/strong&gt; with custom dashboards for token costs and latency&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Loki&lt;/strong&gt; for log aggregation, because why not go full enterprise&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'd even started writing the &lt;code&gt;docker-compose.yaml&lt;/code&gt;. Four services, two config volumes, a shared network — and I hadn't even gotten to the Grafana provisioning scripts yet.&lt;/p&gt;

&lt;p&gt;Then during weekly agentgateway community meeting Lin and John announced new UI in &lt;a href="https://agentgateway1-3-release-blog.agentproxy.pages.dev/blog/2026-06-17-agentgateway-v1.3.0/" rel="noopener noreferrer"&gt;v1.3.0&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I quickly ran &lt;code&gt;git pull&lt;/code&gt; on the AgentGateway repo.&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="nv"&gt;$ &lt;/span&gt;git pull origin main
...
 crates/agentgateway/src/ui.rs    | 423 ++++++++++++++++++++++++
 ui/src/pages/Analytics.tsx       | 311 ++++++++++++++++
 ui/src/pages/Logs.tsx            | 287 +++++++++++++++
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The team had just shipped a brand new built-in UI — complete with an Analytics dashboard, a live Logs Explorer, and a Cost Breakdown view. Everything I was about to spend my weekend building was already there. Native. In the binary. On port &lt;code&gt;15000&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I closed the &lt;code&gt;docker-compose.yaml&lt;/code&gt;. I was never going to open it again.&lt;/p&gt;




&lt;h2&gt;
  
  
  Three Lines of YAML. That's It.
&lt;/h2&gt;

&lt;p&gt;The built-in UI was already serving at &lt;code&gt;http://localhost:15000/ui&lt;/code&gt;. But when I navigated there, the Logs and Analytics pages showed nothing. Just empty charts and a message:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Logs API error — request log database is not configured&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Right. The UI needed somewhere to write request logs. This is where I expected to set up a Postgres instance or at minimum a Docker container for SQLite.&lt;/p&gt;

&lt;p&gt;Instead, I added this to my &lt;code&gt;homelab_config.yaml&lt;/code&gt;:&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;config&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;modelCatalog&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;base-costs.json&lt;/span&gt;
  &lt;span class="na"&gt;database&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sqlite://agentgateway.db&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it.&lt;/p&gt;

&lt;p&gt;One important gotcha I hit: &lt;strong&gt;the &lt;code&gt;database:&lt;/code&gt; key must be nested inside the &lt;code&gt;config:&lt;/code&gt; section&lt;/strong&gt;. I originally tried adding it at the top level of the YAML and got an "unknown field" validation error. The config parser is strict. Nest it correctly and it just works.&lt;/p&gt;

&lt;p&gt;Restarted AgentGateway. Sent a few test requests. Refreshed the dashboard.&lt;/p&gt;

&lt;p&gt;The charts lit up.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's Actually Inside the Dashboard
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Analytics View
&lt;/h3&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%2Fe9k4nlrd6fbbrwtljnqv.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%2Fe9k4nlrd6fbbrwtljnqv.png" alt="AgentGateway Analytics dashboard showing Traffic over time and token Breakdown — 60 calls, 13,929 tokens, $0.0340 in the last 24 hours" width="799" height="354"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The Analytics page groups every request by &lt;code&gt;provider&lt;/code&gt; and &lt;code&gt;model&lt;/code&gt;. In my setup, I have three possible destinations for every request Pi sends:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;qwen2.5-coder:7b&lt;/code&gt; via Ollama&lt;/strong&gt; — local, free, slower&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;gpt-4o&lt;/code&gt; via OpenAI&lt;/strong&gt; — expensive, fast, best reasoning&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;gemini-2.5-flash&lt;/code&gt; via Google&lt;/strong&gt; — cheap cloud, fast, great context window&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;AgentGateway knows which model handled each request because the vLLM Semantic Router adds an &lt;code&gt;x-selected-model&lt;/code&gt; header before forwarding. So the UI doesn't just show me "a request happened" — it shows me which model got it, how many tokens it consumed, and the estimated dollar cost using the built-in model pricing catalog.&lt;/p&gt;

&lt;p&gt;In the 24-hour snapshot above: &lt;strong&gt;60 calls, 13,929 tokens, $0.0340 total.&lt;/strong&gt; That's the entire cost of running Pi's overnight jobs. Fractions of a cent per interaction.&lt;/p&gt;

&lt;p&gt;And I can see the routing is working — the traffic spike on the right corresponds to Pi's 3 AM cron batch. The model breakdown lets me verify that coding tasks are actually hitting the local Ollama and not burning cloud API credits.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Logs Explorer
&lt;/h3&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%2F037dnjopy2a77x9fpgbw.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%2F037dnjopy2a77x9fpgbw.png" alt="AgentGateway Logs page showing individual LLM requests with model, provider, HTTP status, latency, tokens, and cost per request" width="799" height="354"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is the view that genuinely surprised me.&lt;/p&gt;

&lt;p&gt;Every single LLM call shows up as a row with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;HTTP Status&lt;/strong&gt; — &lt;code&gt;200&lt;/code&gt;, &lt;code&gt;400&lt;/code&gt;, &lt;code&gt;404&lt;/code&gt; — the bad ones are impossible to miss&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Duration&lt;/strong&gt; — total time from request received to response delivered&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Model&lt;/strong&gt; — the &lt;em&gt;actual&lt;/em&gt; model called, not my &lt;code&gt;MoM&lt;/code&gt; alias&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Provider&lt;/strong&gt; — &lt;code&gt;gcp.gemini&lt;/code&gt;, &lt;code&gt;openai&lt;/code&gt;, &lt;code&gt;openai&lt;/code&gt; (for Ollama, since it speaks the OpenAI API)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Token counts&lt;/strong&gt; — input and output separately&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Estimated cost&lt;/strong&gt; — per-request dollar amount against the model price catalog&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Look at the screenshot above. You can see real requests: &lt;code&gt;gemini-2.5-flash&lt;/code&gt; calls at a few tenths of a cent each, &lt;code&gt;qwen2.5-coder:7b&lt;/code&gt; calls with zero cost, and a handful of &lt;code&gt;404&lt;/code&gt;s for &lt;code&gt;non-existent-model&lt;/code&gt; at the top — those are the simulated error requests from my traffic test, showing up exactly as expected.&lt;/p&gt;

&lt;p&gt;I can click into any row and see the full request detail — the exact prompt Pi sent and the exact response it got back. When Pi's 3 AM calendar job sends something weird, I can see the raw JSON. That was never possible before.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Full Config
&lt;/h2&gt;

&lt;p&gt;For anyone setting this up, here's the complete &lt;code&gt;homelab_config.yaml&lt;/code&gt; that runs my entire homelab AI stack:&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="c1"&gt;# yaml-language-server: $schema=https://agentgateway.dev/schema/config&lt;/span&gt;

&lt;span class="c1"&gt;# Gateway-level policy: Semantic Router as ExtProc sidecar&lt;/span&gt;
&lt;span class="na"&gt;policies&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="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;semantic-router&lt;/span&gt;
    &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;default&lt;/span&gt;
  &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;gateway&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;gatewayName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;default&lt;/span&gt;
      &lt;span class="na"&gt;gatewayNamespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;default&lt;/span&gt;
  &lt;span class="na"&gt;phase&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gateway&lt;/span&gt;
  &lt;span class="na"&gt;policy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;extProc&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;127.0.0.1:50051&lt;/span&gt;
      &lt;span class="na"&gt;processingOptions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;requestBodyMode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;buffered&lt;/span&gt;
        &lt;span class="na"&gt;responseBodyMode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;none&lt;/span&gt;
        &lt;span class="na"&gt;requestHeaderMode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;send&lt;/span&gt;
        &lt;span class="na"&gt;responseHeaderMode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;skip&lt;/span&gt;
        &lt;span class="na"&gt;requestTrailerMode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;skip&lt;/span&gt;
        &lt;span class="na"&gt;responseTrailerMode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;skip&lt;/span&gt;
      &lt;span class="na"&gt;failureMode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;failOpen&lt;/span&gt;   &lt;span class="c1"&gt;# If SR crashes, requests fall through to Gemini&lt;/span&gt;

&lt;span class="c1"&gt;# Routes based on the header the Semantic Router sets&lt;/span&gt;
&lt;span class="na"&gt;binds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3000&lt;/span&gt;
  &lt;span class="na"&gt;listeners&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;routes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

    &lt;span class="c1"&gt;# x-selected-model: qwen-coder → Local Ollama (free)&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;matches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;headers&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;x-selected-model&lt;/span&gt;
          &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;exact&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;qwen-coder&lt;/span&gt;
      &lt;span class="na"&gt;policies&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;ai&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;modelAliases&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;MoM&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;qwen2.5-coder:7b&lt;/span&gt;
            &lt;span class="na"&gt;inteli-llm&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;qwen2.5-coder:7b&lt;/span&gt;
      &lt;span class="na"&gt;backends&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;ai&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;openAI&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;ollama&lt;/span&gt;
          &lt;span class="na"&gt;hostOverride&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;localhost:11434&lt;/span&gt;

    &lt;span class="c1"&gt;# x-selected-model: gpt-4o → OpenAI&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;matches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;headers&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;x-selected-model&lt;/span&gt;
          &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;exact&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gpt-4o&lt;/span&gt;
      &lt;span class="na"&gt;policies&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;ai&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;modelAliases&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;MoM&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gpt-4o&lt;/span&gt;
            &lt;span class="na"&gt;inteli-llm&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gpt-4o&lt;/span&gt;
      &lt;span class="na"&gt;backends&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;ai&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;openAI&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;openai&lt;/span&gt;
        &lt;span class="na"&gt;policies&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;backendAuth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$OPENAI_API_KEY&lt;/span&gt;

    &lt;span class="c1"&gt;# x-selected-model: gemini-flash → Google&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;matches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;headers&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;x-selected-model&lt;/span&gt;
          &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;exact&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gemini-flash&lt;/span&gt;
      &lt;span class="na"&gt;policies&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;ai&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;modelAliases&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;MoM&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gemini-2.5-flash&lt;/span&gt;
            &lt;span class="na"&gt;inteli-llm&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gemini-2.5-flash&lt;/span&gt;
      &lt;span class="na"&gt;backends&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;ai&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;gemini&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;gemini&lt;/span&gt;
        &lt;span class="na"&gt;policies&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;backendAuth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$GEMINI_API_KEY&lt;/span&gt;

    &lt;span class="c1"&gt;# Fallback (SR down or no header matched)&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;backends&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;ai&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;gemini&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;gemini-default&lt;/span&gt;
        &lt;span class="na"&gt;policies&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;ai&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;modelAliases&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;MoM&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gemini-2.5-flash&lt;/span&gt;
              &lt;span class="na"&gt;inteli-llm&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gemini-2.5-flash&lt;/span&gt;
          &lt;span class="na"&gt;backendAuth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$GEMINI_API_KEY&lt;/span&gt;

&lt;span class="c1"&gt;# Direct LLM proxy on port 4000&lt;/span&gt;
&lt;span class="na"&gt;llm&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4000&lt;/span&gt;
  &lt;span class="na"&gt;models&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;openai&lt;/span&gt;
    &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;openai&lt;/span&gt;
  &lt;span class="na"&gt;providers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[]&lt;/span&gt;
  &lt;span class="na"&gt;virtualModels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[]&lt;/span&gt;

&lt;span class="c1"&gt;# Frontend policy&lt;/span&gt;
&lt;span class="na"&gt;frontendPolicies&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;http&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;maxBufferSize&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;33554432&lt;/span&gt;

&lt;span class="c1"&gt;# The three lines that unlocked full observability&lt;/span&gt;
&lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;modelCatalog&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;base-costs.json&lt;/span&gt;
  &lt;span class="na"&gt;database&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sqlite://agentgateway.db&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The separation of concerns is worth calling out again: the Semantic Router never touches API keys. It classifies the prompt, sets a header, and gets out of the way. AgentGateway owns the downstream auth entirely. This is the same design pattern you'd use in a production Kubernetes cluster — routing intelligence decoupled from security posture.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Not Grafana?
&lt;/h2&gt;

&lt;p&gt;I want to address this directly because I know some people will ask.&lt;/p&gt;

&lt;p&gt;If you're running an enterprise Kubernetes cluster with a dedicated platform team, absolutely export AgentGateway's OpenTelemetry data to your centralized Datadog or Prometheus stack. AgentGateway supports this out of the box — it emits OTLP traces and a &lt;code&gt;/metrics&lt;/code&gt; endpoint. The production observability story is excellent.&lt;/p&gt;

&lt;p&gt;But if you're running a homelab?&lt;/p&gt;

&lt;p&gt;The operational burden of Prometheus + Grafana for a single-node AI gateway is enormous relative to what you get. You need to keep two additional services running and healthy, write and maintain Grafana dashboard JSON, configure Prometheus alerting rules, and keep all of it in sync when your schema changes.&lt;/p&gt;

&lt;p&gt;AgentGateway's built-in dashboard gives you every metric I care about — token usage, cost per model, latency distribution, error rates — with zero operational overhead. The SQLite file lives right next to the binary. There's nothing to maintain, nothing to restart, nothing to provision.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do not build an observability stack if you don't have to.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Numbers After One Week of Real Visibility
&lt;/h2&gt;

&lt;p&gt;Having actual data changes how you think about your setup:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Blind (before)&lt;/th&gt;
&lt;th&gt;With Dashboard&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Routing correctness&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;"Probably fine?"&lt;/td&gt;
&lt;td&gt;Verified per-model in Analytics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Monthly API cost estimate&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;"Maybe $20-30?"&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~$12 projected&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Error rate&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Unknown&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;2.3%&lt;/strong&gt; (mostly 3 AM config edge cases)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Avg. Gemini latency&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Unknown&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~340ms&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Avg. Ollama latency&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Unknown&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;~18 seconds&lt;/strong&gt; (7B model on CPU)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Hidden issues found&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;3&lt;/strong&gt; in first week&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That last row is the one that matters. Three real problems I'd had zero visibility into — a calendar cron sending malformed date ranges to Gemini, a tokenization edge case in Pi's summarization prompt, and one silent API key rotation failure. The dashboard didn't just give me numbers. It gave me &lt;em&gt;answers&lt;/em&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Homelab Stack, Complete
&lt;/h2&gt;

&lt;p&gt;Four posts. One Mac Mini in a living room. Here's the full picture:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Pi (Personal Agent)
       │
       ▼ POST /v1/chat/completions  model: "MoM"
       │
┌──────────────────────────────────────────────────────┐
│                AgentGateway (:3000)                   │
│                                                        │
│  ExtProc → vLLM Semantic Router (:50051)              │
│  mmBERT classifies prompt in ~1ms                     │
│  Sets x-selected-model header                         │
│                                                        │
│  Route match on header → forward to backend           │
│                                                        │
│  Built-in UI (:15000/ui)                              │
│  SQLite → Analytics + Logs Explorer                   │
└───────┬─────────────┬─────────────┬──────────────────┘
        ▼             ▼             ▼
   Ollama:11434   OpenAI API   Gemini API
   qwen2.5-coder  gpt-4o       gemini-2.5-flash
   (free, local)  (~$0.03/1k)  (~$0.0015/1k)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The Agent&lt;/strong&gt; — Pi, running cron jobs and personal tasks 24/7 from a Mac Mini in my living room.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Intelligence Layer&lt;/strong&gt; — vLLM Semantic Router, using mmBERT embeddings to classify every prompt and set routing headers in ~1ms.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Data Plane&lt;/strong&gt; — AgentGateway in Rust, owning all API keys, handling auth, matching routes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Control Plane&lt;/strong&gt; — AgentGateway's built-in UI, backed by SQLite, showing real-time token usage, costs, latency, and errors.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The whole stack runs as a single binary (plus the SR container). Zero cloud spend on infrastructure. The Mac Mini was already sitting in my living room.&lt;/p&gt;




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

&lt;p&gt;This feels like a natural pause point. The stack is stable, observable, and honestly more capable than I expected when I started this series.&lt;/p&gt;

&lt;p&gt;A few things I'm actively exploring:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dockerizing the stack&lt;/strong&gt; — a single &lt;code&gt;docker-compose.yaml&lt;/code&gt; to boot Ollama, the SR container, and AgentGateway together so the Mac Mini fully self-heals after a reboot without me touching anything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;More model cards&lt;/strong&gt; — now that routing is semantic, adding a new specialized model is just writing a new description in the SR's &lt;code&gt;config.yaml&lt;/code&gt;. The router figures out the rest.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OTLP export&lt;/strong&gt; — AgentGateway already emits OpenTelemetry spans. I want to wire it to a lightweight alertmanager that notifies me when Pi's error rate spikes past a threshold during its 3 AM runs.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;If you're building agents — homelab or production — the combination of &lt;a href="https://github.com/agentgateway/agentgateway" rel="noopener noreferrer"&gt;AgentGateway&lt;/a&gt; + &lt;a href="https://github.com/vllm-project/semantic-router" rel="noopener noreferrer"&gt;vLLM Semantic Router&lt;/a&gt; + the built-in SQLite observability is, right now, the most complete single-node AI infrastructure stack I know of. No YAML sprawl. No external dependencies for the happy path. Just a config file, a binary, and a Mac Mini with a green light.&lt;/p&gt;

&lt;p&gt;And it runs silently, 24/7, from my living room. 🏠&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Have questions about the setup? Drop them in the comments — I check daily. And if you've built something similar, I'd love to see how you've adapted it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;#ai&lt;/code&gt; &lt;code&gt;#agents&lt;/code&gt; &lt;code&gt;#observability&lt;/code&gt; &lt;code&gt;#homelab&lt;/code&gt; &lt;code&gt;#agentgateway&lt;/code&gt; &lt;code&gt;#vllm&lt;/code&gt; &lt;code&gt;#sqlite&lt;/code&gt; &lt;code&gt;#llm&lt;/code&gt; &lt;code&gt;#opensource&lt;/code&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>performance</category>
      <category>community</category>
    </item>
    <item>
      <title>Giving AgentGateway a Semantic Brain with vLLM Semantic Router - Inside My Homelab</title>
      <dc:creator>Anup Sharma</dc:creator>
      <pubDate>Sat, 20 Jun 2026 03:00:37 +0000</pubDate>
      <link>https://dev.to/anup_sharma_86fa94612fe3c/giving-agentgateway-a-semantic-brain-with-vllm-semantic-router-inside-my-homelab-542f</link>
      <guid>https://dev.to/anup_sharma_86fa94612fe3c/giving-agentgateway-a-semantic-brain-with-vllm-semantic-router-inside-my-homelab-542f</guid>
      <description>&lt;p&gt;&lt;em&gt;Part 3 of the Homelab AI Series — &lt;a href="https://dev.to/anup_sharma_86fa94612fe3c/i-built-an-ai-that-decides-which-ai-to-talk-to-running-247-from-my-living-room-211p"&gt;Part 1&lt;/a&gt; | &lt;a href="https://dev.to/anup_sharma_86fa94612fe3c/i-traced-personal-agents-source-code-inside-was-pi-and-it-dreams-at-3-am-o0f"&gt;Part 2&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem Was Embarrassing
&lt;/h2&gt;

&lt;p&gt;In &lt;a href="https://dev.to/anup_sharma_86fa94612fe3c/i-built-an-ai-that-decides-which-ai-to-talk-to-running-247-from-my-living-room-211p"&gt;Part 1&lt;/a&gt;, I showed how I built a personal AI agent (Pi) that runs 24/7 from my living room, using &lt;a href="https://github.com/agentgateway/agentgateway" rel="noopener noreferrer"&gt;AgentGateway&lt;/a&gt; to route requests across three models: a local Ollama (&lt;code&gt;qwen2.5-coder:7b&lt;/code&gt;) for coding, OpenAI (&lt;code&gt;gpt-4o&lt;/code&gt;) for deep reasoning, and Gemini (&lt;code&gt;gemini-2.5-flash&lt;/code&gt;) for fast general tasks.&lt;/p&gt;

&lt;p&gt;The routing brain? A 100-line Python script sitting between Pi and AgentGateway:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# router.py — The "AI brain" I was embarrassed to deploy
&lt;/span&gt;&lt;span class="n"&gt;coding_keywords&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;code&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;python&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;javascript&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;bash&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;script&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                   &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;function&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bug&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;html&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;css&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;reasoning_keywords&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;think&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;analyze&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;explain in detail&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;reasoning&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;logic&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;deduce&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;prompt_lower&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;coding_keywords&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;intent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;coding&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;elif&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;prompt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;prompt_lower&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;reasoning_keywords&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;intent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reasoning&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;intent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;simple&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Yes. My "intelligent" AI routing was a glorified &lt;code&gt;if-elif-else&lt;/code&gt; chain.&lt;/p&gt;

&lt;p&gt;It worked — until it didn't. "Explain the async/await pattern in Rust" got classified as &lt;code&gt;simple&lt;/code&gt; because none of the keywords matched. "Help me think about dinner options" got classified as &lt;code&gt;reasoning&lt;/code&gt; because &lt;code&gt;think&lt;/code&gt; was in the keyword list. And anything in Hindi or mixed-language prompts? Straight to the fallback, every single time.&lt;/p&gt;

&lt;p&gt;After running this setup daily for two weeks, I collected some rough numbers:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;With Python Router&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Misrouted requests (spot-checked)&lt;/td&gt;
&lt;td&gt;~18%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Monthly estimated API cost&lt;/td&gt;
&lt;td&gt;~$24&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Routing latency (Python proxy hop)&lt;/td&gt;
&lt;td&gt;~45ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Keyword list maintenance&lt;/td&gt;
&lt;td&gt;Manual, weekly tweaks&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Eighteen percent of requests going to the wrong model doesn't just waste money — it gives &lt;em&gt;bad answers&lt;/em&gt;. When my cron-job agent sends a complex "summarize this week's calendar and suggest optimizations" to the 7B local model instead of Gemini or GPT-4o, the output is noticeably worse.&lt;/p&gt;

&lt;p&gt;I needed something that &lt;em&gt;understood&lt;/em&gt; the prompt, not just scanned it for keywords.&lt;/p&gt;




&lt;h2&gt;
  
  
  Enter vLLM Semantic Router
&lt;/h2&gt;

&lt;p&gt;While discussing with Maintainers of AgentGateway &lt;a href="https://github.com/agentgateway/agentgateway" rel="noopener noreferrer"&gt;AgentGateway&lt;/a&gt;, I discovered a first-class integration with &lt;a href="https://github.com/vllm-project/semantic-router" rel="noopener noreferrer"&gt;vLLM Semantic Router&lt;/a&gt; thanks to &lt;a href="https://www.linkedin.com/in/keithmattix/" rel="noopener noreferrer"&gt;Keith Mattix&lt;/a&gt; and &lt;a href="https://www.linkedin.com/in/-johnhoward/" rel="noopener noreferrer"&gt;John Howard&lt;/a&gt;. The architecture clicked immediately:&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.amazonaws.com%2Fuploads%2Farticles%2Fqtfyqnooqrwbv6uokrt1.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.amazonaws.com%2Fuploads%2Farticles%2Fqtfyqnooqrwbv6uokrt1.png" alt=" " width="800" height="344"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Instead of my Python script sitting &lt;em&gt;in front&lt;/em&gt; of AgentGateway as a janky reverse proxy, the Semantic Router runs as an &lt;strong&gt;Envoy ExtProc sidecar&lt;/strong&gt;. AgentGateway pauses the request, sends the HTTP body to the SR's gRPC endpoint, gets back a header mutation (&lt;code&gt;x-selected-model: qwen-coder&lt;/code&gt;), and resumes routing. Zero proxy hops. Zero Python processes. Just gRPC-native intelligence inside the gateway's own request lifecycle.&lt;/p&gt;

&lt;p&gt;The SR uses an embedded &lt;strong&gt;mmBERT&lt;/strong&gt; model (a 2D Matryoshka embedding model, ~130MB) to semantically classify every prompt and compare it against model descriptions you write in YAML. No keyword lists. No regex. Actual embeddings.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Architecture
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────────────────┐
│                  Client (Pi Agent)                   │
│             POST /v1/chat/completions                │
│                  model: "MoM"                        │
└────────────────────────┬────────────────────────────┘
                         │
                         ▼
┌─────────────────────────────────────────────────────┐
│              AgentGateway (:3000)                     │
│                                                       │
│  1. Receive request                                   │
│  2. Pause → send body to ExtProc (gRPC :50051)       │
│  3. SR analyzes prompt with mmBERT embeddings         │
│  4. SR returns header: x-selected-model: qwen-coder  │
│  5. Resume → match route by header → forward          │
└──────┬──────────────┬──────────────┬────────────────┘
       │              │              │
       ▼              ▼              ▼
   ┌────────┐   ┌──────────┐   ┌──────────┐
   │ Ollama  │   │ OpenAI   │   │ Gemini   │
   │ :11434  │   │ Cloud    │   │ Cloud    │
   └────────┘   └──────────┘   └──────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Setting It Up (Two YAML Files)
&lt;/h2&gt;

&lt;p&gt;The entire setup is defined in two config files. No code. No Python.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Semantic Router Config (&lt;code&gt;config.yaml&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;This tells the SR about your models and how to route between them:&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;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v0.3&lt;/span&gt;

&lt;span class="na"&gt;providers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;defaults&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;default_model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;qwen-coder&lt;/span&gt;
  &lt;span class="na"&gt;models&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;qwen-coder&lt;/span&gt;
      &lt;span class="na"&gt;provider_model_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;qwen2.5-coder:7b&lt;/span&gt;
      &lt;span class="na"&gt;api_format&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;openai&lt;/span&gt;
      &lt;span class="na"&gt;backend_refs&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;local-ollama&lt;/span&gt;
          &lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;host.docker.internal:11434&lt;/span&gt;
          &lt;span class="na"&gt;protocol&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http&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;gpt-4o&lt;/span&gt;
      &lt;span class="na"&gt;provider_model_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gpt-4o&lt;/span&gt;
      &lt;span class="na"&gt;api_format&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;openai&lt;/span&gt;
      &lt;span class="na"&gt;backend_refs&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;openai-cloud&lt;/span&gt;
          &lt;span class="na"&gt;base_url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://api.openai.com/v1&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;gemini-flash&lt;/span&gt;
      &lt;span class="na"&gt;provider_model_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gemini-2.5-flash&lt;/span&gt;
      &lt;span class="na"&gt;api_format&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;openai&lt;/span&gt;
      &lt;span class="na"&gt;backend_refs&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;gemini-cloud&lt;/span&gt;
          &lt;span class="na"&gt;base_url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://generativelanguage.googleapis.com/v1beta/openai&lt;/span&gt;

&lt;span class="na"&gt;routing&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;modelCards&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;qwen-coder&lt;/span&gt;
      &lt;span class="na"&gt;param_size&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;7B&lt;/span&gt;
      &lt;span class="na"&gt;context_window_size&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;32768&lt;/span&gt;
      &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="s"&gt;Specialized coding model optimized for programming tasks.&lt;/span&gt;
        &lt;span class="s"&gt;Excellent at writing code, debugging, algorithms, data structures,&lt;/span&gt;
        &lt;span class="s"&gt;code review, refactoring, and technical implementation in Python,&lt;/span&gt;
        &lt;span class="s"&gt;Rust, JavaScript, Go. Best for code generation, fixing bugs,&lt;/span&gt;
        &lt;span class="s"&gt;writing tests, and technical programming Q&amp;amp;A.&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;gpt-4o&lt;/span&gt;
      &lt;span class="na"&gt;param_size&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;200B+&lt;/span&gt;
      &lt;span class="na"&gt;context_window_size&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;128000&lt;/span&gt;
      &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="s"&gt;Frontier reasoning model with exceptional analytical capability.&lt;/span&gt;
        &lt;span class="s"&gt;Best for complex multi-step reasoning, strategic analysis,&lt;/span&gt;
        &lt;span class="s"&gt;comparing trade-offs, writing long-form essays, nuanced&lt;/span&gt;
        &lt;span class="s"&gt;explanations, math proofs, scientific reasoning.&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;gemini-flash&lt;/span&gt;
      &lt;span class="na"&gt;param_size&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;~100B&lt;/span&gt;
      &lt;span class="na"&gt;context_window_size&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1000000&lt;/span&gt;
      &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="s"&gt;Fast general-purpose model. Ideal for simple factual questions,&lt;/span&gt;
        &lt;span class="s"&gt;quick lookups, summarization, casual conversation, translations,&lt;/span&gt;
        &lt;span class="s"&gt;everyday tasks, and when speed matters more than depth.&lt;/span&gt;

  &lt;span class="na"&gt;decisions&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;MoM&lt;/span&gt;
      &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Mixture&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;of&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Models&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;router"&lt;/span&gt;
      &lt;span class="na"&gt;priority&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;
      &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{}&lt;/span&gt;
      &lt;span class="na"&gt;modelRefs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;qwen-coder&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gpt-4o&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gemini-flash&lt;/span&gt;
      &lt;span class="na"&gt;algorithm&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;multi_factor&lt;/span&gt;
        &lt;span class="na"&gt;multi_factor&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;weights&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;quality&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.1&lt;/span&gt;
            &lt;span class="na"&gt;latency&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.4&lt;/span&gt;
            &lt;span class="na"&gt;cost&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.5&lt;/span&gt;
          &lt;span class="na"&gt;slo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;max_cost_per_1m&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.5&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key insight: &lt;strong&gt;you describe what each model is &lt;em&gt;good at&lt;/em&gt; in natural language, and the SR uses those descriptions as semantic anchors&lt;/strong&gt;. No keyword lists to maintain. When a new prompt arrives, the SR embeds it and compares it against these descriptions using cosine similarity. The model whose description is closest to the prompt wins.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. AgentGateway Config (&lt;code&gt;homelab_config.yaml&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;This tells AgentGateway to use the SR as an ExtProc sidecar, and to route based on the header it sets:&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="c1"&gt;# Gateway-level policy: ExtProc to Semantic Router&lt;/span&gt;
&lt;span class="na"&gt;policies&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="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;semantic-router&lt;/span&gt;
    &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;default&lt;/span&gt;
  &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;gateway&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;gatewayName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;default&lt;/span&gt;
  &lt;span class="na"&gt;phase&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gateway&lt;/span&gt;
  &lt;span class="na"&gt;policy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;extProc&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;127.0.0.1:50051"&lt;/span&gt;
      &lt;span class="na"&gt;processingOptions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;requestBodyMode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;buffered&lt;/span&gt;
        &lt;span class="na"&gt;responseBodyMode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;none&lt;/span&gt;
        &lt;span class="na"&gt;requestHeaderMode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;send&lt;/span&gt;
      &lt;span class="na"&gt;failureMode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;failOpen&lt;/span&gt;   &lt;span class="c1"&gt;# If SR is down, fall through&lt;/span&gt;

&lt;span class="na"&gt;binds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3000&lt;/span&gt;
  &lt;span class="na"&gt;listeners&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;routes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# When SR sets x-selected-model: qwen-coder → Local Ollama&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;matches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;headers&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="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;x-selected-model"&lt;/span&gt;
          &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;exact&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;qwen-coder"&lt;/span&gt;
      &lt;span class="na"&gt;backends&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;ai&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;openAI&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;ollama&lt;/span&gt;
          &lt;span class="na"&gt;hostOverride&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;localhost:11434"&lt;/span&gt;

    &lt;span class="c1"&gt;# When SR sets x-selected-model: gpt-4o → OpenAI&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;matches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;headers&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="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;x-selected-model"&lt;/span&gt;
          &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;exact&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-4o"&lt;/span&gt;
      &lt;span class="na"&gt;backends&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;ai&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;openAI&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;openai&lt;/span&gt;
        &lt;span class="na"&gt;policies&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;backendAuth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$OPENAI_API_KEY&lt;/span&gt;

    &lt;span class="c1"&gt;# When SR sets x-selected-model: gemini-flash → Google&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;matches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;headers&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="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;x-selected-model"&lt;/span&gt;
          &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;exact&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gemini-flash"&lt;/span&gt;
      &lt;span class="na"&gt;backends&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;ai&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;gemini&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;gemini&lt;/span&gt;
        &lt;span class="na"&gt;policies&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;backendAuth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$GEMINI_API_KEY&lt;/span&gt;

    &lt;span class="c1"&gt;# Fallback if SR is down (failOpen)&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;backends&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;ai&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;gemini&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;gemini-default&lt;/span&gt;
        &lt;span class="na"&gt;policies&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;backendAuth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$GEMINI_API_KEY&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the &lt;strong&gt;separation of concerns&lt;/strong&gt;: the Semantic Router &lt;em&gt;never&lt;/em&gt; touches API keys. It classifies the prompt and mutates a header. AgentGateway owns the downstream auth. This is exactly how infrastructure teams design production gateways — the routing intelligence is decoupled from the security posture.&lt;/p&gt;

&lt;p&gt;And that &lt;code&gt;failureMode: failOpen&lt;/code&gt;? It means if the SR container ever crashes or is restarting, AgentGateway seamlessly falls through to the default Gemini route. I've tested this — during SR container restarts, Pi's requests still get answered without a single error. The agent doesn't even notice.&lt;/p&gt;




&lt;h2&gt;
  
  
  The ARM64 Rabbit Hole (Two Bugs, Two PRs)
&lt;/h2&gt;

&lt;p&gt;Here's where the story gets real. I run this on an &lt;strong&gt;Apple Silicon Mac Mini&lt;/strong&gt; (M-series, ARM64). Everything installed fine. The SR container started. And then:&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;"msg"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"embedding_models_init_completed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"embedding_ready"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tools_ready"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&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 mmBERT model loaded but the embedding runtime never became ready. Every routing attempt logged:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Failed to embed model qwen-coder: failed to generate batched embedding (status: -1)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Bug #1: Wrong FFI Dispatch (&lt;a href="https://github.com/vllm-project/semantic-router/issues/2172" rel="noopener noreferrer"&gt;#2172&lt;/a&gt;)
&lt;/h3&gt;

&lt;p&gt;After deep-diving into the SR source code, I discovered the issue. The Go router was calling &lt;code&gt;candle_binding.GetEmbeddingBatched()&lt;/code&gt; for &lt;em&gt;all&lt;/em&gt; model types — but the Rust FFI backend only supports batched embeddings for &lt;code&gt;qwen3&lt;/code&gt; architectures. For &lt;code&gt;mmbert&lt;/code&gt; (the default), it returned &lt;code&gt;status: -1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The fix (&lt;a href="https://github.com/vllm-project/semantic-router/pull/2192" rel="noopener noreferrer"&gt;PR #2192&lt;/a&gt;) was elegant — a 15-line change that adds a dispatch check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// Only qwen3 supports the batched FFI. Others use single-text FFI.&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;candleEmbeddingSupportsBatched&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;modelType&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;modelType&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"qwen3"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For non-qwen3 models, it gracefully falls back to &lt;code&gt;GetEmbeddingWithModelType()&lt;/code&gt;, which works perfectly on ARM64.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bug #2: Missing Model Files on First Boot (&lt;a href="https://github.com/vllm-project/semantic-router/issues/2173" rel="noopener noreferrer"&gt;#2173&lt;/a&gt;)
&lt;/h3&gt;

&lt;p&gt;The second issue was subtler. When the SR container downloaded the mmBERT model files from HuggingFace on first boot, several required files (like &lt;code&gt;tokenizer.json&lt;/code&gt; and &lt;code&gt;config.json&lt;/code&gt;) weren't being fetched. This was a download-completeness bug in the model resolver.&lt;/p&gt;

&lt;p&gt;Fixed in &lt;a href="https://github.com/vllm-project/semantic-router/pull/2195" rel="noopener noreferrer"&gt;PR #2195&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  A Huge Thank You 🙏
&lt;/h3&gt;

&lt;p&gt;Both issues were triaged and fixed within days by the vLLM Semantic Router team, particularly &lt;a href="https://github.com/WUKUNTAI-0211" rel="noopener noreferrer"&gt;@WUKUNTAI-0211&lt;/a&gt; who wrote the fix for the FFI dispatch and &lt;a href="https://github.com/theohsiung" rel="noopener noreferrer"&gt;@theohsiung&lt;/a&gt; for the file completeness fix. The PRs are now merged into &lt;code&gt;main&lt;/code&gt;. If you're running on ARM64/Apple Silicon, just pull the latest and it works. Also shout out to &lt;a href="https://github.com/AayushSaini101" rel="noopener noreferrer"&gt;AayushSaini101&lt;/a&gt; for encouraging me recently to contribute to repo. &lt;/p&gt;

&lt;p&gt;This is open source at its best. I filed two issues with reproduction steps and log snippets, and got working fixes merged into the upstream repo. The community aspect of this project is exceptional.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Proof: Real Routing Logs
&lt;/h2&gt;

&lt;p&gt;Let me show you what it actually looks like when a request flows through. I send this:&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:3000/v1/chat/completions &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "model": "MoM",
    "messages": [
      {"role": "user", "content": "Write me a Python function to compute fibonacci numbers using memoization"}
    ]
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 1: SR Classifies the Prompt (1ms!)
&lt;/h3&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;"msg"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"routing_decision"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"original_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;"MoM"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"selected_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;"qwen-coder"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"reason_code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"auto_routing"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"routing_latency_ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"component"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"extproc"&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;One millisecond. The SR embedded the prompt, compared it against the three model descriptions, and decided this is a coding task → &lt;code&gt;qwen-coder&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: AgentGateway Routes to Ollama
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;info  request
  gateway=default/default
  route=default/route0
  endpoint=localhost:11434
  http.status=200
  gen_ai.request.model=qwen2.5-coder:7b
  gen_ai.response.model=qwen2.5-coder:7b
  gen_ai.usage.input_tokens=41
  gen_ai.usage.output_tokens=366
  duration=22537ms
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;AgentGateway matched the &lt;code&gt;x-selected-model: qwen-coder&lt;/code&gt; header, routed to the local Ollama endpoint, and the entire round-trip (including LLM generation) completed in 22.5 seconds. The routing overhead? &lt;strong&gt;1ms&lt;/strong&gt;. The rest is just Ollama thinking.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: The SR Startup Sequence
&lt;/h3&gt;

&lt;p&gt;On container boot, you see the full model loading pipeline:&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="nl"&gt;"msg"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"embedding_models_init_started"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"mmbert_configured"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"use_cpu"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kc"&gt;true&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;INFO: mmBERT embedding model registered with 2D Matryoshka support
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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="nl"&gt;"msg"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"embedding_models_initialized"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"use_batched"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kc"&gt;false&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;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="nl"&gt;"msg"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"selection_factory_initialized"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"selector_count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;14&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;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="nl"&gt;"msg"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"startup_complete"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"embedding_ready"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"sem_cache_enabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"model_selection"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"extproc_port"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;50051&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"decisions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"MoM"&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;14 selection algorithms available out of the box. Multi-factor, ELO, reinforcement-learning-driven, hybrid, latency-aware, session-aware, KNN, SVM, K-means — all registered and ready. I'm using &lt;code&gt;multi_factor&lt;/code&gt; with cost-heavy weighting, but I can switch to any of these with a single YAML change. Try doing that with a Python keyword list.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Numbers After Two Weeks
&lt;/h2&gt;

&lt;p&gt;After running the SR-powered setup alongside Pi for two weeks, here's the comparison:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Python Router&lt;/th&gt;
&lt;th&gt;vLLM Semantic Router&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Misrouted requests&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~18%&lt;/td&gt;
&lt;td&gt;~3% (subjective spot-checks)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Routing latency&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~45ms (HTTP proxy)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;1-3ms&lt;/strong&gt; (gRPC ExtProc)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Monthly estimated API cost&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~$24&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~$14&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Maintenance effort&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Weekly keyword updates&lt;/td&gt;
&lt;td&gt;Zero (model descriptions are stable)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Failover behavior&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Manual restart&lt;/td&gt;
&lt;td&gt;Automatic failOpen to Gemini&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Language support&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;English keywords only&lt;/td&gt;
&lt;td&gt;Multi-language (embedding-based)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Config&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;100 lines of Python&lt;/td&gt;
&lt;td&gt;2 YAML files&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The cost savings come from fewer misroutes. When "explain the async/await pattern in Rust" correctly goes to the local Ollama instead of GPT-4o, that's a $0.003 request instead of $0.03. Across hundreds of daily requests from Pi's cron jobs and my direct usage, it adds up fast.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Every Agent Builder Needs This
&lt;/h2&gt;

&lt;p&gt;If you're building agents — whether it's a personal Pi running on a Mac Mini or a production fleet of agents in Kubernetes — you need a routing layer that &lt;em&gt;understands&lt;/em&gt; prompts. Here's why:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Cost control is the #1 agent problem.&lt;/strong&gt; Agents generate a lot of requests. Without intelligent routing, every request goes to your most expensive model. The SR's &lt;code&gt;multi_factor&lt;/code&gt; algorithm explicitly weighs cost, latency, and quality.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Keyword routing doesn't scale.&lt;/strong&gt; The moment your agent handles a domain you didn't anticipate (my Pi started doing recipe research — none of my keywords covered "sourdough starter hydration"), keyword-based routing silently fails.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;AgentGateway + SR is production-grade.&lt;/strong&gt; This isn't a hobby-tier setup. AgentGateway is a Gateway API data plane built in Rust. The SR is an Envoy ExtProc server written in Go and Rust, backed by the vLLM project. This is the same architecture you'd deploy in a Kubernetes cluster with 50 models.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Zero code maintenance.&lt;/strong&gt; I haven't touched my routing config since I wrote those model descriptions. The SR learns from the descriptions, not from rules I have to keep updating.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




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

&lt;p&gt;With the routing intelligence sorted, I'm now focused on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Observability&lt;/strong&gt;: Wiring up Jaeger and Prometheus to trace every request from Pi → AgentGateway → SR → Upstream LLM and back. The AgentGateway already emits OpenTelemetry-compatible spans — I just need to set up the collectors.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;More models&lt;/strong&gt;: Now that routing is semantic, I can add specialized models (a medical one, a legal one) with just a new model card in YAML. The SR will automatically figure out when to use them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're running a homelab AI setup — or building agents at any scale — the combination of &lt;a href="https://github.com/agentgateway/agentgateway" rel="noopener noreferrer"&gt;AgentGateway&lt;/a&gt; + &lt;a href="https://github.com/vllm-project/semantic-router" rel="noopener noreferrer"&gt;vLLM Semantic Router&lt;/a&gt; is, in my opinion, the most underrated infrastructure combo in the AI ecosystem right now. It turned my janky Python keyword matcher into a proper ML-powered routing plane.&lt;/p&gt;

&lt;p&gt;And it runs on a Mac Mini in my living room. 🏠&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Follow me for Part 4, where I'll add full observability to this pipeline and show you exactly what happens when Pi dreams at 3 AM — now with traces.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;#ai&lt;/code&gt; &lt;code&gt;#agents&lt;/code&gt; &lt;code&gt;#architecture&lt;/code&gt; &lt;code&gt;#opensource&lt;/code&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>tutorial</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>How Cassandra Compression Actually Works (Chunks, Offsets, and Reads)</title>
      <dc:creator>Anup Sharma</dc:creator>
      <pubDate>Tue, 09 Jun 2026 12:13:51 +0000</pubDate>
      <link>https://dev.to/anup_sharma_86fa94612fe3c/how-cassandra-compression-actually-works-chunks-offsets-and-reads-2ke1</link>
      <guid>https://dev.to/anup_sharma_86fa94612fe3c/how-cassandra-compression-actually-works-chunks-offsets-and-reads-2ke1</guid>
      <description>&lt;p&gt;I got asked about Cassandra compression by someone recently and didn't do it justice on the spot. The questions were good ones: what does &lt;code&gt;chunk_length_in_kb&lt;/code&gt; really control, what happens on a write, and on a read how does Cassandra know how many bytes to pull off disk before it can decompress anything? I work on a database with a Cassandra backend, but we forked Cassandra years ago, before table compression existed, and our data is local so we never leaned on it. So I went and read the actual mechanics. Here's the version I wish I'd had in my head.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;Cassandra stores data in SSTables, which are immutable once written. Compression happens when the SSTable is written and never changes after that. If you &lt;code&gt;ALTER&lt;/code&gt; the compression settings, nothing happens to existing data until those SSTables get rewritten by compaction.&lt;/p&gt;

&lt;p&gt;When compression is on, two files matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Data.db&lt;/code&gt; holds the compressed bytes&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CompressionInfo.db&lt;/code&gt; holds the metadata Cassandra needs to find and decompress those bytes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That second file is the whole trick. Hold onto it.&lt;/p&gt;

&lt;h2&gt;
  
  
  chunk_length_in_kb is the uncompressed size
&lt;/h2&gt;

&lt;p&gt;This is the part I had backwards in my head. &lt;code&gt;chunk_length_in_kb&lt;/code&gt; is &lt;strong&gt;not&lt;/strong&gt; how big each chunk is on disk. It's the size of the &lt;em&gt;uncompressed&lt;/em&gt; buffer Cassandra fills before it compresses and flushes.&lt;/p&gt;

&lt;p&gt;So with the default of 16 KB (it was 64 KB before Cassandra 4.0), Cassandra buffers 16 KB of real data, compresses that block, and writes the result. The compressed output might be 4 KB or 9 KB depending on how squishy the data is. The chunks on disk are all different sizes. But every chunk represents exactly one fixed slice of the uncompressed stream.&lt;/p&gt;

&lt;p&gt;That "fixed in the uncompressed world, variable on disk" split is what the he kept circling, and it's the thing that makes everything else work.&lt;/p&gt;

&lt;h2&gt;
  
  
  The write path
&lt;/h2&gt;

&lt;p&gt;Writing is the easy direction:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Buffer incoming data until you hit &lt;code&gt;chunk_length_in_kb&lt;/code&gt; worth of uncompressed bytes.&lt;/li&gt;
&lt;li&gt;Compress that buffer (LZ4 by default).&lt;/li&gt;
&lt;li&gt;Append the compressed bytes to &lt;code&gt;Data.db&lt;/code&gt;, followed by a 4-byte checksum.&lt;/li&gt;
&lt;li&gt;Record the starting byte offset of this chunk in &lt;code&gt;CompressionInfo.db&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Repeat until the SSTable is done. The checksum is a CRC over the compressed bytes; it's how Cassandra catches bitrot later, and &lt;code&gt;crc_check_chance&lt;/code&gt; controls how often it bothers to verify on read.&lt;/p&gt;

&lt;p&gt;So &lt;code&gt;CompressionInfo.db&lt;/code&gt; ends up looking roughly 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;compressor name        e.g. "LZ4Compressor"
chunk_length           e.g. 16384   (uncompressed bytes per chunk)
data_length            total uncompressed length of the file
chunk_count            N
chunk_offsets[]        long[N]   &amp;lt;-- the important bit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;chunk_offsets&lt;/code&gt; is just an array of byte positions into &lt;code&gt;Data.db&lt;/code&gt;. Offset &lt;code&gt;i&lt;/code&gt; tells you where compressed chunk &lt;code&gt;i&lt;/code&gt; starts.&lt;/p&gt;

&lt;h2&gt;
  
  
  The read path
&lt;/h2&gt;

&lt;p&gt;Now the question that actually matters. Cassandra has resolved a partition through its index and knows the &lt;strong&gt;uncompressed&lt;/strong&gt; byte position it wants, call it &lt;code&gt;position&lt;/code&gt;. The data on disk is compressed and every chunk is a different size, so it can't just seek there. Here's how it gets from an uncompressed position to actual bytes.&lt;/p&gt;

&lt;p&gt;First, figure out which chunk holds that position. Because chunks are a fixed size &lt;em&gt;in uncompressed terms&lt;/em&gt;, this is plain division:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;chunkIndex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;position&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;chunkLength&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;offsetInChunk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;position&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;chunkLength&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then look up where that chunk lives on disk, and figure out how many bytes to read. The length of a compressed chunk isn't stored directly. You get it by subtracting consecutive offsets (minus the 4 checksum bytes):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;chunkOffsets&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;chunkIndex&lt;/span&gt;&lt;span class="o"&gt;];&lt;/span&gt;

&lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunkIndex&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;chunkCount&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;chunkOffsets&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;chunkIndex&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;   &lt;span class="c1"&gt;// next chunk starts here&lt;/span&gt;
    &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;compressedFileLength&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;          &lt;span class="c1"&gt;// last chunk runs to EOF&lt;/span&gt;

&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;compressedLength&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;end&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 4 = CRC checksum&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That answers the "how do we know how many bytes / offsets to read" question directly. You don't store the compressed length, you derive it from the gap between this offset and the next one.&lt;/p&gt;

&lt;p&gt;After that the rest is mechanical:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;seek&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;read&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;compressedLength&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// read exactly this chunk&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shouldCheck&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;crcCheckChance&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;verifyCrc&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;readInt&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;    &lt;span class="c1"&gt;// the trailing 4 bytes&lt;/span&gt;

&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;decompressed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lz4&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;decompress&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// up to chunkLength bytes&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;decompressed&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;offsetInChunk&lt;/span&gt; &lt;span class="o"&gt;...];&lt;/span&gt;   &lt;span class="c1"&gt;// jump to what we wanted&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A concrete pass with 16 KB chunks (16384 bytes). Say Cassandra wants uncompressed &lt;code&gt;position = 50000&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;chunkIndex = 50000 / 16384 = 3&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;offsetInChunk = 50000 % 16384 = 848&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;read the compressed bytes between &lt;code&gt;chunkOffsets[3]&lt;/code&gt; and &lt;code&gt;chunkOffsets[4]&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;decompress that one chunk back into ~16 KB&lt;/li&gt;
&lt;li&gt;skip to byte 848 in the result&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's it. One division to find the chunk, one subtraction to size the read, one decompress, one in-memory skip.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why fixed uncompressed size, and not fixed disk size
&lt;/h2&gt;

&lt;p&gt;You can't index into compressed data, because the compressor changes the size unpredictably. If chunks were a fixed size &lt;em&gt;on disk&lt;/em&gt;, you'd have no idea which uncompressed byte each one started at, and random reads would mean decompressing from the front of the file every time.&lt;/p&gt;

&lt;p&gt;By fixing the uncompressed size instead, the mapping from "byte I want" to "chunk number" becomes a single divide. The offset array handles the other direction, telling you where that chunk sits on disk. The two together give you O(1) random access into compressed data, which is the whole point.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tradeoff in chunk size
&lt;/h2&gt;

&lt;p&gt;To read one tiny cell, Cassandra still has to read and decompress the &lt;em&gt;entire&lt;/em&gt; chunk that contains it. So chunk size is a real knob:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bigger chunks&lt;/strong&gt; give the compressor more context, so better compression ratio and a smaller file. But every small read drags a big block off disk and decompresses it. That's read amplification.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Smaller chunks&lt;/strong&gt; mean less wasted I/O per read, but a worse ratio and more offheap memory, since you keep more offsets around.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's why 4.0 dropped the default from 64 KB to 16 KB. For read-heavy or point-read workloads, dragging 64 KB off disk to return a few hundred bytes is mostly waste. If you're doing big sequential scans or your rows are large, bigger chunks can still win.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;chunk_length_in_kb&lt;/code&gt; sizes the uncompressed buffer. On write, Cassandra compresses one buffer at a time and records each chunk's disk offset in &lt;code&gt;CompressionInfo.db&lt;/code&gt;. On read, it divides the wanted position by the chunk length to pick a chunk, subtracts neighbouring offsets to size the read, pulls exactly those bytes, checks the CRC, decompresses, and skips to the byte it wanted. Fixed uncompressed chunks are what let it do all that without scanning from the start of the file.&lt;/p&gt;

&lt;p&gt;I should have been able to walk through this in the room. Now I can, and writing it down made it stick.&lt;/p&gt;

</description>
      <category>cassandra</category>
      <category>database</category>
      <category>performance</category>
      <category>storage</category>
    </item>
    <item>
      <title>I Traced Personal Agent's Source Code. Inside Was Pi... And It Dreams at 3 AM.</title>
      <dc:creator>Anup Sharma</dc:creator>
      <pubDate>Sat, 30 May 2026 09:17:18 +0000</pubDate>
      <link>https://dev.to/anup_sharma_86fa94612fe3c/i-traced-personal-agents-source-code-inside-was-pi-and-it-dreams-at-3-am-o0f</link>
      <guid>https://dev.to/anup_sharma_86fa94612fe3c/i-traced-personal-agents-source-code-inside-was-pi-and-it-dreams-at-3-am-o0f</guid>
      <description>&lt;p&gt;&lt;em&gt;This is Part 2 of my homelab AI series. In &lt;a href="https://dev.to/anup_sharma_86fa94612fe3c/i-built-an-ai-that-decides-which-ai-to-talk-to-running-247-from-my-living-room-211p"&gt;Part 1&lt;/a&gt;, I built a system where one AI decides which AI to talk to. This time, I popped the hood on the agent itself — and what I found inside changed how I think about AI software.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Last week I wrote about an &lt;a href="https://dev.to/anup_sharma_86fa94612fe3c/i-built-an-ai-that-decides-which-ai-to-talk-to-running-247-from-my-living-room-211p"&gt;autonomous agent OpenClaw running on a Raspberry Pi&lt;/a&gt;: an autonomous agent called &lt;strong&gt;OpenClaw&lt;/strong&gt; running on a Raspberry Pi, routing requests through AgentGateway to three different LLMs based on intent. People loved it. A few folks DMed me asking how OpenClaw &lt;em&gt;actually works&lt;/em&gt; — like, what happens after the routing? How does an autonomous agent that edits PDFs, writes code, schedules research, and finds the best restaurants in Indiranagar every Friday actually... &lt;em&gt;do&lt;/em&gt; all that?&lt;/p&gt;

&lt;p&gt;Honestly? I didn't fully know either. I knew OpenClaw was powerful. I used it daily. I'd even contributed some code. But I'd never really sat down and traced a request all the way through. So last weekend, I did.&lt;/p&gt;

&lt;p&gt;And about 30 minutes in, I hit a line in &lt;code&gt;package.json&lt;/code&gt; that stopped me cold:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"@earendil-works/pi-agent-core"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0.75.4"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"@earendil-works/pi-coding-agent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0.75.4"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;OpenClaw doesn't have its own agent engine. Buried inside it — embedded as an SDK, not a subprocess, not an API call — is a tiny coding agent called &lt;strong&gt;Pi&lt;/strong&gt;. Then I directly jump into youtube and found a great talk from &lt;a href="https://www.youtube.com/watch?v=RjfbvDXpFls&amp;amp;t=16s" rel="noopener noreferrer"&gt;Mario at AI Engineer Conference &lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And Pi might be the most elegant piece of AI software I've ever read.&lt;/p&gt;




&lt;h2&gt;
  
  
  Wait, What is Pi?
&lt;/h2&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.amazonaws.com%2Fuploads%2Farticles%2Fmotdupf9fotifuiheek5.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.amazonaws.com%2Fuploads%2Farticles%2Fmotdupf9fotifuiheek5.png" alt="Pi on ghostty terminal running" width="800" height="629"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Pi is an open-source terminal coding agent written in TypeScript by &lt;strong&gt;Mario Zechner&lt;/strong&gt;. If you've been in the AI coding agent space, you've probably heard of Cursor, Windsurf, Aider, or Claude Code. Pi sits in the same category but takes a radically different approach.&lt;/p&gt;

&lt;p&gt;Where other agents keep adding features, Pi keeps removing them.&lt;/p&gt;

&lt;p&gt;Where other agents have massive system prompts spanning thousands of tokens, Pi's is almost embarrassingly short.&lt;/p&gt;

&lt;p&gt;Where other agents ship with dozens of built-in tools, Pi ships with &lt;strong&gt;four&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Yes. Four.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;read   →  Read a file
write  →  Write a file
edit   →  Edit a file
bash   →  Run a shell command
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. That's the entire toolkit the LLM gets to work with.&lt;/p&gt;

&lt;p&gt;And here's the thing that broke my brain: &lt;em&gt;it's enough.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Think about it. What can you do with a terminal? You can read files, write files, edit files, and run commands. That's literally everything. &lt;code&gt;grep&lt;/code&gt;? That's a bash command. &lt;code&gt;git commit&lt;/code&gt;? Bash. &lt;code&gt;npm install&lt;/code&gt;? Bash. &lt;code&gt;curl&lt;/code&gt; an API? Bash. Run tests? Bash. Deploy to production? ...also bash.&lt;/p&gt;

&lt;p&gt;Pi doesn't try to build a specialized tool for every possible operation. It gives the LLM the same primitives that &lt;em&gt;you&lt;/em&gt; have as a developer, and trusts the model to compose them.&lt;/p&gt;

&lt;p&gt;Armin Ronacher (of Flask fame) wrote about Pi back in January and called it a &lt;a href="https://lucumr.pocoo.org/2026/1/31/pi/" rel="noopener noreferrer"&gt;glimpse into the future of software&lt;/a&gt;. After spending a weekend inside the source code, I think he undersold and explain it very well. &lt;/p&gt;




&lt;h2&gt;
  
  
  How Pi Actually Runs Inside OpenClaw
&lt;/h2&gt;

&lt;p&gt;Here's what surprised me the most: Pi isn't a separate service that OpenClaw calls over HTTP. It's not a subprocess. It's not even an RPC server.&lt;/p&gt;

&lt;p&gt;OpenClaw literally imports Pi as an npm package and runs the agent loop &lt;strong&gt;in the same process&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OpenClaw starts up
    ↓
Calls createAgentSession() from @earendil-works/pi-coding-agent
    ↓
Pi's agent loop starts running in-process
    ↓
OpenClaw subscribes to Pi's events (message_start, tool_execution, turn_end, etc.)
    ↓
OpenClaw replaces Pi's default tools with its own extended set
    ↓
User sends a message on Discord → OpenClaw calls session.prompt(message)
    ↓
Pi takes over: talks to LLM, executes tools, streams responses
    ↓
OpenClaw receives events, formats them, sends back to Discord
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is wild to me. Pi is designed as a standalone CLI agent. You can &lt;code&gt;npm install -g @earendil-works/pi-coding-agent&lt;/code&gt; and use it directly in your terminal. But Mario architected it so cleanly that the entire agent core can be extracted and embedded into another application like a library.&lt;/p&gt;

&lt;p&gt;OpenClaw is the vehicle. Pi is the engine.&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.amazonaws.com%2Fuploads%2Farticles%2Fennrdsthgam7y0jxyqbd.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.amazonaws.com%2Fuploads%2Farticles%2Fennrdsthgam7y0jxyqbd.png" alt="Openclaw dependency on PI" width="799" height="629"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Agent Loop: Where the Magic Happens
&lt;/h2&gt;

&lt;p&gt;Let me walk you through what actually happens when I send a message to OpenClaw on Discord. This is where it gets fun.&lt;/p&gt;

&lt;p&gt;Pi's agent loop lives in a single 743-line file (&lt;code&gt;agent-loop.ts&lt;/code&gt;), and it follows a deceptively simple cycle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────────────┐
│                  USER PROMPT                     │
└─────────────┬───────────────────────────────────┘
              ↓
┌─────────────────────────────────────────────────┐
│   Transform Context (extensions can modify)      │
│   Convert AgentMessages → LLM Messages           │
│   Send to LLM provider (streaming)               │
└─────────────┬───────────────────────────────────┘
              ↓
┌─────────────────────────────────────────────────┐
│          ASSISTANT RESPONSE                      │
│   ┌──────────────┐    ┌──────────────────┐      │
│   │  Text Reply   │    │  Tool Calls       │      │
│   └──────────────┘    └───────┬──────────┘      │
└───────────────────────────────┼──────────────────┘
              ↓                 ↓
        (no tools?)      Execute tools
         ↓                (parallel by default)
    ┌──────────┐              ↓
    │ Check     │      Tool Results
    │ follow-up │         ↓
    │ queue     │   ┌─────────────────────┐
    └──────────┘   │ Check steering queue │
         ↓         │ (user interrupts?)   │
    Empty? STOP    └─────────┬───────────┘
    Has msgs?                ↓
    → loop again       Loop back to LLM
                       with tool results
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But here's where Pi gets clever. See those two queues?&lt;/p&gt;

&lt;h3&gt;
  
  
  The Dual Queue System
&lt;/h3&gt;

&lt;p&gt;Most agents have a simple loop: user says something → agent responds → done. Pi has two hidden message queues that make it far more powerful:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Steering Queue&lt;/strong&gt; — "Hey, change direction."&lt;br&gt;
These messages get injected &lt;em&gt;between&lt;/em&gt; tool results and the next LLM call. If the agent is mid-task and you send a new message saying "actually, use TypeScript instead of Python," Pi doesn't wait for the current task to finish. It slides your message into the conversation right before the next LLM turn. The model sees the tool results AND your course correction, and adapts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Follow-Up Queue&lt;/strong&gt; — "Before you stop, consider this too."&lt;br&gt;
These get checked &lt;em&gt;after&lt;/em&gt; the agent would normally stop (no more tool calls). If there are follow-up messages, the agent continues instead of ending. Extensions use this to chain multi-step workflows without the user having to manually prompt each step.&lt;/p&gt;

&lt;p&gt;This is elegant. Most agents treat conversations as request-response. Pi treats them as &lt;em&gt;navigable streams&lt;/em&gt; that can be redirected mid-flight.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Part That Changed How I Think: Append-Only Tree Sessions
&lt;/h2&gt;

&lt;p&gt;This is where I went from "oh, this is a nice agent" to "okay, this is genuinely brilliant engineering."&lt;/p&gt;

&lt;p&gt;Most AI chat apps store conversations as a flat list. Message 1, message 2, message 3... linear. If you want to try a different approach, you either edit your message (and lose the original response) or start a new conversation entirely.&lt;/p&gt;

&lt;p&gt;Pi stores conversations as an &lt;strong&gt;append-only tree&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                    Session Start
                         │
                    User: "Build me a REST API"
                         │
                    Assistant: "Sure, I'll use Express..."
                         │
              ┌──────────┴──────────┐
              │                      │
         [Branch A]             [Branch B]
    "Use FastAPI instead"    "Add authentication"
              │                      │
    Assistant: "Okay,         Assistant: "I'll add
    switching to Python..."   JWT middleware..."
              │                      │
         [Branch A1]            [Branch B1]
    "Add rate limiting"      "Use OAuth instead"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every message is a node with an &lt;code&gt;id&lt;/code&gt; and a &lt;code&gt;parentId&lt;/code&gt;. When you fork a conversation, Pi creates a new branch from any point in the tree. The original branch stays untouched. You can navigate back and forth between branches, compare approaches, and even branch from a branch.&lt;/p&gt;

&lt;p&gt;The session file is JSONL (one JSON object per line, append-only). It's never rewritten, never mutated. New messages just get appended with pointers to their parent.&lt;/p&gt;

&lt;p&gt;Why does this matter? Three reasons:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. It's crash-proof.&lt;/strong&gt; Append-only means no data corruption on unexpected shutdown. Your Raspberry Pi loses power at 3 AM mid-response? The session is fine. Just re-open and continue from the last complete message.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. It enables time travel.&lt;/strong&gt; You can jump back to any point in the conversation and fork. "What if I'd asked for Rust instead of Python?" Just navigate back and try it. Both histories coexist.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. It makes compaction elegant.&lt;/strong&gt; When the context window fills up, Pi doesn't throw away old messages. It summarizes them into a &lt;code&gt;CompactionEntry&lt;/code&gt; node in the tree. The original messages are still in the file — they're just not loaded into context anymore. You can always go back.&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.amazonaws.com%2Fuploads%2Farticles%2Fmuynsyy7f1ptx36843ym.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.amazonaws.com%2Fuploads%2Farticles%2Fmuynsyy7f1ptx36843ym.png" alt="branching features" width="800" height="603"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Iterative Compaction: How Pi Remembers What Matters
&lt;/h2&gt;

&lt;p&gt;Every AI agent has the same problem: context windows are finite. Eventually, your conversation gets too long and you hit the token limit. Most agents handle this by... well, by crashing. Or by silently dropping the oldest messages. Or by starting a new session.&lt;/p&gt;

&lt;p&gt;Pi does something smarter. It runs &lt;strong&gt;iterative compaction&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;When the context is getting full, Pi:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Walks backward from the newest messages, counting tokens&lt;/li&gt;
&lt;li&gt;Keeps the most recent ~20,000 tokens intact (you want your recent context fresh)&lt;/li&gt;
&lt;li&gt;Takes everything older and generates a structured summary via the LLM itself&lt;/li&gt;
&lt;li&gt;Stores that summary as a &lt;code&gt;CompactionEntry&lt;/code&gt; in the session tree&lt;/li&gt;
&lt;li&gt;On the next context build, it loads the summary instead of the original messages&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;But here's the key word: &lt;strong&gt;iterative&lt;/strong&gt;. When compaction runs a second time, Pi doesn't regenerate the summary from scratch. It takes the &lt;em&gt;existing&lt;/em&gt; summary and &lt;strong&gt;merges&lt;/strong&gt; new information into it. The summary evolves over time, like a living document.&lt;/p&gt;

&lt;p&gt;The summary follows a structured format:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Goal&lt;/span&gt;
&lt;span class="gu"&gt;## Constraints &amp;amp; Preferences  &lt;/span&gt;
&lt;span class="gu"&gt;## Progress (Done / In Progress / Blocked)&lt;/span&gt;
&lt;span class="gu"&gt;## Key Decisions&lt;/span&gt;
&lt;span class="gu"&gt;## Next Steps&lt;/span&gt;
&lt;span class="gu"&gt;## Critical Context&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It also tracks which files were read and modified across the entire session, even across multiple compactions. So if you ask "what files have we changed today?" after 6 hours of work and 3 compactions, Pi knows.&lt;/p&gt;




&lt;h2&gt;
  
  
  OpenClaw's Memory: The Part Where AI Dreams
&lt;/h2&gt;

&lt;p&gt;Okay, this is where things get genuinely sci-fi. And I mean that literally.&lt;/p&gt;

&lt;p&gt;Pi handles context management within a single session beautifully. But what about &lt;em&gt;across&lt;/em&gt; sessions? What about things you told the agent three weeks ago? What about your preferences, your coding style, the fact that you always want biryani recommendations from places with 4.5+ ratings?&lt;/p&gt;

&lt;p&gt;OpenClaw builds a multi-layered memory system on top of Pi:&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 1: File-Based Memory
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;MEMORY.md&lt;/code&gt;&lt;/strong&gt; — Long-term memory, loaded at every session start&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;memory/YYYY-MM-DD.md&lt;/code&gt;&lt;/strong&gt; — Daily notes (today + yesterday auto-loaded)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;DREAMS.md&lt;/code&gt;&lt;/strong&gt; — A dream diary. Yes, really.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Layer 2: Active Memory
&lt;/h3&gt;

&lt;p&gt;Before every reply, a bounded sub-agent runs a quick memory search and injects relevant past context into the prompt. It has a circuit breaker — if it takes too long, it gets skipped.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 3: The Dreaming System 🌙
&lt;/h3&gt;

&lt;p&gt;This is the one that made me put my laptop down and take a walk.&lt;/p&gt;

&lt;p&gt;Every night at 3 AM, OpenClaw runs a &lt;strong&gt;three-phase memory consolidation cycle&lt;/strong&gt; inspired by how human sleep works:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Light Sleep&lt;/strong&gt; — Sorts through recent short-term memories. Stages candidates. Doesn't write anything yet. Just organizes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;REM Sleep&lt;/strong&gt; — Reflects on recurring themes, patterns, and connections across memories. Still no writes. Just thinking.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deep Sleep&lt;/strong&gt; — Scores each memory candidate across 6 weighted signals and decides what gets promoted to long-term storage:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Relevance:            30%   (how useful is this?)
Frequency:            24%   (how often did this come up?)
Query Diversity:      15%   (was it relevant to different topics?)
Recency:              15%   (is it still timely?)
Consolidation:        10%   (does it connect to existing memories?)
Conceptual Richness:   6%   (is it a deep insight or just a fact?)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Memories that score high enough get written to &lt;code&gt;MEMORY.md&lt;/code&gt;. Everything else fades.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The agent literally sleeps, dreams, and wakes up smarter the next morning.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I'm not going to pretend I wasn't a little unsettled the first time I realized my agent had reorganized its own memory overnight without being asked. But also... it remembered that I prefer tabs over spaces three weeks later without me mentioning it again. So, worth it.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Extension System: How OpenClaw Bends Pi to Its Will
&lt;/h2&gt;

&lt;p&gt;Pi ships with 4 tools. OpenClaw's agent has dozens — browser automation, web search, image generation, cron scheduling, subagent spawning, Discord actions, PDF extraction, memory search, and more.&lt;/p&gt;

&lt;p&gt;How? Pi's extension system.&lt;/p&gt;

&lt;p&gt;Extensions are TypeScript files that hook into Pi's &lt;strong&gt;30+ lifecycle events&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Session Events:     session_start, session_before_compact, session_shutdown
Agent Events:       before_agent_start, agent_start, agent_end, turn_start, turn_end
Message Events:     message_start, message_update, message_end
Tool Events:        tool_call (can block!), tool_result (can modify!)
Input Events:       input (can intercept and transform user input)
Model Events:       model_select, thinking_level_select
Resource Events:    resources_discover
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An extension can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Register new tools&lt;/strong&gt; that the LLM can call&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Intercept tool calls&lt;/strong&gt; before they execute (for safety, logging, sandboxing)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Modify tool results&lt;/strong&gt; after execution&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inject messages&lt;/strong&gt; mid-conversation (steering queue!)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Register custom LLM providers&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Override the system prompt&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add UI widgets&lt;/strong&gt; to the terminal&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When OpenClaw boots up, it calls &lt;code&gt;createAgentSession()&lt;/code&gt; from Pi and then runs a &lt;strong&gt;7-stage tool pipeline&lt;/strong&gt; that completely replaces Pi's default 4 tools with OpenClaw's full suite:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Pi's defaults → Custom replacements → OpenClaw tools → Channel-specific tools
    → Policy filtering → Schema normalization → AbortSignal wrapping
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is what good software architecture looks like. Pi doesn't try to be everything. It gives you a clean, minimal core and says: "Here are 30 hooks. Build whatever you want."&lt;/p&gt;




&lt;h2&gt;
  
  
  Why This Architecture Works
&lt;/h2&gt;

&lt;p&gt;After spending a weekend inside this codebase, I think Pi gets three things right that most AI agents get wrong:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Trust the Model, Don't Hand-Hold It
&lt;/h3&gt;

&lt;p&gt;Most agents build a specialized tool for every operation: &lt;code&gt;search_files&lt;/code&gt;, &lt;code&gt;list_directory&lt;/code&gt;, &lt;code&gt;run_tests&lt;/code&gt;, &lt;code&gt;git_commit&lt;/code&gt;, &lt;code&gt;install_package&lt;/code&gt;... &lt;/p&gt;

&lt;p&gt;Pi says: here's &lt;code&gt;bash&lt;/code&gt;. Figure it out.&lt;/p&gt;

&lt;p&gt;This seems reckless until you realize that modern LLMs are &lt;em&gt;really good at shell commands&lt;/em&gt;. They know &lt;code&gt;grep&lt;/code&gt;. They know &lt;code&gt;find&lt;/code&gt;. They know &lt;code&gt;git&lt;/code&gt;. Giving them &lt;code&gt;bash&lt;/code&gt; and getting out of the way produces better results than giving them 50 narrow tools with rigid parameter schemas.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. State is a Tree, Not a Line
&lt;/h3&gt;

&lt;p&gt;Linear chat history is a lie. Real problem-solving is branching. You try approach A, realize it's wrong, backtrack, try approach B. Pi's tree sessions make this a first-class operation instead of a hack.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Extensions &amp;gt; Features
&lt;/h3&gt;

&lt;p&gt;Instead of shipping a monolithic agent with every feature imaginable, Pi ships a tiny core with a powerful extension system. OpenClaw adds 129 extensions. My homelab setup is much simpler. Both work, because the core doesn't care what you bolt onto it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Setting This Up For Yourself
&lt;/h2&gt;

&lt;p&gt;If you want to try Pi standalone (no OpenClaw, just the coding agent):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; @earendil-works/pi-coding-agent
pi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. You now have a terminal coding agent with 4 tools, tree sessions, and iterative compaction.&lt;/p&gt;

&lt;p&gt;If you want the full OpenClaw experience — Discord integration, dreaming, multi-agent orchestration, 129 extensions — check out &lt;a href="https://openclaw.ai" rel="noopener noreferrer"&gt;openclaw.ai&lt;/a&gt;. Fair warning: once you have an agent that dreams and remembers your preferences across weeks, going back to stateless ChatGPT feels like using a typewriter.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I'm Building Next
&lt;/h2&gt;

&lt;p&gt;In Part 1, I built the routing layer (which AI answers). In this post, I explored the engine (how the AI thinks). The next piece of the puzzle: &lt;strong&gt;observability&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;AgentGateway already emits OpenTelemetry traces for every LLM call. Pi tracks token usage, tool execution times, and compaction events. I want to pipe all of this into a Grafana dashboard so I can see, in real-time:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which model is handling which type of request&lt;/li&gt;
&lt;li&gt;How many tokens stay local vs go to the cloud&lt;/li&gt;
&lt;li&gt;How long tool executions take&lt;/li&gt;
&lt;li&gt;When compaction fires and how much context it saves&lt;/li&gt;
&lt;li&gt;What the dreaming system promoted to long-term memory&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Stay tuned.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you made it this far — first of all, respect. Second, if you're building something similar or want to nerd out about agent architectures, hit me up. I live for this stuff.&lt;/em&gt;&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.amazonaws.com%2Fuploads%2Farticles%2F3vu8sulu35sljysd90h6.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.amazonaws.com%2Fuploads%2Farticles%2F3vu8sulu35sljysd90h6.png" alt="Running PI Agent" width="800" height="603"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  AI #CodingAgent #OpenClaw #Pi #LLM #AgentArchitecture #HomeLab #BuildInPublic #AIEngineering #OpenSource
&lt;/h1&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>architecture</category>
      <category>agents</category>
    </item>
    <item>
      <title>I Built an AI That Decides Which AI to Talk To — Running 24/7 From My Living Room</title>
      <dc:creator>Anup Sharma</dc:creator>
      <pubDate>Sat, 23 May 2026 07:43:58 +0000</pubDate>
      <link>https://dev.to/anup_sharma_86fa94612fe3c/i-built-an-ai-that-decides-which-ai-to-talk-to-running-247-from-my-living-room-211p</link>
      <guid>https://dev.to/anup_sharma_86fa94612fe3c/i-built-an-ai-that-decides-which-ai-to-talk-to-running-247-from-my-living-room-211p</guid>
      <description>&lt;p&gt;Last Saturday when I woke up, my AI agent reviewed 14 restaurant ratings in Indiranagar, updated a shared Google Sheet, signed a 20-page PDF I'd been ignoring for a week, and wrote a bash script to clean up my server logs.&lt;/p&gt;

&lt;p&gt;I didn't ask it to do any of that. It just... does things now.&lt;/p&gt;

&lt;p&gt;Meet &lt;strong&gt;OpenClaw&lt;/strong&gt; — my long-running autonomous agent that lives on a Raspberry Pi, plugged into Discord, running 24/7. It manages my memory, handles research, writes code, edits documents, finds the best weekend spots in Bangalore by scraping live ratings — basically, it runs half my life on autopilot.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;But a few weeks ago, I noticed something that bothered me.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I asked it: &lt;em&gt;"Write a Python script to parse JSON logs."&lt;/em&gt; Simple coding task. It sent that request to a cloud API, waited 3 seconds, burned tokens I paid for, and came back with an answer — when I had a perfectly capable local LLM sitting idle on my Mac Mini, three feet away.&lt;/p&gt;

&lt;p&gt;Then I asked: &lt;em&gt;"Think step by step about the trade-offs between event-driven vs polling architecture for my notification system."&lt;/em&gt; That's a hard reasoning question. I want that going to a frontier model. That's worth the tokens.&lt;/p&gt;

&lt;p&gt;Same agent. Same endpoint. Completely different needs.&lt;/p&gt;

&lt;p&gt;And that's when a stupid idea hit me:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What if the system could figure out which brain to use — before the request even reaches a model?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Turns out, it's not stupid at all. And it took me a weekend, a Raspberry Pi, a Mac Mini, 50 lines of Python, and an open-source gateway to build it.&lt;/p&gt;

&lt;p&gt;Here's how.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Setup
&lt;/h2&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.amazonaws.com%2Fuploads%2Farticles%2Fxd9x1tqn50cf1lew8lu8.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.amazonaws.com%2Fuploads%2Farticles%2Fxd9x1tqn50cf1lew8lu8.png" alt=" " width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's what's running in my living room:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Raspberry Pi&lt;/strong&gt; → Runs OpenClaw, my autonomous agent. It takes input from Discord, manages context, memory, and orchestrates everything.&lt;br&gt;
&lt;strong&gt;Mac Mini&lt;/strong&gt; → The brain farm. Runs three things:&lt;br&gt;
Ollama with qwen2.5-coder:7b — a local coding model that never leaves my network&lt;br&gt;
&lt;strong&gt;AgentGateway&lt;/strong&gt; — an open-source AI gateway from Google that handles routing, auth, observability&lt;br&gt;
&lt;strong&gt;A lightweight Python router&lt;/strong&gt; — the "intent classifier" I wrote in ~50 lines of code&lt;br&gt;
The magic? OpenClaw doesn't know any of this is happening. It just sends a request to one endpoint. Behind the scenes, the system figures out the rest.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Architecture
&lt;/h2&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.amazonaws.com%2Fuploads%2Farticles%2Fgy4hte82eh0nergs3xya.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.amazonaws.com%2Fuploads%2Farticles%2Fgy4hte82eh0nergs3xya.png" alt=" " width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Three models. Three price points. One unified endpoint. OpenClaw just hits &lt;a href="http://192.168.1.15:1234/v1/chat/completions" rel="noopener noreferrer"&gt;http://192.168.1.15:1234/v1/chat/completions&lt;/a&gt; and forgets about it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why AgentGateway?
&lt;/h3&gt;

&lt;p&gt;I evaluated a few options — raw Envoy, Nginx with Lua scripting, even building a full proxy from scratch. But &lt;strong&gt;AgentGateway&lt;/strong&gt; stood out for a few reasons:&lt;/p&gt;

&lt;p&gt;What it gives you out of the box:&lt;br&gt;
&lt;strong&gt;Protocol translation&lt;/strong&gt; — It speaks OpenAI-compatible API on the frontend, but can talk to Gemini, Vertex AI, Bedrock, Ollama, and more on the backend. I don't write a single line of provider-specific code.&lt;br&gt;
&lt;strong&gt;Backend authentication&lt;/strong&gt; — API keys are managed at the gateway level. OpenClaw never sees or stores any API key. I just set backendAuth: key: $GEMINI_API_KEY in the config and it handles the rest.&lt;br&gt;
&lt;strong&gt;Model aliasing&lt;/strong&gt; — OpenClaw sends model: "inteli-llm" in every request. AgentGateway silently translates that to qwen2.5-coder:7b, gpt-4o, or gemini-2.5-flash depending on which route matched. The client has no idea.&lt;br&gt;
&lt;strong&gt;Observability&lt;/strong&gt; — Every request gets logged with provider name, model, token counts, and latency. I can see exactly how many tokens are going to OpenAI vs staying local.&lt;br&gt;
&lt;strong&gt;Prompt guards &amp;amp; rate limiting&lt;/strong&gt; — Built-in regex-based PII masking, webhook-based content moderation, and rate limiting. Enterprise-grade features I get for free.&lt;br&gt;
&lt;strong&gt;Weighted load balancing &amp;amp; failover&lt;/strong&gt; — If Ollama crashes (it happens), I can configure automatic failover to a cloud model. No downtime.&lt;br&gt;
&lt;strong&gt;What it doesn't do (yet):&lt;/strong&gt; Content-aware routing. AgentGateway routes based on path, headers, and methods — which is the right design for a gateway. It doesn't peek into your request body to decide where to send it. That's a feature, not a bug — gateways should be fast and protocol-level, not parsing JSON payloads.&lt;/p&gt;

&lt;p&gt;But I needed content-aware routing. So instead of searching for other tool, I extended it.&lt;/p&gt;

&lt;h3&gt;
  
  
  The 50-Line Router That Makes It All Work
&lt;/h3&gt;

&lt;p&gt;I wrote a tiny FastAPI proxy that sits in front of AgentGateway. Here's what it does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Intercepts the incoming OpenAI-compatible request&lt;/li&gt;
&lt;li&gt;Reads the last message in the chat&lt;/li&gt;
&lt;li&gt;Classifies intent using simple keyword matching + prompt length heuristics:

&lt;ul&gt;
&lt;li&gt;Contains &lt;code&gt;code&lt;/code&gt;, &lt;code&gt;python&lt;/code&gt;, &lt;code&gt;script&lt;/code&gt;, &lt;code&gt;function&lt;/code&gt;, &lt;code&gt;bug&lt;/code&gt;? → coding&lt;/li&gt;
&lt;li&gt;Contains &lt;code&gt;think&lt;/code&gt;, &lt;code&gt;analyze&lt;/code&gt;, &lt;code&gt;reasoning&lt;/code&gt;, &lt;code&gt;deduce&lt;/code&gt;? Or prompt &amp;gt; 400 chars? → reasoning&lt;/li&gt;
&lt;li&gt;Everything else? → simple&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Injects an x-intent HTTP header&lt;/li&gt;

&lt;li&gt;Forwards the request to AgentGateway untouched
That's it. No ML model for classification. No vector databases. No semantic similarity. Just good old keyword matching that works 90% of the time — and that's good enough for a homelab.
&lt;/li&gt;

&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;coding_keywords&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;code&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;python&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;javascript&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;bash&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;script&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;function&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bug&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;reasoning_keywords&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;think&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;analyze&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;explain in detail&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;reasoning&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;logic&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;deduce&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;prompt_lower&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;coding_keywords&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;intent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;coding&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;elif&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;prompt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;prompt_lower&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;reasoning_keywords&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;intent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reasoning&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;intent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;simple&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Cost Equation
&lt;/h3&gt;

&lt;p&gt;Here's what this setup actually saves me:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Intent&lt;/th&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Where it runs&lt;/th&gt;
&lt;th&gt;Cost per 1M tokens&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Coding&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;qwen2.5-coder:7b&lt;/td&gt;
&lt;td&gt;Local (Ollama)&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Simple Q&amp;amp;A&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;gemini-2.5-flash&lt;/td&gt;
&lt;td&gt;Google Cloud&lt;/td&gt;
&lt;td&gt;~$0.15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Deep Reasoning&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;gpt-4o&lt;/td&gt;
&lt;td&gt;OpenAI&lt;/td&gt;
&lt;td&gt;~$2.50&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Before this setup, every single request was going to a cloud API. Now, roughly 60-70% of my queries stay local — coding questions, quick lookups, simple formatting tasks. They're fast, free, and private.&lt;/p&gt;

&lt;p&gt;The expensive reasoning model only gets called when I genuinely need it. And the mid-tier Gemini handles everything in between.&lt;/p&gt;

&lt;p&gt;My monthly API bill dropped significantly, and the local responses are actually faster.&lt;/p&gt;

&lt;h3&gt;
  
  
  Design Choices &amp;amp; Why They Worked
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;1. Header-based routing over path-based routing&lt;/strong&gt; Initially, I was going to use URL paths (&lt;code&gt;/coding&lt;/code&gt;, &lt;code&gt;/reasoning&lt;/code&gt;, &lt;code&gt;/simple&lt;/code&gt;) and strip them with URL rewriting. But header injection is cleaner — the original request path stays intact, and AgentGateway's header matching is first-class.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Classification at the proxy, not the gateway&lt;/strong&gt; I could have tried to use AgentGateway's CEL expressions or ExtProc policies for classification. But those run after backend selection, not before. Keeping classification in a separate lightweight layer means I can swap algorithms without touching my gateway config.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Keyword heuristics over ML classifiers&lt;/strong&gt; Could I use a small classifier model or even RouteLLM for smarter routing? Absolutely. But for a homelab, keyword matching is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Zero latency overhead&lt;/li&gt;
&lt;li&gt;Zero dependencies&lt;/li&gt;
&lt;li&gt;Easy to debug (just read the logs)&lt;/li&gt;
&lt;li&gt;Surprisingly accurate for my use cases&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;4. One unified model name&lt;/strong&gt; OpenClaw sends model: &lt;code&gt;"inteli-llm"&lt;/code&gt; for everything. AgentGateway's &lt;code&gt;modelAliases&lt;/code&gt; feature translates it per-route. This means I can swap out backend models without touching a single line of OpenClaw's config. Last week it was &lt;code&gt;gemini-1.5-flash&lt;/code&gt;, this week it's &lt;code&gt;gemini-2.5-flash&lt;/code&gt;. OpenClaw never knew.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Smarter classification&lt;/strong&gt; — Maybe a tiny local classifier model, or even using the first few tokens of a response to reclassify and retry on a better model.&lt;br&gt;
&lt;strong&gt;Metrics dashboard&lt;/strong&gt; — AgentGateway already emits OpenTelemetry traces. I want to hook up a Grafana dashboard to see which models are handling what, with latency and token breakdowns.&lt;br&gt;
&lt;strong&gt;Failover chains&lt;/strong&gt; — If Ollama is under heavy load, automatically fall back to Gemini for coding tasks. AgentGateway supports priority groups for this.&lt;br&gt;
&lt;strong&gt;More agents&lt;/strong&gt; — OpenClaw is just the beginning. I want to run specialized agents for different domains, all routing through the same gateway.&lt;/p&gt;

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

&lt;p&gt;You don't need a Kubernetes cluster or a $10K GPU server to build a multi-model AI system. A Raspberry Pi, a Mac Mini, an open-source gateway, and 50 lines of Python got me:&lt;/p&gt;

&lt;p&gt;✅ An always-on autonomous agent ✅Intelligent routing ✅across 3 different LLMs ✅Local-first for privacy and speed ✅Cloud when I need the horsepower ✅Zero API keys exposed to the client ✅A monthly bill I actually don't mind paying&lt;/p&gt;

&lt;p&gt;The best part? The entire config is a single YAML file and a single Python script. No Docker. No Kubernetes. No Terraform. Just two processes on a Mac Mini and an agent on a Pi.&lt;/p&gt;

&lt;p&gt;Sometimes the best infrastructure is the one you can explain in a napkin sketch.&lt;/p&gt;

&lt;p&gt;If you're building something similar or want to see the config files, drop a comment — happy to share the full setup.&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.amazonaws.com%2Fuploads%2Farticles%2F6rmzapxb0p2vk8wdzjgn.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.amazonaws.com%2Fuploads%2Farticles%2F6rmzapxb0p2vk8wdzjgn.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  AI #HomeAssistant #LLM #AgentGateway #Ollama #OpenAI #Gemini #HomeLab #BuildInPublic #MacMini #RaspberryPi #AIEngineering
&lt;/h1&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>architecture</category>
      <category>agents</category>
    </item>
  </channel>
</rss>
