<?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: Vayun Godara</title>
    <description>The latest articles on DEV Community by Vayun Godara (@vayun).</description>
    <link>https://dev.to/vayun</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%2F4169647%2Fa0814505-8d43-4f8a-808d-68f608db374c.png</url>
      <title>DEV Community: Vayun Godara</title>
      <link>https://dev.to/vayun</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vayun"/>
    <language>en</language>
    <item>
      <title>How I check that my Rust port still does what the Go original does</title>
      <dc:creator>Vayun Godara</dc:creator>
      <pubDate>Mon, 26 Oct 2026 19:36:33 +0000</pubDate>
      <link>https://dev.to/vayun/how-i-check-that-my-rust-port-still-does-what-the-go-original-does-1k91</link>
      <guid>https://dev.to/vayun/how-i-check-that-my-rust-port-still-does-what-the-go-original-does-1k91</guid>
      <description>&lt;p&gt;cliproxy-rs is my Rust port of CLIProxyAPI, a Go proxy that puts Claude, Codex, Gemini and OpenAI-compatible APIs behind one local endpoint. The promise is that a Go user can point the Rust binary at their existing &lt;code&gt;config.yaml&lt;/code&gt; and credential files and get the same routes and the same answers.&lt;/p&gt;

&lt;p&gt;Trying requests by hand only tells you the requests you tried work. I wanted a coverage number I could trace back to evidence, item by item.&lt;/p&gt;

&lt;h2&gt;
  
  
  The checklist comes from the Go tree
&lt;/h2&gt;

&lt;p&gt;A list I wrote by hand would only contain the things I remembered, so the checklist is extracted from the Go source at a pinned commit. Each item is one contract: a Go test file, a config key, a public or management route, a plugin RPC method, a CLI flag. At Go v8.0.35 that's 1,951 items. The biggest groups:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Kind&lt;/th&gt;
&lt;th&gt;Items&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Go test-suite files&lt;/td&gt;
&lt;td&gt;989&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Config leaves&lt;/td&gt;
&lt;td&gt;436&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Management routes&lt;/td&gt;
&lt;td&gt;186&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Public routes&lt;/td&gt;
&lt;td&gt;77&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Plugin RPC methods&lt;/td&gt;
&lt;td&gt;62&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CLI flags&lt;/td&gt;
&lt;td&gt;34&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every item gets one of three statuses:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;covered: implemented, and a Rust test or a fixture recorded from Go exercises it&lt;/li&gt;
&lt;li&gt;partial: implemented in part, or implemented with no test that pins Go's behaviour&lt;/li&gt;
&lt;li&gt;missing: not there&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A Python script (&lt;code&gt;audit.py&lt;/code&gt;) fills in most of it. Fixtures recorded from Go carry the Go test name and line, so it can match each Go test against the names cited in Rust tests and fixtures. A second script starts the server and records which method and path pairs actually route. When the scan gets something wrong I override it in a TSV, one row per item, with the evidence and a note.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first audit was too generous
&lt;/h2&gt;

&lt;p&gt;When I redid the audit for v0.2.6, the generated numbers were 854 covered, 722 partial, 111 missing. The README said 835 / 707 / 145, so it was already stale.&lt;/p&gt;

&lt;p&gt;I went through all 833 items that weren't covered and 164 changed status. Most moved up (73 partial to covered, 36 missing to covered, 42 missing to partial). 13 went from covered down to partial, and those were the useful ones.&lt;/p&gt;

&lt;p&gt;The scanner had counted a route as covered when a fixture hit the same path, even with a different HTTP method, so a GET test was vouching for a POST handler. Now the evidence has to match the method and the complete route.&lt;/p&gt;

&lt;p&gt;Two more rules came out of that pass. A subsystem that works doesn't make a whole Go test suite covered: Go has 23 Claude alias-restoration cases, I replay 20, so that item is partial. And a Go test name in a Rust comment is a search hint. It doesn't prove the assertions were ported.&lt;/p&gt;

&lt;h2&gt;
  
  
  Moving the target
&lt;/h2&gt;

&lt;p&gt;Go keeps shipping. I'd been comparing against v8.0.10, and moving to v8.0.35 meant going through 288 upstream commits and adding 264 checklist items. I kept all 1,687 existing IDs and appended the new ones at the end of each section, so branches I had in flight still point at the right item. Tests Go deleted are marked historical instead of disappearing.&lt;/p&gt;

&lt;p&gt;The last audit (20 Oct, against v8.0.35) reads 1,053 covered, 872 partial and 26 missing.&lt;/p&gt;

&lt;p&gt;Partial is the biggest bucket. It holds everything implemented in part or implemented without a test that pins Go's behaviour, and the audit won't round any of it up to covered.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you're porting something
&lt;/h2&gt;

&lt;p&gt;Generate the checklist from the original's source. Make "covered" need evidence a script can find, and make every manual override say why. My first honest pass moved 13 items backwards.&lt;/p&gt;

&lt;p&gt;The checklist, the scripts and the per-item notes are in the repo under &lt;code&gt;docs/parity-audit&lt;/code&gt;: &lt;a href="https://github.com/vayungodara/cliproxy-rs" rel="noopener noreferrer"&gt;https://github.com/vayungodara/cliproxy-rs&lt;/a&gt;&lt;/p&gt;

