<?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: DHPP</title>
    <description>The latest articles on DEV Community by DHPP (@dhpp).</description>
    <link>https://dev.to/dhpp</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%2F2207535%2Fd5f493e8-3734-4e7e-aca1-fc32040c191a.png</url>
      <title>DEV Community: DHPP</title>
      <link>https://dev.to/dhpp</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dhpp"/>
    <language>en</language>
    <item>
      <title>Why Your Coding Agent Doesn't Need an Electron App: A Minimalist LLM Proxy in Go</title>
      <dc:creator>DHPP</dc:creator>
      <pubDate>Tue, 18 Aug 2026 08:21:35 +0000</pubDate>
      <link>https://dev.to/dhpp/why-your-coding-agent-doesnt-need-an-electron-app-a-minimalist-llm-proxy-in-go-4hpg</link>
      <guid>https://dev.to/dhpp/why-your-coding-agent-doesnt-need-an-electron-app-a-minimalist-llm-proxy-in-go-4hpg</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR (the bottom line, up front)
&lt;/h2&gt;

&lt;p&gt;Your coding agent (Claude Code, Cursor, Codex, ...) doesn't need a dashboard. It needs a &lt;strong&gt;transparent, self-healing, low-overhead pipe&lt;/strong&gt; between its SDK and a pool of LLM providers — because at 3 AM, when the agent is running unattended, nobody is there to click a button.&lt;/p&gt;

&lt;p&gt;That's why we built &lt;a href="https://github.com/bigfatsea/vmr" rel="noopener noreferrer"&gt;VMR&lt;/a&gt;: a local, single-binary LLM router in Go.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;~31K lines of Go, 4 direct dependencies&lt;/strong&gt;, ships as one static binary (~12–15MB)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Zero database, zero web UI, zero Node/Python runtime&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Byte-faithful pass-through&lt;/strong&gt;: requests arrive upstream byte-identical to a direct connection&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Session-sticky cache affinity&lt;/strong&gt;: keeps long conversations on the same endpoint, protecting upstream Prompt Cache (we observe 85–95% hit rates, ~50–70% cost reduction on cache discounts — self-measured, not third-party audited)&lt;/li&gt;
&lt;li&gt;From our benchmark (Apple M1): routing overhead &lt;strong&gt;+0.3–0.9ms p50&lt;/strong&gt;; RSS &lt;strong&gt;15MB&lt;/strong&gt;; cold start &lt;strong&gt;~60ms&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This post is the design reasoning behind the minimalist bet. No hype, just the decision chain.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The problem: agent traffic is server traffic, not desktop traffic
&lt;/h2&gt;

&lt;p&gt;Two nights pushed us here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Night one was the bill.&lt;/strong&gt; After wiring Claude Code / Cursor to multiple providers (Anthropic, DeepSeek, MiniMax, OpenRouter...), I started with the naive approach — hardcode different Base URLs per provider. It fell apart fast. Agent sessions are long-context, high-frequency round-trips. The moment a subagent switch slightly perturbs the request body, the provider's Prompt Cache key misses, and the cost climbs visibly. I was bouncing between "one provider's bill" and "another provider's queue time."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Night two was the outage.&lt;/strong&gt; A batch refactor was supposed to run for ~an hour unattended. An upstream key hit 429, the proxy wedged, and by morning the task had died with progress lost.&lt;/p&gt;

&lt;p&gt;Both nights pointed the same direction: &lt;strong&gt;the proxy layer has to be designed for unattended operation&lt;/strong&gt; — self-healing, self-retrying, and always leaving evidence — not for human dashboard-watching.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. What we looked at (and why we walked away)
&lt;/h2&gt;

&lt;p&gt;I'm not going to pretend we didn't consider "fuller" solutions. We did, and they're genuinely good at what they do:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;LiteLLM-class translation gateways&lt;/strong&gt;: huge coverage, but also huge complexity. The "all protocols → internal format → target protocol" translation layer is something you maintain forever as upstream event formats evolve. For a solo maintainer, that's a real tax.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Electron/desktop control planes (e.g. Claude Code Router)&lt;/strong&gt;: deep, well-made — agent profiles, tool markets, Web UI. For our use case it was a drinking fountain built to get a glass of water. We wanted the invisible, non-invasive layer only.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hardcoded Base URLs + manual switching&lt;/strong&gt;: simplest, but no failover, no health probing, no audit trail. At 3 AM nobody is switching anything manually.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They serve different audiences. The question we kept asking, which became VMR's standing review rule:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Every new feature, ask: does this add capability, or does it add complexity?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;By that test, dashboards, user management, billing, prompt management, plugin systems, and MCP frameworks all got rejected at the door. Useful? Yes. Complexity-adders for the "be a transparent base for coding agents" mission? Also yes.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Three constraints that pushed us to Go + a single binary
&lt;/h2&gt;