</description>
      <category>cli</category>
      <category>go</category>
      <category>rust</category>
      <category>testing</category>
    </item>
    <item>
      <title>Two 32 MB tokenizers: hunting a memory floor in a Rust proxy</title>
      <dc:creator>Vayun Godara</dc:creator>
      <pubDate>Wed, 07 Oct 2026 18:21:46 +0000</pubDate>
      <link>https://dev.to/vayun/two-32-mb-tokenizers-hunting-a-memory-floor-in-a-rust-proxy-4hfl</link>
      <guid>https://dev.to/vayun/two-32-mb-tokenizers-hunting-a-memory-floor-in-a-rust-proxy-4hfl</guid>
      <description>&lt;p&gt;cliproxy-rs is a Rust rewrite of CLIProxyAPI. It's one local server that Codex CLI, Claude Code and other tools talk to, and it forwards to whatever you've configured: your own API keys, your own accounts. I run it on a small Linux box under real coding-agent traffic.&lt;/p&gt;

&lt;p&gt;On 0.2.0 that box showed something I didn't like. The resting memory, meaning what the process holds between requests, rose from 26 MB to 89 MB over about seven hours. Peaks were 300 to 400 MB. Nothing was failing, but a floor that only goes up is how a leak usually starts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproducing it
&lt;/h2&gt;

&lt;p&gt;My soak test only replayed Claude streams of 100 to 500 KB. The real traffic was different: Claude and Codex requests of 1 to 2 MB from long agent sessions, plus &lt;code&gt;count_tokens&lt;/code&gt; calls. So I wrote a "field mix" replay with exactly that shape (four sessions, bodies averaging 1 MB) and ran 0.2.0 and the fix branch side by side on separate GitHub runners for an hour.&lt;/p&gt;

&lt;p&gt;0.2.0 rested at 128 to 145 MB. That was clearly too much for a proxy that does no caching of its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  What heaptrack found
&lt;/h2&gt;

&lt;p&gt;After two batches of 100 field-mix turns, heaptrack showed 67.1 MB still allocated at exit. 63.8 MB of that was two o200k_base tokenizer encoders, 31.9 MB each. They were identical. Claude &lt;code&gt;count_tokens&lt;/code&gt; had built one, and Codex &lt;code&gt;count_tokens&lt;/code&gt; had built the other.&lt;/p&gt;

&lt;p&gt;Five modules each kept their own lazily built tiktoken encoder in a static. Each was built the first time something needed a token count and never freed. On a server that counts tokens for both Claude and Codex models and streams Claude-format requests to Codex models, up to seven copies could exist, 164 MB of tables in total.&lt;/p&gt;

&lt;p&gt;One encoder is bigger than its 31.9 MB of live data suggests. It holds about 600,000 short token allocations, and at malloc's 32-byte minimum chunk that comes to 47 MB resident.&lt;/p&gt;

&lt;p&gt;I also checked whether glibc was just holding freed memory. It wasn't: after a trim, calling &lt;code&gt;malloc_trim(0)&lt;/code&gt; again changed resident memory by 40 kB. The floor was live data.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;One shared encoder per encoding, still built only when a count first needs it. The same mix now holds 71 MB of tables instead of 164.&lt;/p&gt;

&lt;p&gt;Same hour of traffic, same runners:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;0.2.0&lt;/th&gt;
&lt;th&gt;0.2.2&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Resting RSS&lt;/td&gt;
&lt;td&gt;128.2 to 145.4 MB&lt;/td&gt;
&lt;td&gt;80.6 to 97.6 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Peak RSS (VmHWM)&lt;/td&gt;
&lt;td&gt;220.1 MB&lt;/td&gt;
&lt;td&gt;169.0 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;CPU per request didn't change (43.1 ms against 42.9 and 44.1 ms on one 2-vCPU machine).&lt;/p&gt;

&lt;h2&gt;
  
  
  What I haven't solved
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The replay steps up once and then stays flat. It never shows the slow climb from the field. My best guess is that the field server built its encoders hours apart, which would look like a slow rise, but I don't know that yet. A 24-hour trace of 0.2.2 is running now.&lt;/li&gt;
&lt;li&gt;Peaks are still big. A Codex request peaks at 8.8 times its body size, because request shaping rewrites the body pass by pass and each pass is a new copy. Claude requests peak at 6.0x.&lt;/li&gt;
&lt;li&gt;A fixed &lt;code&gt;MALLOC_MMAP_THRESHOLD_&lt;/code&gt; took 26.6 to 33.8 MB off the peak, but cost 16 to 31% more CPU per request, so I didn't ship it.&lt;/li&gt;
&lt;li&gt;The Go original is still faster on small requests. In my 0.1.0 benchmark, it served 1,568 non-streamed chat requests a second to cliproxy-rs's 1,168.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Keeping it fixed
&lt;/h2&gt;

&lt;p&gt;Every release tag now runs an hour of the field mix, and a 5-hour run happens every Saturday. A run fails if the resting floor rises, if the peak goes over its bound or if any request fails. Heap budgets in the tests now cover the Codex and &lt;code&gt;count_tokens&lt;/code&gt; routes as well as Claude.&lt;/p&gt;

&lt;p&gt;The method and the raw rows are in &lt;a href="https://github.com/vayungodara/cliproxy-rs/blob/master/docs/BENCHMARKS.md#field-mix-claude-codex-and-count_tokens" rel="noopener noreferrer"&gt;docs/BENCHMARKS.md&lt;/a&gt;, and the code is at &lt;a href="https://github.com/vayungodara/cliproxy-rs" rel="noopener noreferrer"&gt;github.com/vayungodara/cliproxy-rs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Built with some help from AI.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>performance</category>
      <category>debugging</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