&lt;p&gt;Not "Go for the sake of Go." Three constraints stacked:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Unattended = server-traffic semantics.&lt;/strong&gt; High concurrency, long sessions, zero supervision. The proxy must be low-latency, high-throughput, and self-healing. This workload is closer to a reverse proxy / load balancer than a desktop GUI. Go's goroutine model, its server-grade GC behavior, and single-file cross-compilation make it the cheapest fit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Byte-level fidelity keeps you compatible with upstream.&lt;/strong&gt; LLM providers ship new params and event types fast. If your proxy does &lt;code&gt;JSON.parse → mutate → stringify&lt;/code&gt; on every request, any unknown field is theoretically at risk of being reordered, escaped, or dropped. The only way to make "new upstream features work the day they ship, without waiting for a VMR release" is to &lt;strong&gt;not touch any byte we don't need to touch&lt;/strong&gt;. Go's &lt;code&gt;json.RawMessage&lt;/code&gt; makes this clean.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Solo maintenance = fewer dependencies = safer.&lt;/strong&gt; Four Go module deps vs a 50+ npm package tree + Electron + React. Supply-chain surface, audit cost, build time — all orders of magnitude apart. For a solo project, "I can write this with only the standard library" is a survival advantage.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Byte-faithful pass-through (not "good-enough" pass-through)
&lt;/h2&gt;

&lt;p&gt;"Byte-faithful" is not marketing copy in VMR. Three concrete mechanisms:&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;// 1) json.RawMessage: keep the whole request body raw; parse only model/stream&lt;/span&gt;
&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;CanonicalRequest&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Model&lt;/span&gt;  &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;Stream&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;
    &lt;span class="n"&gt;Raw&lt;/span&gt;    &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RawMessage&lt;/span&gt; &lt;span class="c"&gt;// every other byte passes through untouched&lt;/span&gt;
    &lt;span class="n"&gt;Header&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Header&lt;/span&gt;
    &lt;span class="n"&gt;Facts&lt;/span&gt;  &lt;span class="n"&gt;RequestFacts&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;// 2) MarshalNoEscape: refuse Go's default HTML escaping.&lt;/span&gt;
&lt;span class="c"&gt;// Default json.Marshal turns "&amp;lt;div&amp;gt;" into "\u003Cdiv\u003E" — semantically&lt;/span&gt;
&lt;span class="c"&gt;// equal, byte-different. VMR cares about bytes.&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;MarshalNoEscape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;buf&lt;/span&gt; &lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Buffer&lt;/span&gt;
    &lt;span class="n"&gt;enc&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewEncoder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;enc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SetEscapeHTML&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;enc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&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;bytes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TrimSuffix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Bytes&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;// 3) Byte-level model rewrite: scan bytes, replace the "model" key's value.&lt;/span&gt;
&lt;span class="c"&gt;// No JSON unmarshal, no object round-trip. Key order, whitespace, unknown&lt;/span&gt;
&lt;span class="c"&gt;// fields stay exactly as the client sent them.&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;RewriteModel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;realModel&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="c"&gt;/* byte-level state machine */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;VMR never unpacks the request body into an object. &lt;code&gt;json.RawMessage&lt;/code&gt; is a zero-copy byte container; the &lt;code&gt;model&lt;/code&gt; substitution is pure string manipulation. That's why &lt;code&gt;go.mod&lt;/code&gt; stays at four deps:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;github.com/fsnotify/fsnotify  → config hot-reload
github.com/klauspost/compress → zstd audit-log compression
golang.org/x/image            → image downscale
gopkg.in/yaml.v3              → config parsing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No ORM, no web framework, no DI container, no logging library. HTTP uses &lt;code&gt;net/http&lt;/code&gt;; logging uses &lt;code&gt;log&lt;/code&gt;. ~31K lines of Go, one static binary.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Why we refuse cross-protocol translation
&lt;/h2&gt;

&lt;p&gt;VMR exposes both an OpenAI and an Anthropic entrance, but routes each &lt;strong&gt;within its own protocol family&lt;/strong&gt; — it never translates between them. This is a stance, not a gap:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SSE event semantics can't be losslessly mapped.&lt;/strong&gt; Anthropic's &lt;code&gt;message_start&lt;/code&gt;/&lt;code&gt;content_block_delta&lt;/code&gt;/&lt;code&gt;tool_use&lt;/code&gt; stream vs OpenAI's &lt;code&gt;choices[0].delta.tool_calls[]&lt;/code&gt; chunks — every new event type upstream adds is another patch to the translation layer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forward compatibility breaks.&lt;/strong&gt; New upstream event type → translation layer doesn't know it → dropped or error. Byte pass-through is natively forward-compatible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mainstream providers already expose both faces.&lt;/strong&gt; MiniMax, DeepSeek, OpenRouter offer OpenAI and Anthropic compatible surfaces. The translation layer no longer creates value.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We could do it — we've tested both protocol faces against real providers. We just think the complexity isn't worth it. (This is also the crux of the difference from LiteLLM-class gateways, where the translation layer is a core feature.)&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Agent-first, not human-first
&lt;/h2&gt;

&lt;p&gt;Two design decisions that only make sense if the user is an agent, not a person:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Session-Sticky affinity&lt;/strong&gt;: keep a long conversation pinned to one physical endpoint so the provider's Prompt Cache stays warm. Observed hit rates 85–95%, ~50–70% cost reduction on cache discounts (self-measured).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Error-class-aware cooldowns&lt;/strong&gt;: rate-limit (429) honors &lt;code&gt;Retry-After&lt;/code&gt;; auth failure (401) gets a long cooldown; content-filter rejection (403) &lt;strong&gt;switches provider without penalizing the endpoint&lt;/strong&gt; — the provider is healthy, it's just doing its compliance job; 5xx gets exponential backoff. Most gateways treat all failures the same. VMR doesn't.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Plus the flight recorder: per-request JSONL audit (client↔VMR, VMR↔upstream) grouped by &lt;code&gt;Session → Task → Turn&lt;/code&gt; — built to answer "which model did this agent session use, for how many tokens, and where did it go off the rails," not "what does this chat look like in a table."&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Honest tradeoffs
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No dashboard, no web UI.&lt;/strong&gt; Observability is &lt;code&gt;vmr report&lt;/code&gt; / &lt;code&gt;vmr story&lt;/code&gt; on the CLI. Deliberate, but it does filter out users who want a graphical view.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Providers are config, not built-in.&lt;/strong&gt; Currently validated: MiniMax, DeepSeek, OpenRouter. Adding one is near-zero code (a config block), but you verify compatibility yourself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Strategy dimensions are partial.&lt;/strong&gt; &lt;code&gt;priority&lt;/code&gt; works; &lt;code&gt;weight&lt;/code&gt;/&lt;code&gt;round_robin&lt;/code&gt; are "in progress."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two known architectural tradeoffs&lt;/strong&gt;: image downscaling happens at the server layer (can't be un-done after failover), and the concurrency gate is global, not per-model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The real one&lt;/strong&gt;: the project is young and small (community, iteration velocity are nothing like CCR's). If you pick it, accept you may write your own provider configs and verify compatibility. The upside: 4 deps and 31K lines mean even if development stalled, handover cost is low.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  8. Try it
&lt;/h2&gt;

&lt;p&gt;If you're running your own coding agents and want a transparent, self-healing, evidence-keeping data plane between them and your LLM providers, the code is on GitHub: &lt;a href="https://github.com/bigfatsea/vmr" rel="noopener noreferrer"&gt;github.com/bigfatsea/vmr&lt;/a&gt;. We're actively working on the scheduling dimensions (&lt;code&gt;weight&lt;/code&gt;/&lt;code&gt;round_robin&lt;/code&gt;) and the &lt;code&gt;vmr story&lt;/code&gt; diff-and-replay tooling. Issues and PRs welcome.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>go</category>
      <category>llm</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
